ja ja ja ja ja ja ja ja ja en ja ja ja ja ja ja ja ja ja ja ja en ja ja ja zh ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja es en en en ja en ja ja en ja en ja ja en ja ja en ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja en ja ja ja ja ja ja ja en ja ja ja en ja ja en ja ja ja ja ja ja ja ja ja pl en ja ja ja ja ja ja ja ja en ja ja en ja ja vi ja sv eu ja en en hu ja ja en en pl ja ja jbo hbs_lat en ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja
LDD '10 Winter: メールフィルタの作り方 - Rubyで作るmilter - 2010-02-15 - ククログ
先日、LOCAL DEVELOPER DAY '10 WinterでRubyでmilterを作る方法について話してきました。どのタイミングでどのmilterプロトコルのコマンドが発行されるかについても説明しているので、Rubyではなく(libmilterを使って)Cでmilterを実装する場合にも参考になる部分があるはずです。むしろ、Rubyとmilterの組み合わせについて話している部分は薄めです。これは、Rubyそのものとmilterの仕組みを理解していればRubyとmilterを組み合わせることは容易だからです。
まず、メールフィルタそのものを「milter」と呼びます。
メールフィルタとMTAは別プロセスで動作するため、プロセス間通信でフィルタ対象のメールやフィルタ結果などをやりとりする必要があります。そのやりとりのきまりを「milterプロトコル」と呼びます。
そして、「milter」と「milterプロトコル」をサポートしたMTAを含んだメールフィルタの仕組み全体を「milterシステム」と呼びます。
「milter」といった場合は「メールフィルタそのもの(ここでいうmilter)」という意味で使う場合と、「メールフィルタの仕組み(ここでいうmilterシステム)」という意味で使われる場合が多いです。「milter」という単語が使われている場合はどちらの意味かを判断できるようになってください。
milterプロトコルはSMTPと密接に関連したプロトコルです。そのため、milterプロトコルについて説明する前に、SMTPについて確認します。
SMTPは以下の4つのコマンドが基本となるシンプルなプロトコルです。
まず、「HELO」で接続したSMTPクライアントの情報を伝えます。以下の例ではSMTPサーバ(MTA)からのメッセージは先頭に「 」をつけて示します。
% telnet localhost smtp   HELO localhost.example.com < 250 note-pc.example.com
挨拶が済んだらSMTPセッションのスタートです。1つのSMTPのセッションで複数のメールを送ることができます。「MAIL FROM」、「RCPT TO」、「DATA」で1つのメールを送ります。
まず、「MAIL FROM」で送信者を伝えます。
> MAIL FROM:   < 250 2.1.0 Ok
次に、「RCPT TO」で宛先を伝えます。
> RCPT TO:   < 250 2.1.5 Ok
同じメールを複数の宛先に送ることもできます。その場合は「RCPT TO」を複数回実行します。
最後に「DATA」でメールの内容を伝えます。メールの最後は「.」だけの行になります。
> DATA   .   > Subject: Hello > From:   > To:   > > This is a test mail! > . < 250 2.0.0 Ok: queued as 054C624FB
これで、1通のメールを送信できました。続けてメールを送信する場合はまた「MAIL FROM」から始めます。
SMTPセッションを終了する場合は「QUIT」です。
> QUIT < 221 2.0.0 Bye
これで1つのSMTPセッションが終了しました。
milterプロトコルはSMTPと密接に関わっています。それでは、milterプロトコルの詳細を説明します。
SMTPとmilterプロトコル
milterプロトコルにもSMTPと同じようにコマンドがあります。そして、そのコマンドはSMTPのコマンドと対応したものになっています。まずSMTPのコマンドを説明したのはそのためです。
例えば、SMTPで「HELO」というコマンドが実行された場合、「HELO」に対応する「helo」というmilterプロトコルのコマンドが発行されます。このとき、SMTPクライアントが指定したHELOコマンドの引数がmilterに渡されます。
MTAはmilterにコマンドを送った後、milterからの返答があるまでSMTPクライアントには返答しません。つまり、milterが「helo」でrejectを返すことで、SMTPクライアントの「HELO」コマンドへの返答をrejectとすることができます。これにより、MTAがSMTPレベルでできることとほとんど同じことをmilterで実現できます。
milterプロトコルのコマンドとSMTPのコマンドの対応
mitlerプロトコルのコマンドはほとんどSMTPのコマンドに対応していますが、milterプロトコルのコマンドの方がより細かくなっています。例と一緒にコマンドの対応を説明します。
SMTPでの最初のコマンドは「HELO」ですが、milterプロトコルでは「helo」よりも前にコマンドが発行されます。それが、SMTPクライアントがSMTPサーバに接続したときに発行される「connect」コマンドです。
「connect」コマンド以外はSMTPのコマンドとmilterプロトコルのコマンドは1対1で対応します。「envfrom」の「env」は「envelope」の略で、「封筒」という意味です。「envfrom」で「差出人」という意味、「envrcpt」で「宛先」という意味です。「rcpt」は「recipient」の略で「受信者」という意味です。
SMTPでは1つのメールを複数の宛先に送信できます。この場合、複数回「RCPT TO」を指定します。STMPで複数回「RCPT TO」が指定されるので、milterプロトコルでも「envrcpt」コマンドが複数回発行されます。
SMTPの「DATA」コマンドはmilterプロトコルではより細かいコマンドに分解されています。
まず、「DATA」コマンド時にはmilterプロトコルの「data」コマンドがすぐに発行されます1。その後、SMTPクライアントはメール本体を送信しますが、「header」などのイベントはすぐには発生しません。SMTPクライアントがデータの終了を示す「.」のみの行を入力するまでは何も起きません。「.」のみの行が入力されると、MTA側でメール本文をパースして「header」、「eoh」(end of header: ヘッダーの終わり)、「body」、「eom」(end of message: メッセージの終わり)コマンドを発行します。もちろん、ヘッダーもパースしてあるので、MTAは「ヘッダー名」と「ヘッダー値」と分解した状態で情報を渡します。
このようにmilterプロトコルはSMTPと密接に関わっています。milterプロトコルのコマンドがわかれば、自分が必要な機能を持つmilterを実現するためにはどのコマンドを利用すればよいかを考えることができるでしょう。
milterサンプル: メール検索
説明用のサンプルとしてメール検索を実現するmilterを作成します。今回はSubject、From、Toと本文のみを扱うことにします。
メール検索を実現するために、全文検索エンジンとしてgroongaを、milterライブラリとしてmilter managerのRubyバインディングを使います。
groonga: カラム指向データストア
groongaは全文検索のためのインデックス作成機能だけではなく、データストアの機能も持っています。groongaのデータストアは列指向データベースマネジメントシステムで、関係データベース管理システムとは違い、レコード(行)毎にデータをまとめて持つのではなく、カラム(列)毎にデータをまとめて持っています。
このようにデータを持つと、同じカラムの複数の値へのアクセスを高速に行うことができます。このため、カラムの値を使った集計処理を高速に実行できます。集計処理とは、例えば、SQLでいうGROUP BYのような処理です。
集計処理を用いると絞り込み検索をしやすいユーザーインターフェイスを提供することができます。例えば、ショッピングサイトで商品に複数のタグがついているとします。このとき、同じタグがついている商品が何項目あるかを表示してリンクにします。1つも商品が属していないタグは表示しないようにすれば、ユーザは無駄な絞り込み操作を行わずにすみます。
全商品(123件) タグ スポーツ(58件)← リンクにする 映画(45件) ← リンクにする 食べ物(36件) ← リンクにする 旅行(0件) ← 表示しない
この状態で「スポーツ」をクリックしたとします。
全商品(123件) > スポーツ(58件) タグ スポーツ ← 選択済みなので表示しない 映画(26件) ← リンクにする 食べ物(0件) ← 表示しない 旅行(0件) ← 表示しない
このように、絞り込んだ後にがっかりするような操作を示さないことにより、絞り込み検索をしやすいユーザインターフェイスを作ることができます。がっかりするような操作かどうかを判断するために、同じ値を持つレコードの個数を数える、といった集計処理をしています。
groonga: バイナリパトリシアトライ
groongaはキー管理のためのデータ構造としてハッシュテーブルとバイナリパトリシアトライを採用しています。バイナリパトリシアトライは基数木の一種です。
ここにB+木とパトリシアトライの説明を書く予定でしたが、もう、だいぶ長くなっているので省略します。また別の機会があれば紹介します。
パトリシアトライを利用すると効率よく最長一致検索を実現できます。これを試してみるためのサンプルアプリケーションを用意しました。
リンク先ではキーワードを変えて試すことができます。
最長一致機能を利用してキーワード検出している部分のソースは以下の通りです。
target_text = "..." keywords = request["keywords"].split words = Groonga::PatriciaTrie.create(:key_type => "ShortText", :key_normalize => true) keywords.each do |keyword| words.add(keyword) end tagged_text = words.tag_keys(target_text) do |record, word| " #{word} " end
まず、パトリシアトライを作り、キーワードを登録します。Groonga::PatriciaTrieにはtag_keysという便利メソッドがあり、これを使うと「最長一致検索」→「キーワードにタグ付け」をより簡潔に記述することができます。
全体のソースはリンク先にあるソース一式の中に含まれています。
groongaのRubyバインディングであるRuby/groongaはスキーマ定義のためのドメイン固有言語を提供しています。
メールを保存するMessagesテーブルにはsubject、from、to、bodyカラムを定義しています。今回は簡単のため、宛先は1つのみ扱うことにしています。
次に、高速に全文検索を行うために索引を作成します。Termsテーブルのキーに単語(ここではbigramを利用しているので1文字か2文字の文字列)、カラムにその単語が出現するMessagesレコードのID(とN-gramなので単語の出現位置)を保持します。
subjectカラムとbodyカラムでそれぞれに対して索引を作成しています。こうすることにより、「どこかに○○が含まれているメールを検索」といった検索だけではなく、「Subjectに○○が含まれているメールを検索」、「本文に○○が含まれているメールを検索」というような細かい検索ができるようになります。細かい検索が必要ない場合はMessagesテーブルに検索対象をすべて入れたカラムを1つ作り、そのカラムに対して索引を作成してもよいでしょう。
Groonga::Schema.define do |schema| schema.create_table("Messages") do |table| ... table.text("text") end schema.create_table("Terms", :type => :patricia_trie, :default_tokenizer => "TokenBigram", :key_normalize => true) do |table| table.index("text") end end messages = Groonga["Messages"] from = "kou@clear-code.com" to = "info@clear-code.com" body = "Hello Ruby and milter!" text = "#{from} #{to} #{body}" #   from :to => to, :body => body, :text => text) query = "Ruby" messages.select do |record| record["text"].match(query) # <- textカラムで全文検索 end
Rubyでmilterを作る
データの保存・検索の仕組みはできたので、あとは、groongaのデータベースにメールを登録するだけです。
milter managerのRubyバインディングのAPIでは、ユーザがmilterプロトコルのコマンドに対応するメソッドを定義し、ライブラリ側がそのメソッドを呼び出します。今回必要な情報はヘッダーと本文にあります。そのため、今回のmilterは以下のようになります2。
class ArchiveMilter < Milter::ClientSession def initialize @messages = Groonga["Messages"] @values = {} @encoding = nil @body = "" end def header(context, name, value) case name when /\A(Subject|From|To)\z/i key = $1.to_s.downcase utf8_value = NKF.nkf("-w", value) @values[key] = utf8_value when /\AContent-Transfer-Encoding\z/i @encoding = value end end def body(context, chunk) @body << chunk end def end_of_message(context) nkf_option = "-w" nkf_option << " -MB" if @encoding == "base64" @values["body"] = NKF.nkf(nkf_option, @body) @messages.add(@values) end end
このように、Rubyでmilterを作るときは必要な処理の部分だけを記述するだけですみます。つまり、やりたいことを実現するためにどういうデータが必要で、どのタイミングでそのデータを手に入れられるかがわかれば、Rubyでmilterを作ることは簡単だということです。
登録したメールは以下のように検索・表示することができます。
query = "Ruby" # <- 検索キーワード messages = Groonga["Messages"] result = messages.select do |record| record["subject"].match(query) | record["body"].match(query) end result.sort([["_score", :desc]]).each do |message| puts "-" * 78 puts "score: #{message.score}" puts "Subject: #{message.subject}" puts "From: #{message.from}" puts "To: #{message.to}" puts puts message.body puts "-" * 78 end
Rubyでmilterを作る方法について説明しました。そのために必要な技術として、milterプロトコルの具体的な動作も説明しました。ここで説明されている内容を理解していれば、より詳細なmilter関連情報も理解しやすくなるでしょう。英語ですが、milterに関する情報はmilter.orgにまとまっています。より詳しい情報を知りたい場合はチェックするとよいでしょう。
札幌はやはりやさいい雰囲気に包まれていました。札幌Ruby会議02とは少し違う雰囲気でしたが、似ているとは感じました。
一度、札幌の人たちに会いに行ってみてはいかがでしょうか。
2010-02-14 - iakioの日記 - postgresqlグループ 「C言語でPostgreSQLを拡張する」というタイトルで石田さんが淡々とライブコーディングされていました。会場とやりとりをしながらコーディングする様子を見ていると、札幌っぽい雰囲気を感じることができるでしょう。
「data」コマンドはmilterプロトコルのバージョン4から追加されたコマンドなのでそれより古い2などを使っている場合は利用できません ↩
milter managerでは「eom」というような省略した名前を「end_of_message」という省略しない自己記述的な名前になっているので注意してください。 ↩
milter manager
現在クリアコードではソフトウェア開発者を募集しています! フリーソフトウェア開発での開発の仕方をベースに、成果や知識を公開し共有しながら業務に携われます。
Ruby/groonga 0.9.0, 0.9.1: 高速で使いやすい検索エンジンライブラリへ向けて
Debian GNU/Linux上でRPMパッケージ・Yumリポジトリを作って公開する方法: milter managerを例にして
Apache Arrow
milter manager
ククログの記事はCC BY-SA 4.0とGFDLのデュアルライセンスで自由に利用できます。
クリアコードはプログラミングが楽しいソフトウェア開発者を1名募集しています。
クリアコードは「クリアコードをいい感じにする人」を1名募集しています。
クリアコードはフリーソフトウェア開発で培った技術力を提供しています。特にMozilla製品(Mozilla FirefoxとMozilla Thunderbird)とRubyとGroonga(全文検索)に関連した開発を得意としています。