Perl からメール送信

2011年12月。 自分用メモ。
email を外部 MUA (sendmail など) に頼らず Perl から直接送信する方法をざくっと検索してみた結果を、単にメモる。 基本的には Email::Sender まわりの使い方。

自分用の、引用ではない手元へのコピペ保存 (本文ほぼまるごと) なので、 ワシ以外の人はオリジナルの各サイトを直接参照してください。 一応気休めに検索ロボットよけとかもしてあるけど。


モダンPerlの世界へようこそ:第20回 Email::Sender:メールを送信する|gihyo.jp … 技術評論社

メール送信のあれこれ

たとえばウェブアプリケーションでなにかの注文を受け取ったとき,あるいはシステム管理ツールでなにか異常を発見したとき,ユーザや管理者にメールを送れるようにしたい,というのはよくある要件です。昔はヒアドキュメントやテンプレートエンジンなどを使って送信したいメールを用意したあと,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を使って自分でエンコードしたほうがよいでしょう(ISO-2022-JPについてはEmail::MIME::Creator::ISO_2022_JPというモジュールを用意しておきました)。

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などを押しのけ,CatalystやJiftyなどの標準的なメーラとして採用されるようになりました。

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を使うほうが無難です。


Perl/メール/Email-Senderによるメール送信 - yanor.net/wiki

スクリプトはEUC-JPで保存する事とする。

#!/usr/bin/env perl

use strict;
use warnings;

use Email::Sender::Simple qw(sendmail);
use Email::Simple;
use Email::Simple::Creator;
use Encode qw/from_to/;

my $from_name  = '名前';
my $from_email = 'saburo@example.net';
my $subject    = '件名';
my $body       = '本文';
my @to_emails  = ( 'taro@example.com', 'jiro@example.co.jp' );

my $_from_email = sprintf( '%s <%s>', $from_name, $from_email );

from_to( $_from_email, 'euc-jp', 'MIME-Header-ISO_2022_JP' );
from_to( $subject,     'euc-jp', 'MIME-Header-ISO_2022_JP' );
from_to( $body,        'euc-jp', 'iso-2022-jp' );

foreach my $to_email (@to_emails) {
    my $email = Email::Simple->create(
        header => [
            To                          => $to_email,
            From                        => $_from_email,
            Subject                     => $subject,
            'Content-Type'              => 'text/plain; charset=iso-2022-jp',
            'Content-Transfer-Encoding' => '7bit',
        ],
        body       => $body,
    );

    sendmail($email);
}

テスト実行

EMAIL_SENDER_TRANSPORT=Print ./1.pl

のようにすると、標準出力にメール情報が表示される。

MIMEエンコードによるヘッダ行の分割について

ただし、上のスクリプトでは、件名等が長いと、

From: =?ISO-2022-JP?B?GyRCTD5BMBsoQg==?= <test@test.com>
Subject:
  =?ISO-2022-JP?B?GyRCN29MPjdvTD43b0w+N29MPjdvTD43b0w+N29MPjdvTD43b0w+GyhC?=
  =?ISO-2022-JP?B?GyRCN29MPjdvTD43b0w+N29MPjdvTD43b0w+N29MPhsoQg==?=
Date: Sat, 29 Oct 2011 16:12:36 +0900

のように分割されるが、この改行コードがCRLFな為、プロバイダ(のメールサーバ)によってはメールヘッダとメールボディの処理が上手く出来ずに、ヘッダが本文に混じる事がある。これを避けるにはEmail::Simple::Creatorの_crlf()内の記述をLFに変更する。


Perlによるメール送信(Email::Sender) | UCWD-Studio.【ホームページ制作 / 京都】

Perlでのメール送信といえばEmail::Sendを利用する方法がスタンダードですが、メンテが終了し、後継のEmail::Senderへの移行が促されているようなのでEmail::Senderの学習テストを兼ねてみました。

今回はEmail::Senderを使った初めってことで、単純にPerlからテキストのメールを送信するだけのケースについて。

use strict;
use warnings;
use utf8;
use Encode qw(encode);
use Email::MIME;
use Email::MIME::Creator;
use Email::Sender::Simple qw(sendmail);
 
my $from    = q("送信者の名前" <from_mail@example.jp>);
my $to      = q("受信者の名前" <to_mail@example.jp>);
my $subject = 'メールの送信テスト';
my $body    = 'ちゃんとメールを受信できてますか?';
 
my $mail = Email::MIME->create(
               'header' => [
                   'From'    => encode( 'MIME-Header-ISO_2022_JP', $from    ),
                   'To'      => encode( 'MIME-Header-ISO_2022_JP', $to      ),
                   'Subject' => encode( 'MIME-Header-ISO_2022_JP', $subject ),
               ],
               'attributes' => {
                   'content_type' => 'text/plain',
                   'charset'      => 'ISO-2022-JP',
                   'encoding'     => '7bit',
               },
               'body' => encode( 'iso-2022-jp', $body ),
           );
 
sendmail($mail);
 
1;

Email::Senderの使い方自体はEmail::Sender::Manual::QuickStart ? search.cpan.orgはもちろん、モダンPerlの世界へようこそ:第20回 Email::Sender:メールを送信する|gihyo.jp … 技術評論社などでも挙げられているので、別段こうしてサンプルコードを晒す意味合いは薄かったりしますが……。


Email::Senderから外部のSMTPサーバ経由でメールを送る | UCWD-Studio.【ホームページ制作 / 京都】

Email::Senderを利用して外部のSMTPサーバ経由でメールを送る場合、Perlによるメール送信(Email::Sender)のサンプルを以下のように書き換えて対応できます。

use strict;
use warnings;
use utf8;
use Encode qw(encode);
use Email::MIME;
use Email::MIME::Creator;
use Email::Sender::Simple qw(sendmail);
use Email::Sender::Transport::SMTP;
 
my $from    = q("送信者の名前" <from_mail@example.jp>);
my $to      = q("受信者の名前" <to_mail@example.jp>);
my $subject = 'メールの送信テスト';
my $body    = 'ちゃんとメールを受信できてますか?';
 
my $mail = Email::MIME->create(
    'header' => [
        'From'    => encode( 'MIME-Header-ISO_2022_JP', $from    ),
        'To'      => encode( 'MIME-Header-ISO_2022_JP', $to      ),
        'Subject' => encode( 'MIME-Header-ISO_2022_JP', $subject ),
        ],
    'attributes' => {
        'content_type' => 'text/plain',
        'charset'      => 'ISO-2022-JP',
        'encoding'     => '7bit',
    },
    'body' => encode( 'iso-2022-jp', $body ),
    );

my $smtp = Email::Sender::Transport::SMTP->new({
    'host'          => 'smtp.example.com',
    'port'          => 587,
    'sasl_username' => 'hoge',
    'sasl_password' => 'foo',
});

sendmail($mail, { 'transport' => $smtp });
 
1;

強調した部分が前回のサンプルから追加したコードです。

外部のSMTPサーバを利用するところまでは以前のエントリーでも取り上げたモダンPerlの世界へようこそ:第20回 Email::Sender:メールを送信する|gihyo.jp … 技術評論社にも書かれてるのですが、「その場合SMTP認証ってどうするの?」という場合は Email::Sender::Transport::SMTP オブジェクトを生成する際に “sasl_username” と “sasl_password” を指定すると認証をクリアできます。


Email::Sender+Email::MIMEで添付ファイル | UCWD-Studio.【ホームページ制作 / 京都】

Email::Sender + Email::MIMEでのメール送信で、ファイルを添付したい場合、CPANにあるEmail::MIMEのサンプルにある通りIO::Allを利用すると、あっという間に対応可能です。

これまで同様、Perlによるメール送信(Email::Sender)のサンプルを元に添付ファイル付きメール送信のコードを書いてみます。

use strict;
use warnings;
use utf8;
use Encode qw(encode);
use Email::MIME;
use Email::MIME::Creator;
use Email::Sender::Simple qw(sendmail);
use IO::All;

my $from    = q("送信者の名前" <from_mail@example.jp>);
my $to      = q("受信者の名前" <to_mail@example.jp>);
my $subject = 'メールの送信テスト';
my $body    = '今度はファイルを添付してみます!!';
my $file    = './test.pdf';

my @parts   = (
    Email::MIME->create(
        'attributes' => {
            'content_type' => 'text/plain',
            'charset'      => 'ISO-2022-JP',
            'encoding'     => '7bit',
        },
        'body' => Encode::encode( 'iso-2022-jp', $body ),
        ),
    Email::MIME->create(
        'attributes' => {
            'fimename'     => $file,
            'content_type' => 'application/pdf',
            'encoding'     => 'base64',
            'name'         => 'sample.pdf',
        },
        'body' => io($file)->all,
        ),
    );

my $mail = Email::MIME->create(
    'header' => [
        'From'    => encode( 'MIME-Header-ISO_2022_JP', $from    ),
        'To'      => encode( 'MIME-Header-ISO_2022_JP', $to      ),
        'Subject' => encode( 'MIME-Header-ISO_2022_JP', $subject ),
        ],
    'parts'  => [ @parts ],
    );

sendmail($mail);

1;

サンプルコードのうち、強調した部分が変更部分です。

で、Email::MIMEのサンプルの通り、encodingを “quoted-printable” しつつマルチバイトを含むファイルを添付しちゃうと受信側で添付ファイルを開けられなくなるので、その場合はbase64を指定します。

あと添付ファイル分のEmail::MIME->create時にnameを指定すると、名前を変更してファイルを添付することが可能です。

それにしてもio('ファイル名')->allで全部片付いてしまうのは便利。


目次へ