title: "Node.js + IMAP + SpamAssassin でスパムメールを自動振り分けするツールを作った話"
date: 2025-04-22
背景と目的
筆者はあるクラウド型のメールサービス(SpamAssassin が導入されている)を長年使用してきたが、年々迷惑メール(特に詐欺メール)の量が増加してきた。SpamAssassin によるスパム判定はされているものの、スパムフォルダに自動で振り分けられる仕組みはあるにはある。ただし、フィルタの感度を上げると誤検知のリスクが高まるため、あえて低めに設定していた。実際に X-Spam-Status
のスコアを見ると、1点台などの軽微なスパム判定も多く、閾値の調整だけでは根本的な解決に至らなかった。
また、ブラックリスト登録による対策も行っていたが、送信元を変えてくるスパムも多く、いわゆる「もぐらたたき」になってしまい限界を感じていた。
これらの課題を解決するため、IMAP経由で受信ボックスを巡回し、SpamAssassinの付与する X-Spam-Status
ヘッダーに基づいて自動的にメールを振り分けるスクリプトを Node.js で実装した。
本記事では、このツールの構成、実装のポイント、運用上の工夫について紹介する。
今回もChatGPT(武志くん)が文書化してくれました。
構成
今回のスクリプトは以下のような構成になっている:
- Node.js (v18) で実装
- IMAP クライアントには
imapflow
を使用 - スパム判定には
X-Spam-Status
ヘッダーの内容を参照 - スパムと判定されたメールは IMAP 上の専用フォルダ(例:
INBOX.SuspectedSpam
)に移動 - 誤検知を避けるため、送信元アドレスによるホワイトリスト機能を併用
- 移動前後で UID を再確認し、移動処理が正しく行われたかチェック
この構成により、既存のメールサーバ設定やSpamAssassinの閾値を変更することなく、クライアント側のスクリプトで柔軟に対応できるようにしている。
実装のポイント
X-Spam-Status
の解析
X-Spam-Status
ヘッダーには、SpamAssassin による判定結果がテキスト形式で含まれており、例えば次のような内容になる:
X-Spam-Status: No, score=5.7 required=6.0 tests=HTML_MESSAGE,RDNS_NONE
この文字列から tests=
以下のスパム判定ルールを抽出し、複数の特定ルール(例: HTML_MESSAGE
と RDNS_NONE
)が同時にマッチした場合のみスパムと判定するようにしている。初期は OR
条件でマッチさせていたが、誤検知を避けるため AND
条件(every()
)に変更した。
判定ルール HTML_MESSAGE
と RDNS_NONE
について
今回スパム判定に使用しているルールのうち、特に次の2つが効果的だった:
-
HTML_MESSAGE
メール本文が HTML のみで構成されている場合に付与される。これは「表示されているリンクの文字列」と「実際のリンク先URL」が異なるなど、詐欺メールでよく使われる手法に悪用されやすいため。
一般的に、出所が不明な HTML メールは危険性が高く、隔離対象とする方針をとっている。一方で、企業からの通知など出所が明確な HTML メールではこの限りではない。 -
RDNS_NONE
メールの送信元 IP アドレスに逆引き(rDNS)が設定されていない場合に付与される。正規のメールサーバであれば逆引きが設定されているのが一般的なため、rDNS が無い送信元からのメールは信頼性が低いとみなされる。
これらのルールはそれぞれ単体ではスパムと断定するには弱いため、両方がマッチしたときにのみスパムと判定するようにしている(AND条件)。
ヘッダの複数行対応
RFCでは、メールヘッダの値が複数行にわたる場合、改行の次の行は空白またはタブで始まると定義されている。そのため、単純な split では正確に抽出できない。ヘッダのパース処理ではこの点に注意し、継続行を適切に結合するようにしている。
UIDベースでの移動と検証
スパム判定されたメールは IMAP の MOVE
コマンドを使って専用フォルダに移動させる。imapflow
では client.messageMove(uid, dest, { uid: true })
を使うことで UID 指定の移動が可能。
移動直後に同じ UID のメールが INBOX
に残っていないかを再検索し、失敗していた場合はログを残すようにした。
運用と結果
実際に運用を開始してから、1日で 70 通以上のスパムメールを自動で振り分けることができた。Webメール画面で手動で削除していた頃に比べ、心理的・時間的な負担が大きく軽減された。
ホワイトリスト機能も併用することで、必要なメールが誤ってスパムとして扱われるリスクも抑えている。
本スクリプトは Raspberry Pi 4 上で運用しており、cron
を使って5分ごとに実行することでリアルタイム性もある程度確保できている。
IMAPでの運用のため、スクリプトによってメールが振り分けられると、どの端末(PC、スマホ)でメールを確認しても INBOX
からは消えている。ただし、スマートフォンの通知機能などにより、振り分け処理が行われる前にスパムメールの通知が出てしまうことがある。この点については、メールアプリの挙動に依存するため完全な制御は難しいが、メールボックスを再同期(取り直し)すれば INBOX
からは確実に消えている。
なお、これを完全に回避する手段として、該当アカウント宛のメールを一度中継サーバに自動転送し、そこでSMTPレベルでスパムチェックを行った上で別アカウントに転送するという方法も考えられる。ただし、これは構成が大掛かりになるため、今回の実装では採用していない。
まとめ
SpamAssassin による判定スコアを活用しつつ、IMAP 経由でメールを動的にフィルタリングすることで、既存のメール環境に最小限の変更でスパム対策が可能になった。
本記事で紹介した構成は、SpamAssassin を使っている任意のメールサービスでも同様に応用できる。大量の詐欺メールに悩まされている方にとって、一つの有効な選択肢となれば幸いである。