TMail パーサのデバッグ方法
あれ!? TMail のメール解析がおかしいぞ!? というときは、パーサまで手を入れないといけないかも知れない。そのデバッグの仕方を書いておく。
パーサのデバッグフラグをONにする
TMailを使うスクリプトの中で、
require "tmail" TMail::Parser.const_set(:MAILP_DEBUG, true) # <== ココ mail = TMail::Mail.parse(IO.read(mailfile))
こうするとデバッグ情報が出力される。また通常は、ヘッダの中身を解析していてエラーが起こった場合は、通常何も出力されずに無視されて、エンコード時にそのヘッダが捨てられてしまう。デバッグをONにすると、中断こそしないがそのエラーも出力される。
まずこれで実行して、構文解析エラーが原因なのかどうかが調べてみるとよい。エラーメッセージだけで、対応方法が分かる場合がある。
パーサの中の、どの解析ルールが原因なのかを調べないといけない場合は、次のステップに進もう。
raccのインストール
gem install racc
デバッグ情報付きパーサの生成
TMailのパーサは、 lib/tmail/parser.rb である。これは、ルール定義ファイル parser.y から、racc によって自動生成される。
cd tmail-1.2.3.1 racc -t -o lib/tmail/parser.rb lib/tmail/parser.y
オプション"-t"はデバッグ情報付きのパーサが生成される。
ふたたび実行
デバッグ情報入りのパーサだと、豊富な情報がたくさん出てくる。
(例)解析部分:
Content-Type: multipart/mixed;
(例)出力:
read :CTYPE(CTYPE) :CTYPE
shift CTYPE
[ (CTYPE :CTYPE) ]
goto 12
[ 0 12 ]
read :TOKEN(TOKEN) "multipart"
shift TOKEN
[ (CTYPE :CTYPE) (TOKEN "multipart") ]
goto 62
[ 0 12 62 ]
read "/"("/") "/"
shift "/"
[ (CTYPE :CTYPE) (TOKEN "multipart") ("/" "/") ]
goto 99
[ 0 12 62 99 ]
read :TOKEN(TOKEN) "mixed"
shift TOKEN
[ (CTYPE :CTYPE) (TOKEN "multipart") ("/" "/") (TOKEN "mixed") ]
goto 126
[ 0 12 62 99 126 ]
reduce <none> --> params
[ (CTYPE :CTYPE) (TOKEN "multipart") ("/" "/") (TOKEN "mixed") (params {}) ]
goto 136
[ 0 12 62 99 126 136 ]
read ";"(";") ";"
shift ";"
[ (CTYPE :CTYPE) (TOKEN "multipart") ("/" "/") (TOKEN "mixed") (params {}) (";" ";") ]
発見した TMail-1.2.3.1 のバグ各種
TMail-1.2.3.1 のバグを色々直した。それぞれ詳しいことは以下。各修正を適用したTMailのgemをダウンロードに置いてあるので、手っ取り早く使いたい方は、これをダウンロードして、 gem install tmail-1.2.3.1.gem でインストールしてほしい。
ついでにこの gem には、leave a note [message] behind on Rails: RailsのMailer(TMail)のメールアドレスドット問題の修正も取り込ませていただいた。
正しく動作する保証は無いですが、不具合があればコメントしていただけると助かります。
Content-Typeで値がクォートを含んだまま取り込まれる
TMailプロジェクトに送信したパッチ: RubyForge: TMail: Modify: 23165 - ContentTypeHeader should unquote parameter's value on encode
例えば
Content-Type: image/jpeg; name="\e$B4A;z\e(B.jpg"
という生JISのファイル名がクォートされて指定されていると、なぜかエンコードした後に、
Content-Type: image/jpeg; name*=iso-2022-jp'ja'%22%1b$B4A%3bz%1b%28B.jpg%22
%22(")が前後にくっついてしまう。本当は、
Content-Type: image/jpeg; name*=iso-2022-jp'ja'%1b$B4A%3bz%1b%28B.jpg
でないといけない。
AppleMail 添付ファイルパートの Content-Type が解析エラー
TMailプロジェクトに送信したパッチ: RubyForge: TMail: Modify: 23681 - Fix parse error on AppleMail's bad Content-Type parameter value - unquoted but bencoded
AppleMailに日本語ファイル名を添付させてみよう。するとこんなパートが生成される。
--Apple-Mail-1-993553537 Content-Transfer-Encoding: base64 Content-Type: application/pdf; x-mac-type=50444620; x-unix-mode=0644; x-mac-creator=4341524F; name==?ISO-2022-JP?B?GyRCQXdFRTdPRX0/XhsoQi5wZGY=?= Content-Disposition: inline; filename*=ISO-2022-JP''%1B%24BAwEE7OE%7D%3F%5E%1B%28B.pdf
これをパースし、このパートのcontent_typeを得ようとすると
part.content_type => nil
nilが帰ってくる。さらに、encodedでエンコードすると、
--Apple-Mail-1-993553537 Content-Transfer-Encoding: base64 Content-Disposition: inline; filename*=ISO-2022-JP''%1B%24BAwEE7OE%7D%3F%5E%1B%28B.pdf
Content-Type そのものが消えてしまう。(メーラ上の動作では、本文として展開されてしまう)
実は、Content-Type をパースする際に構文エラーが発生しているのだ。 name==?ISO- の2つ目の = に出会った時点で、これは有効なトークンではないとエラーになってしまう。確かに、RFC 2045(対訳)多目的インターネットメール拡張 パート1 Content-Type ヘッダフィールドの文法 によれば、これはクォートしないといけない。 AppleMail が間違っているのだ。
Content-Type をパースする前に、このようなパターンを見つけてクォートさせてやることで解決した。
ちなみに、AppleMailに長いファイル名で添付をさせると
Content-Type: application/pdf; x-unix-mode=0644; name="=?ISO-2022-JP?B?GyRCRDkhPCQkRDkhPCQkRDkhPCQkRDkhPCQkGyhC?= =?ISO-2022-JP?B?GyRCRDkhPCQkRDkhPCQkRDkhPCQkRDkhPCQkGyhC?= =?ISO-2022-JP?B?GyRCRDkhPCQkRDkhPCQkRDkhPCQkRDkhPCQkGyhC?= =?ISO-2022-JP?B?GyRCRDkhPCQkRDkhPCQkRDkhPCQkGyhCLnBkZg==?="
ちゃんとクォートされる。1行で収まる場合のみ、クォートしてくれないらしい。
TMailで作成した添付ファイル名の文字化け
TMailで添付ファイル付きのメールを1から作成しようとすると、このようなスクリプトになるが、
filename = "任意の日本語ファイル名" # JISにすること
filedata = "ファイルのデータ"
require "base64"
require "tmail"
mail = TMail::Mail.new()
part = TMail::Mail.new()
part.transfer_encoding = "7bit"
part.set_content_type('text', 'plain', 'charset'=>'iso-2022-jp')
part.body = "honbun."
mail.parts << part
part = TMail::Mail.new()
part.set_content_type('application', 'octet-stream', 'name' => filename)
part.set_disposition("attachment")
part.transfer_encoding = "base64"
part.body = Base64.encode64(filedata)
mail.parts << part
mail.encodedfilename によっては、途中でファイル名が化けたりする。
例えば、「何かチラシ.pdf」をこのスクリプトで作成すると、メーラで開いたときにはファイル名が「何か汁初酬.pdf」になってしまう。まあ笑えるからいいか、ってんなわけない。
このヘッダを見ると
Content-Type: application/octet-stream; name*=iso-2022-jp'ja'%1b$B2%3f$+%A%i%7%1b%28B.pdf
となっている。原因は、エンコード時にJIS文字列中の % をエスケープしてくれてないからである。正しくは、
Content-Type: application/octet-stream; name*=iso-2022-jp'ja'%1b$B2%3f$+%25A%25i%257%1b%28B.pdf
でないといけない。
encode.rb の encode_value の中で、 TOKEN_UNSAFE という正規表現を %xx に置き換えている。TOKEN_UNSAFEの宣言は utils.rb にある。それが使っている値である、 aspecial/tspecial に % を追加して修正した。
しかし、これだと他のメソッドにも影響が出そうだが…。ちょっと不安である。それに aspecial/tspecial の定義が、 RFC と違ってきてしまう。
このパッチはトラッカーにsubmitしていない。パッチは以下の通りである。
Index: lib/tmail/utils.rb
===================================================================
--- lib/tmail/utils.rb (リビジョン 261)
+++ lib/tmail/utils.rb (作業コピー)
@@ -109,8 +109,8 @@
# It also provides methods you can call to determine if a string is safe
module TextUtils
- aspecial = %Q|()<>[]:;.\\,"|
- tspecial = %Q|()<>[];:\\,"/?=|
+ aspecial = %Q|()<>[]:;.\\,"%|
+ tspecial = %Q|()<>[];:\\,"/?=%|
lwsp = %Q| \t\r\n|
control = %Q|\x00-\x1f\x7f-\xff|
AWT/Swing でエラーダイアログを表示する。(Sunの実装限定)
後でこのプロパティ名で検索したら、sun.awt.exception.handler - ある学生さんのシステム開発日記に書かれていた。
うぐぐ、 "awt 例外 キャッチ" とか "awt 例外 ダイアログ" とかで検索して見つからなかったから、諦めて調べたが…。 "awt 例外 ハンドラ" と検索すればよかったのか。(2番目に出てくる)
良い検索キーワードが閃けば、時間を節約できるなあ…。
AWT/Swingのイベントハンドラで例外が発生すると、ユーザにはそんなことは何も分からずに、ただスタックトレースが吐き出されるだけである。
これを自動的に例外が発生したら、エラーダイアログを出すなり何なりの処理をさせたいと思った。
しかし、例えば addExceptionHandler() とか、そのような目的のためのAPIは無いようだ。
"sun.awt.exception.handler" に例外処理クラスを設定
AWTの例外処理ソースを追っていくと、
- EventDispatchThread.pumpOneEventForHierarchy で例外が catch される。
- EventDispatchThread.processException
- EventDispatchThread.handleException
という流れで例外が処理されている。この handleException の中のソースを見て、どうすればよいかが分かった。
次の例外処理クラスを作って、
public class AWTExceptionHandler {
public static void setup() {
System.setProperty("sun.awt.exception.handler", AWTExceptionHandler.class.getName());
}
public void handle(Throwable ex) {
// エラー処理を書いてくれぃっ!
ex.printStacktrace();
}
}アプリケーションの初期化処理の中で、
AWTExceptionHandler.setup();
を呼べばよい。
これにより、イベントハンドラで発生した例外すべてを、 handle() の中で処理できる。
もちろんこの中で、ダイアログ表示も可能。
ThreadGroup.uncaughtException を使う方法
ちなみに、
catch を書かなくても例外発生時にエラーメッセージを出したい - Java Solution
ThreadGroup.uncaughtException を使う方法もあるようだ。
ActiveRecord のような軽量O/Rの調査
activeobjects: ホーム
civic site : ActiveObjectsすげー
結局hibernateにしました。
全然軽量じゃねえよ、とツッコまれそう。
http://www.hibernate.org/
これにしようと沢山勉強した後で、
英語読むの大変だなあ…、エラーメッセージ分かりにくいなあ…、
とか思ってたら、
Kuina-Dao(from Seasar2)
を発見。
魅力的だ………うぬぬ、しかし、今さら乗り換えてイチから勉強できるかぁ!
既にアプリのコード、べったりhibernate依存で書いてるし。
Ramaze
Rails を使ったアプリをさくらサーバで動かそうとしたら、現状は次の方法しかない。
- dispatch.cgi
- CGI経由でRailsを毎回起動。むちゃくちゃ重い。
- gateway.cgi
- 1回目の処理以降しばらく常駐するので、最初のアクセス後しばらくは速い。でもプロセスは一定時間で強制終了させられるので、あくまでもしばらく。それに、さくら側が常駐プログラム禁止ですよとしているのに、こういう抜け穴のようなやり方をしても、いずれ規制されるのではないだろうか。この手法が広がっている様子にちょっと懸念を抱く。
Rails がもっとCGI起動で軽く動くようになってくれるか、 mod_rails のようなものが登場してくれるかすれば良いのだろうが、現時点では Rails をさくらで動かすのは難しそう。
なので、代替となるWebアプリケーションフレームワークを色々と模索してたら、 Ramaze を発見。なかなか良さそうなので、これを WEBrick や mongrel のような常駐式ではなく CGI で動かせるかどうか試みてみる。
[Ramaze : The Modular Web Framework] - http://ramaze.net/
Ramaze を CGI として動かす
public/dispatch.cgi を作成
#!/usr/bin/env ruby
require 'rubygems'
require 'ramaze'
Ramaze::Log.loggers = [ Ramaze::Logger::Informer.new( __DIR__("../ramaze.log") ) ]
Ramaze::Global.adapter = :cgi
$ramaze_0 = __DIR__("../start.rb")
alias $0 $ramaze_0
Dir.chdir File.dirname($0)
load File.basename($0)なんで、 $0 = __DIR__("../start.rb") ではないのかというと、ディレクトリのフルパスが長すぎる(例えば /path/to/very/long/path )と $0 に格納したら、途中で切れて( /path/to/ve )しまうからだ。
なお、start.rb と同じフォルダに chdir してから start.rb に制御を移さないと、なぜか "no such action: /page" というエラーになってしまう。
(追記)今はもっとシンプルに書いて、こうしている。
#!/usr/bin/env ruby
# Go to application directory.
Dir.chdir(File.dirname($0))
Dir.chdir("..")
require 'rubygems'
require 'ramaze'
Ramaze::Log.loggers = [ Ramaze::Logger::Informer.new("ramaze.log") ]
Ramaze::Global.adapter = :cgi
$0 = "start.rb"
load $0
start.rb を編集
Ramaze.start :adapter => :webrick, :port => 7000
となっているのを単に、
Ramaze.start
としてやる。
public/.htaccess作成
RewriteEngine on RewriteRule ^(.*)$ dispatch.cgi [QSA,L]
Illegal seek エラー?
なんかエラーになった!
Errno::ESPIPE - Illegal seek 原因箇所は: /opt/local/lib/ruby/gems/1.8/gems/rack-0.9.1/lib/rack/request.rb: in rewind @env["rack.input"].rewind if @env["rack.input"].respond_to?(:rewind)
これは、
$ ruby -e "IO.pipe[0].rewind"
-e:1:in `rewind': Illegal seek (Errno::ESPIPE)
from -e:1としたのと同じこと。 CGI Adapter はあんまりテストされていないのかな…。とりあえず、
Index: request.rb
===================================================================
--- request.rb (revision 6)
+++ request.rb (working copy)
@@ -113,7 +113,9 @@
Utils::Multipart.parse_multipart(env)
@env["rack.request.form_vars"] = @env["rack.input"].read
@env["rack.request.form_hash"] = Utils.parse_query(@env["rack.request.form_vars"])
- @env["rack.input"].rewind if @env["rack.input"].respond_to?(:rewind)
+ begin
+ @env["rack.input"].rewind if @env["rack.input"].respond_to?(:rewind)
+ rescue Errno::ESPIPE; end
end
@env["rack.request.form_hash"]
elseという風に直して、
再度アクセスする
http://localhost/path/to/scratch/public/
めでたく成功〜。
Welcome to Ramaze! Ramaze is working correctly with this application, now you can start working.
Intel Core2Duo 2.1GHz のマイマシンで、実行時間は約 0.3 秒だった。実際アプリを組めばさらに時間がかかるだろうが、これなら十分CGIとして動かせそうである。
$ command time ./scratch/public/dispatch.cgi < /dev/null > /dev/null
0.29 real 0.23 user 0.05 sys
