カテゴリ: Ruby, Ruby on Rails, プログラミング
携帯サイトで悩まされるのが、世界に迷惑を振りまくdocomo仕様のメールアドレス。RailsのActionMailerも、その影響を逃れられなかった。ActionMailerは下位ライブラリとして日本人作のTMailを使用しており、TMailはdocomo仕様に対応する修正が行われているが、残念ながらピリオド2連続までしか対応していない。解決するにはTMailのraccの文法ファイルを修正してパーサを再生成するしかない。それについては下記の記事を参考にしてもらうとして、気になる、というか困るのが、postfixで受信したメールをTMailで処理するとFromやToのアドレスが正しく得られないという問題の方だ。
postfixは、メールアドレスのlocalpartがRFC2822のフォーマットでない場合は、ダブルクォートで括ることでRFC的に正しい形に補正する(バージョンによってはそうしないらしい)。「aa..bb@example.com」なら「"aa..bb"@example.com」になるという訳だ。この形式ならばTMailは扱えるはずだが、Fromに入っているはずのこのメールアドレスを取得しようとすると、パースに失敗し、nilが返ってくる。このメールアドレスはRFCに準拠しているのでTMailが対応していないとは思えないし、事実、文法ファイルではそれに対応したルールが定義されている。どういうことなのだろうか。
考えても分からないので
actionmailer-1.3.6/lib/action_mailer/vendor/tmail/parser.rbのTMail::Parser::MAILP_DEBUGとTMail::Parser::Racc_debug_parserをtrueに書き換えて、デバッグ出力を見てみよう。ちょっと長いが、次のログが "foo"@example.com をパースしたときのものになる。
>> TMail::Mail.parse('From: "foo"@example.com').from
read :MADDRESS(MADDRESS) :MADDRESS
shift MADDRESS
[ (MADDRESS :MADDRESS) ]
goto 7
[ 0 7 ]
read :QUOTED(QUOTED) "foo"
shift QUOTED
[ (MADDRESS :MADDRESS) (QUOTED "foo") ]
goto 32
[ 0 7 32 ]
reduce QUOTED --> word
[ (MADDRESS :MADDRESS) (word "foo") ]
goto 35
[ 0 7 35 ]
reduce word --> local_head
[ (MADDRESS :MADDRESS) (local_head ["foo"]) ]
goto 28
[ 0 7 28 ]
read :ATOM(ATOM) "@example"
reduce local_head --> addr_phrase
[ (MADDRESS :MADDRESS) (addr_phrase "foo") ]
goto 26
[ 0 7 26 ]
shift ATOM
[ (MADDRESS :MADDRESS) (addr_phrase "foo") (ATOM "@example") ]
goto 25
[ 0 7 26 25 ]
reduce ATOM --> atom
[ (MADDRESS :MADDRESS) (addr_phrase "foo") (atom "@example") ]
goto 18
[ 0 7 26 18 ]
reduce atom --> word
[ (MADDRESS :MADDRESS) (addr_phrase "foo") (word "@example") ]
goto 35
[ 0 7 26 35 ]
reduce word --> local_head
[ (MADDRESS :MADDRESS) (addr_phrase "foo") (local_head ["@example"]) ]
goto 67
[ 0 7 26 67 ]
shift "."
[ (MADDRESS :MADDRESS) (addr_phrase "foo") (local_head ["@example"]) ("." ".") ]
goto 101
[ 0 7 26 67 101 ]
read :ATOM(ATOM) "com"
reduce "." --> dots
[ (MADDRESS :MADDRESS) (addr_phrase "foo") (local_head ["@example"]) (dots 0) ]
goto 76
[ 0 7 26 67 76 ]
shift ATOM
[ (MADDRESS :MADDRESS) (addr_phrase "foo") (local_head ["@example"]) (dots 0) (ATOM "com") ]
goto 25
[ 0 7 26 67 76 25 ]
reduce ATOM --> atom
[ (MADDRESS :MADDRESS) (addr_phrase "foo") (local_head ["@example"]) (dots 0) (atom "com") ]
goto 18
[ 0 7 26 67 76 18 ]
reduce atom --> word
[ (MADDRESS :MADDRESS) (addr_phrase "foo") (local_head ["@example"]) (dots 0) (word "com") ]
goto 110
[ 0 7 26 67 76 110 ]
reduce local_head dots word --> local_head
[ (MADDRESS :MADDRESS) (addr_phrase "foo") (local_head ["@example", "com"]) ]
goto 67
[ 0 7 26 67 ]
reduce addr_phrase local_head --> addr_phrase
[ (MADDRESS :MADDRESS) (addr_phrase "foo @example.com") ]
goto 26
[ 0 7 26 ]
=> nil
何かおかしい感じがする。パーサ的にはクォートされている部分はwordとしてちゃんと解釈されている。が、全体としては (word "foo") (atom "@example") (dots 0) (atom "com") という並びとして解釈されているため、マッチするルールが無くてパースに失敗している。これはなんか、普通のメールアドレスでもうまく行かない感じがするのだが、どうなのだろうか。foo@example.com をパースすると、以下のログが出力された。
> TMail::Mail.parse('From: foo@example.com').from
read :MADDRESS(MADDRESS) :MADDRESS
shift MADDRESS
[ (MADDRESS :MADDRESS) ]
goto 7
[ 0 7 ]
read :ATOM(ATOM) "foo@example"
shift ATOM
[ (MADDRESS :MADDRESS) (ATOM "foo@example") ]
goto 25
[ 0 7 25 ]
reduce ATOM --> atom
[ (MADDRESS :MADDRESS) (atom "foo@example") ]
goto 18
[ 0 7 18 ]
reduce atom --> word
[ (MADDRESS :MADDRESS) (word "foo@example") ]
goto 35
[ 0 7 35 ]
reduce word --> local_head
[ (MADDRESS :MADDRESS) (local_head ["foo@example"]) ]
goto 28
[ 0 7 28 ]
read "."(".") "."
shift "."
[ (MADDRESS :MADDRESS) (local_head ["foo@example"]) ("." ".") ]
goto 75
[ 0 7 28 75 ]
read :ATOM(ATOM) "com"
reduce "." --> dots
[ (MADDRESS :MADDRESS) (local_head ["foo@example"]) (dots 0) ]
goto 76
[ 0 7 28 76 ]
shift ATOM
[ (MADDRESS :MADDRESS) (local_head ["foo@example"]) (dots 0) (ATOM "com") ]
goto 25
[ 0 7 28 76 25 ]
reduce ATOM --> atom
[ (MADDRESS :MADDRESS) (local_head ["foo@example"]) (dots 0) (atom "com") ]
goto 18
[ 0 7 28 76 18 ]
reduce atom --> word
[ (MADDRESS :MADDRESS) (local_head ["foo@example"]) (dots 0) (word "com") ]
goto 110
[ 0 7 28 76 110 ]
reduce local_head dots word --> local_head
[ (MADDRESS :MADDRESS) (local_head ["foo@example", "com"]) ]
goto 28
[ 0 7 28 ]
read false($end) "$"
reduce local_head --> local
[ (MADDRESS :MADDRESS) (local ["foo@example", "com"]) ]
goto 33
[ 0 7 33 ]
reduce local --> spec
[ (MADDRESS :MADDRESS) (spec #) ]
goto 34
[ 0 7 34 ]
reduce spec --> mbox
[ (MADDRESS :MADDRESS) (mbox #) ]
goto 48
[ 0 7 48 ]
reduce mbox --> addr
[ (MADDRESS :MADDRESS) (addr #) ]
goto 47
[ 0 7 47 ]
reduce addr --> addrs
[ (MADDRESS :MADDRESS) (addrs [#]) ]
goto 50
[ 0 7 50 ]
reduce addrs --> addrs_TOP
[ (MADDRESS :MADDRESS) (addrs_TOP [#]) ]
goto 49
[ 0 7 49 ]
reduce MADDRESS addrs_TOP --> content
[ (content [#]) ]
goto 5
[ 0 5 ]
shift $end
[ (content [#]) ($end "$") ]
goto 42
[ 0 5 42 ]
shift $end
[ (content [#]) ($end "$") ($end "$") ]
goto 81
[ 0 5 42 81 ]
accept
=> ["foo@example.com"]
パースは成功しているが、なんと、ローカルアドレスとして解釈されている! なんだろうこれ。良く考えてみると、そもそもatomに「@」が含まれているのがおかしい。atomの元のルールはATOMで、すべて大文字のルールは終端記号なので、スキャナの方からやって来る。したがってその定義はスキャナの方にあるはずだ。スキャナはピュアRuby版とバイナリ版があるようだが、ピュアRuby版のソースを見てみると、ATOMに含まれる文字の定義が見つかった。
actionmailer-1.3.6/lib/action_mailer/vendor/tmail/scanner_r.rb
atomsyms = %q[ _#!$%&`'*+-{|}~^@/=? ].strip
tokensyms = %q[ _#!$%&`'*+-{|}~^@. ].strip
確かに「@」が含まれている。これではうまく動くはずがない。しかし、こんな恐ろしいバグがTMailに存在するとは思えないのだが......
実はここまで来るのにかなり時間がかかっているのだが、それはparse.yの中味を確認するために、TMailの配布元から入手したソースコードの見ていたからだった。実は、こちらの方のscanner_r.rbのATOMの定義には「@」は含まれていない。だからどうしてこんな結果になってしまうのか全く理解できなかったのだが、ActionMailerに添付されている方のソースコードを見た時に全ての謎が解けた。おそらくtokensymsに「@」を追加した際に、勢いでatomsymsの方にも追加してしまったのだろう。こうなっている原因がActionMailerに添付されているTMailが0.10.7というものすごく古いバージョンだからなのか、それともActionMailer独自の修正が行われているからなのかは分からないが、とにかく、動いているように見えるのは偶然に過ぎないということなのだ。
ちなみにこのスキャナのバグは、ActionMailer 2.0.0以降に添付されているTMailでは修正されている。