モダンPerlの世界へようこそ

第20回Email::Sender:メールを送信する

メール送信のあれこれ

たとえばウェブアプリケーションでなにかの注文を受け取ったとき、あるいはシステム管理ツールでなにか異常を発見したとき、ユーザや管理者にメールを送れるようにしたい、というのはよくある要件です。昔はヒアドキュメントやテンプレートエンジンなどを使って送信したいメールを用意したあと、sendmailへのパイプを開いてメールを流し込んでいたものですが、いまはメールの作成から送信まで、すべてモジュールを使って実現できるようになっています。

とはいえ、需要が大きいだけにメール関係のモジュールは山のようにあります。ディストリビューション(パッケージ)の数だけで500を数えますし、メール関係の名前がついたモジュールは現在CPANにアップロードされている7万以上ものモジュールのほぼ1割を占めるほどです。これではどれを使えばよいのかわからないという声があがるのも当然でしょう。

そこで今回はPerl Email Projectというコミュニティがおすすめするモジュールを中心に、モダンなメールの送信法をまとめてみます。

Perl Email Projectとは?

PEPことPerl Email Projectは、当時よく使われていた「Mail::」ないし「MIME::」で始まるモジュール群の不具合や複雑さに業を煮やしたサイモン・カズンズSimon Cozens氏やリチャード・クランプRichard Clamp氏、サイモン・ウィストウSimon Wistow氏らが2003年頃に始めたプロジェクトです。いまでは新しいツールの作成にとどまらず、バグが放置されている古い関連モジュールのメンテナンスや、既知の問題とその解決策をまとめたりする作業も行われていますが、当初の目的は、Email::SimpleのPODにあるように「使うのもメンテも簡単な、無駄のない、高速で外部依存も最小限にとどめた、正しい」メール関係モジュールをつくることでした。PEPのモジュールは既存のモジュールとの混乱を避けるために「Email::」以下の名前空間にまとめられています。だから、モダンな実装が欲しければ基本的にはその名前空間にあるモジュール群を使っていけばよい、ということになります(逆にいうと、このプロジェクトに関係のないメール関連モジュールにはEmail::という名前をつけないほうがよいでしょう⁠⁠。

Perl Email Projectが管理しているモジュールは、Task::Email::PEP::AllあるいはClass::DBIに依存しているEmail::Storeを外したTask::Email::PEP::NoStoreを使うとまとめてインストールできます。

> cpan Task::Email::PEP::NoStore

また、同プロジェクトのWikiにはおすすめモジュールの一覧などもまとまっています。

Email::MIMEを使ってメールを作成する

さて、今日の本題ですが、メールを送信するにはまずメールそのものを作成する必要があります。そのためのツール自体いくつもありますが、日常的な用途ではEmail::Simpleや、そのラッパであるEmail::MIMEを使うのがおすすめです(Email::Simpleでは添付ファイルなどの処理ができないので、ふつうはEmail::MIMEを使うことになるでしょう⁠⁠。

Email::MIMEを使ってISO-2022-JPベースのごく一般的なメールを作成する場合はこのようになります。

use strict;
use warnings;
use utf8;
use Email::MIME;
use Email::MIME::Creator;
use Encode;
# use Encode::compat::MIME::Header::ISO_2022_JP; # perl < 5.8.8

my $email = Email::MIME->create(
  header => [
    From    => encode('MIME-Header-ISO_2022_JP' => '"F.U." <foo@example.com>'),
    To      => encode('MIME-Header-ISO_2022_JP' => '"B.A." <bar@example.jp>'),
    Subject => encode('MIME-Header-ISO_2022_JP' => 'タイトル'),
  ],
  attributes => {
    content_type => 'text/plain',
    charset      => 'ISO-2022-JP',
    encoding     => '7bit',
  },
  body => encode('iso-2022-jp' => '本文'),
);

# print $email->as_string;

headerには追加したいヘッダのフィールド名とその内容の組を列挙します。このフィールド名はそのままメールヘッダに流用されます(省略表記などはありません⁠⁠。必須ヘッダのひとつであるDateヘッダについては、指定がなければEmail::MIME(や、Email::Simple)のほうで自動的に用意してくれます。特定の時間を指定したい場合はEmail::Date::Formatを使うのが簡単です。

use Email::Date::Format 'email_date'

my $email = Email::MIME->create(
  header => [
    Date => email_date( time - 60 * 60 ),
    ...,
  ],
  body => '...',
);

また、Encodeのバージョンが2.11より古い場合はMIMEヘッダのISO-2022-JPエンコードに対応していません。Perl 5.8.8以降であればコアに入っていますが、それ以前のPerlを使っている方でEncodeをアップグレードできない場合は、Encode::compat::MIME::Header::ISO_2022_JPというモジュールをご利用ください。

単純な用途では特に指定する必要はありませんが、attributesを利用すると一部のヘッダを簡単に表記できます(attributesを使わずheaderのなかで直接指定してしまってもかまいません⁠⁠。本稿執筆時点で対応しているキーはcontent_type、charset、encoding、disposition、name、filename、format、boundaryです。

bodyにはメール本文をスカラー、または配列リファレンスの形で渡せます。

なお、先日公開されたEmail::MIME 1.900以降、headerやbodyのかわりにheader_str、body_strというキーを使うと、attributesで指定したcharsetやencodingの値をもとに内部でエンコードしてくれるようになりました。これは、UTF-8のメールを送る際には非常に便利です。

use utf8;

my $email = Email::MIME->create(
  header_str => [
    From    => '"F.U." <foo@example.com>',
    To      => '"B.A." <bar@example.jp>',
    Subject => 'タイトル',
  ],
  attributes => {
    content_type => 'text/plain',
    charset      => 'UTF-8',
    encoding     => 'base64',
  },
  body_str => '本文',
);

ただし、body_strの方はこれでよいのですが、header_strについてはEncode::MIME::HeaderのエンコードがUTF-8にしか対応していないため、charsetの指定によらずUTF-8でエンコードされてしまいます。ISO-2022-JP、あるいは携帯電話向けにShift-JISなどのヘッダが必要な場合は、strのつかないheaderを使って自分でエンコードしたほうがよいでしょう[1]⁠。

Email::MIMEで添付ファイル付きのメールを作成する

添付ファイル付きのメールを用意する場合は、bodyではなくpartsにEmail::MIMEなどで別途作成したオブジェクトを渡します。

use Path::Class;

my $email = Email::MIME->create(
  header => [ ... ],
  parts => [
    Email::MIME->create(
      attributes => {
        content_type => 'text/plain',
        charset      => 'ISO-2022-JP',
        encoding     => '7bit',
      },
      body => encode(jis => '本文'),
    ),
    Email::MIME->create(
      attributes => {
        content_type => 'application/msword',
        name         => 'myword.doc',
        filename     => 'myword.doc',
        encoding     => 'base64',
        disposition  => 'attachment',
      },
      body => scalar file('myword.doc')->slurp,
    ),
  ],
);

ただし、この場合、現在はEmail::MIMEが各パーツについてもDateヘッダやMIME-Versionヘッダを自動的に追加してしまうため、気になる場合はオブジェクト生成後にパーツを取り出して該当のヘッダを上書き/削除してください。

my @parts = $email->parts;
for my $part (@parts) {
  $part->header_set(Date => ());
  $part->header_set('MIME-Version' => ());
}
$email->parts_set(\@parts);

なお、HTMLメールの場合は上記のやり方のほか、専用のEmail::MIME::CreateHTMLというモジュールを使うこともできます(本稿執筆時点では依存モジュールの指定とPODの表記に問題があるためテストに失敗することがありますが、インストール後の動作には特に問題ないようです⁠⁠。これを使うと、HTML部分に外部のCSSや画像が指定されている場合はネット上からダウンロードしたものを埋め込んでくれるようになります。各パーツに余分なヘッダがついてしまう問題の対処法は素のEmail::MIMEを使う場合と同じです(埋め込み機能を有効にしている場合は埋め込まれたパーツに対しても同様の処理を実行する必要があります⁠⁠。

use strict;
use warnings;
use Email::MIME::CreateHTML;
use Template;

my $vars = {};
my $tt = Template->new({...});
$tt->process('html_mail.tt2', $vars, \my $html);
$tt->process('text_mail.tt2', $vars, \my $text);

my $email = Email::MIME->create_html(
  header => [ ... ],
  body_attributes => {
    content_type => 'text/html',
    charset      => 'UTF-8',
  },
  body      => $html,
  text_body_attributes => {
    content_type => 'text/plain',
    charset      => 'UTF-8',
  },
  text_body => $text,
);

その他の形式のオブジェクトに変換する

日常的な用途ではEmail::MIMEがあれば十分ですが、場合によってはいっしょに使うモジュールの都合で、どうしてもほかの形式のオブジェクトを用意しなければならないこともあります。そのような場合はEmail::Abstractを使うと、Email::MIMEのオブジェクトから適切な形式のオブジェクトを生成できます。

たとえば、メールボックスの管理を行うときに便利なMail::Boxで使われているMail::Message形式に変換する場合はこのようになります。

use Email::Abstract;
use Email::MIME;
use Email::MIME::Creator;

my $email_mime_obj   = Email::MIME->create(...);
my $mail_message_obj = Email::Abstract->new($email_mime_obj)
                                      ->cast('Mail::Message');

Email::Sendでメールを送信する

メールオブジェクトができたので、今度は送信しましょう。

Perl Email Projectの送信用モジュールとしては、ケーシー・ウェストCasey West氏が2004年に書いたEmail::Sendが長らく使われてきました。このモジュールを使うと、先ほどのEmail::MIMEオブジェクトはこのように送信できます。

use strict;
use warnings;
use Email::MIME;
use Email::Send;

my $email = Email::MIME->create(...);

my $ret = Email::Send->new({ mailer => 'Sendmail' })->send($email);
die "$ret" unless $ret;

ここではシステム標準のsendmailを使って送信しましたが、Windows環境のようにsendmailが入っていない場合はNet::SMTPを使って送信することもできます。

my $sender = Email::Send->new({
  mailer      => 'SMTP',
  mailer_args => [ Host => 'smtp.example.com:587' ],
});
my $ret = $sender->send($email);
die "$ret" unless $ret;

また、all_mailersのようなメソッドを使えば、送信に成功するまでありとあらゆる手段を使ってメールの送信を試みることもできます。

my $sender = Email::Send->new;
for my $mailer ($sender->all_mailers) {
  $sender->mailer($mailer);
  my $ret = $sender->send($email);
  last if $ret;
}

単純な使い方をする分には十分だったため、Email::Sendはそれまでよく使われていたMIME::Liteなどを押しのけ、CatalystJiftyなどの標準的なメーラとして採用されるようになりました。

Email::Sendの問題点

ところが、業務で大量にメールを処理する企業などで使われていくうちに、Email::Sendにはさまざまな問題があることがわかってきました。

たとえば、Email::Sendは内部でModule::Pluggableを使っているため、Email::Sendの名前空間のなかに独自の拡張モジュールが入っているとそれもすべて読み込んでしまうのですが、そのせいで一時期Jiftyがインストールされていると、Jifty用のテストモジュールを読み込んでしまうためにEmail::Sendのインストールが失敗するという問題が起こっていました。また、先ほど紹介したall_mailersを使う例のように、ひとつの方法で失敗したら次の方法を試す、というアプローチを取る場合、うっかりしているとEmail::Send::Testで大事なメールをすぐに消えてしまうテスト環境に送信してしまうようなことにもなりかねません。実際の送信を行うプラグインの設定は裏で使われているモジュールにそのまま渡すような形になっていたため、複数のプラグインを利用する際には設定の互換性も問題になりました。送信の途中でメールオブジェクトに手を入れて、特定の条件では特殊なヘッダを追加する、といったこともできるのですが、その変更はあとあとまで残ってしまうため、修正を元に戻す処理を書く必要もあります。値を返すのに使われているReturn::Valueというモジュールにはあまりに落とし穴が多かったため、2010年6月をもって廃止の警告を出すようにする、というアナウンスも行われています。

そのような実装面の問題だけでなく、たとえばメールニュースの送信のようにメールに書かれている送受信者と実際の送受信者が異なる場合の対処など、さまざまな実務上の要望に応えようと思ったとき、現在のEmail::Sendの設計では後方互換性を捨てない限り十分に対応しきれないといった設計上の問題もありました。

そのため、以前から新しいメール送信用モジュール作成の必要性は認識されていましたし、2007年のYAPC::NAでは現在50以上の関連モジュールを管理しているリカルド・シグネスRicardo Signes氏がPEPについての発表のなかで新しいプロジェクトの計画を公表していました。その実装はMIME::Liteを含むほかのさまざまなメール関連ツールのメンテナンスなどもあって遅れに遅れていたのですが、2008年末にはついに氏が勤務先のPobox社でさまざまな改造をほどこしていた私家版Email::Sendをもとにした新しいモジュールが登場しました。それが、現在Email::Sendにかわって推奨されているEmail::Senderです。

Email::Senderの使い方

Email::Senderを使うと、先ほどの例はこのように書き換えられます。

use strict;
use warnings;
use Email::Sender::Simple 'sendmail';
use Email::MIME;

my $email = Email::MIME->create(...);

sendmail($email);

これで、sendmailがあればsendmailで、なければローカルのSMTPサーバを使った送信が行われます。

外部のSMTPサーバを利用したい場合などは、トランスポート層を担当するモジュールのインスタンスを明示的に渡したり、環境変数で設定を切り替えることもできるようになっています。

use Email::Sender::Transport::SMTP;

my $transport = Email::Sender::Transport::SMTP->new(
  host => 'smtp.example.com',
  port => 587,
);

sendmail($email, { transport => $transport });

具体的に送信先を指定したい場合はこのようにします(指定がなければメールヘッダから取得されます⁠⁠。

sendmail($email, { to => [qw( other@example.com )] });

エラーが発生した場合、例外が発生します。evalや、Try::Tinyのようなモジュールでトラップすると、$@(Try::Tinyの場合は$_)でエラー情報が格納されたオブジェクトを受け取れます。

use Try::Tiny;

try   { sendmail($email) }
catch { my $error = $_; warn $error->message };

特にエラーの内容を気にしない場合は、sendmail()のかわりにtry_to_sendmail()を使うと、例外のかわりに単に偽値を返すだけになります。

try_to_sendmail($email) or die "sendmail error!";

その他の使い方についてはEmail::Sender::Manual::QuickStartをご覧ください。

移行状況

Email::Senderの登場以降、Email::Sendの更新履歴などにはEmail::Senderへの移行をうながすメッセージが記されています。Email::SenderはMooseベースということもあり、CPANモジュールレベルではまだそれほど移行は進んでいないようですが、今後本格的なメール送信ツールをつくるのであれば、公式におすすめから外されたEmail::SendやMIME::Liteなどより、Email::Senderを使うほうが無難です。

おすすめ記事

記事・ニュース一覧