こんにちはCTOの馬場です。
突然ですが、最近milterがアツいんです。 matsumotoryさんがPmilterというmrubyを使ったプロダクトを公開されました。
milterというとSendmailというイメージがあるかもしれませんが、Postfixと組み合わせて利用できます。捗りますね。
・・・と思いきや、自分でmilterを書こうとすると、milterについての情報がすごく少ないのが難点です。
クリアコードさんのこのエントリが唯一の頼みと言っても過言ではないくらい。 自作する場合は熟読しましょう。
milterプロトコル - ククログ(2014-12-10)
ところでハートビーツではPythonとGolangを推奨プログラミング言語としています。
というわけで今回はPythonでmilterを作成する方法を紹介します。
※ milter
は厳密にはプロトコル名なので、 milterを作成
というのは表現として不正確です。厳密には milterプロトコルを利用してメールを処理するデーモンを作成
となります。長い。
なお弊社では諸般の事情によりRHEL7/CentOS7標準のPython 2.7で動かしています。
基本のコード
Pure Pythonのmilterライブラリであるppymilterを利用し、ppymilterのお作法に則ってmilterを作成します。
ppymilterはpypiに新しいものが登録されていないようなので、githubから直接インストールします。
pip install git+https://github.com/jmehnle/ppymilter.git@v1.0.7
ppymilterbase.PpyMilter
を継承したクラスを作成し、
ppymilterserver.AsyncPpyMilterServer()
でデーモン化します。
OnXxxx
をオーバーライドすることでmilterの挙動をカスタマイズします。
どのような OnXxxx
があるのかは、
ppymilterbase.PpyMilter
のコードと pydoc
を見ましょう。
前出のクリアコードさんのエントリをよく見てmilterプロトコルについて理解を深めるとコードが理解しやすくなります。
例えばメールのヘッダをもとにいろいろする場合は
OnHeader
でいろいろ実装します。
例えばメール全体をもとにいろいろする場合は、以下のように本文を受け取った後の
OnEndBody
でメールに対していろいろするコードが基本になるとおもいます。
# coding: utf-8
from ppymilter import ppymilterbase
from ppymilter import ppymilterserver
import asyncore
import email
class MyMilter(ppymilterbase.PpyMilter):
def __init__(self, context=None):
ppymilterbase.PpyMilter.__init__(self)
self.__mutations = []
self.headers = []
self.body_chunks = []
def OnConnect(self, cmd, *args, **kwargs):
self.reset()
return self.Continue()
def OnQuit(self, cmd):
pass
def OnHeader(self, cmd, *args, **kwargs):
self.headers.append(args)
return self.Continue()
def OnBody(self, cmd, *args, **kwargs):
for chunk in args:
self.body_chunks.append(chunk)
return self.Continue()
def OnEndBody(self, cmd):
mail_lines = [": ".join(h) for h in self.headers]
mail_lines.append("")
mail_lines.extend(self.body_chunks)
msg = email.message_from_string("\n".join(mail_lines))
# do something
return self.Continue()
def OnResetState(self):
self.reset()
return self.Continue()
def reset(self):
self.__mutations = []
self.headers = []
self.body_chunks = []
if __name__ == '__main__':
context = {}
ppymilterserver.AsyncPpyMilterServer(8000, MyMilter, context=context)
asyncore.loop()
上記のように email.message_from_string()
できるので、
ここまでくればあとは普通の email
の操作と同じです。
msg.walk()
したりしていろいろしましょう。
ひととおり書けたら普通に python milter.py
みたいに実行すると、
上記のコードの場合は作成したmilterが 8000/tcp
で待ち受けてくれます。
今はsystemdで簡単にデーモン化できるので、楽でいいですね。
メールの拒否など挙動のカスタマイズ
メールを拒否したり破棄したりする場合は、
基本のコードで self.Continue()
になっているところを
self.Reject()
や self.Discard()
に変更することで実現できます。
流量制限などを行う場合は
self.TempFail()
を使うのもよさそうです。
動作テスト方法
単体で動作確認する場合はDocker、 Postfixと組み合せて動作確認する場合はVagrantで環境を作るのがお勧めです。
テストするにあたり curl
でできれば楽でいいのですが、
そもそもmilterプロトコルをお話できるツールが少ないです。
動作確認にはクリアコードさん作の milter-manager
に付属の
milter-test-server
コマンドが便利です。
an effective anti-spam with milter - milter manager
たとえば自作milterが 8000/tcp
で待ち受けている場合、
milter-manager
をインストールしたうえで以下のようなコマンドを実行するとテストできます。
sudo -u milter-manager -H \
milter-test-server \
--connection-spec inet:8000 \
--output-message \
--body "a\nb\nc\nなどあれこれ"
本番投入時の注意点
もしmilterが止まった場合にメール全体が止まると困ります。
Postfix側でmilterを組み込みときに
milter_default_action=accept
としておくことで、
milterがダメな場合にはmilterをパスできるので安全です。
(とはいえパスしていいかはmilterの目的によるので、パスでいいかは個別に判断してくださいね)
ppymilterを使ってPythonでmilterを自作する方法を紹介しました。 メールはSlackなどの普及でだいぶ少なくなってきましたが、それでもまだまだ現役です。 milterを使うと簡単にメールをプログラムで処理することができます。 メールとうまく・楽しく付き合っていきましょう。