■ perlでUnicode(UTF-8)で書かれたメールを送信する方法
事始め

ISO-2022-JPじゃ扱えない文字が多くなってきたので、Unicodeを符号化する方法の一つであるUTF-8で書かれたメールを送るにはどうしたら良いかを考えてみました。 UTF-8は8bitのバイト列から構成されるので、7bitデータしか扱えない古いメールサーバーを経由された際に、文字化けしないようbase64形式でMIMEエンコードしてからメールを送るのが良いです。

UTF-8で書かれたメールをやり取りするためには、メールを受信する相手が、UTF-8に対応したメーラーを使っている事、使いたい文字が入ったフォントを持っている事が必要となります。


実行環境

perl-5.8.5、perl-5.10.0、perl-5.12.0、perl-5.14.0、perl-5.24.1、perl-5.32.1 Active Perl 5.12.4 Build 1205 で動作を確認しました。(5.8.0以降なら動くような気がします。)

UTF-8やMIMEヘッダを扱うためにEncodeモジュール、base64エンコードを行うためにMIME::Base64モジュール、SMTPの通信にNet::SMTPモジュールを使いますが perl-5.8.0 以降では、いずれもperlに内蔵されていますので特に用意する必要はありません。


ソース
CGI等で利用する場合には、クロスサイトスクリプティングなどを起こさないように変数に危険な文字列が入っていないかどうかをチェックする処理を追加してください。従来からのメタ文字の変換に加えて、不正なUTF-8文字列などは排除する必要があります。

use strict;
use utf8;
use Encode;
use MIME::Base64;
use Net::SMTP;

# メール送信に使うSMTPサーバーと、ポート番号、送信者のドメインを設定する。
my $smtp_server = 'mail.example.jp';
my $smtp_port = '25';
my $smtp_helo = 'senderdomain.example.jp';

# 送信者の名前とメールアドレスを設定する。
my $mail_from_name = '佐藤';
my $mail_from = 'satou@example.jp';

# 宛先の宛名とメールアドレスを設定する。
my $mail_to_name = '鈴木';
my $mail_to = 'suzuki@example.jp';

# メールの件名を設定する。
my $subject = 'UTF-8でメールを送ってみます。';

# メールの本文を設定する。
my $message = "Unicode sample.\n";
$message .= "日本語 本日は晴天なり。\n";
$message .= "髙橋\n";
$message .= "ハンカクカナ アイウエオ カキクケコ サシスセソ タチツテト\n";
$message .= "ナニヌネノ ハヒフヘホ マミムメモ ヤユヨ ワヨン\n";
$message .= "السلام عليكم\n";
$message .= "Ⅰ Ⅱ Ⅲ Ⅳ Ⅴ Ⅵ Ⅶ Ⅷ Ⅸ Ⅹ Ⅺ Ⅻ \n";
$message .= "ⅰ ⅱ ⅲ ⅳ ⅴ ⅵ ⅶ ⅷ ⅸ ⅹ ⅺ ⅻ \n";
$message .= "① ② ③ ④ ⑤ ⑥ ⑦ ⑧ ⑨ ⑩ \n";
$message .= "⑪ ⑫ ⑬ ⑭ ⑮ ⑯ ⑰ ⑱ ⑲ ⑳ \n";
$message .= "㊑ ㊒ ㊓ ㊔ ㊕ ㊖ ㊗ ㊘ ㊙ ㊚ ㊛ ㊜ \n";
$message .= "㊝ ㊞ ㊟ ㊠ ㊡ ㊢ ㊣ ㊤ ㊥ ㊦ ㊧ ㊨ \n";
$message .= "☀ ☁ ☂ ☃ ☄ ★ ☆ ☇ ☈ ☉ \n";
$message .= "♠ ♡ ♢ ♣ ♤ ♥ ♦ ♧ ♨ ♩ ♪ ♫ ♬ ♭ ♮ ♯\n";
$message .= "IVS:𪜀𪜁𪜂𪜃𪜄𪜅𪜆𪜇𪜈𪜉𪜊𪜋𪜌𪜍𪜎𪜏\n";
$message .= "IVS:𪜐𪜑𪜒𪜓𪜔𪜕𪜖𪜗𪜘𪜙𪜚𪜛𪜜𪜝𪜞𪜟\n";
$message .= "emoji:😊😇🙂🙃😉😀😃😄😁😆😅😂\n";

# メールヘッダを作成する。
# from、to、件名共にMIME-Header(UTF-8)へエンコードします。

my $mail_header;

# 送信者名、送信者のメールアドレスを、
# From: 送信者名 <送信者メールアドレス> 形式へ変換する。

$mail_header = make_name_addr('From:',$mail_from_name,$mail_from);

# 宛名、宛先のメールアドレスを、
# To: 宛名 <宛先メールアドレス> 形式へ変換する。

$mail_header .= make_name_addr('To:',$mail_to_name,$mail_to);

# 件名をMIMEエンコードする。
$mail_header .= 'Subject: '.encode('MIME-Header',$subject)."\n";

# UTF-8とbase64エンコードを使う事を明記します。
$mail_header .= "MIME-Version: 1.0\n";
$mail_header .= "Content-type: text/plain; charset=UTF-8\n";
$mail_header .= "Content-Transfer-Encoding: base64\n";

# メールヘッダの終わり。(これ以降は本文となります。)
$mail_header .= "\n";

# SMTPでメールを送る。
my $SMTP=Net::SMTP->new($smtp_server,Port=>$smtp_port,Hello=>$smtp_helo);
if (!$SMTP) { die "Error : Can not connect to mail server.\n"; }
$SMTP->mail($mail_from);
$SMTP->to($mail_to);
$SMTP->data();
$SMTP->datasend($mail_header);
$SMTP->datasend(encode_base64(encode('utf8',$message)));
$SMTP->dataend();
$SMTP->quit;

exit;

# 名前とメールアドレスから、name_addr形式のフォーマットを作るサブルーチン。
sub make_name_addr {
# 引数を受け取る。
my ($mail_direction,$mail_name,$mail_address) = @_;

# 末尾にスペースを追加して"From: "または "To: "を作る。
my $name_addr = $mail_direction.' ';

# メールアドレスを囲むブラケットのデフォルト値を設定する。
# 送信者名や宛名が指定されていない時はブラケット無しとする。

my $left_bracket = ' ';
my $right_bracket = ' ';

# 名前(送信者名または宛名)が設定されているか調べる。
if ($mail_name ne "") {
# 名前が設定されていたら、
# 名前をMIMEエンコードして、末尾にスペースを追加する。

$name_addr .= encode('MIME-Header',$mail_name).' ';
# メールアドレスを囲むブラケットを設定する。
$left_bracket = '<';
$right_bracket = '>';
}
# メールアドレスを追加する。
return ($name_addr .= $left_bracket . $mail_address . $right_bracket . "\n");
}

解説

先頭の方から順に解説していきます。

use strict;
use utf8;
use Encode;
use MIME::Base64;
use Net::SMTP;

上記の箇所では、まず use strict; とする事によって変数の宣言を強制しています。変数名の間違いを指摘してくれるので妙なバグに悩まされなくなります。

use utf8; はperlのソースコードがUTF-8で書かれている事を宣言しています。これを行うことによりUTF-8を単なるバイト列ではなく、UTF-8で書かれた文字列としてperlに認識させます。これにより正規表現で異なる文字列が同一のバイトデーターにマッチしてしまう煩わしさが無くなります。

use Encode;はEncodeモジュールを使用する事を宣言しています。Encodeモジュールは様々なエンコード形式の変換をサポートするperlの標準モジュールです。もはや jis / euc-jp / shift_jis / cp932 / UTF-8 を相互に変換するのにJcode.plやJcode.pmは必要ありません。(pageの最後にTipsあり)

use MIME::Base64;はMIME::Base64モジュールを使用する事を宣言しています。これを使ってメールデータをbase64エンコードします。これはperlの標準モジュールです。

use Net::SMTP; はNet::SMTPモジュールを使用する事を宣言しています。これを使ってSMTPサーバーとの交信を、mailやsendmail等の外部コマンドに頼ることなくperlのみで簡単に行うことができます。これもperlの標準モジュールです。

# メール送信に使うSMTPサーバーと、ポート番号、送信者のドメインを設定する。
my $smtp_server = 'mail.example.jp';
my $smtp_port = '25';
my $smtp_helo = 'senderdomain.example.jp';

上記の箇所では、Net::SMTPがSMTPサーバーと接続に必要なメールサーバーのホスト名、ポート番号、送信者のドメイン名を設定しています。

$smtp_portはSMTPサーバーとの接続に使うポート番号です。SMTPの標準的なポート番号は25番なので、25としています。

$smtp_heloはSMTPのHELOコマンド送信時に使われる引数で、メール送信者が名乗るべきドメイン名を指定します。メールを送るperl scriptを設置するコンピューターのFQDN(ホスト名+ドメイン名)を入れるのが理想的です。

# 送信者の名前とメールアドレスを設定する。
my $mail_from_name = '佐藤';
my $mail_from = 'satou@example.jp';

# 宛先の宛名とメールアドレスを設定する。
my $mail_to_name = '鈴木';
my $mail_to = 'suzuki@example.jp';

# メールの件名を設定する。
my $subject = 'UTF-8でメールを送ってみます。';

# メールの本文を設定する。
my $message = "Unicode sample.\n";
$message .= "日本語 本日は晴天なり。\n";
$message .= "髙橋\n";
$message .= "ハンカクカナ アイウエオ カキクケコ サシスセソ タチツテト\n";
$message .= "ナニヌネノ ハヒフヘホ マミムメモ ヤユヨ ワヨン\n";
$message .= "السلام عليكم\n";
$message .= "Ⅰ Ⅱ Ⅲ Ⅳ Ⅴ Ⅵ Ⅶ Ⅷ Ⅸ Ⅹ Ⅺ Ⅻ \n";
$message .= "ⅰ ⅱ ⅲ ⅳ ⅴ ⅵ ⅶ ⅷ ⅸ ⅹ ⅺ ⅻ \n";
$message .= "① ② ③ ④ ⑤ ⑥ ⑦ ⑧ ⑨ ⑩ \n";
$message .= "⑪ ⑫ ⑬ ⑭ ⑮ ⑯ ⑰ ⑱ ⑲ ⑳ \n";
$message .= "㊑ ㊒ ㊓ ㊔ ㊕ ㊖ ㊗ ㊘ ㊙ ㊚ ㊛ ㊜ \n";
$message .= "㊝ ㊞ ㊟ ㊠ ㊡ ㊢ ㊣ ㊤ ㊥ ㊦ ㊧ ㊨ \n";
$message .= "☀ ☁ ☂ ☃ ☄ ★ ☆ ☇ ☈ ☉ \n";
$message .= "♠ ♡ ♢ ♣ ♤ ♥ ♦ ♧ ♨ ♩ ♪ ♫ ♬ ♭ ♮ ♯\n";
$message .= "IVS:𪜀𪜁𪜂𪜃𪜄𪜅𪜆𪜇𪜈𪜉𪜊𪜋𪜌𪜍𪜎𪜏\n";
$message .= "IVS:𪜐𪜑𪜒𪜓𪜔𪜕𪜖𪜗𪜘𪜙𪜚𪜛𪜜𪜝𪜞𪜟\n";
$message .= "emoji:😊😇🙂🙃😉😀😃😄😁😆😅😂\n";

上記の箇所では、送信者(from)の名前とメールアドレス、宛先(to)の宛名とメールアドレス、件名(subject)、メールの本文(message)を設定しています。

# メールヘッダを作成する。
# from、to、件名共にMIME-Header(UTF-8)へエンコードします。

my $mail_header;

# 送信者名、送信者のメールアドレスを、
# From: 送信者名 <送信者メールアドレス> 形式へ変換する。

$mail_header = make_name_addr('From:',$mail_from_name,$mail_from);

# 宛名、宛先のメールアドレスを、
# To: 宛名 <宛先メールアドレス> 形式へ変換する。

$mail_header .= make_name_addr('To:',$mail_to_name,$mail_to);

# 件名をMIMEエンコードする。
$mail_header .= 'Subject: '.encode('MIME-Header',$subject)."\n";

# 名前とメールアドレスから、name_addr形式のフォーマットを作るサブルーチン。
sub make_name_addr {
# 引数を受け取る。
my ($mail_direction,$mail_name,$mail_address) = @_;

# 末尾にスペースを追加して"From: "または "To: "を作る。
my $name_addr = $mail_direction.' ';

# メールアドレスを囲むブラケットのデフォルト値を設定する。
# 送信者名や宛名が指定されていない時はブラケット無しとする。

my $left_bracket = ' ';
my $right_bracket = ' ';

# 名前(送信者名または宛名)が設定されているか調べる。
if ($mail_name ne "") {
# 名前が設定されていたら、
# 名前をMIMEエンコードして、末尾にスペースを追加する。

$name_addr .= encode('MIME-Header',$mail_name).' ';
# メールアドレスを囲むブラケットを設定する。
$left_bracket = '<';
$right_bracket = '>';
}
# メールアドレスを追加する。
return ($name_addr .= $left_bracket . $mail_address . $right_bracket . "\n");
}

上記の箇所では、メールヘッダを格納する$mail_headerという変数を宣言した後に、実際のメールヘッダを作成しています。

make_name_addrはFromやToの指定に使われる、
From: 送信者名 <送信者メールアドレス>
To: 宛名 <宛先メールアドレス>
というフォーマットを作るためのサブルーチンです。

送信者名 や 宛名 が設定されていない時は、
メールアドレスにブラケットの < または > を付加しないようにしてあります。

メールヘッダは7bitである事が求められますが、UTF-8は8bitなのでencodeモジュールを使ってMIMEエンコードを行い、7bitへとデータを変換しています。

文中のMIME-Headerという指定は、UTF-8をMIMEエンコードするという事です。これによりFrom、To、SubjectのいずれにもUTF-8を書けるようになります。

# UTF-8とbase64エンコードを使う事を明記します。
$mail_header .= "MIME-Version: 1.0\n";
$mail_header .= "Content-type: text/plain; charset=UTF-8\n";
$mail_header .= "Content-Transfer-Encoding: base64\n";

上記の箇所では、送信するメールがUTF-8を使って書かれている事、そしてそれがbase64エンコードされて格納されている事を、メールヘッダへ明記しています。

# メールヘッダの終わり。(これ以降は本文となります。)
$mail_header .= "\n";

上記の箇所では、メールヘッダの終了を表す、2つの連続した改行を作るために、改行コードを追加しています。上記のbase64エンコーディングを指定している箇所の、行末にある改行と合わせて2つの連続した改行となります。

# SMTPでメールを送る。
my $SMTP=Net::SMTP->new($smtp_server,Port=>$smtp_port,Hello=>$smtp_helo);

上記の箇所では、Net::SMTPモジュールを使って、SMTPサーバーとの接続を試みています。接続が成功するとSMTPオブジェクトが生成されます。

if (!$SMTP) { die "Error : Can not connect to mail server.\n"; }

上記の箇所では、SMTPオブジェクトが生成されたかどうかを調べて、生成されていない場合にはエラーメッセージを表示してプログラムを終了させています。

$SMTP->mail($mail_from);
$SMTP->to($mail_to);

上記の箇所では、生成したSMTPオブジェクトを使って、FromとToをSMTPサーバーへ送信しています。

$SMTP->data();
$SMTP->datasend($mail_header);

上記の箇所では、$SMTP->data();によってSMTPサーバーにDATAコマンドを送り、これからメールデータ(メールヘッダ+本文)を送信する事を知らせています。

$SMTP->datasend($mail_header);で、先ほど前の方で作っておいたメールヘッダを送信しています。

$SMTP->datasend(encode_base64(encode('utf8',$message)));

上記の箇所では、まずencode('utf8',$message)でメールの本文をperlの内部コードからUTF-8へ変換しています。(※1)

次にencode_base64( )によって、UTF-8となったメールの本文をbase64エンコードしています。

最後に、base64エンコードされたメールの本文を$SMTP->datasend( );によってSMTPサーバーへ送信しています。

※1

perlがUTF-8を単なるバイト列ではなく、UTF-8文字列として認識している状態を、utf8フラグが立った状態といい、utf8フラグが立ったUTF-8文字列はperlの内部コードとなります。

perlの内部コードをprintやopenやその他の手段でperlの外部へ出力する際には encodeを使って何らかの外部コード(UTF-8、shift_jis、euc-jp、jis等)に変換しなければなりません。この変換作業をutf8フラグを取るといいます。

perlの内部コードはUTF-8なのですが、それをUTF-8として外部へ出力する場合にもutf8フラグを取る作業は行わなければなりません。

もしutf8フラグを取らないまま、perlの外部へ内部コードを出力しようとすると(printしたり、openしたり、etc…)perlに「内部コードが外部へ垂れ流れちゃってるよ」と怒られます。
例:Wide character in print at test.pl line 7.

use utf8;したソースの中に記述されたUTF-8文字列には、自動的にutf8フラグが立ちます。
例:use utf8; my $utf8_str = "日本語"; #こうしたとき"日本語"や$utr8_strにはutf8フラグが立っています。

今回は使っていませんがWeb上のフォームから受け取ったUTF-8の文字列をperlにUTF-8の文字列として認識させるためには、decode_utf8を使ってutf8フラグを立てる必要があります。utf8::decode()というものもありますが、不正なUTF-8文字列をスルーしてしまうのでEncode::decode_utf8のほうを使いましょう。
例:use utf8; use Encode; my $utf8_str = decode_utf8 $input;


$SMTP->dataend();
$SMTP->quit;

exit;

上記の箇所では、$SMTP->dataend();でSMTPサーバーに対してメール本体のデータ送信が終わった事を告げ、$SMTP->quit;でSMTPサーバーとの通信を終了しています。


Encodeで文字コード変換を行う際のTips

WindowsのテキストデータをUnicodeに変換する際に、sihift_jis→Unicodeという変換を行うと、"~"という字が"〜"に変わってしまう事があります。これはWindowsで使われているshift_jisというのは厳密にいうとcp932というコードで、shift_jisとは似て非なる物だからです。字形を崩さずに変換する為にはcp932→Unicodeという変換をしなければなりません。Encodeはcp932もサポートしているので使い分けると上手く変換できます。


更新履歴
2010.2.17初版
2011.8. 6メールサーバー接続時のエラーチェックを追加した。
メールサーバー接続時にHELOコマンドを送るようにした。
メールアドレスをname-addr形式に対応させた。
perlのバージョンを5.10.1から5.14.0へ更新した。
libnetライブラリの内蔵時期の説明がおかしかったのを直した。
2017.4.23perl 5.24.1 で動作確認したのでその旨を追記した。
2022.1.1送信者名や宛名が無い時は、メールアドレスにブラケットを付加するのを止めた。
perl 5.32.1 で動作確認したのでその旨を追記した。

Written at 2011.8.6 by ふゆ

UP