メールをフックしてgitをさわりつつ返信

シリーズ:さくらサーバーで遊ぶ

設定等のメモ
http://qiita.com/cielavenir/items/67a7c631713b4c816b6d

SSL化
http://qiita.com/cielavenir/items/aad29b2348fc4d3f9155

(さくらサーバー導入の主目的)
メールをフックする
http://qiita.com/cielavenir/items/5ce4568fc405329d421a

Linuxbrewを導入する(旧)
http://qiita.com/cielavenir/items/741921fcecb281555f77

Homebrewを導入する
https://qiita.com/cielavenir/items/67ce0ec9cd8d43ed00f1

用途が非常にspecificですが、汎用的に使える部分もあるのではないかと。

確認用に自分のtwitterにDMする機能も。


メールフィルタ


  • qmailの場合


    • USER-SUFFIX@DOMAIN

    • ~/.qmail-SUFFIX



|/usr/bin/spamfilter

|$HOME/mailhook.rb


  • maildropの場合(さくらサーバー)


    • MAIL_ACCOUNT@DOMAIN

    • ~/MailBox/MAIL_ACCOUNT/.mailfilter

    • 実行時ディレクトリが.mailfilterと同じディレクトリになるようなので注意。



xfilter "/usr/local/bin/spamc"

to "|/home/USER/mailhook.rb"


フックスクリプト

mail gemとtwitter gemが必要です。


mailhook.rb

#!/usr/bin/env /home/cielavenir/.rbenv/shims/ruby

#coding:utf-8
#shebangが効かない場合のみenvを経由(さくらでは効きませんでした)

#mailhookとwebhook(cgi)を別のサーバーに置く場合はコメントアウト
$stdout=File.open('mailhook.log','w')
$stderr=$stdout
def msg(str)
$stdout.puts "Content-Type: text/plain\n\n"+str
end
#msg("Debug mode.\n"+RUBY_VERSION)

begin

Encoding.default_external='UTF-8'
require 'nkf'
require 'net/smtp'
require 'time'
require 'stringio'
require 'etc'

USERDIR=Etc.getpwuid.dir
#USERDIR='/home/USER'

#ENV['GEM_HOME']=USERDIR+'/.gem/ruby/2.2.0'
#変わったサーバーだとsshとcgiでUSERDIRの値が違うことがあるので確認
#system('stat '+USERDIR)

require 'mail'
#require 'git'
require 'twitter'

### config ###
#BLOB_NAME_FILE='https://github.com/USER/REPO/blob/__REVISION__/
#BLOB_NAME_FILE='https://gitlab.com/USER/REPO/blob/__REVISION__/
#BLOB_NAME_FILE='https://bitbucket.org/USER/REPO/src/__REVISION__/'
#BLOB_NAME_DIR='https://github.com/USER/REPO/tree/__REVISION__/
#BLOB_NAME_DIR='https://gitlab.com/USER/REPO/tree/__REVISION__/
#BLOB_NAME_DIR='https://bitbucket.org/USER/REPO/src/__REVISION__/'

REPO_LOCAL=USERDIR+'/git_dir'

MAIL_HOOKER=''
#Your primary email
SENDER_ADDR=''
SENDER_NAME=''
#Server info
SERVER_HOST='example.jp'
SERVER_PORT='587'
#nil/:enable_ssl/:enable_starttls
#enable_sslとenable_starttlsはどちらか片方しか対応していない場合がありますので注意して下さい。サーバーの設定を熟読。
SMTP_SSL_METHOD=:enable_starttls
#Envelope from (ドメインは基本的にSMTPサーバーと同じである必要があります)
SMTP_FROM=''
SMTP_PASSWORD=''
#OpenSSL::SSL::VERIFY_NONE/OpenSSL::SSL::VERIFY_PEER
SSL_VERIFY=OpenSSL::SSL::VERIFY_PEER
#CAファイル(問題がなければnilでも可)
SSL_CA='/usr/local/share/certs/ca-root-nss.crt'

HEADER=<<EOM
THIS_IS_HEADER
EOM

FOOTER=<<EOM
THIS_IS_FOOTER
EOM

MY_TWITTER='user'
client=Twitter::REST::Client.new{|config|
config.consumer_key = ""
config.consumer_secret = ""
config.access_token = ""
config.access_token_secret = ""
}

def predicate(name,*args)
#nameがargsに合致するか
args.any?{|arg|name.include?(arg)}
end

def predicate_subject(subject)
#なんでもかんでも転送すると問題があるので、特定の件名を持つメールにのみ反応する
!subject.start_with?('Re:') && !subject.start_with?('RE:')
end

def make_arg(body)
arg=[]
body.each_line{|line|
if line=~/(\d+)/
arg<<$1
end
}
arg
end

### end of config ###

#twitter gemはSSLContextを触れない
OpenSSL::SSL.module_eval{
remove_const(:VERIFY_PEER)
const_set( :VERIFY_PEER, SSL_VERIFY )
}
ENV['SSL_CERT_FILE']||=SSL_CA

def do_send(query)
boundary="webhook_scripter_boundary"
subject_mime=NKF.nkf("-wM",query[:subject])
begin
smtp=Net::SMTP.new(SERVER_HOST,SERVER_PORT)
if SMTP_SSL_METHOD
ctx = smtp.send(SMTP_SSL_METHOD)
#ctx.verify_mode = SSL_VERIFY
#ctx.ca_file = SSL_CA if SSL_CA
end
smtp.start('localhost',SMTP_FROM,SMTP_PASSWORD,:plain){
body = <<EOM
From:
#{NKF.nkf("-wM",SENDER_NAME)} <#{SENDER_ADDR}>
To:
#{query[:to]}
CC:
#{query[:cc]*', '}
Sender:
#{SMTP_FROM}
Subject:
#{subject_mime.chomp}
In-Reply-To: <
#{query[:msgid]}>
Date:
#{Time.now.rfc2822}
MIME-Version: 1.0
X-Mailer: webhook_scripter
Content-Type: multipart/mixed; boundary="
#{boundary}"

--#{boundary}
Content-Type: text/plain; charset=UTF-8

#{query[:body]}
EOM

smtp.send_mail body+"--#{boundary}--", SMTP_FROM, query[:to], *query[:cc]
true
}
rescue => err
puts err.inspect
false
end
end

def do_git(body)
pwd=Dir.pwd
Dir.chdir(REPO_LOCAL)

arg=make_arg(body)
#g=Git.open('.')
system("git pull origin master")
#g.pull('origin','master')
revision=`git rev-parse HEAD`.strip
#revision=g.object('HEAD').sha
BLOB_NAME_FILE.sub!('__REVISION__',revision)
BLOB_NAME_DIR.sub!('__REVISION__',revision)

body=StringIO.new
body.puts HEADER
body.puts

Dir.open('.'){|dir|
#せっかくpredicateはargを複数受け取れるけど、ファイル名がargでソートされてほしいので
arg.each{|ar|
dir.each{|name|
if predicate(name,ar)
body.puts BLOB_NAME_FILE+name if FileTest.file?(name)
body.puts BLOB_NAME_DIR+name if FileTest.directory?(name)
end
}
}
}

body.puts FOOTER
Dir.chdir(pwd)
body
end

### main
mail_str=STDIN.read
File.write('mailhook.eml',mail_str)
mail=Mail.read_from_string(mail_str) # eml given by mailhook
mail_body=mail.body.decoded.encode('UTF-8',mail.charset)
subject=mail.subject||''
if !predicate_subject(subject)
msg("email subject is not correct.")
exit
end
body=do_git(mail_body)

mail_from=mail.from
query={
:to => mail_from.shift,
:cc => ( mail_from+mail.to+(mail.cc||[]) )-[MAIL_HOOKER,SENDER_ADDR]+[SENDER_ADDR],
:subject => 'Re: '+(mail.subject||''),
:msgid => mail.message_id,
:body => body.string,
}
do_send(query)

client.create_direct_message(MY_TWITTER, "[mailhook performed]\n"+query[:body])

###
msg("Performed successfully.")

rescue => e
msg(e.to_s)
client.create_direct_message(MY_TWITTER, "[mailhook error]\n"+e.to_s)
end



メールフックサーバーを別の場所に

都合によりメールフックサーバーを別の場所に置く場合は、上のスクリプトをCGIとして設置した上で、以下のようなラッパーを用意します。


mailhook_wrapper.rb

#!/usr/bin/ruby

#coding:utf-8
require 'net/https'
require 'stringio'
http=Net::HTTP.new('SERVER',PORT)
http.use_ssl=true
#http.verify_mode=OpenSSL::SSL::VERIFY_NONE
s=StringIO.new;$stderr=s # stderrはフィルタの動作に邪魔なので潰す

DIR='/home/USER'

http.start{
mail_str=STDIN.read
File.open(DIR+'/webhook.eml','w'){|f|f.write mail_str}
resp=http.post('/cgi-bin/webhook.cgi',mail_str)
File.open(DIR+'/webhook.log','w'){|f|f.write resp.body}
}



最後に

メールフックの仕様上、認証を設けることは不可能です。いくら件名でフィルタを掛けているとは言え、使用の際は慎重を期して下さい。