Symfony Advent Calendar 2019 5日目の記事です。
昨日は@77webさんの Symfonyで再利用可能なバンドルのコントローラをテストする方法 でした!
はじめに
Symfony 4.4.0は11/21にリリースだったのですね。
私は、その翌日の、11/22からSymfonyを初めました。
LTSだと長くメンテされるし、安定しているだろうと考えて飛びつきましたが、Symfony/Mailerは4.3系から導入されたので、まだまだ成熟には遠く、またノウハウも蓄積されていません。更に検索結果も前任の「swiftmailer」しか出てこないので、試行錯誤を致しました。
参考にしていただければ幸いです。
sendmailで送信する方法
TL;DR
まず、はじめに困ったのがsendmailでの送信方法。
結論を先に書くと、DSNで以下の指定をすると送信ができます。
MAILER_DSN=sendmail+smtp://default
または
MAILER_DSN=sendmail://default
ただし、後述しますが、少し困ったちゃんなのです。
sendmailのDSN調査
「Transport Setup」でSMTPやサードパーティの「Amazon SES」、「Gmail」、「Sendgrid」などの設定方法は書いてありますが、「sendmail」はないのです。
未対応なのかな?とソースを確認すると、「SendmailTransportFactory.php」と「SendmailTransport.php」が存在します。
「SendmailTransportFactory.phpの中にsendmailのスキーマの記載がありました!
final class SendmailTransportFactory extends AbstractTransportFactory
{
~~~省略~~~
protected function getSupportedSchemes(): array
{
return ['sendmail', 'sendmail+smtp'];
}
}
dockerの開発環境で送信ができない。。。
これでsendmail用のDSNがわかったので、「sendmailでメール送信ができる!」とワクワクして、「[Creating & Sending Messages](Creating & Sending Messages)」のサンプルコードを実行しました。
Connection to "process /usr/sbin/sendmail -bs" has been closed unexpectedly.
どうやらsendmail -bs
でないと送信ができないようです。
確認した環境は、docker上のAlpine+ssmtpなのでsendmail -t
には対応していますが、sendmail -bs
には未対応なのです。
postfixを入れたコンテナ作るのも面倒ですしね。
レンタルサーバで確認 & 困ったことが。。。
面倒ですが、サンプルコードをさくらのレンタルサーバにアップして確認をしてみました。
メールの構造は以下のような感じです。
Delivered-To: fuga@gmail.com
Received: by 2002:a5d:841a:0:0:0:0:0 with SMTP id i26csp4330355ion;
Mon, 25 Nov 2019 22:05:37 -0800 (PST)
~~~中略~~~
Return-Path: <hoge@example.com>
Received: from www1970.sakura.ne.jp (localhost [127.0.0.1]) by www1970.sakura.ne.jp (8.15.2/8.15.2) with ESMTP id xAQ65aic007417 for <fuga@gmail.com>; Tue, 26 Nov 2019 15:05:36 +0900 (JST) (envelope-from hoge@example.com)
Received: from [127.0.0.1] (hoge@localhost) by www1970.sakura.ne.jp (8.15.2/8.15.2/Submit) with SMTP id xAQ65aQW007416 for <fuga@gmail.com>; Tue, 26 Nov 2019 15:05:36 +0900 (JST) (envelope-from hoge@example.com)
X-Authentication-Warning: www1970.sakura.ne.jp: hoge owned process doing -bs
From: hoge@example.com
To: fuga@gmail.com
Subject: Time for Symfony Mailer!
Message-ID: <0ab330134ce12286539d0ae3112374a4@example.com>
MIME-Version: 1.0
Date: Tue, 26 Nov 2019 15:05:36 +0900
Content-Type: multipart/alternative; boundary="_=_symfony_1574748336_057222644d28500198a452dc1b84b1b1_=_"
--_=_symfony_1574748336_057222644d28500198a452dc1b84b1b1_=_
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable
Sending emails is fun again!
--_=_symfony_1574748336_057222644d28500198a452dc1b84b1b1_=_
Content-Type: text/html; charset=utf-8
Content-Transfer-Encoding: quoted-printable
<p>See Twig integration for better HTML integration!</p>
--_=_symfony_1574748336_057222644d28500198a452dc1b84b1b1_=_--
よくよく見ると、ヘッダーに警告が追加されています。
X-Authentication-Warning: www1970.sakura.ne.jp: hoge owned process doing -bs
こちらをググると、
「メールヘッダに X-Authentication-Warning: が付かないようにする」が詳しく紹介されています。
MTAの設定を変更するというのは、できれば避けたいところですね。。。
sendmail -t
で送信する(ペンディング)
さきほどDSNを調査している時に、
「SendmailTransport.php」で、sendmail -bs
をsendmail -t
に変更する記載があったことを思い出しました。
class SendmailTransport extends AbstractTransport
{
private $command = '/usr/sbin/sendmail -bs';
private $stream;
private $transport;
/**
* Constructor.
*
* If using -t mode you are strongly advised to include -oi or -i in the flags.
* For example: /usr/sbin/sendmail -oi -t
* -f<sender> flag will be appended automatically if one is not present.
*
* The recommended mode is "-bs" since it is interactive and failure notifications are hence possible.
*/
public function __construct(string $command = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null)
{
parent::__construct($dispatcher, $logger);
if (null !== $command) {
if (false === strpos($command, ' -bs') && false === strpos($command, ' -t')) {
throw new \InvalidArgumentException(sprintf('Unsupported sendmail command flags "%s"; must be one of "-bs" or "-t" but can include additional flags.', $command));
}
$this->command = $command;
}
$this->stream = new ProcessStream();
if (false !== strpos($this->command, ' -bs')) {
$this->stream->setCommand($this->command);
$this->transport = new SmtpTransport($this->stream, $dispatcher, $logger);
}
}
コンストラクタの第1引数の$command
に、'/usr/sbin/sendmail -oi -t'
を指定できれば実現できそうです。
しかし、呼び出し元を見ると無情にもNULL指定でした><
final class SendmailTransportFactory extends AbstractTransportFactory
{
public function create(Dsn $dsn): TransportInterface
{
if ('sendmail+smtp' === $dsn->getScheme() || 'sendmail' === $dsn->getScheme()) {
ここ!! => return new SendmailTransport(null, $this->dispatcher, $this->logger);
}
~~省略~~
}
}
まだ、完全に使い方を理解していないDIでなんとかできないかな?と調べてみたり、SlackのSymfony Devsのsupportで質問してみましたが、無理そうです。
提案されたのは、**「自分でクラスを拡張したらよいよ!」**ということでした。
https://symfony-devs.slack.com/archives/C3EQ7S3MJ/p1574840970312800?thread_ts=1574811879.302500&cid=C3EQ7S3MJ
最終手段で拡張しましょう。。。
後日、見つけましたが、senmail -t
については、Issueも上がっていました。Feature指定でした;;
=> Mailer component: change sendmail command
SMTPで配信する
SMTP配信なら使えるということで、MailDevに接続を試みます。
本家がDocker Hubで公開していますし、日本語メールに対応されたバージョンもあります。
docker composeで構成したサービス名がmaildevなので、それを指定します。
MAILER_DSN=smtp://maildev
「[Creating & Sending Messages](Creating & Sending Messages)」のサンプルコードを実行して完了!と思ったら。。。
25ポートで接続をするはずのに、465ポートのSMTPSで接続しようとしています。。。
まだまだ、symfony/mailerには知らない秘密があるようです。
ポート番号を指定してみました。
MAILER_DSN=smtp://maildev:25
今度は、25番ポートに接続をするのですが、SSL通信をしようとして失敗しているようです。
ErrorExceptionをよく見ると、EsmtpTransport.php(112)でstartTLSを実行しています。
SMTPSは、SSL通信をするために465ポートを用意する必要があります。
しかし、465ポートを用意できない場合があるため、startTLS
は、通信をしているポートを利用して、平文から暗号化通信に切り替えるための仕組みのようです。
※STARTTLS by ウィキペディア
該当のソースを確認をすると、startTLS
を実行する条件がわかります。
protected function doHeloCommand(): void
{
~~中略~~
/** @var SocketStream $stream */
$stream = $this->getStream();
if (!$stream->isTLS() && \defined('OPENSSL_VERSION_NUMBER') && \array_key_exists('STARTTLS', $capabilities)) {
$this->executeCommand("STARTTLS\r\n", [220]);
~~中略~~
}
まず、startTLS
になるための1つ目の条件である!$stream->isTLS()
というのは、465ポートを指定していない場合になります。
以下、EsmtpTransport.php(48)のコンストラクタの処理の抜粋になります。
public function __construct(string $host = 'localhost', int $port = 0, bool $tls = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null)
{
~~~中略~~~
if (null === $tls) {
if (465 === $port) {
$tls = true;
} else {
$tls = \defined('OPENSSL_VERSION_NUMBER') && 0 === $port && 'localhost' !== $host;
}
}
if (!$tls) {
$stream->disableTls();
}
if (0 === $port) {
$port = $tls ? 465 : 25;
}
$tls
はデフォルトNULLなので、ポート番号が465の場合に$tls
がTrueになります。また、ポート番号が465でない場合は、52行目のelseに入りますが、ポート番号が0でlocalhost以外の場合は、$tls
がTrueになります。
話はかわりますが、はじめに、DSNでMAILER_DSN=smtp://maildev
の場合は、465に接続されたのは、この判定のためですね。
さて、2回目のDSNでは25番ポートを指定しましたので$tls
はfalseになり、結果として、!$stream->isTLS()
となります。
startTLS
になる2つ目の条件は、OPENSSL_VERSION_NUMBER
のバージョンです。こちら最新の環境なら問題になることはないと思います。
startTLS
になる最後の判定は、\array_key_exists('STARTTLS', $capabilities)
になります。
$capabilities
は、EsmtpTransport.php(130)にありますが、通信対象のサーバ、今回はMailDevから送られてくるもののようです。
private function getCapabilities(string $ehloResponse): array
{
$capabilities = [];
$lines = explode("\r\n", trim($ehloResponse));
array_shift($lines);
foreach ($lines as $line) {
if (preg_match('/^[0-9]{3}[ -]([A-Z0-9-]+)((?:[ =].*)?)$/Di', $line, $matches)) {
$value = strtoupper(ltrim($matches[2], ' ='));
$capabilities[strtoupper($matches[1])] = $value ? explode(' ', $value) : [];
}
}
return $capabilities;
}
MailDevのSMTPサーバは、NodeMailerを使っているので、startTLS
には対応しているようなのですが、MailDevはISSUE(Support for TLS (via ngrok or somehow?) )もあがっているのですが、対応をしてないようです。
対応していないなら、対応していないで、startTLS
が使えるなんてMailDevが言わなきゃいいのです。
NodeMailerのオプションを見ていくと、options.hideSTARTTLS
というコマンドがありました。
MailDevの方にもオプションがありました!
docker-compose.ymlで、MailDevの起動オプションに、--hide-extensions STARTTLS
を追加します。
maildev:
image: kanemu/maildev-with-iconv
ports:
- 8025:80
command: bin/maildev -w 80 -s 25 --hide-extensions STARTTLS
これで「[Creating & Sending Messages](Creating & Sending Messages)」のサンプルコードを実行したら、無事に送信ができました!
最後に
HTMLメールを送信したいのですが、テキストを配信するだけでいろいろと躓いてしまいました。
枯れている技術のswiftmailerに浮気しようか悩み中。
でも、sendgridとか拡張性もあるので、将来性を買って、もう少し使ってみます。