Ppymilterを利用してPythonでmilterを自作する

こんにちは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を使うと簡単にメールをプログラムで処理することができます。 メールとうまく・楽しく付き合っていきましょう。

株式会社ハートビーツのインフラエンジニアから、ちょっとした情報をお届けします。