Perl/CGI研究室 'PERL-LABO'

Perl/CGI研究室 'PERL-LABO' TOPへ
戻る(History.Back)

JcodeとBASE64の研究

研究内容

文字コードをJISに変換し、メールヘッダ内の日本語をBASE64エンコードする研究です。モジュールを使います。

詳細

メールの決まりごと

前回作ったのは、メール件名も本文も日本語文字コードについて考慮することなく そのままのコードで送信するというものでした。CGIプログラムとウェブページを 共にS-JISで作成していますので、文字は全てS-JISで送信されていることに なります。しかしメールの文字コードには決まりごとがあって、 メールヘッダ内の日本語はJISの文字列をBASE64にエンコードしたもの、 本文はJIS、となっているらしいです。

これらの決まりは MIME というそうです。Multipurpose Internet Mail Extensions の略だそうです。 RFC1341、RFC1342で定められ、最新版はRFC2045とRFC2046とのこと。ただ原文は英語だし 厳密すぎて分かりにくいので読んでません (^^;

ここまでに作ったCGIプログラムでは、 メール送信時に次のようなメールヘッダ(MIMEメッセージヘッダというのが正しいようです)を付けました。

Content-Type: text/plain; charset="ISO-2022-JP"

ここにでてくる ISO-2022-JP というのはJISコードという意味なんだそうです。 このメールはJISコードで書かれていますよ、ということですね。 にもかかわらず、実際はS-JISなわけですから、嘘付きです。 それでもテストしたときに文字化けしなかったのは、メーラーが自動的に文字コードを 判別してくれていたからですね。賢いですね…

さて、正しくは、メールヘッダ内の日本語はJISの文字列をBASE64にエンコードしたもの、 本文はJIS、ということでした。このうち、メールヘッダの中の日本語部分は 正確には次のような書式になるようです。

=?ISO-2022-JP?B?(BASE64でエンコードした文字列)?=

両端の =? ?= は、この部分はなにかルールに従ってエンコードされてますよ、という意味ですね。 最初のISO-2022-JPはJISですよという意味でした。次の B はBASE64という意味だそうです。

この BASE64 というのは文字列を別の文字列に変換するルールのことです。 64というのは、64種類の文字でデータを表現するという意味 らしいです。普通、データというのは 0〜255 までの数値(これが1バイトです)の 集まりですが、それを、0〜63 までの数値の集まりに変換するっていう感じですね。 なんでこれが必要かっていうと、日本語文字の中にはいわゆる制御コードが 含まれていることがあって、そのせいでメールヘッダが変なことになってしまう ことがあるから、ということのようです。 で、BASE64での変換の例ですが、例えば「テスト」はBASE64で変換すると「GyRCJUYlOSVIGyhC」と なります。このように、まったく別の文字列に変換されますが、 変換ルールに従って元の「テスト」に戻すこともできます。 このように、データを普通のアルファベットに変換したり戻したりするルールがBASE64なのですね。

さて、メールヘッダの中で日本語が出てくるのは、差出人名、宛先名、それに件名でした。

From: 名前<メールアドレス>
To: 名前<メールアドレス>
Subject: 件名

これらを、上のルールに従って正しいものにするには、 それぞれをS-JISからJISに変換した上でさらにBASE64エンコードし、 次のようにしなければいけないということです。

From: =?ISO-2022-JP?B?(BASE64でエンコードしたもの)?=<メールアドレス>
To: =?ISO-2022-JP?B?(BASE64でエンコードしたもの)?=<メールアドレス>
Subject: =?ISO-2022-JP?B?(BASE64でエンコードしたもの)?=

あと、本文をJISコードにする。これらが正しいメール送信に必要です。

メールフォームが自分宛専用のものなら、自分が使っているメーラーで読むことができれば ルール違反でもS-JISのまま送っちゃうっていう手もありますが、 そうでないなら、ちゃんと対応しなきゃいけませんね。

ということで、今回の研究課題は2つあります。「S-JISをJISに変換する」「BASE64エンコードする」です。 これ、どうやってやるんでしょうか?

結果

jcode.pl について

日本語文字コードの変換は、なんだかとても複雑で難しいようです。 自前でそれをやるのはちょっと大変そう。 そこで、私も知っている、有名なPerlライブラリを利用させて頂きます。 そのライブラリは jcode.pl というもので、日本語の文字コードをあれこれ変換してくれたり、 自動的にどの文字コードかを識別してくれたりする関数があります。 いやー、こんな便利なものを無料で提供してくださっている作者様を心から尊敬します。 jcode.pl の配布元公式ページは jcode.pl official page です。

さてそれでは早速 jcode.pl をダウンロードさせて頂いて…と思ったんですが、 この jcode.pl はあまりにも便利、有名、大勢の方が使用させて頂いているということからか、 Perlに標準で付いてくるようになっているそうです!公式ページからダウンロードしなくても、 最初からPerlに付いているんです。凄いです。作者さんは一体どんな方なんでしょうね。 無料ですよ。凄いなあ…

モジュール?

ここまでネットで調べていて、新しい言葉に出会いました。モジュールという言葉です。 jcode.pl はファイル名で、Perlのライブラリです。 今まで、便利な関数は別ファイルにする、それをライブラリと呼ぶ、名前を付けたものをパッケージと呼ぶ、 といったことは学んできました。これは、require という命令を使ってCGIプログラムに取り込んで 利用するんでした。ですから、jcode.pl も require で取り込んで使うものなんですが、 Perlに標準で付いてくるものは、ライブラリではなくてモジュールという形なんだそうです。 jcode.pl でなくて Jcode.pm というそうです。 1文字目が大文字になっているのが特徴的ですね。 オフィシャルページはこちら Jcode - Japanese Charset Handler です (と思います)。 この、モジュールって、なんでしょう?

Jcode - Japanese Charset HandlerJcode.pm のソースコード が 公開されていましたので、モジュール Jcode.pm の中身をちょっとみてみました。 もしかしてモジュールはバイナリ形式かなと思ったんですが、テキスト形式なんですね。 で、Jcode.pm の中には =head とか =item といった、初めて見るなにか暗号のようなものが ちりばめられているんですが、これは podドキュメント という、 ソースコードの中でプログラムの説明を記述したものなんだそうです。 Perlコンパイラがこの部分をどう処理するのか、別のプログラムで 実行前にこれらの部分を削除するのか気になりますが podドキュメント については今回は置いといて、 コメントらしい部分をスキップしつつ中身を確認してみます。でも…うーん、暗号ですね (^^; 私にはまだ無理です。 sub とかで関数が定義されているのは分かりましたが、ほとんどは意味不明です。

モジュールの中身を理解するのはまだ当分先として、モジュールというものが ライブラリと同じように1つのファイルになっていることや、それがテキスト形式のものであることや、 基本的にはPerl文法に従って書かれたPerlのプログラムなんだっていうことが分かりました。 それでは、本題である、モジュールの使い方を学びましょう。

モジュールの使い方

ライブラリは require で取り込むんでしたが、モジュールの場合は次のように取り込むんだそうです。

use Jcode;

これだけ。簡単ですね。ホッ…。

次は、モジュール内の関数の呼び出し方ですが、 これはパッケージの場合と同じように

Jcode::関数名(引数)

とすればいいようです。良かった、違いは結局 require が use に変わったことでしょうか。 実際は、なんでも Jcode.pm はオブジェクト指向で書かれているんだそうで、-> という記号を使った 関数呼び出し(?)とかができるようなんですが、それはまだ研究していないしそのような使い方は 今回はしないで進めます。それに、ライブラリとモジュールには、もっと沢山の違いがあるんだと思いますが、 とりあえず深く考えずに進めようと思います。

Jcode.pm を利用してJISに変換する

やりたいことは、S-JISをJISに変換することでした。これは、次のようにすればできるようです。 変換したい文字列が$strに入っているとして、

$jis = Jcode::convert($str, 'jis');

いやー簡単。ありがたや…

BASE64でエンコードする

次は、BASE64でのエンコードです。 メール本文についてはS-JISからJISへ変換するだけでしたが、 メールヘッダ内についてはBASE64で文字列をエンコードする必要がありました。

このBASE64エンコードですが、ナントありがたいことに Jcode.pm の中にそれを行う関数が用意されている ではないですか!それも、単にBASE64でエンコードしてくれるだけではなく、 メールヘッダに必要な次の形式の文字列にしてくれるんだそうです。

=?ISO-2022-JP?B?(BASE64でエンコードした文字列)?=

やり方は、次のとおりです。変換したい文字列が$strに入っているとして、

$mimehead = Jcode::mime_encode([\$str]);

…なんだか関数の引数が暗号です。 最初、単純に Jcode::mime_encode($str) としてみたんですが、これでは動きませんでした。 それでいろいろ試行錯誤したらこうなりました。 一応、意味としては、[ ] は名無しの配列への参照(リファレンス)で、 [ ] の中は配列の要素、 \ は変数への参照を得る記号で、結局 [\$str] は 「文字列$strへの参照を要素に持つ名無しの配列への参照」となります。 参照(リファレンス)という新しい言葉が出てきてしまいました。

なお、この mime_encode は MIME::Base64 というものが必要なんだそうです。 なので、これが無いと動かないらしいのですが、このサイトが使っているサーバーには ありました。標準で入っているものなのかな?

リファレンス

参照、リファレンスという言葉がでてきました。 だんだん難しくなってきました。 ちょっとだけ覚え書きしておきます。 今まで、変数というと、1つの文字列または数値を入れておくスカラー変数、 スカラー変数が複数集まった配列、それに連想配列の3種類がありました。記号は $ @ % でした。 で、例えば、配列の配列とか、連想配列の配列とか、そんなものが欲しくても、 それは出来ませんでした。それができるようになるのが、リファレンスというものだそうです。

リファレンスを得るには、\(変数)とします。

\$scalar
\@array
\%hash

という感じです。で、このリファレンスというやつは、普通の変数 $変数 に入れることができます。 あるいは、配列の要素や連想配列の値などにも入れられるようです。

$scalarref = \$scalar;
$arrayref = \@array;
$hashref = \%hash;

こうすると、配列や連想配列をあたかも1つの変数かのように扱うことができます。 なので、配列の配列とか、配列の連想配列などが簡単に扱えるわけです。 そして、もう1つポイントがあります。これを使うと、関数の引数に配列や連想配列渡すことができますね。 …こいつはびっくり。

今まで、配列を関数に渡したいとか、連想配列を関数に渡したいとかいうことがありました。 私はそれは出来ないんだと思ってました。配列は展開されて複数の要素が、 連想配列も展開されてキー、値、キー、値…というように引数に渡されるんだと。 でも、このリファレンスという仕組みを使えば、配列や連想配列を関数の1つの引数として 渡すことができたんですね!!!もっと早く知っていたら良かったんですが、知りませんでした…。 なんだか、すごく便利そうな機能です。 今度、関数に配列や連想配列を渡したいときなどはこれを使ってみようと思います。

さて、ここで mime_encode の引数の話をすると、 mime_encode は引数として、「参照を要素にもつ配列の参照」を必要とするみたいなんです。 参照が渡されることを前提に書かれた関数には、ちゃんと参照を渡さないとだめなんですね。 それで、上に書いたような形の引数になっています。

セキュリティとmime_encode

メールヘッダに埋め込む変数に改行が入らないように注意するという メール送信のセキュリティ上の鉄則ですが、mime_encodeを通すと改行は消えるようです。 ちょっとありがたいですね。

作成したCGIプログラム

sendmail.cgi
#!/usr/bin/perl

require 'getformdata.pl';
require 'sendmail.pl';

%form = plab::getformdata();

print "Content-type: text/html\n";
print "\n";

if (plab::sendmail(
	$form{'name'},
	$form{'from'},
	"PerlLabo",
	'info@perl-labo.org',
	$form{'subject'},
	$form{'message'}))
{
	print "メールを送信しました。ありがとうございました";
}
else
{
	print "メールの送信に失敗しました。";
}
sendmail.pl
package plab;

# Jcode.pm を使用します。
use Jcode;

# sendmailのパス
$sendmailpath = '/usr/sbin/sendmail';

# 引数 (差出人名, 差出人メールアドレス, 送信先名, 送信先メールアドレス, 件名, 本文)
# いずれも空欄可
sub sendmail
{
	local ($ip, $host, @tim, $year, $mon, $t);
	local ($jis_body, $jis_from_name, $jis_to_name, $jis_subject);
	local ($mime_from_name, $mime_to_name, $mime_subject);
	local ($from_name, $from_adr, $to_name, $to_adr, $subject, $body) = @_;

	# Fromメールアドレスがからっぽだとエラーになるので
	# もしからっぽなら仮に送信先アドレスを入れます
	if ($from_adr eq "") {
		$from_adr = $to_adr;
	}

	# 汚染チェック
	$from_adr  =~ s/\r|\n//g;
	$to_adr    =~ s/\r|\n//g;

	# JIS に変換します。
	$jis_body      = Jcode::convert($body, 'jis');
	$jis_from_name = Jcode::convert($from_name, 'jis');
	$jis_to_name   = Jcode::convert($to_name, 'jis');
	$jis_subject   = Jcode::convert($subject, 'jis');

	# MIMEヘッダ内の日本語をエンコードします。
	$mime_from_name = Jcode::mime_encode([\$jis_from_name]);
	$mime_to_name   = Jcode::mime_encode([\$jis_to_name]);
	$mime_subject   = Jcode::mime_encode([\$jis_subject]);

	if (! open(MAIL, "| $sendmailpath -t")) {
		return 0;
	}

	# メールヘッダ〜本文を出力します。
	print MAIL "From: $mime_from_name<$from_adr>\n";
	print MAIL "To: $mime_to_name<$to_adr>\n";
	print MAIL "Subject: $mime_subject\n";
	print MAIL "Content-Transfer-Encoding: 7bit\n";
	print MAIL "Content-Type: text/plain; charset=\"ISO-2022-JP\"\n";
	print MAIL "\n";
	print MAIL "$jis_body\n";

	# ホスト名、送信時間を付加します。
	$ip   = $ENV{'REMOTE_ADDR'};
	$host = gethostbyaddr(pack("C4", split('\.', $ip)), 2) || $ip;
	@tim  = localtime(time());
	$year = $tim[5] + 1900;
	$mon  = $tim[4] + 1;
	$t    = sprintf('%02d:%02d:%02d', $tim[2], $tim[1], $tim[0]);

	print MAIL "\n";
	print MAIL "----------------------------\n";
	print MAIL "DateTime : $year/$mon/$tim[3] $t\n";
	print MAIL "Host     : $host\n";

	close MAIL;

	return 1;
}

1;

実行結果

適当にメッセージを入れて送信しちゃってください (^^ 送信先は当サイト管理人ですので、なにかメッセージなど頂けると嬉しいです。

お名前(任意):
メールアドレス(任意):
件名(任意):
本文(任意):
 

考察

動きました

無事動きました。 これで、メールの送信が正しく行えるようになりました。 文字コードの変換はしなくても賢いメーラーならちゃんと読めるわけですが、 大切な連絡メールが文字化けして読めないなんてことになると悲しいですからね!

新しくでてきたこと

今回の研究では、新しいことがたくさんでてきました。その中で、オブジェクト指向、-> という記号での 関数呼び出し、参照、そんなものについてはちょっと触れただけでまだ良く分かっていません。 Perlって深いんですねぇ。Perlの仕様書とか、膨大な量だったりするんでしょうね。 でも、Perlの文法とかを研究するのが目的ではないので、それらは必要なときに 研究することにして、今回はそういうものがあるんだっていう程度で済ませています。 でもなんだか、知識が増えてきて、Perlそのものが面白く感じるようになりつつあります (^^

分かったこと

  1. メール本文はJISコードで送るのが正しいです。
  2. メールヘッダ内の日本語はJISコードをBASE64でエンコードしたものにするのが正しいです。
  3. メールヘッダ内では日本語部分は =?ISO-2022-JP?B?(BASE64でエンコードした文字列)?= となります。
  4. ISO-2022-JP というのはJISコードという意味です。
  5. BASE64というのは文字列の変換を行うルールです。
  6. 日本語文字コードの変換ライブラリ jcode.pl というものが無料で配布されています。
  7. jcode.pl は Jcode.pm モジュールとなってPerlに標準で付いてきます。
  8. モジュールはテキストファイルで中身はPerlのプログラムです。
  9. ソースコードの中にドキュメントを記述する方法として pod というものがありますがよく分かりません。
  10. モジュールを使うには use モジュール名; とします。
  11. モジュール内の関数を呼び出すにはパッケージ内の関数を呼び出すのと同じやり方が使えます。
  12. Perlはオブジェクト指向で書くことができて->という記号で関数呼び出しなどができるようですが よく分かりません。
  13. 文字コードをJISにするには Jcode::convert($str, 'jis'); とします。
  14. MIMEヘッダ用にBASE64エンコードするには Jcode::mime_encode([\$str]); とします。
  15. mime_encode は MIME::Base64 というものが必要らしいです。
  16. [ ] は名無しの配列への参照(リファレンス)です。
  17. \ は変数への参照を得る記号です。
  18. リファレンスを使うと、配列の配列とか、連想配列の配列などが作れます。
  19. リファレンスを使うと、関数の引数に配列や連想配列渡すことができます。
Perl/CGI研究室 'PERL-LABO' TOPへ
戻る(History.Back)

Copyright (c) 'PERL-LABO' All Rights Reserved.  リンクフリーです。