この記事は 🎄GMOペパボ エンジニア Advent Calendar 2024 - Adventar の21日目の記事です。
最近業務でメールサーバを扱っており、Milterを触る場面が増えてきた。 メールサーバはPostfixを使っておりsmtpd_miltersを利用してMilterと連携するわけだが、そんな時にチームメンバーからMilter連携ができてるかどうかを検証する手段として、go-milterを利用したダミーのMilterを実装しPostfixのMilter連携をデバッグする方法を教えてもらった。
というわけでgo-milterを実際に使ってみた。 早速go-milterの使い方から。。。と行きたいところだが、go-milterを使うにはMilterの仕組みを理解していることが大前提になるので、まずはMilterの説明から。
Milterとは
Mail filterの略で、MTAに対してメールフィルタリングの機能を提供する仕組みおよびそれを提供するAPIである。 Milterによって、MTAで処理するメールに対してスパムメールやウイルスが添付されたメールを検出したり、必要に応じて駆除することができる。元々はSendmail用に開発されたものだが、後にPostfixなどの他のMTAでも採用されるようになった。 スパム対策ソフトとして有名なRspamdや、ウイルス駆除ソフトとして有名なClamAVも、Milterとして利用するための機能を提供している。
Milterの仕組み
MTAが受信処理する際のSMTPセッション内で実行される各種ハンドシェイクそれぞれについてcallback関数を定義し、各ハンドシェイクの段階で対応するcallback関数が実行される仕組みになっている。 全体図を書くと以下の通り。 以下は各callback関数での結果がOKである前提で書いているが、どこかのcallback関数でRejectなどの結果だった場合は、そこでAbortされる。
sequenceDiagram participant MUA as MUA (ex. OutLook) participant MTA as MTA (ex. Postfix) participant Milter as Milter (ex. Rspamd/ClamAV) MUA->>MTA: CONNECT MTA->>Milter: CONNECT情報を送信 Note over Milter: CONNECT callback関数 Milter->>MTA: フィルタリング結果 (ex. OK, Reject) MTA->>MUA: 220 MUA->>MTA: HELO example.com MTA->>Milter: HELO情報を送信 Note over Milter: HELO callback関数 Milter->>MTA: フィルタリング結果 (ex. OK, Reject) MTA->>MUA: 250 Hello example.com MUA->>MTA: MAIL FROM:<user@example.com> MTA->>Milter: MAIL FROM情報を送信 Note over Milter: MAIL FROM callback関数 Milter->>MTA: フィルタリング結果 (ex. OK, Reject) MTA->>MUA: 250 OK MUA->>MTA: RCPT TO:<recipient@example.com> MTA->>Milter: RCPT TO情報を送信 Note over Milter: RCPT TO callback関数 Milter->>MTA: フィルタリング結果 (ex. OK, Reject) MTA->>MUA: 250 OK MUA->>MTA: DATA MTA->>MUA: 354 Start mail input MUA->>MTA: メールヘッダーを送信 MTA->>Milter: メールヘッダーを送信 Note over Milter: HEADER callback関数 Milter->>MTA: フィルタリング結果 (ex. OK) MUA->>MTA: メール本文を送信 MTA->>Milter: メール本文を送信 Note over Milter: BODY callback関数 Milter->>MTA: フィルタリング結果 (ex. OK, Reject, Virus Detected) MTA->>MUA: 250 OK MUA->>MTA: QUIT MTA->>Milter: セッション終了情報を送信 Note over Milter: CLOSE callback関数 Milter->>MTA: 応答 MTA->>MUA: 221 Goodbye
go-milterとは?
ここでようやく本題。 go-milterとは、名前通りMilterを実装できるgolangのパッケージである。
https://pkg.go.dev/github.com/emersion/go-milter#Milter の通り、各コマンドに対するcallback関数のインターフェースが定義されている。 このインターフェースを満たす実装をすればよい。
例えば以下はCONNに対するcallback関数のインターフェースである。 引数は、ホスト名・プロトコルファミリ・ポート・IPアドレス・Modifier構造体となっている。
// Connect is called to provide SMTP connection data for incoming message. // Suppress with OptNoConnect. Connect(host string, family string, port uint16, addr net.IP, m *Modifier) (Response, error)
ちなみにModifier構造体はこのようになっている。 Macros(MTAからMilterへ渡される付加情報)とヘッダ情報からなっている。 Modifier構造体は、全てのcallback関数の実装で引数として必要となる。
type Modifier struct { Macros map[string]string Headers textproto.MIMEHeader // contains filtered or unexported fields }
戻り値はResponse構造体とエラーの二つである。 Response構造体は以下の通り。 callback関数での処理結果と、後続のコマンドの受付を継続するか中止するかを表すbool値で構成される。 Response構造体はAbort以外のcallback関数の実装で戻り値として必要になる。
type Response interface { Response() *Message Continue() bool }
サンプルコード
こちらをどうぞ。 やっていることはとても簡単で、各コマンドでMUAから送信された情報をslogを使用してログに出力しているだけ。
実行例
このようにコマンドごとにログが出ていることがわかる。 最後はCloseではなくAbortになってしまう点だけは原因が掴めておらず。 おそらくだがQUITコマンドが送信されずにコネクションが切断されたときにこうなるのだろう。
$ sudo ./dummy-milter 2024/12/19 13:46:30 INFO Connect from host=localhost :="port=58866" 2024/12/19 13:46:30 INFO HELO name=mail-server 2024/12/19 13:46:30 INFO MAIL FROM: from=user@example.com 2024/12/19 13:46:30 INFO RCPT TO: rcpt=recipient@example.com 2024/12/19 13:46:30 INFO Header: name=From ": "="value=user@example.com" 2024/12/19 13:46:30 INFO Header: name=To ": "="value=recipient@example.com" 2024/12/19 13:46:30 INFO Header: name=Subject ": "="value=test" 2024/12/19 13:46:30 INFO Header: name=Message-Id ": "="value=<20241219044630.1DBEA809AE2@mail-server>" 2024/12/19 13:46:30 INFO Header: name=Date ": "="value=Thu, 19 Dec 2024 13:46:30 +0900 (JST)" 2024/12/19 13:46:30 INFO Header: name=X-Virus-Scanned ": "="value=clamav-milter 0.103.12 at milter-server" 2024/12/19 13:46:30 INFO Header: name=X-Virus-Status ": "="value=Clean" 2024/12/19 13:46:30 INFO Headers: headers="map[Date:[Thu, 19 Dec 2024 13:46:30 +0900 (JST)] From:[user@example.com] Message-Id:[<20241219044630.1DBEA809AE2@mail-server>] Subject:[test] To:[recipient@example.com]" 2024/12/19 13:46:30 INFO Body chunk: chunk="This is a test email\r\n\r\n" 2024/12/19 13:46:30 INFO Body 2024/12/19 13:46:30 INFO Abort 2024/12/19 13:46:30 INFO Abort
ちなみに
go-milterでは各コマンド以外にもいろんな関数を提供している。 例えば、処理中のメールに新しいヘッダーを付与するAddHeader関数や、処理中のメールを隔離するQuarantine関数などがある。 これらの関数を組み合わせれば、独自のスパム検知ソフトウェアを実装することができそうだ。