これはPostfix Advent Calendar 2014の10日目の記事です。
Postfixは2.3からmilterという仕組みをサポートしました。milterとは「mail filter」の略で、送信したメールまたは受信したメールになんらかの処理を行う仕組みです。もともとはSendmailが作った仕組みですが、Sendmail・Postfix以外のMTAでもサポートしているMTAがあります。
Postfix 2.3でのmilterサポートは限定的な機能のみのサポートでしたが、Postfix 2.6ではSendmailとほぼ同等の機能をサポートしています。SendmailとPostfixでマクロ(後述)名が違うなど一部非互換な部分もありますが、SendmailでもPostfixでも同様に使えます。
Postfixユーザーの人にとっては、milterでどのようなことができるか、content filterとの使い分けはどうすればよいか、という情報が有用かと思いますが、ここではそのような説明をしません。そのような説明ではなくmilterプロトコルについて説明します。なお、milterプロトコルについての情報は、Postfixユーザーの人にとっては有用ではなく、milter開発者にとってもそんなに有用ではなく、一部のmilterライブラリー開発者にとっては有用*1です。Postfix Advent Calendarっぽくなくてごめんなさい。
しかも、残念ながら2014/12/10中に完成しませんでした。。。続きはいつかどこかで。。。
多くのmilterはlibmilterというSendmailが提供しているライブラリーを利用して実装します。libmilter内でmilterプロトコルを実装しているのでmilter開発者がプロトコルの詳細を気にする必要はありません。libmilterのAPIを知っていれば十分です。しかし、ここではlibmilterのAPIは説明しません。milterを作るための情報を探している人は次のページを参考にしてください。
普通の人向けの情報は提供したので、ここからは普通の人向けではない情報です。
実はmilterプロトコルの仕様書はありません。Sendmailでの挙動が事実的な仕様になっています。Postfixや各言語でのmilterプロトコルの実装は、Sendmail・libmilterのソースコード*2や、実際のSendmail・libmilterの動きを参考にmilterプロトコルを実装したものです*3。
ただ、milterプロトコルについてまとめたメモはあります。Perlでmilterプロトコルを実装した人が残したもの(英語)です。まとめた時期が古い(milterプロトコルのバージョンでいうと2)ので新しいmilterプロトコル(最新はバージョン6)についての情報はないのですが、基本的なことからまとまっているので有用な情報です*4。
それではmilterプロトコルについて説明します。
まずはベースとなるやりとりの方法について説明します。
milterプロトコルはMTAとmilter間でやりとりします。基本的に、MTAからコマンドを送り、milterが応答する、という流れになります。
MTA ー コマンド → milter
← 応答 ー
コマンドと応答のフォーマットは同じです。フォーマットは次の通りです。
| データサイズ(4バイト)| データID(1バイト) | データ |
「データサイズ」はネットワークバイトオーダーで表現されています。データサイズはデータサイズ自体のサイズ(4バイト)を含みません。「データID」と「データ」を合わせたサイズ(バイト数)です。
「データID」はこのデータの種類を示す1文字のASCII文字です。例えば、「CONNECTというコマンド」を表すデータIDは「C」です。
「データ」はデータIDに関連するデータです。データIDによって中身が変わります。0バイトのときもあります。
milterプロトコルを実装するときは、まずは、このフォーマットをデコードする処理とこのフォーマットにエンコードする処理を実装します。リンク先はmitler managerでの実装です。
ベースのやりとりがわかったところで、次は一連のやりとりについて説明します。個々のやりとりはこのあと説明します。
milterプロトコルはSMTPと関連が深いです。SMTPの1つのセッションがmilterプロトコルでの1つのセッションに対応します。milterプロトコルでの1つのセッションは次のようになります。ただし、一部省略しています。
MTA milter
ネゴシエーションコマンド →
← ネゴシエーション応答
接続コマンド →
← 応答
HELOコマンド →
← 応答
MAIL FROMコマンド →
← 応答
RCPT TOコマンド1 →
← 応答
RCPT TOコマンド2 →
← 応答
...
RCPT TOコマンドn →
← 応答
DATAコマンド →
← 応答
ヘッダーコマンド1 →
← 応答
ヘッダーコマンド2 →
← 応答
...
ヘッダーコマンドn →
← 応答
ヘッダー終了コマンド →
← 応答
本文チャンクコマンド1 →
← 応答
本文チャンクコマンド2 →
← 応答
...
本文チャンクコマンドn →
← 応答
メッセージ終了コマンド →
(← メッセージ変更コマンド)
← 応答
なお、SMTPでメールトランザクションが複数ある場合は、それに対応して「MAIL FROMコマンド」から「メッセージ終了コマンド」の「応答」までを複数回繰り返します。
SMTPのセッションと似ていますね。「HELOコマンド」、「MAIL FROMコマンド」、「RCPT TOコマンド」、「DATAコマンド」はそれぞれSMTPの対応するコマンドを実行したときに実行されます。SMTPで「DATA」の後に指定したメッセージはパースされて個別にmilterに送られます。
次は個々のやりとりのうち「MTAから送るデータ」について説明します。
MTAから送るデータをコマンドと呼びます。
コマンドのID、名前、説明は次の通りです。最初の行が凡例です。
IDにはコマンドの名前と関連がある文字が使われていることが多いので、データを生で見てもなんとなくわかることがあります。
重要なコマンドについて補足します。
マクロ定義コマンドは特殊なコマンドです。
マクロ定義コマンドは次に送るコマンドの付加情報を送るコマンドです。付加情報はキーと値のペアのリストです。なお、マクロ定義コマンドに対してmilterは応答しません。
次の各コマンドの前にMTAが送ります。
図にすると次のようになります。
MTA milter
ネゴシエーションコマンド →
← ネゴシエーション応答
マクロ定義コマンド →
接続コマンド →
← 応答
マクロ定義コマンド →
HELOコマンド →
← 応答
マクロ定義コマンド →
MAIL FROMコマンド →
← 応答
マクロ定義コマンド →
RCPT TOコマンド1 →
← 応答
マクロ定義コマンド →
RCPT TOコマンド2 →
← 応答
...
マクロ定義コマンド →
RCPT TOコマンドn →
← 応答
マクロ定義コマンド →
DATAコマンド →
← 応答
ヘッダーコマンド1 →
← 応答
ヘッダーコマンド2 →
← 応答
...
ヘッダーコマンドn →
← 応答
マクロ定義コマンド →
ヘッダー終了コマンド →
← 応答
本文チャンクコマンド1 →
← 応答
本文チャンクコマンド2 →
← 応答
...
本文チャンクコマンドn →
← 応答
マクロ定義コマンド →
メッセージ終了コマンド →
(← メッセージ変更コマンド)
← 応答
ただし、これはSendmailの動作です。Postfixは「ヘッダーコマンド」と「本文チャンク」のときもマクロ定義コマンドを送ってきます。「ヘッダーコマンド」の場合は「ヘッダー終了コマンド」用のマクロ定義コマンドを送ってきて、「本文チャンクコマンド」の場合は「メッセージ終了コマンド」用のマクロ定義コマンドを送ってきます。
マクロ定義コマンドのフォーマットは次の通りです。
| データサイズ(4バイト)| M | コマンドID(1バイト) | マクロ定義リスト |
「コマンドID」はどのコマンド用のマクロ定義なのかを示すIDです。前述のコマンドIDと同じ値です。例えば、HELOコマンド用のマクロ定義コマンドなら「H」です。
「マクロ定義リスト」のフォーマットは次の通りです。
| キー1(NULL終端文字列) | 値1(NULL終端文字列) | キー2(NULL終端文字列) | 値2(NULL終端文字列) | ... |
キーは「{...}」というように「{」と「}」で囲まれている場合もあれば囲まれていない場合もあります。
Postfixが送るマクロ定義はmilter_connect_macrosなどで指定します。
ネゴシエーションコマンドはかなり特殊なコマンドです。セッション開始時にMTAからmilterに1度だけ送ります。
ネゴシエーションコマンドでMTAとmilter間で次のことを決めます。
ネゴシエーションコマンドでは次のようにMTAとmilterで送りあうデータのフォーマットは同じです。
MTA ー ネゴシエーションコマンド → milter
← ネゴシエーションコマンド ー
MTAからmilterに送るときはMTAがサポートしている機能の情報を送り、milterがMTAに応答するときはmilterが要求する機能の情報を送ります。MTAがサポートしていない機能をmilterが要求するとネゴシエーションは失敗し、セッションは確立しません。
データのフォーマットは次の通りです。
| データサイズ(4バイト)| O | バージョン(4バイト) | アクション(4バイト) | ステップ(4バイト) | マクロリスト(0バイト以上) |
「バージョン」、「アクション」、「ステップ」はすべて4バイトの符号なし整数で、ネットワークバイトオーダーです。アクション、ステップは各ビットに意味を割り当てています。(フラグになっているということです。)
「バージョン」はMTAが提示したバージョンよりも低いバージョンをmilterが指定しても構いません。例えば、Postfixは「6」を提示し、milterが「2」を返してもよいです*5。
「アクション」のフラグは次の通りです。最初の行が凡例です。
1 << 0: ヘッダーを追加できる1 << 1: 本文を変更できる1 << 2: 宛先を追加できる1 << 3: 宛先を削除できる1 << 4: ヘッダーを変更できる1 << 5: 隔離(配送せずにholdキューに入れる)できる1 << 6: 差出人を変更できる1 << 7: パラメーター付きで宛先を追加できる(RCPT TO:<forward-path> [ SP <rcpt-parameters> ] <CRLF>の<rcpt-parameters>を使うかどうか)1 << 8: milterがマクロ定義を上書きできるか(詳細は後述するマクロリストを参照)「ステップ」のフラグは次の通りです。最初の行が凡例です。
1 << 0: MTAが接続コマンドを送らない1 << 1: MTAがHELOコマンドを送らない1 << 2: MTAがMAIL FROMコマンドを送らない1 << 3: MTAがRCPT TOコマンドを送らない1 << 4: MTAが本文チャンクコマンドを送らない1 << 5: MTAがヘッダーコマンドを送らない1 << 6: MTAがヘッダー終了コマンドを送らない1 << 7: milterがヘッダーコマンドに応答しない1 << 8: MTAが未知コマンドを送らない1 << 9: MTAがDATAコマンドを送らない1 << 10: MTAがスキップ応答(後述)をサポートしているかどうか1 << 11: MTAが拒否した宛先もmilterに送るかどうか1 << 12: milterが接続コマンドに応答しない1 << 13: milterがHELOコマンドに応答しない1 << 14: milterがMAIL FROMコマンドに応答しない1 << 15: milterがRCPT TOコマンドに応答しない1 << 16: milterがDATAコマンドに応答しない1 << 17: milterが未知コマンドに応答しない1 << 18: milterがヘッダー終了コマンドに応答しない1 << 19: milterが本文チャンクコマンドに応答しない1 << 20: MTAがヘッダーの値の先頭の空白を削除しない。「Subject: xxx」とあった場合、先頭の空白を削除しないで「 xxx」をmilterに送るということ。このフラグを落とすと先頭の空白を削除して「xxx」をmilterに送る。「MTAが○○コマンドを送らない」について補足します。このフラグを使うとmilterが必要のないコマンドを送ってこないようにMTAに指示することができます。例えば、メール本文が必要ない場合は本文チャンクコマンドを送らないようにすることで、通信量が減りパフォーマンスがあがります。
同様に「milterが○○コマンドに応答しない」について補足します。後述しますが、応答時にはmilterは「このメールを拒否する」、「このメールは受け取る」などをMTAに伝えることができます。milterが特定のコマンドに対して必ず「次の処理にいってくれ」と応答することが事前にわかっている場合は、MTAはmilterの応答を待たずに次の処理にいきます。これもパフォーマンス向上につながります。
「マクロリスト」はユーザーがmilter_connect_macrosなどで指定した値をmilterが上書きするためにあります*6。指定した場合は次のようなフォーマットになります。
| マクロの種類(4バイト) | 空白またはコンマ区切りのマクロ名のリスト(NULL終端の文字列) |
「マクロの種類」は4バイトの符号なし整数で次の値のどれかです。最初の行が凡例です。
マクロ定義コマンドでは「コマンドID」を使っていましたが、ここでは独自の値になることに注意してください。
2014/12/10中ではまとめきれなかったので他のコマンドは省略します。ごめんなさい。
次は個々のやりとりのうち「milterから送るデータ」について説明します。
milterから送るデータを応答と呼びます。
応答のID、名前、説明は次の通りです。最初の行が凡例です。
「宛先追加」や「ヘッダー変更」などメッセージ本体を変更する応答は「メッセージ終了コマンド」の応答のときでないと返せないことに注意してください。
2014/12/10中ではまとめきれなかったので応答のデータフォーマットなどの説明は省略します。ごめんなさい。
milterプロトコルについてまとめきれませんでした。もしかしたら後で追記するかもしれません。
文章にするのは時間がかかるのですが、口頭で説明するのは文章にするよりも時間がかからないので、興味のある人は、イベントなどでmilter managerの作者にばったり会ったときにでも聞いてください。
(途中ですが)milterプロトコルの説明を見るとmilterではいろんなことができることがわかります。milterを使うとPostfixの設定だけではできないようなことも実現できます。Postfixの設定だけでは実現できないなぁと思ったときは、milterも組み合わせることも検討してみてください。実際、クリアコードではRubyで小さなmilter(100行以内のもの)を書いて、Postfixの設定だけでは実現できない(実現しようとすると複雑になる)メールシステムの構築をお手伝いしていたりします*8。困ったときは、公開できる情報ならmilter managerのメーリングリスト、有償でもよいならお問い合わせフォームを使って相談してみてください。
*1 libmilterのバインディング開発者にはそんなに有用ではないが、自分でmilterプロトコルを実装する開発者には有用。
*2 オープンソースでよかったですね!
*3 たぶん。少なくともmilter managerのmilterプロトコル実装はそう。
*4 milter managerを実装しているときはこの情報を知らなかったので一から調べました。
*5 以前のPostfixはダメでしたが、Postfixにパッチを送って取り込んでもらったので、今はできるようになっています。
*6 こんな機能あったのか。。。milter managerでは実装していないな。。。
*7 MTAの実装依存かも。
*8 100行以上になるような込み入った機能を実現するmilterをRubyで開発することもあります。