Message::* Perl modules

はじめのはじめに

たとえば 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 に違反する 結果を出力するとか、そもそもそれ以前に、 $hdr->addr ('Foo Bar <foo@bar.example>') のようにメッセージ形式をモジュール内に隠匿しきれていないとか、非 ASCII 文字を考慮していないとかの不満があります。

(実装方針としては不正な値はモジュールに渡す前に弾くべきという考え方もあるでしょうけど、一般的な利用に際しては賢い設計だとは思えません。)

ということで、はじめは既存のモジュールの wrapper (あるいは補完) を書くつもりでしたが、なんだかごちゃごちゃしていて、 それなら車輪の再発明になっても一から書いてみようと考えました。

特色 (という程のものでもない。)

  1. 結構オブジェクト指向です。
  2. RFC 822/2822 の group を解釈出来ます。
  3. draft-ietf-usefor-msg-id-alt-00 に基づいた送信アドレスなどによる Message-ID を生成出来ます。
  4. 文字コード独立 (CSI) です。 (但し RFC 822 である都合上(謎)、 ASCII 互換である必要はあります。 EBCDIC とかは無理です:-< (というのはメッセージ構造の部分のことです。 MIME を使って EBCDIC などをメッセージ本文に入れることは可能です。))
  5. MIME (RFC 2045, 2046) にほぼ完全に対応しています。

各仕様への対応状況

  1. 電子メイルのメッセージ (RFC 822, RFC 2822) の全機能に対応しています。
  2. 電子ニュース記事 (RFC 1036, son-of-RFC1036, draft-usefor-article (06)) の頭領域の多くに対応しています。
  3. MIME の本文部分 (body part) に対応しています。
  4. MIME の頭領域 (RFC 2045, Content-Disposition) に対応しています。 パラメーター値拡張 (RFC 2231) も入出力ともに実装しました。
  5. MIME 符号化語 (encoded-word) の解読に対応しています:-)
  6. HTTP/1.0, HTTP/1.1, CGI/1.1, CGI/1.2 の頭領域のうち、 ごく一部に対応しています。 MHTML の Content-Location にも対応しています。
  7. 日付形式では RFC 822/1123, RFC 733, asctime, ISO 8601 (HTML) などに対応しています。日付の出力は sprintf の様な書式文字列を与えることで、多種多様な形式に対応。
  8. X-Moe シリーズに対応しています:-)

制限事項

  1. 類似モジュール(謎)のように、ファイル名やファイル・ハンドルを 渡して読み込ませることが出来ません。
  2. 大きなメッセージでも一気に読み込み、全て主記憶領域で 保持しています。ですからあまり大きなメッセージの処理には 向いていないでしょう。
  3. CRLF が単体で出現する場合、 正しく処理出来ませんないことがあります (近い将来の版で改善の予定)。 (CRLF と等価とみなします。) 将来の版ではオプションで制御可能になるかもしれません。
  4. あったら良さそうな機能が未実装かもしれません。 (欲しい機能が未実装だったら、 電子メイルsuika.msg などで教えて下さい。)
  5. 各モジュールのオプション体系があまり整備されていません。 (それでも気持ち悪くない程度には体系的だと思います。)
  6. 説明文 (document) が良い加減です。

今後の予定

  1. 電子ニュースの頭領域 (RFC 1036, son-of-RFC1036, draft-usefor-article) の完全実装
  2. 追加/非標準の頭領域の実装。
  3. documentation。
  4. 使用例の作成。
  5. 既存モジュールが利用出来る部分は、それを呼び出すようにするか その code を流用する。
  6. 類似モジュールとの界面の共通化

必要環境

  1. Perl (perl 5.6 以降または人間解析者:-))

    comment を表すのに正規表現 (??{ code }) を使っているので、これを解釈出来る、 5.6 以降の版である必要があります。

  2. Digest::MD2, Digest::MD5, Digest::SHA1

    Message-ID の生成にこれらを使用する場合のみ、 Message::Field::MsgID が使います。

    これらが用意されていない環境ではエラーになるので、 (現状では) 上記モジュールの該当部分を書き換えて対処して下さい。

  3. MIME::Base64

    ちなみに、 Quoted-Printable や RFC 2231 の % 符号化は自力で復号します。

  4. 文字コード変換処理

    日本語メッセージを扱うなら必須でしょう。 詳しくは文字コードの扱い の章をご参照下さい。

入手

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 や Jcode.pm などのための設定は予め用意してあります。

## どちらか好きな方をどうぞ。
use Message::MIME::Charset::Jcode 'jcode.pl';
use Message::MIME::Charset::Jcode 'Jcode';

この1行だけで、 ISO-2022-JP, EUC-JP, Shift_JIS および幾つかの関連 charset が利用可能になります。

Perl 5.8 になって Encode モジュールが使えるようになれば、 もっと色々な文字コードが楽に利用できるようになると期待しています。

ところで、このように charset 対応処理をしなくても、 MIME で charset 札付けされてメッセージに含められている未知の charset のデータが破壊されることはありません。 (はずです。) (そこいらが、 Unicoder のソフトウェアとの違いです(笑)。)

$Date: 2002/06/14 12:46:34 $