たとえば Perl で書かれた CGI script, それも掲示板なんかには、 こんなくだらない code が載っていたりします。
jcode'convert(*from, "jis"); jcode'convert(*subject, "jis"); jcode'convert(*message, "jis"); open (MAIL, "| $sendmail"); print MAIL "From: $mail ($from)\n"; print MAIL "To: $mailto\n"; print MAIL "Subject: $subject\n"; print MAIL "\n"; print MAIL "$message"; print MAIL "\n"; close (MAIL);
これでは視認性も良くないですし、うっかり修正し間違えると 変なメッセージを送信してしまいます。 (筆者はしょっちゅうはまってました:-) (それに多くの code では、 HTML でのクロスサイトスクリプティング (CSS) 問題と 類似の問題への対処をしていません。)
オブジェクト指向を取り入れて次のような感じでメッセージを 構成したいところです。
use Message::Entity; my $msg = new Message::Entity; my $hdr = $msg->header; $hdr->add ('From')->add ('me@bar.example'); $hdr->add ('To')->add ('foo@bar.example', display_name => 'Mr. foo'); $hdr->add ('Subject' => $subject); $msg->body ($body); # $smtp->send は SMTP で送信する method と仮定。 $smtp->send ($msg);
CPAN を探すと、
これに似たようなことができそうなモジュールはあるようですが、
実際に使ってみると、与える値によっては RFC 822/2822 に違反する
結果を出力するなどの不満があります。 (例えば今の例で
To:
領域に使っている
display_name
で「.」が含まれますが、
RFC 2822 的には新しいメッセージでは互換性のため
quoted-string
にする必要があります。しかしそのまま出力されます。)
参考: 「.」の場合は RFC 2822 的には正しく解釈
されなければなりませんが (出力はすべきでない)、
これ以外の文字、例えば制御文字 ESCAPE
でも同じようになります。
こちらは完全に間違いです。
参考: 実装方針としては不正な値はモジュールに 渡す前に弾くべきという考え方もあるでしょう。 でもそんなのは不便です。
ということで、はじめは既存のモジュールの wrapper (あるいは補完) を書くつもりでしたが、なんだかごちゃごちゃしていて、 それなら車輪の再発明になっても一から書いてみようと考えました。
group
を解釈出来ます。Message-ID
を生成出来ます。Content-Transfer-Encoding
と一緒に実装予定)Content-Disposition
) に対応しています。
パラメーター値拡張 (RFC 2231)
も入出力ともに実装しました。encoded-word
)
の解読に対応しています:-) 但し別途変換処理を指定する必要があります。
(文字コードの扱い参照)Content-Location
にも対応しています。CR
や LF
が単体で出現する場合、
正しく処理出来ません。 (CRLF
と等価とみなします。)
将来の版ではオプションで制御可能になるかもしれません。body
) の実装。comment
を表すのに正規表現 (??{ code })
を使っているので、これを解釈出来る、
5.6 以降の版である必要があります。
Message-ID の生成にこれらを使用する場合のみ、
Message::Field::MsgID::MsgID
が使います。
これらが用意されていない環境ではエラーになるので、 (現状では) 上記モジュールの該当部分を書き換えて対処して下さい。
日本語メッセージを扱うなら必須でしょう。 詳しくは文字コードの扱い の章をご参照下さい。
suika.fam.cx の SSH account をお持ちの場合、 CVS から入手出来ます。
$ cvs -d :ext:username@suika.fam.cx:/home/cvs -d perl/lib/Message/
Web からも取り出せます。 <http://suika.fam.cx/gate/cvs/perl/lib/Message/> (tarball で一括取得も出来ます。)
Message::* Perl modules は自由ソフトウェアです。 GNU GPL に従って利用出来ます。詳しくは各ファイルを御覧下さい。
卑しいことで頭を悩ますのは嫌なので(藁)、 Message::* は符号化方法独立 (CSI) を目指して実装しています。 (但し ASCII のしがらみだけは断ち切っていません:-)) 0x00 〜 0x7F が ASCII (または ASCII と見なして良いもの) である 場合は、 Message::* を通したことでデータが壊れることは 無いと思います。
(もちろん、 RFC 822 など各仕様に照らして正統(的)で
ある必要があります。 atom
に8ビット・コードが含まれていると正しく扱えません。)
(早い話が、 quoted-string
などでは8ビット透過だということです。回りくどくてごめんなさい。)
既定の状態では文字コードに関係する変換処理は行われません。 しかし、フック関数っぽいもの(謎)を指定することで、 変換処理をさせられます。
指定出来るフック関数っぽいものは2種類です。
DECODER
は、元のメッセージを解析する時
(parse ()
) に適宜呼び出されます。
ENCODER
は、メッセージとして文字列化する際
(stringify ()
など) に適宜呼び出されます。
これらの関数は、当然、当該処理が呼び出される前に指定しておく 必要があります。 Message::Entity->parse などする前に 定義しておくと良いでしょう。
require Message::MIME::Charset; $Message::MIME::Charset::DECODER{'*default'} = sub {jcode::euc ($_[1])}; $Message::MIME::Charset::ENCODER{'*default'} = sub {jcode::jis ($_[1], 'euc')};
この例では、 jcode.pl を変換処理に使います。
(もちろん、既に require
されていると仮定しています。)
最初の require
で、変換処理を担当している
Message::MIME::Charset
を読み込みます。
(こうしておかないと、後から既定値 (= 無変換) で
*default
が上書きされてしまいます。)
この code を使ったスクリプトは内部処理を日本語 EUC
で行うと仮定しています。ですから、 DECODER
で日本語 EUC に変換します。
また、日本語メッセージでは ISO-2022-JP
を使うのが慣習ですから、 ENCODER
では 7ビット JIS に変換しています。
処理を行う関数は、引数が2つ以上与えられます。
1つ目の引数は呼び出した class module, いわゆる
$self
です。(この場合 self ではありませんが:-)
でも普通は必要ないでしょう。
2つ目の引数は処理対象の文字列です。
3つ目以降の引数は、追加オプションのハッシュです。 ただし、現在追加オプションは定義されていません。
関数が返す値は(今のところ)一つだけです。
処理が終わった文字列です。変換結果として何もなくなってしまったら、
もちろん空文字列を返して構いません。 (undef
よりも空文字列の方が望ましいでしょう。)
さて、上記の例では「*default
」の EN/DECODER
を指定しましたが、ここには代わりに charset 名を指定出来ます。
$Message::MIME::Charset::DECODER{'iso-2022-jp'} = sub {jcode::euc ($_[1], 'jis')};
ここでは、 ISO-2022-JP
を内部コードに変換する
方法を定義しています。 charset 名 (および「*default
」
は必ず小文字で書いて下さい!)
MIME body や、 encoded-word, RFC 2231 の拡張パラメーター値
など、 charset が指定されている時はその charset 名の変換関数が
呼び出されます。 (指定された charset 名の変換関数が未定義の時は、
何も処理しません。) これ以外の場面では、 *default
で定義された関数が使われます。
最後に、日本語メッセージを扱う際の例を挙げておきます。
## jcode.pl を使用 require 'jcode.pl'; require Message::MIME::Charset; $Message::MIME::Charset::DECODER{'*default'} = sub {jcode::euc ($_[1])}; $Message::MIME::Charset::DECODER{'iso-2022-jp'} = sub {jcode::euc ($_[1], 'jis')}; $Message::MIME::Charset::DECODER{'euc-jp'} = sub {$_[1]}; $Message::MIME::Charset::DECODER{'shift_jis'} = sub {jcode::euc ($_[1], 'sjis')}; $Message::MIME::Charset::ENCODER{'*default'} = sub { my $s = $_[1]; ## 正規化 jcode::tr(\$s, "\xa3\xb0-\xa3\xb9\xa3\xc1-\xa3\xda\xa3\xe1-\xa3\xfa\xa1\xf5". "\xa1\xa4\xa1\xa5\xa1\xa7\xa1\xa8\xa1\xa9\xa1\xaa\xa1\xae". "\xa1\xb0\xa1\xb2\xa1\xbf\xa1\xc3\xa1\xca\xa1\xcb\xa1\xce". "\xa1\xcf\xa1\xd0\xa1\xd1\xa1\xdc\xa1\xf0\xa1\xf3\xa1\xf4". "\xa1\xf6\xa1\xf7\xa1\xe1\xa2\xaf\xa2\xb0\xa2\xb2\xa2\xb1". "\xa1\xe4\xa1\xe3\xA1\xC0\xA1\xA1" => '0-9A-Za-z&,.:;?!`^_/|()[]{}+$%#*@=\'"~-><\\ '); jcode::jis ($s, 'euc', 'z') };
## Jcode.pm を使用
use Jcode;
require Message::MIME::Charset;
$Message::MIME::Charset::DECODER{'*default'} = sub {jcode::euc ($_[1])};
$Message::MIME::Charset::DECODER{'iso-2022-jp'} = sub {Jcode->new ($_[1], 'jis')->euc};
$Message::MIME::Charset::DECODER{'euc-jp'} = sub {$_[1]};
$Message::MIME::Charset::DECODER{'shift_jis'} = sub {Jcode->new ($_[1], 'sjis')->euc};
$Message::MIME::Charset::DECODER{'utf-8'} = sub {Jcode->new ($_[1], 'utf8')->euc};
$Message::MIME::Charset::ENCODER{'*default'} = sub {Jcode->new ($_[1], 'euc')->jis};
$Message::MIME::Charset::ENCODER{'utf-8'} = sub {Jcode->new ($_[1], 'euc')->utf8};
Perl 5.8 で Encode モジュールが使えるようになれば、 もっと楽になると期待しています。