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.encoded
filename によっては、途中でファイル名が化けたりする。
例えば、「何かチラシ.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