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


| |シリーズ：さくらサーバーで遊ぶ|
|:--|:--|
|設定等のメモ|http://qiita.com/cielavenir/items/67a7c631713b4c816b6d|
|SSL化|http://qiita.com/cielavenir/items/aad29b2348fc4d3f9155|
|(さくらサーバー導入の主目的)<br>メールをフックする|http://qiita.com/cielavenir/items/5ce4568fc405329d421a|
|Linuxbrewを導入する(旧)|http://qiita.com/cielavenir/items/741921fcecb281555f77|
|Homebrewを導入する|https://qiita.com/cielavenir/items/67ce0ec9cd8d43ed00f1|
|さくらサーバーでgccへの依存が発生しない理由<br>(HOMEBREW_BUILD_FROM_SOURCE=1は不要)|https://qiita.com/cielavenir/items/410ded536d9b520989c4|

用途が非常に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が必要です。

```rb: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として設置した上で、以下のようなラッパーを用意します。

```rb: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}
}
```

## 最後に

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