ククログ

株式会社クリアコード > ククログ > メールフィルタープラグインであるmilterをPythonで簡単実装! - milterを動かしてみよう編

メールフィルタープラグインであるmilterをPythonで簡単実装! - milterを動かしてみよう編

前回と前々回の記事では、milter managerというメールフィルタを管理するための自由ソフトウェアを、GObject Introspectionに対応させてバインディングを生成することについて紹介しました。

milter managerは従来からRubyでmilterを作るためのライブラリーを提供してきましたが、今回のGObject Introspection対応によって、Pythonでmilterを作るためのライブラリーも提供するようになりました。

今回は、milter managerの機能を使ってPythonで書いてみます。 また、書いたmilterを実際に動かす方法も紹介します。

milter

milterとは、SendmailやPostfixなどのMTA(Mail Transfer Agent)に組み込むメールフィルタープラグインのことです。 が、ThunderbirdやOutlookなら分かるけれど、SendmailとかPostfixとかMTAとか聞いたことがない!という方もいらっしゃるかもしれません。 まずは、メールが届く仕組みから簡単に解説します。

メールが届く仕組み

ThunderbirdやOutlookなどのメールクライアントでメールを作成し、送信するケースを考えてみます。 このように送信されたメールは、送信先に直接届くのではなく、幾つかのメールサーバーを経由して届きます。

まずメールクライアントは近くの(メールクライアントで設定をした)メールサーバーにメールを送信します。 するとメールサーバーが、メールの宛先ドメインに応じた別のメールサーバーへとメールを転送します。 こうしてメールの宛先ドメインであるメールサーバーに届けられたメールは、そのサーバー内で保管されます。 最終的に、受信側のメールクライアントがそのメールサーバーに問い合わせることで、そのメールが届きます。

ユーザーが直接操作をする、メールの送信/受信を行うメールクライアント(メーラー)のことを、MUA(Mail User Agent)と呼びます。 代表的なMUAとして、ThunderbirdやOutlookなどがあります。

一方でメールサーバーにおいて、メールの転送を担う機能、もしくはそのソフトウェアのことをMTA(Mail Transfer Agent)と呼びます。 代表的なMTAとして、SendmailやPostfixなどがあります。 普段ユーザーが意識することはないかもしれませんが、MUAにSMTPサーバーとして登録するのがMTAです。 MTAは、MUAやMTAから受信したメールを、別のMTAに転送します。 自身が宛先である、すなわちゴールである場合は、転送せずに自身のサーバー内でメールを保管します1

MTAは、宛先のドメインをDNSに問い合わせることで、転送先のMTAを特定します2。 最終的な宛先であるMTAに届くまでに、複数のMTAを経由することもあります。 最終的な宛先である「To」に対して、転送時など実際に送信する先のことを「Envelope To」と呼んで区別します3。 「From」も同様に区別します。 例えば複数のMTAを経由する場合は、「To」と「Envelope To」が異なることがあります。

milterとmilter manager

milterとは「mail filter」の略で、SendmailやPostfixといったMTAのメールフィルタプラグイン、またはその仕組み、プロトコルのことです。 MTAにおいて迷惑メールフィルタやウィルスチェックをするためにmilterを利用できます。 元々はSendmailの仕組みでしたが、Postfixもサポートするようになった経緯があります。 SendmailやPostfixのように、milterをサポートしているMTAであれば、共通のmilterを利用できます。

前回と前々回の記事では、milter managerGObject Introspectionに対応させた事例を紹介してきました。 milter managerとは、milterを効果的に利用するためのmilterであり、クリアコードが開発した自由ソフトウェアです。 milter managerは、MTAとmilterとの間のプロキシとして動作し、柔軟にmilterを適用できます。 またmilter managerは、milterをRuby/Pythonで簡単に作るためのライブラリーも提供しています(前回と前々回の記事で、Pythonをサポートしました)。

milter managerのwebページに、milterとmilter managerについて分かりやすい説明があるので、ぜひご覧ください。

また、milterプロトコルについてはこちらの記事で説明しています。

Pythonで作ったmilterを動かす

実際にmilterを作って動かしてみましょう。

Postfixの用意

milterは、milterプロトコルをサポートしているMTAのプラグインとして機能します。 まずは、MTAとしてPostfixを動かしてみましょう。 以下、Ubuntu 20.04環境の例になります。

Postfixのインストール

$ sudo apt install postfix

インストール中に、Postfix Configurationの設定画面が出てくるので、以下のように設定します。

  • configuration typeは、Local onlyを選択します。
    • 今回はローカルで動かすだけだからです。
  • mail nameはデフォルトで問題ありません。

メールを送信してみる

Postfixにメールを送信してみましょう。 メールを送る方法は色々ありますが、今回はSMTPプロトコルに従ったメッセージを直接送信することで、メールを送信してみます。

SMTPプロトコルは、メールを送信するのに用いるプロトコルです。 MUAからMTAへのメール送信や、MTAからMTAへのメール転送に使われます。 このプロトコルに従ってメッセージを送信することで、メールを送信したことになります。

クライアント側のSMTPプロトコルの簡単な使い方は、次のようになります。

  1. EHLO {hostname}を送信して、自身のhostnameを伝える。
  2. MAIL FROM: {Envelope From}を送信して、メールのEnvelope Fromを伝える。
  3. RCPT TO: {Envelope To}を送信して、メールのEnvelope Toを伝える。
  4. DATAを送信して、実際のメール内容の送信を開始することを伝える。
  5. タイトルやFrom/Toなど、メールのヘッダーを1行ずつ送信する。
  6. ヘッダーを全て送信し終えたら、空行(改行)を送信し、ヘッダーの送信を完了したことを伝える。
  7. メールの本文を送信する。
  8. 本文を全て送信し終えたら、.のみの行を送信し、メールの送信を全て完了したことを伝える4

例えば、タイトルも本文もHello, world!であるメールを自身に送る場合、次のようなメッセージになります。 $USERは現在のユーザー名を表す環境変数です。

EHLO localhost
MAIL FROM: $USER@localhost
RCPT TO: $USER@localhost
DATA
Subject: Hello, world!
From: $USER@localhost
To: $USER@localhost

Hello, world!
.

実際にメールを送信してみましょう。 例えばechoncコマンドを使って次のように送信できます。

$ {
    echo "EHLO localhost"
    echo "MAIL FROM: $USER@localhost"
    echo "RCPT TO: $USER@localhost"
    echo "DATA"
    echo "Subject: Hello, world!"
    echo "From: $USER@localhost"
    echo "To: $USER@localhost"
    echo
    echo "Hello, world!"
    echo "."
} | nc -q 1 127.0.0.1 25

メールを確認してみましょう。 /var/mail/${USER}に記録されているはずです。

$ cat /var/mail/${USER}
From root@localhost  Thu Jan 12 09:13:07 2023
Return-Path: <root@localhost>
X-Original-To: root@localhost
Delivered-To: root@localhost
Received: from localhost (localhost [127.0.0.1])
    by tmp2.lxd (Postfix) with ESMTP id E86341046D
    for <root@localhost>; Thu, 12 Jan 2023 09:13:06 +0000 (UTC)
Subject: Hello, world!
From: root@localhost
To: root@localhost
Message-Id: <20230112091306.E86341046D@tmp2.lxd>
Date: Thu, 12 Jan 2023 09:13:06 +0000 (UTC)

Hello, world!

milterを動かしてみる

さて、いよいよmilterを動かして、送信したメールにフィルタリング処理をさせてみましょう。

一度話を戻すと、クリアコードが開発したmilter managerは、その1つの機能として、Rubyで簡単にmilterを作るためのライブラリーを提供していました。 今回それをGObject Introspectionに対応させたことで、Pythonでもmilterを作ることができるようになりました。

Rubyによるmilter開発のチュートリアルがあります。Pythonも基本的に同じですので、こちらもぜひご覧ください。

今回は、Pythonでmilterを作って動かしてみましょう。 2つの実装例がmilter manager v2.2.5に梱包されています。

まずは、特定のワードを置換する例を参考にして、 メール中のワードを置換させてみましょう。 最後のpatternsの定義部分を少し修正して、worldClearCodeに置換するようにしてみます。 また最初のsys.pathの更新処理は、今回は必要ないので消してあります。

#!/usr/bin/env python3
#
# Copyright (C) 2022  Sutou Kouhei <kou@clear-code.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import re

import milter.client

class MilterReplace(milter.client.Session):
    def __init__(self, context, patterns):
        super().__init__(context)
        self._patterns = patterns

    def header(self, name, value):
        self._headers.append([name, value])

    def body(self, chunk):
        self._body += chunk

    def end_of_message(self):
        header_indexes = {}
        for name, value in self._headers:
            if name not in header_indexes:
                header_indexes[name] = 0
            header_indexes[name] += 1
            for pattern, replaced in self._patterns.items():
                replaced_value, _ = pattern.subn(replaced, value)
                if value != replaced_value:
                    self._change_header(name,
                                        header_indexes[name],
                                        replaced_value)
                    break

        for pattern, replaced in self._patterns.items():
            body = self._body.decode("utf-8")
            replaced_body, _ = pattern.subn(replaced, body)
            if body != replaced_body:
                self._replace_body(replaced_body)

    def reset(self):
        self._headers = []
        self._body = b""

command_line = milter.client.CommandLine()
with command_line.run() as (client, options):
    # "world" を "ClearCode" に置換する!
    patterns = {
        re.compile("world", re.IGNORECASE): "ClearCode",
    }
    client.register(MilterReplace, patterns)

このPythonコードを、milter_replace.pyファイルとして保存します。

さて、このmilterを実行するには、milter managerが提供しているライブラリーが必要です。 以下のようにインストールします。

$ sudo add-apt-repository ppa:milter-manager/ppa
$ sudo apt install python3-milter-core python3-milter-client

以上で準備はできました。milterを実行してみます。 --verboseオプションを付けることで、詳細なログを出力してくれます。

$ python3 milter_replace.py --verbose

すると、milterが動作している状態になります。 デフォルトでは、20025番ポートで処理を待ち受けています。 Postfixにこのmilterを登録します。

smtpd_milters = inet:127.0.0.1:20025の設定を追加します。

$ echo "smtpd_milters = inet:127.0.0.1:20025" | sudo tee -a /etc/postfix/main.cf
$ sudo systemctl restart postfix

以上でmilterをPostfixに登録できました。 先ほどと同様にメールを送信して動作を確認してみましょう。 milterが多くのログを出力し、何やら動作したのが分かるはずです。 メールを確認してみましょう。

$ cat /var/mail/${USER}
From root@localhost  Thu Jan 12 09:13:07 2023
Return-Path: <root@localhost>
X-Original-To: root@localhost
Delivered-To: root@localhost
Received: from localhost (localhost [127.0.0.1])
    by tmp2.lxd (Postfix) with ESMTP id E86341046D
    for <root@localhost>; Thu, 12 Jan 2023 09:13:06 +0000 (UTC)
Subject: Hello, world!
From: root@localhost
To: root@localhost
Message-Id: <20230112091306.E86341046D@tmp2.lxd>
Date: Thu, 12 Jan 2023 09:13:06 +0000 (UTC)

Hello, world!

From root@localhost  Thu Jan 12 15:29:10 2023
Return-Path: <root@localhost>
X-Original-To: root@localhost
Delivered-To: root@localhost
Received: from localhost (localhost [127.0.0.1])
    by tmp2.lxd (Postfix) with ESMTP id 11E2010D21
    for <root@localhost>; Thu, 12 Jan 2023 15:29:10 +0000 (UTC)
Subject: Hello, ClearCode!
From: root@localhost
To: root@localhost
Message-Id: <20230112152910.11E2010D21@tmp2.lxd>
Date: Thu, 12 Jan 2023 15:29:10 +0000 (UTC)

Hello, ClearCode!

前回のメールに続き、新しくメールが記録されています。 新しいメールでは、タイトル(Subject)と本文の双方で、Hello, world!Hello, ClearCode!に置換されています!

以上で、Pythonで作ったmilterを動かすことができました。 milterを使うことで、MTAにおいてメールの内容を加工したり、条件によってメールをブロックしたりすることができます。 そして、milter managerが提供するライブラリーを使うことで、PythonやRubyで簡単にmilterを開発することができるのです。

まとめ

前回と前々回の記事では、milter managerというメールフィルタを管理するための自由ソフトウェアを、GObject Introspectionに対応させてバインディングを生成することについて紹介しました。

本記事ではこれらに続いて、実際にPythonで書かれたmilterを動かしてみることについて紹介しました。 milter managerを使えば、PostfixやSendmailなどのMTAにおけるメールのフィルタリングをこんなに便利にできるんだ、と実感していただけたら幸いです。

次回は、milterの実装の仕方についてより詳しく紹介をする予定です。

クリアコードではmilter managerを始め、様々な自由ソフトウェアの開発・サポートを行っております。 詳しくは次をご覧いただき、こちらのお問い合わせフォームよりお気軽にお問い合わせください。

また、クリアコードではこのように業務の成果を公開することを重視しています。 業務の成果を公開する職場で働きたい人はクリアコードの採用情報をぜひご覧ください。

  1. 受信したメールを各ユーザーのメールボックスに配信する機能はMTAとは別にMDA(Mail Delivery Agent)と呼ばれます。MUAからのリクエストに応じて受信したメールを返す機能はMRA(Mail Retrieval Agent)と呼ばれます。POPサーバーやIMAPサーバーと呼ばれるソフトウェアのことです。

  2. MXレコードを引くことでMTAのホスト名を取得し、さらにそのAレコードまたはAAAAレコードを引くことでIPアドレスを取得します。MTAの候補が複数あることもあります。その場合は、MXレコードの優先度に応じて順に試行します。

  3. メールを配送するために封筒(envelope)に包んでいるイメージです。メール自体のFrom/Toと封筒のFrom/Toがある、と考えるとイメージしやすいかもしれません。

  4. この後に再びMAIL FROMから繰り返して、次のメールの送信処理を続けることもできます。