Nette Mail
ニュースレターや注文の確認のようなメールを送ろうと考えていますか。Nette Framework は、とても使いやすい API とともに必要な道具を差し出します。ここでは次のことをお見せします。
- 添付ファイルも含めたメールの作り方
- その送り方
- メールとテンプレートの組み合わせ方
インストール
ライブラリは Composerでダウンロードしてインストールします。
composer require nette/mail
メールを作る
メールは Nette\Mail\Messageオブジェクトです。次のように作ります。
$mail = new Nette\Mail\Message;
$mail->setFrom('John <john@example.com>')
->addTo('peter@example.com')
->addTo('jack@example.com')
->setSubject('Order Confirmation')
->setBody("Hello,\nYour order has been accepted.");
指定するパラメータはすべて UTF-8 の文字コードでなければなりません。
jan@příklad.cz
のような国際化されたドメインのアドレスは、メールのサーバーが求める punycode と呼ばれる ASCII
の形へ自動的に変えられます。それには intl の拡張が要ります。
addTo() で受取人を指定するほかに、addCc()
で複写の受取人を、addBcc()
で見えない複写の受取人を指定できます。これらのメソッドは setFrom() も含め、宛先を
3 とおりの書き方で受け取ります。
$mail->setFrom('john.doe@example.com');
$mail->setFrom('john.doe@example.com', 'John Doe');
$mail->setFrom('John Doe <john.doe@example.com>');
HTML で書いたメールの本文は setHtmlBody() メソッドで渡します。
$mail->setHtmlBody('<p>Hello,</p><p>Your order has been accepted.</p>');
テキストの代わりの本文を作る必要はありません。Nette
が自動的に作ってくれます。そしてメールに件名が設定されていなければ、<title>
の要素から取ろうとします。
画像も HTML の本文にきわめて簡単に埋め込めます。画像が実際に置かれているパスを第 2 パラメータで渡すだけで、Nette が自動的にそれをメールに入れます。
// /path/to/images/background.gif を自動的にメールへ足します
$mail->setHtmlBody(
'<b>Hello</b> <img src="background.gif">',
'/path/to/images',
);
画像を埋め込むアルゴリズムは、次の形を探します。<img src=...>、<body background=...>、HTML
の style 属性の中の url(...)、そして特別な書き方 [[...]] です。
メールを送るのがこれ以上簡単になるでしょうか。
メールは絵はがきのようなものです。パスワードやそのほかの資格情報をメールで送っては決していけません。
そのほかのオプション
Message
のオブジェクトでは、返信先のアドレス、戻ってきたメッセージの戻り先、メッセージの優先度も設定できます。
$mail->addReplyTo('reply@example.com', 'Support')
->setReturnPath('bounces@example.com')
->setPriority(Nette\Mail\Message::High);
優先度は定数 Message::High、Message::Normal、Message::Low
のどれかです。
ワンクリックでの購読解除
Gmail と Yahoo
は、ニュースレターのような一斉配信のメールに、メールのクライアントの中でひとクリックの購読解除を差し出すよう求めています。これは
RFC 8058 が定めるヘッダーの組が受け持ち、setUnsubscribe()
メソッドがそれを正しく用意してくれます。
$mail->setUnsubscribe('https://example.com/unsubscribe?token=xyz');
その URL は、ほかの確認なしに、素の HTTP の POST
のリクエストに応えて受取人の購読を解除しなければなりません。第 2 パラメータには、POST
を送れないクライアントのための代わりのメールアドレスを渡せます。それだけでも働きます。$mail->setUnsubscribe(email: 'unsubscribe@example.com')
のようにです。
添付ファイル
もちろんメールにファイルを添えられます。そのために
addAttachment(string $file, ?string $content = null, ?string $contentType = null) メソッドを使います。
// /path/to/example.zip のファイルを example.zip の名前でメールに添えます
$mail->addAttachment('/path/to/example.zip');
// /path/to/example.zip のファイルを info.zip の名前で添えます
$mail->addAttachment('info.zip', file_get_contents('/path/to/example.zip'));
// "Hello John!" の中身の example.txt のファイルを添えます
$mail->addAttachment('example.txt', 'Hello John!');
addEmbeddedFile() でファイルを HTML
の本文に直接埋め込むこともできます。これは作られた MIME の部分を返し、その
Content-ID を HTML
で参照します(これはまさに、画像の自動の埋め込みが内側で使っているしくみです)。
$file = $mail->addEmbeddedFile('/path/to/logo.png');
$mail->setHtmlBody('<img src="cid:' . trim($file->getHeader('Content-ID'), '<>') . '">');
テンプレート
HTML のメールを送るなら、Latteのテンプレートシステムで書くのがよい選択です。どうするのでしょうか。
$latte = new Latte\Engine;
$params = [
'orderId' => 123,
];
$mail = new Nette\Mail\Message;
$mail->setFrom('John <john@example.com>')
->addTo('jack@example.com')
->setHtmlBody(
$latte->renderToString('/path/to/email.latte', $params),
'/path/to/images',
);
email.latte のファイルです。
<html>
<head>
<meta charset="utf-8">
<title>Order Confirmation</title>
<style>
body {
background: url("background.png")
}
</style>
</head>
<body>
<p>Hello,</p>
<p>Your order number {$orderId} has been accepted.</p>
</body>
</html>
Nette はすべての画像を自動的に埋め込み、<title>
の要素をもとに件名を設定し、HTML のテキストの代わりの本文を作ります。
Nette Application での使い方
メールを Nette Application と、つまりプレゼンターと一緒に使っているなら、テンプレートで
n:href の属性や {link}
のタグを使ってリンクを作りたくなるかもしれません。Latte
は既定ではそれらを知りませんが、足すのはとても簡単です。Nette\Application\LinkGenerator
のオブジェクトがリンクを作れて、dependency
injectionで渡してもらえます。
use Nette;
class MailSender
{
public function __construct(
private Nette\Application\LinkGenerator $linkGenerator,
private Nette\Bridges\ApplicationLatte\TemplateFactory $templateFactory,
) {
}
private function createTemplate(): Nette\Application\UI\Template
{
$template = $this->templateFactory->createTemplate();
$template->getLatte()->addProvider('uiControl', $this->linkGenerator);
return $template;
}
public function createEmail(): Nette\Mail\Message
{
$template = $this->createTemplate();
$html = $template->renderToString('/path/to/email.latte', $params);
$mail = new Nette\Mail\Message;
$mail->setHtmlBody($html);
// ...
return $mail;
}
}
テンプレートでは、いつもどおりにリンクを作れます。LinkGenerator で作られたリンクはすべて絶対 URL になります。
<a n:href="Presenter:action">リンク</a>
CSS のインライン化
Nette\Mail\CssInlinerは CSS
の決まりをインラインの style
の属性に変えるので、メールはどのクライアントでも同じように描かれます。Outlook
との互換のために HTML の属性も作ります。
PHP 8.4 以降と dom の拡張が要ります。
ほとんどのメールのクライアントは <style>
のタグへの対応が限られているか、まったく無視します。正しく描かれるようにするには、CSS
の決まりを個々の要素のインラインの style の属性へ変える必要があります。HTML を
inline() に通すだけです。
$inliner = new Nette\Mail\CssInliner;
$html = $inliner->inline($html);
たとえば HTML に次のものが入っているとします。
<style>
p { margin: 0; color: #333; }
a { color: #a0704e; }
</style>
<p>Hello <a href="#">world</a></p>
インライン化のあとの結果は次のようになります(<style>
のタグは残りますが、ここでは短さのために省いています)。
<p style="margin: 0; color: #333">Hello <a href="#" style="color: #a0704e">world</a></p>
<style> のタグはいつも出力に残るので、@media
のクエリなど、インラインにできない決まりも働き続けます。
<style> のタグからスタイルを取り出すほかに、addCss() メソッドで CSS
を渡すこともできます。CSS のインライン化は、HTML を setHtmlBody()
へ渡す前に行う必要があります。
$latte = new Latte\Engine;
$params = [
'orderId' => 123,
];
$html = $latte->renderToString('/path/to/email.latte', $params);
$html = (new Nette\Mail\CssInliner)
->addCss(file_get_contents('/path/to/email.css'))
->inline($html);
$mail = new Nette\Mail\Message;
$mail->setHtmlBody($html);
ある要素の同じプロパティを複数の決まりが指している場合、勝敗はブラウザと同じように
CSS のカスケードが決めます。!important
の宣言はふつうのものに勝ち、すでにあるインラインの style
の属性はどのセレクタにも勝ち、より詳細なセレクタはより漠然としたものに勝ち、同じなら後の決まりが勝ちます。<style>
のタグの決まりは addCss()
で足したものより先に処理され、勝った値だけが書き出されます。
@media や @font-face のような at
規則は、インライン化のときに飛ばされます。:hover
のような擬似クラスは意味のある形でインラインにできないことに注意してください。インラインのスタイルは動的な状態に対応しないからです。
Outlook のための HTML の属性
デスクトップ版の Microsoft Outlook は Word の描画のエンジンを使っていて、多くの CSS
のプロパティを理解しません。互換のために、CssInliner は CSS の決まりから対応する HTML
の属性を、インラインのスタイルと合わせて自動的に作ります。
| CSS のプロパティ | HTML の属性 | 当たる要素 |
|---|---|---|
background-color |
bgcolor |
<table>、<td>、<th>、<body>、<tr> |
width |
width |
<table>、<td>、<th>、<img> |
height |
height |
<table>、<td>、<th>、<img> |
border-spacing |
cellspacing |
<table> |
width、height、cellspacing では px
の単位が自動的に取り除かれ(たとえば width: 600px は width="600"
になります)、百分率は % を保ち、auto や calc()
のように属性で表せない値では属性そのものが作られません。インラインのスタイルと HTML
の属性は一緒に設定されるので、今どきのクライアントでも Outlook
でもメールは正しく描かれます。
HTML の属性は CssInliner が処理した CSS の決まりからだけ作られ、もとの HTML
にすでにある style の属性からは作られません。
メールを送る
Mailer はメールを送ることを受け持つクラスです。Nette\Mail\Mailerインターフェースを実装していて、できあいの mailer がいくつか用意されています。それらを紹介します。
フレームワークは設定をもとに Nette\Mail\Mailer のサービスを DI
コンテナに自動的に足すので、dependency
injectionで渡してもらえます。
SendmailMailer
既定の mailer は SendmailMailer で、PHP の mail関数を使います。使い方の例です。
$mailer = new Nette\Mail\SendmailMailer;
$mailer->send($mail);
returnPath
を設定したいのにサーバーがそれを上書きしてしまうなら、$mailer->commandArgs = '-fmy@email.com'
を使ってください。
既定では、SendmailMailer は送信者のアドレスを mail()
の関数へエンベロープの送信者(-f の引数)として渡します。これは
$mailer->setEnvelopeSender(false) で切れます。
SmtpMailer
SMTP のサーバー経由でメールを送るには SmtpMailer を使います。
$mailer = new Nette\Mail\SmtpMailer(
host: 'smtp.gmail.com',
username: 'john@gmail.com',
password: '*****', // あなたのパスワード
encryption: 'ssl', // または 'tls'
);
$mailer->send($mail);
コンストラクタには次の追加のパラメータも渡せます。
port– 設定しなければ既定が使われます。sslなら 465、tlsなら 587、そのほかは 25 ですtimeout– SMTP の接続の制限時間persistent– 持続する接続を使いますclientHost– クライアントのホストのヘッダーを指定しますstreamOptions– 接続の SSL の文脈のオプション を設定できます
OAuth 2.0 による認証
Gmail と Microsoft 365 は SMTP のパスワードによる認証をやめつつあり、代わりに OAuth 2.0
のアクセストークン(XOAUTH2 の仕組み)を求めます。トークンは setAccessToken()
メソッドで渡します。ユーザー名はそのままで、パスワードは空にします。
$mailer = new Nette\Mail\SmtpMailer(
host: 'smtp.gmail.com',
username: 'john@gmail.com',
password: '',
encryption: 'tls',
);
$mailer->setAccessToken($accessToken);
アクセストークンには期限があるので、代わりにコールバックを渡せます。それは接続のたびに呼ばれるので、いつでも新しいトークンを与えられます。トークンを得たり更新したりするのは、あなたか OAuth のライブラリの仕事のままです。
$mailer->setAccessToken(fn() => $oauthProvider->getFreshToken());
FallbackMailer
この mailer はメールを直接送らず、いくつかの mailer を通した送信の仲立ちをします。ひとつの mailer が失敗したら、次のもので試します。最後のものが失敗したら、また最初のものから始めます。
$mailer = new Nette\Mail\FallbackMailer([
$smtpMailer,
$backupSmtpMailer,
$sendmailMailer,
]);
$mailer->send($mail);
コンストラクタのほかのパラメータは、やり直しの回数(既定は
3)と、そのあいだの待ち時間をミリ秒で表したもの(既定は
1000)です。どの試みでもすべての mailer が失敗すると
Nette\Mail\FallbackMailerException が投げられ、その $failures
のプロパティに集められた例外が入ります。
たとえば SMTP のサーバーが資格情報を拒むといった、失敗が変わらない mailer は、そのあとの試みから外されます。やり直しても結果は変わらないからです。
addMailer() であとから別の mailer を足せますし、失敗した試みのたびに呼ばれる
$onFailure のイベントも登録できます。
$mailer->onFailure[] = function ($mailer, $exception, $failedMailer, $mail) {
// たとえば失敗した試みを記録します
};
FileMailer
この mailer は何も送りません。それぞれのメッセージを .eml
のファイルとして、指定したディレクトリに書きます。このファイルはどのメールのクライアントでも開けるので、何が送られたはずかを正確に確かめられます。テストや開発のときに便利です。
$mailer = new Nette\Mail\FileMailer('/path/to/mails');
$mailer->send($mail);
メールのデバッグ
開発中やステージングのサーバーでは、試しのメールが本物のお客さまへ漏れてほしくはありません。それが決して起きないようにする方法が 2 つあります。
手元の環境でおすすめの構えは、Mailpit や MailHog のような軽い SMTP
の受け皿を自分の機械で動かすことです。これらはすべてのメッセージを受け取り、ウェブの画面で見せ、決してどこへも転送しません。Nette
Mail を 127.0.0.1:1025 に向けるだけです。
mail:
smtp: true
host: 127.0.0.1
port: 1025
ステージングや、手元の受け皿を動かせない環境のために、Nette Mail
には組み込みの転送があります。設定で宛先を決めると、すべての
To、Cc、Bcc の受取人がそれに置き換わります。Nette Mail
はもとの宛先を X-Original-*
のヘッダーに残すので、そのメールが誰宛てだったかを見られますし、件名に印を付けることもできます。
mail:
redirect:
to: dev@example.com
subjectPrefix: '[debug]' # 省略できます
件名の印が要らないなら、短い形 redirect: dev@example.com
が使えます。デバッグモードでは、送られたすべてのメールを並べる Tracy Barのパネルが自動的に付きます。
内側ではこれを Nette\Mail\Interceptorが受け持ち、独自の聞き手(監査のログ、計測など)のために
$onSent のイベントも差し出します。
DKIM
DKIM(DomainKeys Identified Mail)はメールの信頼性を高める技術で、なりすましのメッセージを見つけるのにも役立ちます。送られるメッセージは送信者のドメインの秘密鍵で署名され、その署名はメールのヘッダーに置かれます。受取人のサーバーは、その署名をそのドメインの DNS のレコードに置かれた公開鍵と照らし合わせます。署名が合えば、そのメールが本当に送信者のドメインから来たもので、伝わる途中で変えられていないことが証明されます。
メールに署名する mailer は設定で直接用意できます。dependency injection を使っていないなら、次のように使います。
$signer = new Nette\Mail\DkimSigner(
domain: 'yourdomain.com',
selector: 'dkim', // DNS のレコードのセレクタ
privateKey: file_get_contents('/path/to/dkim.key'), // あなたの秘密鍵へのパス
passPhrase: 'your_passphrase', // 秘密鍵のパスフレーズ(あれば)
);
$mailer = new Nette\Mail\SendmailMailer; // または SmtpMailer
$mailer->setSigner($signer);
$mailer->send($mail);
秘密鍵は PEM 形式の RSA の鍵でも、base64 で符号化された生のバイトとしての
Ed25519 の鍵(RFC 8463
をご覧ください)でもかまいません。種類は鍵そのものから見分けられます。Ed25519
での署名には sodium の拡張が要ります。
oversignHeaders
のパラメータには、すでに署名されたメッセージにもうひとつの複製が足されるのを防ぎたいヘッダーを並べられます。それはなりすましのメールが使う手口で、よくある候補は
From です。
設定
Nette Mail の設定オプションの一覧です。フレームワーク全体ではなくこのライブラリだけを使っているなら、設定の読み込み方をご覧ください。
既定ではメールの送信に Nette\Mail\SendmailMailer
が使われ、ほかの設定は要りません。とはいえ Nette\Mail\SmtpMailer
に切り替えられます。
mail:
# SmtpMailer を使います
smtp: true # (bool) 既定は false
host: ... # (string) SMTP のサーバーのホスト名
port: ... # (int) SMTP のサーバーのポート
username: ... # (string) SMTP の認証のユーザー名
password: ... # (string) SMTP の認証のパスワード
timeout: ... # (int) SMTP の接続の制限時間
encryption: ... # (ssl|tls|null) 既定は null(別名 'secure')
clientHost: ... # (string) クライアントのホスト名。既定は $_SERVER['HTTP_HOST'] か 'localhost'
persistent: ... # (bool) 持続する接続を使います。既定は false
# SMTP の接続のストリームの文脈のオプション。既定は stream_context_get_default()
context:
ssl: # すべてのオプションは https://www.php.net/manual/en/context.ssl.php
allow_self_signed: ...
...
http: # オプションの一覧は https://www.php.net/manual/en/context.http.php
header: ...
...
SSL の証明書の確認は context › ssl › verify_peer: false
のオプションで切れます。それは強くおすすめしません。
アプリケーションが弱くなるからです。代わりに 証明書を信頼の一覧に足して ください。
信頼性を高めるために、DKIM の技術でメールに署名できます。
mail:
dkim:
domain: myweb.com # あなたのドメイン
selector: lovenette # DKIM のセレクタ
privateKey: %appDir%/cert/dkim.key # あなたの秘密鍵のファイルへのパス
passPhrase: ... # 秘密鍵のパスフレーズ(必要なら)
すべてのメールを転送するオプションと、デバッグのパネルを有効にするオプションは、メールのデバッグの節で説明しています。
mail:
# すべてのメールをひとつのアドレスへ転送します
redirect: dev@example.com
# Tracy のパネルとメールの横取りを有効(true)にするか無効(false)にします
debugger: ... # (bool) 既定は null。つまりデバッグモードでは自動
DI のサービス
DI コンテナには次のサービスが足されます。
| 名前 | 型 | 説明 |
|---|---|---|
mail.mailer |
Nette\Mail\Mailer | メールを送るクラス |
mail.signer |
Nette\Mail\Signer | DKIM での署名 |
新しい版へ上げるなら、アップグレードのページをご覧ください。