はじめに
PHPのメーラーライブラリといえば
こいつが定番でしたが,脆弱性がたびたび見つかったりとかインタフェースがレガシー感満載とかだったりで,正直あまり進んで使う気にはなれませんでした。そんな気持ちでたまたまGitHubを巡回していたら,とてもいい感じのライブラリと巡り会えたので紹介させていただきます。
特長として以下のようなものが紹介されています。
- SMTPサーバもしくはPHPの
mail
関数どちらでも送れる - HTMLパートからプレーンテキストパートを自動生成する
- SMTPサーバへの接続が失敗したときに,指定回数接続を自動リトライする
- SMTPサーバへの送信が失敗したときに,自動でキューに追加する
- ASCII,Base64,QuotedPrintableの中から最適なものを自動選択してエンコードする
- リソースを使っているストリームとコネクション以外はすべてイミュータブルオブジェクト
- Intlエクステンションだけは使うが,それ以外には何も依存しない
ソースコード本体もかなり美しく抽象化されています。
基本的な利用方法
-
new FormattedMessageFactory()
でファクトリクラスを生成 -
->with*()
で本文や添付ファイルなどを付与してボディを組み立てていく -
->createMessage()
でボディを完成 -
->with*()
で送信先や件名などのヘッダーを付与していってメッセージ全体を完成 -
new SmtpTransport()
またはnew PhpMailTransport()
でトランスポータークラスを生成 - 作成したメッセージをトランスポーターの
->send()
に投げて送信
1. ファクトリクラスの生成
use Genkgo\Mail\FormattedMessageFactory;
$factory = new FormattedMessageFactory();
ここは特に説明不要。
2. ボディの組み立て
文章の追加
HTMLとテキストの2種類があります。HTMLからテキストも同時に自動生成する方法がおすすめです。
$factory = $factory->withHtml('<html><body><p>Hello World</p></body></html>');
$factory = $factory->withHtmlAndNoGeneratedAlternativeText('<html><body><p>Hello World</p></body></html>');
$factory = $factory->withAlternativeText('Hello World');
添付ファイルの追加
FileAttachment
を使う方法と ResourceAttachment
を使う方法があります。 FileAttachment
のみContent-Typeの自動判定に対応しています。
use Genkgo\Mail\Mime\FileAttachment;
use Genkgo\Mail\Header\ContentType;
$factory = $factory->withAttachment(new FileAttachment(
__DIR__ . '/example.jpg', // 第1引数 $filename は fopen() するためのパス
new ContentType('image/jpeg'), // 第2引数 $charset は省略すると指定無し
'example.jpg' // 第3引数 $attachmentName は省略すると basename($filename) になる
));
$factory = $factory->withAttachment(new FileAttachment(
__DIR__ . '/example.txt',
new ContentType('text/plain', 'UTF-8') // 第2引数 $charset は省略すると "text/*" の場合 UTF-8 がデフォルト
));
use Genkgo\Mail\Mime\FileAttachment;
$factory = $factory->withAttachment(FileAttachment::fromUnknownFileType(
__DIR__ . '/example.jpg', // 第1引数 $filename は fopen() するためのパス
'example.jpg' // 第2引数 $attachmentName は省略すると basename($filename) になる
)); // $charset は "binary" と判定された場合は追加されない
$factory = $factory->withAttachment(FileAttachment::fromUnknownFileType(
__DIR__ . '/example.txt'
)); // $charset は自動判定で追加される
use Genkgo\Mail\Mime\ResourceAttachment;
use Genkgo\Mail\Header\ContentType;
$factory = $factory->withAttachment(new ResourceAttachment(
fopen(__DIR__ . '/example.jpg', 'rb'), // 第1引数 $resource
'example.jpg', // 第2引数 $filename は名前がややこしいが ↑ の $attachmentName の意味
new ContentType('image/jpeg') // 第3引数 $contentType
));
use Genkgo\Mail\Mime\ResourceAttachment;
use Genkgo\Mail\Header\ContentType;
$factory = $factory->withAttachment(new ResourceAttachment(
fopen(__DIR__ . '/example.jpg', 'rb'), // 第1引数 $resource
'example.jpg', // 第2引数 $filename は名前がややこしいが ↑ の $attachmentName の意味
ContentType::unknown() // 第3引数 $contentType には new ContentType('application/octet-stream') のショートハンドを渡している
));
use Genkgo\Mail\Mime\ResourceAttachment;
use Genkgo\Mail\Header\ContentType;
$factory = $factory->withAttachment(ResourceAttachment::fromString(
file_get_contents(__DIR__ . '/example.jpg'), // 第1引数 $string はバイナリデータ
'example.jpg', // 第2引数 $filename は名前がややこしいが ↑ の $attachmentName の意味
new ContentType('image/jpeg') // 第3引数 $contentType
));
埋め込み画像の追加
HTMLメール本文から参照するための画像を埋め込みます。第1引数にはストリームオブジェクトを渡します。バイナリデータの場合はほぼBase64エンコード1択なので Base64EncodedStream
で問題無いと思います。ファイルパスには未対応のようです。
use Genkgo\Mail\Mime\EmbeddedImage;
use Genkgo\Mail\Stream\Base64EncodedStream;
use Genkgo\Mail\Header\ContentType;
use Genkgo\Mail\Header\ContentID;
$factory = $factory->withEmbeddedImage(new EmbeddedImage(
new Base64EncodedStream(fopen(__DIR__ . '/example.jpg', 'rb')), // 第1引数 $stream
'example.jpg', // 第2引数 $filename は名前がややこしいが ↑ の $attachmentName の意味
new ContentType('image/jpeg'), // 第3引数 $contentType
new ContentID('<unique-image-id%3CXXXXX%3E@example.com>') // 第4引数 $contentID はRFC2392の形式に従って, "<URLエンコードされたユニークな識別子@送信元>" で記述する
));
埋め込んだ画像は以下の形式でHTMLメール本文から参照することができます。
<img src="cid:unique-image-id%3CXXXXX%3E@example.com">
せめて <>
でくくるのとURLエンコードぐらいは自動でやってほしい感。あと作った EmbeddedImage
オブジェクトから ->getCid()
でContent-IDを一発で取ったりしたい。ちょっとこのあたりのサポートがまだ弱く,なんだかんだ自前で書く部分が多いので,今後の開発に期待します。
3. ボディの完成
$message = $factory->createMessage();
ここも特に説明不要。
4. ヘッダーの追加とメッセージの完成
以下の3つのオペレーションがあります。基本的に ->withHeader()
だけで事足りると思います。
-
->withHeader()
… ヘッダーの追加 (存在した場合は上書き) -
->withAddedHeader()
… ヘッダーの追加 (存在した場合も追加) -
->withoutHeader()
… ヘッダーの削除 (存在しなかった場合は無視)
静的なヘルパーメソッドを使うのが一番簡単です。
use Genkgo\Mail\Header\From;
use Genkgo\Mail\Header\To;
use Genkgo\Mail\Header\Subject;
$message = $message
->withHeader(From::fromAddress('mur@example.com', '三浦'))
->withHeader(new Subject('Hello World'))
->withHeader(To::fromArray([['kmr@example.com', '木村'], ['szk@example.com', '鈴木']]));
use Genkgo\Mail\Header\From;
use Genkgo\Mail\Header\To;
use Genkgo\Mail\Header\Subject;
use Genkgo\Mail\Address;
use Genkgo\Mail\AddressList;
$message = $message
->withHeader(new From(new Address(new EmailAddress('mur@example.com', '三浦')))
->withHeader(new Subject('Hello World'))
->withHeader(new To(new AddressList([
new Address(new EmailAddress('kmr@example.com'), '木村'),
new Address(new EmailAddress('szk@example.com'), '鈴木'),
])));
あまり出番は無いですがこういうこともできます。
use Genkgo\Mail\Header\From;
use Genkgo\Mail\Header\To;
use Genkgo\Mail\Header\Subject;
use Genkgo\Mail\Address;
use Genkgo\Mail\AddressList;
$message = $message
->withHeader(new From(Address::fromString('=?UTF-8?B?5LiJ5rWm?= <mur@example.com>'))
->withHeader(new Subject('Hello World'))
->withHeader(new To(AddressList::fromString('=?UTF-8?B?5pyo5p2R?= <kmr@example.com>,=?UTF-8?B?6Yi05pyo?= <szk@example.com>')));
他にも CC
BCC
ReplyTo
Sender
Date
MessageID
とかお馴染みのヘッダーがありますが紹介は割愛。
-
CC
BCC
ReplyTo
はTo
とほとんど同じ扱いで,AddressList
を必要とします。 -
Sender
Date
MessageID
はこのあと紹介する方法で自動付与できます。
5. トランスポータークラスの生成
- SMTPサーバを経由させる場合は
SmtpTransport
クラスを使用します。 - PHPの
mail
関数を利用する場合はPhpMailTransport
クラスを使用します。
メールプロトコルまわりあまり詳しくないのでちゃんと説明できない…
SMTPサーバを経由させる
use Genkgo\Mail\Transport\SmtpTransport;
use Genkgo\Mail\Protocol\Smtp\ClientFactory;
use Genkgo\Mail\Transport\EnvelopeFactory;
$transport = new SmtpTransport(
ClientFactory::fromString('smtp://user:pass@host/')->newClient(), // 第1引数 $client にはSMPTPクライアントを渡す(このようにスキームを含む文字列形式で書くのが最も簡単)
EnvelopeFactory::useExtractedHeader() // 第2引数 $envelopeFactory にはエンベロープのファクトリクラスを渡す
); // ::useExtractedHeader() 以外の例が無いのでとりあえずこれを渡しておけばいいと思う(適当
use Genkgo\Mail\Transport\SmtpTransport;
use Genkgo\Mail\Protocol\Smtp\ClientFactory;
use Genkgo\Mail\Transport\EnvelopeFactory;
$transport = new SmtpTransport(
ClientFactory::fromString('smtp://user:pass@host/')->withEhlo('example.com')->newClient(), // ->withEhlo() では自分のドメインを指定
EnvelopeFactory::useExtractedHeader()
);
PHPのmail
関数を利用する
use Genkgo\Mail\Transport\PhpMailTransport;
use Genkgo\Mail\Transport\EnvelopeFactory;
$transport = new PhpMailTransport(EnvelopeFactory::useExtractedHeader()); // 第1引数 $envelopeFactory にはエンベロープのファクトリクラスを渡す
use Genkgo\Mail\Transport\PhpMailTransport;
use Genkgo\Mail\Transport\EnvelopeFactory;
$transport = new SmtpTransport(EnvelopeFactory::useExtractedHeader(), ['-X /path/to/logfile']); // 第2引数 $parameters には sendmail に渡す -f 以外のパラメータを指定する (-f は自動付与される)
作成したトランスポーターに Sender,Date,Message-ID を自動付与させる
use Genkgo\Mail\Transport\InjectStandardHeadersTransport;
$transport = new InjectStandardHeadersTransport($transport, 'example.com'); // 第1引数にトランスポーター,第2引数に自分のドメインを指定
6. 送信!
$transport->send($message);
例外が飛ばなければ送信成功です。(返り値はvoid
)
発生しうる例外は mail/src/Exception at master · genkgo/mail に一覧があります。
とりあえずすべての例外を捕捉したい,というときは
use Genkgo\Mail\Exception\AbstractException as GenkgoMailException;
try {
} catch (GenkgoMailException $e) {
}
という感じで全体を括っておきましょう。
発展: 失敗時の対応
InjectStandardHeadersTransport
ですでにトランスポーターをデコレートするパターンについては紹介していますが,この他に失敗の対応に関わるものとして以下の2つがあります。
RetryIfFailedTransport
QueueIfFailedTransport
失敗時にリトライ
use Genkgo\Mail\Transport\QueueIfFailedTransport;
$transport = new QueueIfFailedTransport($transport, 5); // 第1引数にトランスポーター,第2引数にリトライ回数を指定
試行間隔は 0秒 です。指数的に試行間隔を増やしたいなどのニーズがあれば,これに相当する処理は自分で書くべきでしょう。
失敗時にキューイング
キューには以下のものがあります。ArrayObjectQueue
以外は外部プロセスとして起動し,一定時間ごとにQueueProcessor::process()
をコールするイメージです。sleep
関数を挟んだ無限ループ,および定期的なcron実行によって運用します。
-
RedisQueue
… ストレージとしては一番優れているがRedisおよびpredis/predis
が必要 -
FilesystemQueue
… 気軽に使える -
ArrayObjectQueue
… ReactPHPやSwooleなど,変数が永続的に保持されるWebサーバで動かしているときだけ使える
エンキュー処理
use Genkgo\Mail\Transport\QueueIfFailedTransport;
use Genkgo\Mail\Queue\RedisQueue;
use Predis\Client;
$redis = new Client('tcp://127.0.0.1:6379');
$queue = new RedisQueue($redis, 'GenkgoMailQueue'); // 第1引数にRedisクライアント,第2引数にRPUSH/LPOPの際に使用するキーを指定
$transport = new QueueIfFailedTransport([$transport], [$queue]); // 第1引数にトランスポーターの配列,第2引数にキューの配列を指定
use Genkgo\Mail\Transport\QueueIfFailedTransport;
use Genkgo\Mail\Queue\FilesystemQueue;
$queue = new FilesystemQueue(__DIR__ . '/GenkgoMailQueue'); // 第1引数にディレクトリ,第2引数に作成されるEMLファイルのパーミッションを指定(デフォルトは0750)
$transport = new QueueIfFailedTransport([$transport], [$queue]); // 第1引数にトランスポーターの配列,第2引数にキューの配列を指定
デキュー処理
use Genkgo\Mail\Queue\QueueProcessor;
$processor = new QueueProcessor($transport, [$queue]); // 第1引数にトランスポーター,第2引数にキューの配列を指定
while (true) {
$processor->process();
sleep(30);
}
注意
**今日初めてGitHub上で見たソースを追って脳内デバッグしながら書いた記事です。**信憑性に難があります。ぜひ実際に動作させておかしいところとか教えてください。まだドキュメントがあまり整備されていないので書くだけで疲れてしまった…