Mailman がメールを受理・配送する前に、投稿者自身による確認を行う

# 概要

Mailman でメーリングリストを運用している時、非会員からもメールを受理・配送したい場合があります。しかし、非会員によって投稿された全てのメールを単純に受理・配送すると、spam が大量にあふれかえってしまい、使い物になりません。本稿では、Mailman の司会依頼メールの仕組みを利用して、spam ボットによる投稿を抑止しながらも、任意の投稿者からのメールを受理・配送する方法を説明します。

# 背景

Mailman でメーリングリストを運用している時、非会員からもメールを受理・配送したい事例があります。例えば、筆者の所属部門では、事務室の問合せ窓口に Mailman を使っています。これには2つの理由があります。第1は、過去の問合せ及び対応の記録を残すためです。窓口アドレスを、単純なエイリアスとして実装すると、過去の問合せ及び回答の記録が残らず、事務室メンバーの交代時に引継ぎが困難になります。少なくとも、新人がアクセスできる過去メールの書庫が必要です。第2は、メール以外のコミュニケーション手段と併用する場合の利便性です。タスク管理システムやチャット、Wikiなどのメール以外のコミュニケーション手段を併用している場合、そのコミュニケーション手段から個別のメールを参照するには、個別のメールに対してURLが発行されている（言い換えれば、ウェブ上に書庫が存在している）必要があります。この2つの理由により、窓口アドレスを Mailman を用いたメーリングリストとして実装していますが、当然ながら、部門外の所属組織構成員だけでなく、所属組織外の関係者からもメールを受け付ける必要があります。

Mailman でメーリングリストを運用している時、非会員によって投稿されたメールを受理・配送するには、(1) generic_nonmember_action オプションを「承認」に設定する、または、(2) accept_these_nonmembers オプションに適当な正規表現を設定する、などの選択肢があります。しかし、前者(1)はあらゆるメールを受け付けますし、後者(2)も詐称が容易なヘッダー From に依存していますから、spam に対して脆弱な設定になってしまいます。

それに対して、所属組織構成員が利用する送信用メールサーバは固定されているという前提が利用できる場合には、[特定のメールサーバから投稿されたメールは全て配送する](https://qiita.com/tsuchm/items/44a76392ca94d28f1617)という設定が可能です。しかし、この設定にも2つの問題がありました。第1は、所属組織の送信用メールサーバを経由せずに、Gmail やプロバイダのメールサーバからメールを送信する所属組織構成員が多いという問題です。第2は、所属組織外の関係者からのメールについては、送信用メールサーバが固定されていないため、この設定では原理的に対応できないという問題です。この2つの問題により、メーリングリストサーバ管理者による司会作業が追いつかずに、メールの滞留が多発していました。

# 提案手法

本稿では、Mailman の司会依頼メールの仕組みを利用して、投稿者自身による確認を組み込むことにより、任意の投稿者・メールサーバからメールを受理・配送する方法を説明します。

具体的には、以下のような挙動を実現します。

 1. Mailman の司会依頼メールを、アップロードスクリプトを経由して、確認用CGIに転送する。
 2. 確認用CGIは確認用URLを生成し、投稿者に対して「この確認用URLにアクセスしてください」という確認依頼メールを送信する。
 3. 指定された確認用URLにアクセスがあると、確認用CGIは、当該メールの配信を依頼するコマンドメールを Mailman に送信する。

確認用URLにアクセスした投稿者のメールアドレスは、データベースに登録されて、30日間は確認なしに配送されるようになります。

# 設定

必要なスクリプト類は、[GitHub に公開](https://github.com/tsuchm/mmpostauth)しました。まずは、このリポジトリを clone してください。

```shell-session
$ git clone https://github.com/tsuchm/mmpostauth.git
```

重要なスクリプトは2つだけです。

 1. `action`: 確認用CGI
 2. `mmpostauth-handler`: 司会依頼メールのアップロードスクリプト


## 司会用パスワードの設定

まず、Mailman にテスト用メーリングリストを作成し、司会者として自分自身を設定、テストメールをテスト用メーリングリストに送って、司会依頼メールのサンプルを入手してください。

次に、Mailman のウェブインターフェースを使って、テスト用メーリングリストの司会用パスワードを設定して下さい。設定した司会用パスワードは、確認用CGIと同じディレクトリにある設定ファイル `mmpostauth.ini` に以下のように書き込んでおいて下さい。

```ini:mmpostauth.ini
[testlist@lists.example.net]
password = 司会用パスワード
```

## 確認用CGIのインストール

まず、[mod_fcgid](http://httpd.apache.org/mod_fcgid/) と必要な perl モジュールをインストールしておきます。

```shell-session
$ apt-get install libapache2-mod-fcgid libfcgi-perl libwww-perl libhtml-template-perl libcgi-session-perl libconfig-simple-perl libio-stringy-perl libmail-dkim-perl libdbix-class-perl libdbix-class-timestamp-perl libdbd-sqlite3-perl libdatetime-format-sqlite-perl
```

最初に、確認用CGIが mod_fcgid 経由で動作していることを確認します。例えば、確認用CGIのURLが、`https://www.example.net/mmpostauth/action` であるとします。以後、このURLを**基準URL**と呼びます。基準URLの末尾に `/status` を付け加えた `https://www.example.net/mmpostauth/action/status` に、ウェブブラウザでアクセスしてみて、「OK」という出力が得られたならば、確認用CGIは正しく動作しているでしょう。

次に、確認用CGIに対して司会依頼メールをアップロードできるネットワークの範囲を制限しておきます。初期状態では、アップロード用URL（基準URL+`/post`）に対するアクセス制限（抜粋）は、以下のようになっています。

```config:.htaccess
SetEnvIf Request_URI "/action/post$" post_action
SetEnvIf Remote_Addr "^192\.168\." !post_action
<Files action>
    SetHandler fcgid-script
    Order Allow,Deny
    Allow from all
    Deny from env=post_action
</Files>
```

この設定は、以下のように振る舞います。

 * アップロード用URL（基準URL+`/post`）に対するアクセスは基本的に拒否する。
 * ただし、アクセス元のIPアドレスが正規表現（`^192\.168\.`）に一致する場合は、許可する。

司会依頼メールのアップロードスクリプトをインストールするサーバのIPアドレスと一致するように、適切に正規表現を書き換えて下さい。

また、設定ファイルには、秘密を要する情報（司会用パスワードなど）が記入されていますから、適切にアクセス制限しておく必要があります。初期状態は、以下のようになっています。

```config:.htaccess
<FilesMatch "\.(ini|tmpl)$">
    Order Deny,Allow
    Deny from all
</FilesMatch>
```

## 司会依頼メールのアップロードスクリプトのインストール

その上で、司会依頼メールのアップロードスクリプトが正しく動作していることを、以下のコマンドで確認してください。

```shell-session
$ cat サンプル | perl mmpostauth-handler --debug --url https://www.example.net/mmpostauth/action/post
```
`--url` オプションで、アップロード用URL（基準URL+`/post`）を指定して下さい。このコマンドが正常に動作すると、以下のように確認依頼メールのヘッダと文面を含む結果が得られるはずです。

```http
HTTP/1.1 200 OK
Connection: Keep-Alive
Date: Fri, 02 Mar 2018 01:25:15 GMT
Server: Apache/2.4.25 (Debian)
Vary: Accept-Encoding
Content-Type: text/plain; charset=UTF-8

From: testlist-owner@lists.example.net
To: alice@example.com
Subject: Your message requires the additional confirmation step
Date: Fri, 02 Mar 2018 01:25:15 GMT
X-Mailer: MIME-tools 5.508 (Entity 5.508)

The additional confirmation step is required, because your message
posted for testlist@lists.example.net is received from an untrusted mail server.
Access the following url:

    https://www.example.net/mmpostauth/action/confirm/ランダム文字列
```

アップロードされた司会依頼メールは、確認用CGIと同じディレクトリの `spool/` ディレクトリに貯まるようになっています。確認用CGIの動作権限に留意して、`spool/` ディレクトリの所有者・パーミッションを適切に変更して下さい。

最後に、司会依頼メールのアップロードスクリプトが、MTA 経由で呼び出されるように、`/etc/aliases` または `~/.forward` を適当に変更して下さい。本稿では、`mmpostauth@mail.example.net` 宛のメールがアップロードされるものとします。以下は設定例です。

```config:/etc/aliases
mmpostauth: |"/somewhere/mmpostauth-handler --url https://www.example.net/mailman/action/post"
```

## 司会者の追加

 1. Mailman のウェブインターフェースを使って、テスト用メーリングリストの司会者に `mmpostauth@mail.example.net` を加えて下さい。
 2. テスト用メーリングリストに、非会員のアドレスからテストメールを投稿し、投稿者自身に確認依頼メールが戻ってくることを確認して下さい。
 3. 確認依頼メールに記載されている確認用URL（基準URL+`/confirm/`+ランダム文字列）にアクセスします。

## アクセス制御

初期状態では、確認用URL（基準URL+`/confirm/`+ランダム文字列）に対するアクセス制御は行わない（あらゆるアクセスを受け付ける）設定になっています。この状態でも、確認依頼メールから確認用URLを取り出し、更にアクセスするような高度な対応は spam ボットには到底不可能ですから、spam の投稿は十分に抑止されると期待されます。

ただ、対象のメーリングリストの性質によっては、より厳しいアクセス制御が必要になる場合も考えられます。その場合は、以下の設定を検討して下さい。

### reCAPTCHA によるアクセス制御

確認用URL（基準URL+`/confirm/`+ランダム文字列）にアクセスしているクライアントが人間であることを、reCAPTCHA によって確認します。この機能を利用する場合は、https://www.google.com/recaptcha/intro/android.html から reCAPTCHA 用のサイト鍵と秘密鍵を取得して、以下のように設定ファイルに記入して下さい。

```ini:mmpostauth.ini
[testlist@lists.example.net]
password = 司会用パスワード
sitekey = Googleから取得したサイト鍵
secretkey = Googleから取得した秘密鍵
```

なお、default セクションで設定すると、全てのメーリングリストに対して reCAPTCHA が有効になります。

### ウェブサーバ上の認証・認可モジュールによるアクセス制御

Shibboleth 認証や LDAP 認証、Basic 認証など、上位ウェブサーバ上で動作する各種の認証・認可モジュールを用いてアクセス制御することが可能です。最も簡単な方法は、確認用CGI全体を、認証・認可モジュールの管理下に置くことです。

```config:.htaccess
Satisfy Any
SetEnvIf Remote_Addr "^192\.168\." trusted_network

AuthType Shibboleth
ShibRequestSetting requireSession 1
Require valid-user

<Files action>
    SetHandler fcgid-script
    Order Deny,Allow
    Deny from all
    Allow from env=trusted_network
</Files>
```
この設定例は、以下のように振る舞います。

 * 確認用CGIに対するアクセスは、基本的に拒否する。
 * ただし、Shibboleth モジュールによって認証されている場合は、アクセスを許可する。
 * または、信頼できるネットワークからは、アクセスを許可する。

しかし、アクセス制御の必要水準が異なるメーリングリストが混在している場合には、この設定例では対応できません。

アクセス制御の必要水準が異なるメーリングリストが混在している場合には、ログイン用URLのみを、認証・認可モジュールの管理下に置くという設定を行います。この機能を利用する場合は、まず以下のように設定ファイルで `login` オプションを設定して下さい。

```ini:mmpostauth.ini
[testlist@lists.example.net]
password = 司会用パスワード
login = 1
```

このように設定すると、確認用URL（基準URL+`/confirm/`+ランダム文字列）に対するアクセスは一旦、ログイン用URL（基準URL+`/login/`+ランダム文字列）にリダイレクトされるようになります。

その上で、ログイン用URL（基準URL+`/login/`+ランダム文字列）に対して、ウェブサーバ側で認証・認可を行うように設定します。

```config:.htaccess
Satisfy Any
SetEnvIf Request_URI "/action/login/" login_action

AuthType Shibboleth
ShibRequestSetting requireSession 1
Require valid-user

SetEnvIf Request_URI "/action/post$" post_action
SetEnvIf Remote_Addr "^192\.168\." !post_action

<Files action>
    SetHandler fcgid-script
    Order Allow,Deny
    Allow from all
    Deny from env=login_action
    Deny from env=post_action
</Files>
```

この設定例は、以下のように振る舞います。

 * ログイン用URL（基準URL+`/login/`+ランダム文字列）に対するアクセスは、基本的に拒否する。
 * ただし、Shibboleth モジュールによって認証されている場合は、アクセスを許可する。
 * 司会依頼メッセージのアップロード用URL（基準URL+`/post`）に対するアクセスは、信頼できるネットワークのみ許可する。

ログイン用URL（基準URL+`/login/`+ランダム文字列）にアクセスがあると、確認用CGIは、環境変数 `REMOTE_USER` の値をチェックして、確認用URL（基準URL+`/confirm/`+ランダム文字列）に更にリダイレクトします。環境変数 `REMOTE_USER` が空の場合には、認証・認可されていないものと見なして、403 エラーページを返します。

## 文面の調整

実際の運用にあたっては、以下の文面を調整する必要があるでしょう。

 * `notify.tmpl`: 確認依頼メール
 * `confirm.tmpl`: 確認用ウェブページ
 * `approve.tmpl`: 確認・送信承認後の結果表示ウェブページ
 * `revoke.tmpl`: 確認・送信取消後の結果表示ウェブページ

## 確認用URLにアクセスした投稿者のメールアドレスのキャッシュ

確認用URLにアクセスした投稿者のメールアドレスは、データベースに保存され、30日間は確認なしにメールが配送されるようになります。この挙動を変更したい場合は、以下のように設定ファイルで `senderperiod` オプションを設定して下さい。値を0にすると「キャッシュせずに、毎回確認する」設定になります。

```ini:mmpostauth.ini
[default]
senderperiod = 0
```

## 古い司会依頼メールの破棄

30日以上経過した司会依頼メールについては、自動的に破棄する設定になっています。この挙動を変更したい場合は、以下のように設定ファイルで `expire` オプションを設定して下さい。値を0にすると「破棄しない」設定になります。

```ini:mmpostauth.ini
[default]
expire = 0
```