Help us understand the problem. What is going on with this article?

symfony/mailerで試行錯誤したこと

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で以下の指定をすると送信ができます。

.env
MAILER_DSN=sendmail+smtp://default
または
MAILER_DSN=sendmail://default

ただし、後述しますが、少し困ったちゃんなのです。

sendmailのDSN調査

Transport Setup」でSMTPやサードパーティの「Amazon SES」、「Gmail」、「Sendgrid」などの設定方法は書いてありますが、「sendmail」はないのです。

未対応なのかな?とソースを確認すると、「SendmailTransportFactory.php」と「SendmailTransport.php」が存在します。

image.png

SendmailTransportFactory.phpの中にsendmailのスキーマの記載がありました!

SendmailTransportFactory.php
final class SendmailTransportFactory extends AbstractTransportFactory
{
~~~省略~~~

    protected function getSupportedSchemes(): array
    {
        return ['sendmail', 'sendmail+smtp'];
    }
}

dockerの開発環境で送信ができない。。。

これでsendmail用のDSNがわかったので、「sendmailでメール送信ができる!」とワクワクして、「Creating & Sending Messages」のサンプルコードを実行しました。

Connection to "process /usr/sbin/sendmail -bs" has been closed unexpectedly.

image.png

どうやらsendmail -bsでないと送信ができないようです。
確認した環境は、docker上のAlpine+ssmtpなのでsendmail -tには対応していますが、sendmail -bsには未対応なのです。

postfixを入れたコンテナ作るのも面倒ですしね。

レンタルサーバで確認 & 困ったことが。。。

面倒ですが、サンプルコードをさくらのレンタルサーバにアップして確認をしてみました。

きちんと送信ができて、以下のように受信できました。
image.png

メールの構造は以下のような感じです。

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 -bssendmail -tに変更する記載があったことを思い出しました。

SendmailTransport.php
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指定でした><

SendmailTransportFactory.php
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で質問してみましたが、無理そうです。

提案されたのは、「自分でクラスを拡張したらよいよ!」ということでした。

image.png
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なので、それを指定します。

.env
MAILER_DSN=smtp://maildev

Creating & Sending Messages」のサンプルコードを実行して完了!と思ったら。。。

image.png

25ポートで接続をするはずのに、465ポートのSMTPSで接続しようとしています。。。
まだまだ、symfony/mailerには知らない秘密があるようです。

ポート番号を指定してみました。

.env
MAILER_DSN=smtp://maildev:25

今度は、25番ポートに接続をするのですが、SSL通信をしようとして失敗しているようです。

image.png

ErrorExceptionをよく見ると、EsmtpTransport.php(112)startTLSを実行しています。

SMTPSは、SSL通信をするために465ポートを用意する必要があります。
しかし、465ポートを用意できない場合があるため、startTLSは、通信をしているポートを利用して、平文から暗号化通信に切り替えるための仕組みのようです。
STARTTLS by ウィキペディア

該当のソースを確認をすると、startTLSを実行する条件がわかります。

EsmtpTransport.php(112)
    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)のコンストラクタの処理の抜粋になります。

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から送られてくるもののようです。

EsmtpTransport.php(130)
    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というコマンドがありました。

image.png

MailDevの方にもオプションがありました!

image.png

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」のサンプルコードを実行したら、無事に送信ができました!

image.png

image.png

最後に

HTMLメールを送信したいのですが、テキストを配信するだけでいろいろと躓いてしまいました。

枯れている技術のswiftmailerに浮気しようか悩み中。

でも、sendgridとか拡張性もあるので、将来性を買って、もう少し使ってみます。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした