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 に違反する 結果を出力するなどの不満があります。 (例えば今の例で To: 領域に使っている display_name で「.」が含まれますが、 RFC 2822 的には新しいメッセージでは互換性のため quoted-string にする必要があります。しかしそのまま出力されます。)

参考: 「.」の場合は RFC 2822 的には正しく解釈 されなければなりませんが (出力はすべきでない)、 これ以外の文字、例えば制御文字 ESCAPE でも同じようになります。 こちらは完全に間違いです。

参考: 実装方針としては不正な値はモジュールに 渡す前に弾くべきという考え方もあるでしょう。 でもそんなのは不便です。

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

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

  1. 結構(謎)オブジェクト指向です。
  2. RFC 822/2822 の group を解釈出来ます。
  3. draft-ietf-usefor-msg-id-alt-00 に基づいた送信アドレスなどによる Message-ID を生成出来ます。
  4. 文字コード独立 (CSI) です。 (但し RFC 822 である都合上(謎)、 ASCII 互換である必要はあります。 EBCDIC とかは無理です:-<)

各仕様への対応状況

  1. 電子メイルのメッセージ (RFC 822, RFC 2822) の全機能に (抜けが無ければ) 対応しています。 但し長さ制限などはチェックしていません。 (MIME の Content-Transfer-Encoding と一緒に実装予定)
  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) などに対応しています。

制限事項

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

今後の予定

  1. 電子ニュースの頭領域 (RFC 1036, son-of-RFC1036, draft-usefor-article) の完全実装
  2. MIME の頭領域の実装。
  3. 追加/非標準の頭領域の実装。
  4. MIME 本体 (body) の実装。
  5. 文字符号変換のための hook の実装?
  6. documentation。
  7. 使用例の作成。

必要環境

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

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

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

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

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

  3. 文字コード変換処理

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

入手

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 モジュールが使えるようになれば、 もっと楽になると期待しています。

$Date: 2002/04/01 09:14:50 $