![]() |
![]() |
![]() |
![]() |
milter managerが提供するライブラリを用いてRubyでmilterを開発 する方法を説明します。
milterプロトコルの説明は milter.org の 開発者向けドキュメント を参照してください。
Rubyでmilterを開発する場合はconfigure時に--enable-ruby-milterオ プションを指定します。Debian GNU/Linux、Ubuntu、CentOS用のパッ ケージでは専用のパッケージがあるのでそれをインストールします。
Debian GNU/Linux、Ubuntuの場合:
% sudo aptitude -V -D -y install ruby-milter-core ruby-milter-client ruby-milter-server
CentOSの場合:
% sudo yum install -y ruby-milter-core ruby-milter-client ruby-milter-server
パッケージがない環境では以下のように configureに--enable-ruby-milterオプションを指定してください。
% ./configure --enable-ruby-milter
インストールが成功しているかは以下のコマンドで確認できます。
% ruby -r milter -e 'p Milter::VERSION' [1, 8, 0]
バージョン情報が出力されればインストールは成功しています。
Rubyで開発したmilterは以下のようになります。
require 'milter/client' class Session < Milter::ClientSession def initialize(context) super(context) # 初期化処理 end def connect(host, address) # ... end # その他のコールバック定義 end command_line = Milter::Client::CommandLine.new command_line.run do |client, _options| client.register(Session) end
それでは、指定された正規表現を含むメールを拒否するmilterを作っ てみましょう。
イベントが発生する毎にmilterのコールバックメソッドが呼び出さ れます。ほとんどのイベントには付加情報があります。イベントの 付加情報の受け渡し方法は2種類あります。1つはコールバックの引 数として渡される方法で、もう1つはマクロとして渡される方法です。 マクロについては後述します。ここではコールバックの引数として 渡される情報についてだけ説明します。
以下がコールバックメソッドとその引数の一覧です。一覧を見た後 に、今回のmilterで必要なコールバックを選びます。
SMTPクライアントがSMTPサーバに接続したときに呼ばれます。
例えば、localhostから接続した場合は以下のようになります。 |
|
SMTPクライアントがHELOまたはEHLOコマンドを送ったときに呼 ばれます。
例えば、「EHLO mail.example.com」とした場合は以下のように なります。 |
|
SMTPクライアントがMAILコマンドを送ったときに呼ばれま す。
例えば、「MAIL FROM: <user@example.com>」とした場合は以下 のようになります。 |
|
SMTPクライアントがRCPTコマンドを送ったときに呼ばれます。 複数回RCPTコマンドを送った場合は複数回呼ばれます。
例えば、「RCPT TO: <user@example.com>」とした場合は以下 のようになります。 |
|
SMTPクライアントがDATAコマンドを送ったときに呼ばれます。 |
|
送信するメールの中にあるヘッダーの数だけ呼ばれます。
例えば、「Subject: Hello!」というヘッダーがあった場合は以 下のようになります。 |
|
送信するメールのヘッダー部分が終わったら呼ばれます。 |
|
送信するメールの本文が送られてきたら呼ばれます。本文が小 さいときは1回だけ呼ばれますが、大きい場合はいくつかの塊に 分割されて複数回呼ばれます。
例えば、本文が「Hi!」だけであれば、1回だけ呼ばれて、以下 のような値になります。 |
|
SMTPクライアントがデータ終了を表す「<CR><LF>.<CR><LF>」を 送ったときに呼ばれます。 |
|
SMTPのトランザクションがリセットされたときに呼ばれます。 具体的にはend_of_messageの後や、SMTPコマンドのRSETが送られたときです。
|
|
milterプロトコルで定義されていないコマンドが与えられたときに 呼ばれます。
|
|
initializeのときとメールトランザクションが終了したときに呼ばれます。 メールトランザクション が終了するのは以下のときです。
|
|
milterプロトコルの処理が完了したときに呼ばれます。 TODO: 呼ばれるタイミングについて書く |
今回作るmilterは指定された正規表現を含むメールを拒否するmilter です。正規表現はSubjectまたはメッセージ本文にマッチさせるこ とにします。とすると、必要になるコールバックはヘッダー毎に呼 び出されるheaderとメッセージ本文毎に呼び出されるbodyです。雛 形は以下のようになります。
require 'milter/client' class MilterRegexp < Milter::ClientSession def initialize(context, regexp) super(context) @regexp = regexp end def header(name, value) # ... Subjectをチェック end def body(chunk) # chunkをチェック end end command_line = Milter::Client::CommandLine.new command_line.run do |client, _options| # バイアグラを含むメールを拒否 client.register(MilterRegexp, /viagra/i) end
まず、Subjectをチェックしましょう。
class MilterRegexp < Milter::ClientSession # ... def header(name, value) case name when /\ASubject\z/i if @regexp =~ value reject end end end # ... end
ヘッダー名(name)がSubjectのときに、ヘッダーの値(value)が 指定された正規表現(@regexp)にマッチしていれば拒否 (reject)しています。自然に書けていますね。
それでは、実際に動かして試してみましょう。
現在は、以下のようになっているはずです。
require 'milter/client' class MilterRegexp < Milter::ClientSession def initialize(context, regexp) super(context) @regexp = regexp end def header(name, value) case name when /\ASubject\z/i if @regexp =~ value reject end end end def body(chunk) # chunkをチェック end end command_line = Milter::Client::CommandLine.new command_line.run do |client, _options| # バイアグラを含むメールを拒否 client.register(MilterRegexp, /viagra/i) end
この状態ですでにmilterとして実行可能です。milter-regexp.rbと いうファイル名で保存した場合、以下のように実行します。-vオプ ションは詳細なログを出力するためのオプションで、動作を確認し やすいようにつけています。
% ruby milter-regexp.rb -v
milterはデフォルトではフォアグラウンドで動作します。別の端末 からアクセスして動作を確認しましょう。
milterのテストには milter-test-server が便利です。Rubyで 実装されたmilterはデフォルトで「inet:20025@localhost」で起動 するので、そのアドレスに接続します。
% milter-test-server -s inet:20025 status: pass elapsed-time: 0.00254348 seconds
正常に接続できた場合は以上のように「status: pass」と表示され ます。milterを起動している端末も確認してみましょう。以下のよ うに表示されているはずです。
[2010-08-01T05:44:34.157419Z]: [client][accept] 10:inet:55651@127.0.0.1 [2010-08-01T05:44:34.157748Z]: [1] [client][start] [2010-08-01T05:44:34.157812Z]: [1] [reader][watch] 4 [2010-08-01T05:44:34.157839Z]: [1] [writer][watch] 5 [2010-08-01T05:44:34.158050Z]: [1] [reader] reading from io channel... [2010-08-01T05:44:34.158140Z]: [1] [command-decoder][negotiate] [2010-08-01T05:44:34.158485Z]: [1] [client][reply][negotiate] #<MilterOption version=<6> action=<add-headers|change-body|add-envelope-recipient|delete-envelope-recipient|change-headers|quarantine|change-envelope-from|add-envelope-recipient-with-parameters|set-symbol-list> step=<no-connect|no-helo|no-envelope-from|no-envelope-recipient|no-end-of-header|no-unknown|no-data|skip|envelope-recipient-rejected>> [2010-08-01T05:44:34.158605Z]: [1] [client][reply][negotiate][continue] [2010-08-01T05:44:34.158895Z]: [1] [reader] reading from io channel... [2010-08-01T05:44:34.158970Z]: [1] [command-decoder][header] <From>=<<kou+send@example.com>> [2010-08-01T05:44:34.159092Z]: [1] [client][reply][header][continue] [2010-08-01T05:44:34.159207Z]: [1] [reader] reading from io channel... [2010-08-01T05:44:34.159269Z]: [1] [command-decoder][header] <To>=<<kou+receive@example.com>> [2010-08-01T05:44:34.159373Z]: [1] [client][reply][header][continue] [2010-08-01T05:44:34.159485Z]: [1] [reader] reading from io channel... [2010-08-01T05:44:34.159544Z]: [1] [command-decoder][body] <71> [2010-08-01T05:44:34.159656Z]: [1] [client][reply][body][continue] [2010-08-01T05:44:34.159774Z]: [1] [reader] reading from io channel... [2010-08-01T05:44:34.159842Z]: [1] [command-decoder][define-macro] <E> [2010-08-01T05:44:34.159882Z]: [1] [command-decoder][end-of-message] <0> [2010-08-01T05:44:34.159941Z]: [1] [client][reply][end-of-message][continue] [2010-08-01T05:44:34.160034Z]: [1] [command-decoder][quit] [2010-08-01T05:44:34.160081Z]: [1] [agent][shutdown] [2010-08-01T05:44:34.160118Z]: [1] [agent][shutdown][reader] [2010-08-01T05:44:34.160162Z]: [1] [reader][eof] [2010-08-01T05:44:34.160199Z]: [1] [reader] shutdown requested. [2010-08-01T05:44:34.160231Z]: [1] [reader] removing reader watcher. [2010-08-01T05:44:34.160299Z]: [1] [writer][shutdown] [2010-08-01T05:44:34.160393Z]: [0] [reader][dispose] [2010-08-01T05:44:34.160452Z]: [client][finisher][run] [2010-08-01T05:44:34.160492Z]: [1] [client][finish] [2010-08-01T05:44:34.160536Z]: [1] [client][rest] [] [2010-08-01T05:44:34.160578Z]: [sessions][finished] 1(+1) 0
何も出力されていない場合はそもそもmilterに接続できていません。 milterが起動しているか、milter-test-serverに正しいアドレスを 指定しているかを確認してください。
それでは、Subjectに「viagra」と含んだメールの場合の動作を確 認しましょう。「--header 'Subject:Buy viagra!!!'」というオプ ションを指定することでそのようなメールの動作を再現します。
% milter-test-server -s inet:20025 --header 'Subject:Buy viagra!!!' status: reject elapsed-time: 0.00144477 seconds
「status: reject」とでているので、期待通り拒否していることが 確認できます。
milterの端末の方にも以下のようなログがでているはずです。
... [2010-08-01T05:49:49.275257Z]: [2] [command-decoder][header] <Subject>=<Buy viagra!!!> [2010-08-01T05:49:49.275405Z]: [2] [client][reply][header][reject] ...
Subjectヘッダーのときにrejectしていることがわかります。
MTAなしでmilterをテストできるコマンドや詳細なログ出力など、 milter managerはmilterの開発に便利なツール・ライブラリを提供 しています。
次にメッセージ本体をチェックしましょう。
class MilterRegexp < Milter::ClientSession def body(chunk) if @regexp =~ chunk reject end end end
メッセージ本文の一部(chunk)が指定された正規表現(@regexp) にマッチしていれば拒否(reject)しています。こちらも自然に書 けていますね。
試してみましょう。milter-test-serverは「--body」オプションで メッセージ本文を指定できます。
% tool/milter-test-server -s inet:20025 --body 'Buy viagra!!!' status: reject elapsed-time: 0.00195496 seconds
「status: reject」となっているので、期待通り動作しています。
このmilterは説明のために簡略化されているため、いくつか問題点 があります。例えば、以下のようなメールに対しては期待通り動き ません。
ヘッダーの値がMIMEエンコードされている場合。例えば、 「=?ISO-2022-JP?B?GyRCJVAlJCUiJTAlaRsoQnZpYWdyYQ==?=」は デコードすると「バイアグラviagra」になるが、この場合は正 規表現にマッチしないため、拒否しない。
メッセージ本体で単語が複数のチャンクにまたがった場合。 例えば、1つめのチャンクで「via」がきて2つめのチャンク で「gra」がきた場合は正規表現にマッチしないため、拒否 しない。
ヘッダーの値に関しては以下のようにNKFなどを使ってMIMEエンコー ドをデコードすれば解決できます。
require 'nkf' class MilterRegexp < Milter::ClientSession # ... def header(name, value) case name when /\ASubject\z/i if @regexp =~ NKF.nkf("-w", value) reject end end end # ... end
メッセージ本体に関しては、メッセージ本文を全部受信した後にも チェックする方法があります。
class MilterRegexp < Milter::ClientSession ... def initialize(context, regexp) super(context) @regexp = regexp @body = "" end def body(chunk) if @regexp =~ chunk reject end @body << chunk end def end_of_mesasge if @regexp =~ @body reject end end ... end
複数のチャンクにわかれた状態をテストするためには以下のように 複数回「--body」オプションを指定します。
% milter-test-server -s inet:20025 --body 'Buy via' --body 'gra!!!' status: reject elapsed-time: 0.00379063 seconds
このように複数のチャンクにわかれてしまった場合でも期待通りに 動きます。
ただし、これではすべてのメッセージをメモリ上に置いてしまうな ど、効率の問題があります。また、メッセージ本文がBASE64でエン コードされている場合も動作しないという問題があります。これら は、ストリームとして処理したり、Content-Typeヘッダーの値など を確認した上でメッセージ本文を処理したりする必要があります。
メールを解析するライブラリとして Mail があるので、それ を使うとよいでしょう。