これは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を作るための情報を探している人は次のページを参考にしてください。
- CまたはC++でmilterを実装したい人向け
- libmilterのAPI(英語):Sendmail提供のライブラリー。スレッドベース。
- milter-test-client.c:milter manager提供のライブラリーlibmilter-clientを使ったmilterの実装例。libmilter-clientはイベント駆動ベース。
- Rubyでmilterを実装したい人向け
- milter managerリファレンスマニュアル: Rubyでmilter開発 - milter manager:milter manager提供のlibmilter-clientのRubyバインディングの使い方。
- milter gem:Rubyでmilterプロトコルを実装したライブラリー。イベント駆動ベース。ppymilter(後述)をRubyで実装したもの。
- Pythonでmilterを実装したい人向け
- Perlでmilterを実装したい人向け
- Sendmail::Milter:libmilterのPerlバインディング。
- Sendmail::PMilter:Perlでmilterプロトコルを実装したライブラリー。
- Haskellでmilterを実装したい人向け
- RPF:HaskellでのReceiver's Policy Frameworkの実装。この中に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}: #{名前}: #{説明}
- A: アボート: メールトランザクション中で終了するコマンド。
- B: 本文チャンク: 本文のチャンク(一部)を送るコマンド。本文が大きい場合は複数のチャンクにわけて渡される。(複数の本文チャンクコマンドが送られる。)
- C: 接続: 接続してきたSMTPクライアントに関する情報を送るコマンド。
- D: マクロ定義: コマンドに対する付加情報を送るコマンド。詳細は後述。
- E: メッセージ終了: メッセージ全体を送ったことを知らせるコマンド。このコマンドの応答でだけメールを変更できる。詳細は後述。
- H: HELO: SMTPのHELO/EHLOに関する情報を送るコマンド。
- L: ヘッダー: メッセージのヘッダーを1つ送るコマンド。複数のヘッダーがある場合は複数回送られる。
- M: MAIL FROM: SMTPのMAIL FROMに関する情報を送るコマンド。
- N: ヘッダー終了: すべてのヘッダーを送ったことを知らせるコマンド。
- O: ネゴシエーション: milterセッション開始時に動作を調整するコマンド。特殊なコマンド。詳細は後述。
- Q: 終了: このセッションを終了することを指示するコマンド。
- R: RCPT TO: SMTPのRCPT TOに関する情報を送るコマンド。複数のRCPT TOを指定した場合は複数回送られる。
- T: DATA: SMTPのDATAに関する情報を送るコマンド。
- U: 未知: SMTPで未知のコマンドを指定されたときに送るコマンド。
IDにはコマンドの名前と関連がある文字が使われていることが多いので、データを生で見てもなんとなくわかることがあります。
重要なコマンドについて補足します。
マクロ定義コマンド
マクロ定義コマンドは特殊なコマンドです。
マクロ定義コマンドは次に送るコマンドの付加情報を送るコマンドです。付加情報はキーと値のペアのリストです。なお、マクロ定義コマンドに対してmilterは応答しません。
次の各コマンドの前にMTAが送ります。
- 接続コマンド
- HELOコマンド
- MAIL FROMコマンド
- RCPT TOコマンド(それぞれのRCPT TOコマンド毎)
- DATAコマンド
- ヘッダー終了コマンド
- メッセージ終了コマンド
図にすると次のようになります。
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間で次のことを決めます。
- milterプロトコルのバージョン
- アクション(milterがどんな操作をするか)
- ステップ(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:[ SP の] を使うかどうか) 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バイトの符号なし整数で次の値のどれかです。最初の行が凡例です。
- #{値}: #{説明}
- 0: CONNECTコマンド用マクロ
- 1: HELOコマンド用マクロ
- 2: MAIL FROMコマンド用マクロ
- 3: RCPT TOコマンド用マクロ
- 4: DATAコマンド用マクロ
- 5: メッセージ終了コマンド用マクロ
- 6: ヘッダー終了コマンド用マクロ
マクロ定義コマンドでは「コマンドID」を使っていましたが、ここでは独自の値になることに注意してください。
他のコマンド
2014/12/10中ではまとめきれなかったので他のコマンドは省略します。ごめんなさい。
応答の種類
次は個々のやりとりのうち「milterから送るデータ」について説明します。
milterから送るデータを応答と呼びます。
応答のID、名前、説明は次の通りです。最初の行が凡例です。
- #{ID}: #{名前}: #{説明}
- +: 宛先追加: 宛先を追加する応答。複数の宛先を追加するときは複数回送る。
- -: 宛先削除: 宛先を削除する応答。「何番目の宛先」という形式で指定する。
- 2: パラメーター付きで宛先追加:
付きで宛先を追加する。 - a: 受理: このメールは受理するという応答。milterプロトコルでのメールトランザクションの処理はここで終了し、これ以降MTAからコマンドは送られてこない。
- b: 本文置換: 本文を変更する応答。変更後の本文が大きい場合は複数のチャンクに分けて応答する。
- c: 継続: 次のコマンドへいってくれという応答。基本はこれを返す。
- d: 破棄: 処理中のメッセージを破棄する。SMTPでのメールトランザクションの処理はここで終了するので、milterプロトコルでのメールトランザクションも終了する。
- e: 差出人変更: 差出人を変更する応答。
- h: ヘッダー追加: ヘッダーを追加する応答。複数のヘッダーを追加するときは複数回送る。ヘッダーは末尾に追加される7。
- i: ヘッダー挿入: 指定した位置にヘッダーを挿入する応答。
- m: ヘッダー変更: 指定した位置のヘッダーを変更する。
- p: 処理中: milterの処理に時間がかかっていることをMTAに知らせる応答。MTA側のタイムアウト時間を伸ばせる。
- q: 隔離: このメールを隔離するという応答。
- r: 拒否: このメールの受信を拒否するという応答。SMTPでは5XX系のレスポンスになる。
- s: スキップ: このメールトランザクションの処理を途中でやめるという応答。
- t: 一時拒否: このメールの受信を一時的に拒否するという応答。SMTPでは4XX系のレスポンスになる。
- y: SMTPレスポンス設定: SMTPレスポンスのコードとメッセージを設定する。
「宛先追加」や「ヘッダー変更」などメッセージ本体を変更する応答は「メッセージ終了コマンド」の応答のときでないと返せないことに注意してください。
2014/12/10中ではまとめきれなかったので応答のデータフォーマットなどの説明は省略します。ごめんなさい。
まとめ
milterプロトコルについてまとめきれませんでした。もしかしたら後で追記するかもしれません。
文章にするのは時間がかかるのですが、口頭で説明するのは文章にするよりも時間がかからないので、興味のある人は、イベントなどでmilter managerの作者にばったり会ったときにでも聞いてください。
(途中ですが)milterプロトコルの説明を見るとmilterではいろんなことができることがわかります。milterを使うとPostfixの設定だけではできないようなことも実現できます。Postfixの設定だけでは実現できないなぁと思ったときは、milterも組み合わせることも検討してみてください。実際、クリアコードではRubyで小さなmilter(100行以内のもの)を書いて、Postfixの設定だけでは実現できない(実現しようとすると複雑になる)メールシステムの構築をお手伝いしていたりします8。困ったときは、公開できる情報ならmilter managerのメーリングリスト、有償でもよいならお問い合わせフォームを使って相談してみてください。
-
libmilterのバインディング開発者にはそんなに有用ではないが、自分でmilterプロトコルを実装する開発者には有用。 ↩
-
オープンソースでよかったですね! ↩
-
たぶん。少なくともmilter managerのmilterプロトコル実装はそう。 ↩
-
milter managerを実装しているときはこの情報を知らなかったので一から調べました。 ↩
-
以前のPostfixはダメでしたが、Postfixにパッチを送って取り込んでもらったので、今はできるようになっています。 ↩
-
こんな機能あったのか。。。milter managerでは実装していないな。。。 ↩
-
MTAの実装依存かも。 ↩
-
100行以上になるような込み入った機能を実現するmilterをRubyで開発することもあります。 ↩