spam と判定されたメールは転送せず、ローカル配送のみを行う

## 概要

3000人程度の利用者に対するメールシステムを、かれこれ12年間、オンプレミスで運用してきたのですけれど、ついに G Suite に移転することになりました。その間に得たノウハウを、このままロストテクノロジーにするのも惜しいので、過去の作業ログの中から、ある程度汎用性がありそうなトピックを取り出して、公開しようと思います。

近年、spam メールが急増しており、なんらかの spam 対策は必須の状況です。もっとも一般的な対策は、商用またはフリーの spam 検出エンジンを導入するという対策になります。しかし、spam 検出エンジンには必ず、誤検出（実際には spam ではないメールを spam と誤って判定する）問題があります。そのため、spam と判定されたメールについても、単に破棄するのではなく、利用者が必要に応じて探し出せる場所（迷惑メールフォルダ）に保存しておくことは必須になります。

また、多くの利用者は、大学のメールサーバに届いたメールを、自分の携帯電話や ISP のメールサーバに転送しています。そのため、多くの携帯電話利用者から「月極め転送容量を無駄遣いしないために、spam は転送しないようにしてほしい」という要望を受けます。

そこで本稿では、

* spam と判定されなかったメールについては、通常のローカル配送（利用者による転送設定による転送を含む）を行う。
* spam と判定されたメールについては、利用者による転送設定を無視して、迷惑メールフォルダへの配送のみを行う。

という設定を説明します。

## 前提

幾つかの前提があります。

* SMTP は Postfix で処理している。
* IMAP/POP3 は Dovecot で処理している。
* ローカルなメールボックスへの配送（Postfix → Dovecot）は、LMTP で行う。
* ユーザによる転送先設定は LDAP に格納されている。

## 構成

全体的な構成は、以下の通りです。

* [高度なコンテンツフィルタを利用](http://www.postfix-jp.info/trans-2.3/jhtml/FILTER_README.html#advanced_filter)して、spam メールに `X-Spam-Flag: YES` というヘッダを付加する。
* `X-Spam-Flag: YES` ヘッダを持たないメールは、通常通りにローカル配送を行う。
* `X-Spam-Flag: YES` ヘッダを持つメールは、特別な spam 配送用ドメインに配送する。特別なドメインは、LMTP 配送のみを行う。
* `X-Spam-Flag: YES` ヘッダを持つメールを迷惑メールフォルダに振り分ける処理は、Dovecot IMAP サーバから呼び出された sieve スクリプトが行う。

配送経路を図示すると、以下のようになります。

```
 ↓
+-------------+
|smtpd(25/tcp)|
+-------------+
  ↓
+----------+
|cleanup(a)| (receive_override_options=no_header_body_checks により header_checks 無効)
+----------+
  ↓
+----+
|scan| (content_filter)
+----+
  ↓
+--------------+
|spam検出エンジン| (例えば ClamSMTP など)
+--------------+
  ↓
+----------------+
|smtpd(10025/tcp)|
+----------------+
  ↓
+----------+                   +--------+
|cleanup(b)|→(X-Spam-Flag:YES)→|spamsmtp| (alice@example.jp → alice@spam-forward.example.jp に書き換え)
+----------+                   +--------+ (卒業・退職済みアカウント宛は，ここで bounce → discard)
  ↓                             　 ↓
+-----+ +-----------+ +----------------+
|queue|←|spamcleanup|←|smtpd(10024/tcp)| (receive_override_options=no_header_body_checks により header_checks 無効)
+-----+ +-----------+ +----------------+ (alice@spam-forward.example.jp → alice@example.jp に書き戻し)
  ↓
内部または外部への配送
```

## 設定内容

### spam 判定を行う

[高度なコンテンツフィルタを利用する設定](http://www.postfix-jp.info/trans-2.3/jhtml/FILTER_README.html#advanced_filter)を行う。

### spam と判定されたメールを spam 配送用ドメインに配送する

ここでは、spam と判定されたメールを、ヘッダおよび本文を変更することなく spam 配送用ドメインに配送し、spam と判定されていないメールは通常配送する設定について述べる。

最初に、`X-Spam-Flag: YES` ヘッダを有するメールを spamsmtp 配送に送るように `header_checks` オプションを設定する。

```conf:/etc/postfix/main.cf
header_checks = regexp:/etc/postfix/check_spam_header
```

`/etc/postfix/check_spam_header` の中身は以下の通り。

```conf:/etc/postfix/check_spam_header
/^X-Spam-Flag:\s+YES$/  FILTER spamsmtp:[127.0.0.1]:10024
```

`header_checks` オプションや `content_filter` オプションによって経路がループにならないように注意が必要。以下のように整理する。

* cleanup(b) のみが `header_checks` オプションによる経路変更を行い、cleanup(a) と spamcleanup は `header_checks` オプションによる経路変更は行わない。
* cleanup(a) のみが `content_filter` オプションによる配送を行い、cleanup(b) と spamcleanup は `content_filter` オプションによる配送を行わない。

そのため、`header_checks` オプションを（原則として）無効化しておく．

```conf:/etc/postfix/main.cf
receive_override_options = no_header_body_checks
```

その上で，10025番ポートで待ち受けている smtpd から呼び出される cleanup(b) にのみ`header_checks` オプションを有効にするため、`receive_override_options` オプションを下記のように上書きしておく。

```conf:/etc/postfix/master.cf
10025           inet    n       -       n       -       64      smtpd
  -o syslog_name=postfix/scan
  -o content_filter=
  -o receive_override_options=no_unknown_recipient_checks,no_address_mappings
  -o smtpd_helo_restrictions=
  -o smtpd_client_restrictions=
  -o smtpd_sender_restrictions=
  -o smtpd_recipient_restrictions=permit_mynetworks,reject
  -o mynetworks=127.0.0.0/8,133.15.2.64,133.15.2.65
  -o smtpd_authorized_xforward_hosts=127.0.0.0/8,133.15.2.0/24
```

これで、cleanup(b) は `header_checks` オプションによるヘッダ検査を行うようになる。

`/etc/postfix/check_spam_header` は、`X-Spam-Flag: YES` というヘッダを持つメールは spamsmtp 配送に送るよう指定している。spamsmtp 配送は、以下のように定義しておく。

```conf:/etc/postfix/master.cf
spamsmtp        unix    -       -       n       -       64      smtp
  -o syslog_name=postfix/spam
  -o smtp_send_xforward_command=yes
  -o smtp_enforce_tls=no
  -o smtp_generic_maps=hash:/etc/postfix/spam_forward
  -o bounce_service_name=discard
```

spamsmtp 配送は、`smtp_generic_maps` オプションを用いて envelope/header に出現する全ての alice@example.jp を、特別な spam 配送用ドメインのアドレス alice@spam-forward.example.jp に置換する配送である。`smtp_generic_maps` オプションに指定されているファイル `/etc/postfix/spam_forward` の中身は以下の通り。

```conf:/etc/postfix/spam_forward
@example.jp  @spam-forward.example.jp
```

また、退職・卒業済みアカウントに対する spam は、この段階（spamsmtp → 127.0.0.1:10024）で bounce させる。bounce メールが送信者（つまり spam 送信元）に戻らないように `bounce_service_name` オプションを用いて bounce メールを破棄している[^spam-bounce]。

[^spam-bounce]: なお、spam 送信元に bounce メールを返送していると、大規模メールプロバイダ（例えば Yahoo や Google など）によって、spam 中継サーバであると誤判定され、メールをブロックされるトラブルが発生することがある。

spamsmtp 配送で書き換えられたメール（すなわち、spam 配送用ドメイン宛のメール）を受け付けるために、10024 番ポートで待ち受ける smtpd を用意する。

```conf:/etc/postfix/main.cf
127.0.0.1:10024 inet    n       -       n       -       64      smtpd
  -o syslog_name=postfix/spam
  -o content_filter=
  -o smtpd_helo_restrictions=
  -o smtpd_client_restrictions=
  -o smtpd_sender_restrictions=
  -o smtpd_recipient_restrictions=permit_mynetworks,reject
  -o mynetworks=127.0.0.0/8
  -o smtpd_authorized_xforward_hosts=127.0.0.0/8
  -o cleanup_service_name=spamcleanup
```

最後の `cleanup_service_name` オプションが重要で、spam 用に特別に用意した spamcleanup 配送に送るように指定している。また、10025 番ポートで待ち受けている smtpd とは異なり、退職・卒業済みアカウントに対する spam を bounce するために、`receive_override_options=no_unknown_recipient_checks` は指定していない．

spamcleanup 配送は、spamsmtp 配送によって行われた置換（alice@example.jp → alice@spam-forward.example.jp）の内、不要な置換を書き戻してからキューに投入する配送である。具体的には、以下のように `/etc/postfix/master.cf` に定義しておく．

```conf:/etc/postfix/master.cf
spamcleanup     unix    n       -       -       -       0       cleanup
  -o syslog_name=postfix/spam
  -o canonical_classes=envelope_sender,header_sender,header_recipient
  -o canonical_maps=hash:/etc/postfix/spam_backward
```

`/etc/postfix/spam_backward` の中身は以下の通り．

```conf:/etc/postfix/spam_backward
@spam-forward.example.jp     @example.jp
```

また、`canonical_classes` オプションおよび `canonical_maps` オプションによる書き戻しを行わせるため、`/etc/postfix/main.cf` に以下の設定が必要．

```conf:/etc/postfix/main.cf
local_header_rewrite_clients = static:all
```

なお，spamsmtp 配送の段階で envelope recipient のみを置換（alice@example.jp → alice@spam-forward.example.jp）できれば， spamcleanup 配送で書き戻す処理は不要になる。 かなりマニュアルを調べたが、smtp 配送で使えるアドレス置換用オプションは `smtp_generic_maps` のみであり、このオプションは envelope/header の全てを置換してしまうため、やむを得ず spamcleanup 配送で書き戻している。

### メールボックスに配送する

ここまでの設定で、

* `X-Spam-Flag: YES` ヘッダを持つメールは、spam-forward.example.jp 宛である。
* `X-Spam-Flag: YES` ヘッダを持たないメールは、example.jp 宛である。

とヘッダの有無に応じて、配送先ドメインが異なるようになっている。よって、後は、各ドメインのためのローカル配送を用意すれば良い。

筆者の場合は、example.jp 宛のメールは local 配送で処理し、spam-forward.example.jp 宛のメールは virtual 配送で処理するという方針を採った。その設定内容は以下の通り。

```conf:/etc/postfix/main.cf
mydomain = example.jp
mydestination = $myhostname, $mydomain
alias_maps = ldap:/etc/postfix/forward.cf
local_recipient_maps = $mailbox_transport_maps
mailbox_transport_maps =
	ldap:/etc/postfix/active_mailbox.cf
	ldap:/etc/postfix/expired_mailbox.cf

virtual_mailbox_domains = spam-forward.example.jp
virtual_mailbox_maps = ldap:/etc/postfix/active_mailbox.cf
virtual_transport = lmtp:imap.example.jp:8024
```

この設定の前半部分は、local 配送に関する部分である。前半部分では、[卒業・退職済み利用者宛のメールは転送のみを行う](https://qiita.com/tsuchm/items/e64389a0c8aa80a19fd9)ように設定している。

この設定の後半部分は、spam と判定されたメールを配送するための virtual 配送に関する部分である。後半部分の設定は、以下の3つからなる。

* spam 配送用ドメイン spam-forward.example.jp 宛のメールが virtual 配送によって処理されるように、`virtual_mailbox_domains` オプションを設定。
* spam と判定されたメールを受け取るユーザは、`/etc/postfix/active_mailbox.cf` の指定に基づいて現役の利用者のみに限定（言い換えれば、卒業・退職済み利用者宛の spam と判定されたメールは破棄）
* virtual 配送に届いたメールは LMTP に転送するように、`virtual_transport` に設定。