go-milterで遊ぶ

この記事は 🎄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のパッケージである。

pkg.go.dev

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を使用してログに出力しているだけ。

github.com

実行例

このようにコマンドごとにログが出ていることがわかる。 最後は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関数などがある。 これらの関数を組み合わせれば、独自のスパム検知ソフトウェアを実装することができそうだ。