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

[Symfony Advent Calendar 2019](https://qiita.com/advent-calendar/2019/symfony) 5日目の記事です。
昨日は[@77webさん](https://twitter.com/77web)の [Symfonyで再利用可能なバンドルのコントローラをテストする方法](https://tech.quartetcom.co.jp/2019/12/04/testing-controller-in-reusable-bundles/) でした！

# はじめに

[Symfony 4.4.0](https://symfony.com/blog/symfony-4-4-0-released)は11/21にリリースだったのですね。

私は、その翌日の、11/22からSymfonyを初めました。

LTSだと長くメンテされるし、安定しているだろうと考えて飛びつきましたが、[Symfony/Mailer](https://symfony.com/doc/4.4/mailer.html)は4.3系から導入されたので、まだまだ成熟には遠く、またノウハウも蓄積されていません。更に検索結果も前任の「[swiftmailer](https://swiftmailer.symfony.com/)」しか出てこないので、試行錯誤を致しました。

参考にしていただければ幸いです。

# sendmailで送信する方法

## TL;DR

まず、はじめに困ったのがsendmailでの送信方法。
結論を先に書くと、DSNで以下の指定をすると送信ができます。

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

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

## sendmailのDSN調査

「[Transport Setup](https://symfony.com/doc/4.4/mailer.html#transport-setup)」でSMTPやサードパーティの「Amazon SES」、「Gmail」、「Sendgrid」などの設定方法は書いてありますが、「sendmail」はないのです。

未対応なのかな？とソースを確認すると、「[SendmailTransportFactory.php](https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Mailer/Transport/SendmailTransportFactory.php)」と「[SendmailTransport.php](https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Mailer/Transport/SendmailTransport.php)」が存在します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/133291/2ec197fb-8b5b-08d5-1cce-e3c8491ba3da.png)


「[SendmailTransportFactory.php](https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Mailer/Transport/SendmailTransportFactory.php)の中にsendmailのスキーマの記載がありました！

```php:SendmailTransportFactory.php
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.**

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/133291/777779c8-16b9-b2f3-a6c6-c44717a484a7.png)

どうやら<code>sendmail -bs</code>でないと送信ができないようです。
確認した環境は、[docker上のAlpine＋ssmtp](https://qiita.com/idani/items/ed363e9fdc97d79d4b47)なので<code>sendmail -t</code>には対応していますが、<code>sendmail -bs</code>には未対応なのです。

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


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

面倒ですが、サンプルコードを[さくらのレンタルサーバ](https://www.sakura.ne.jp/)にアップして確認をしてみました。

きちんと送信ができて、以下のように受信できました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/133291/444dae7b-884e-ae44-6767-8440e4f034e2.png)

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

```text
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_=_--
```

よくよく見ると、ヘッダーに警告が追加されています。


```bash
X-Authentication-Warning: www1970.sakura.ne.jp: hoge owned process doing -bs
```

こちらをググると、
「[メールヘッダに X-Authentication-Warning: が付かないようにする](http://www.omakase.org/mail/_x-authentication-warning.html)」が詳しく紹介されています。

MTAの設定を変更するというのは、できれば避けたいところですね。。。

## <code>sendmail -t</code> で送信する(ペンディング)

さきほどDSNを調査している時に、
「[SendmailTransport.php](https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Mailer/Transport/SendmailTransport.php)」で、<code>sendmail -bs</code>を<code>sendmail -t</code>に変更する記載があったことを思い出しました。

```php: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引数の<code>$command</code>に、<code>'/usr/sbin/sendmail -oi -t'</code>を指定できれば実現できそうです。

しかし、呼び出し元を見ると無情にもNULL指定でした＞＜

```php: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](https://symfony.com/slack)で質問してみましたが、無理そうです。

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/133291/e1e23cbc-a79e-edef-c3e0-54fee880f3ce.png)
https://symfony-devs.slack.com/archives/C3EQ7S3MJ/p1574840970312800?thread_ts=1574811879.302500&cid=C3EQ7S3MJ


最終手段で拡張しましょう。。。


後日、見つけましたが、<code>senmail -t</code>については、Issueも上がっていました。Feature指定でした；；
=> [Mailer component: change sendmail command](https://github.com/symfony/symfony/issues/34386)


# SMTPで配信する

SMTP配信なら使えるということで、[MailDev](http://maildev.github.io/maildev/)に接続を試みます。

本家が[Docker Hub](https://hub.docker.com/r/djfarrelly/maildev/)で公開していますし、[日本語メールに対応されたバージョン](https://qiita.com/kanemu/items/1f2da063c7e5b5477502)もあります。

docker composeで構成したサービス名が**maildev**なので、それを指定します。

```bash:.env
MAILER_DSN=smtp://maildev
```

「[Creating & Sending Messages](Creating & Sending Messages)」のサンプルコードを実行して完了！と思ったら。。。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/133291/998f3d55-ac96-9ec1-3994-4e9b3c6a2080.png)

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

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

```bash:.env
MAILER_DSN=smtp://maildev:25
```

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/133291/6ae82d06-e22d-2710-8387-c57cab28d8ef.png)

ErrorExceptionをよく見ると、[EsmtpTransport.php(112)](https://github.com/symfony/symfony/blob/6c163906f2ca4e3498061380d8f6b40e374badd2/src/Symfony/Component/Mailer/Transport/Smtp/EsmtpTransport.php#L112)で**startTLS**を実行しています。

SMTPSは、SSL通信をするために465ポートを用意する必要があります。
しかし、465ポートを用意できない場合があるため、<code>startTLS</code>は、通信をしているポートを利用して、平文から暗号化通信に切り替えるための仕組みのようです。
※[STARTTLS by ウィキペディア](https://ja.wikipedia.org/wiki/STARTTLS)

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

```php: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]);

           ~~中略~~
        }
```

まず、<code>startTLS</code>になるための1つ目の条件である<code>!$stream->isTLS()</code>というのは、465ポートを指定していない場合になります。

以下、[EsmtpTransport.php(48)](https://github.com/symfony/symfony/blob/6c163906f2ca4e3498061380d8f6b40e374badd2/src/Symfony/Component/Mailer/Transport/Smtp/EsmtpTransport.php#L48)のコンストラクタの処理の抜粋になります。

```php: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;
        }
```

<code>$tls</code>はデフォルトNULLなので、ポート番号が465の場合に<code>$tls</code>がTrueになります。また、ポート番号が465でない場合は、52行目のelseに入りますが、ポート番号が0でlocalhost以外の場合は、<code>$tls</code>がTrueになります。

話はかわりますが、はじめに、DSNで<code>MAILER_DSN=smtp://maildev</code>の場合は、465に接続されたのは、この判定のためですね。

さて、2回目のDSNでは25番ポートを指定しましたので<code>$tls</code>はfalseになり、結果として、<code>!$stream->isTLS()</code>となります。


<code>startTLS</code>になる2つ目の条件は、<code>OPENSSL_VERSION_NUMBER</code>のバージョンです。こちら最新の環境なら問題になることはないと思います。

<code>startTLS</code>になる最後の判定は、<code>\array_key_exists('STARTTLS', $capabilities)</code>になります。

<code>$capabilities</code>は、[EsmtpTransport.php(130)](https://github.com/symfony/symfony/blob/6c163906f2ca4e3498061380d8f6b40e374badd2/src/Symfony/Component/Mailer/Transport/Smtp/EsmtpTransport.php#L130)にありますが、通信対象のサーバ、今回はMailDevから送られてくるもののようです。

```php: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](https://nodemailer.com/extras/smtp-server/)を使っているので、<code>startTLS</code>には対応しているようなのですが、MailDevはISSUE([Support for TLS (via ngrok or somehow?) ](https://github.com/maildev/maildev/issues/182))もあがっているのですが、対応をしてないようです。

対応していないなら、対応していないで、<code>startTLS</code>が使えるなんてMailDevが言わなきゃいいのです。

[NodeMailer](https://nodemailer.com/extras/smtp-server/)のオプションを見ていくと、<code>options.hideSTARTTLS</code>というコマンドがありました。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/133291/6f84c6b8-f7e9-9505-b413-1c50e9953b1d.png)

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/133291/194cf1da-2f97-efd2-9668-3035c25fc4e5.png)


docker-compose.ymlで、MailDevの起動オプションに、<code>--hide-extensions STARTTLS</code>を追加します。

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/133291/e0bdb1d1-ac35-44b1-a324-35b39d3846f9.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/133291/3bad66a3-e25c-ac06-7f27-15d1c9d5c089.png)

# 最後に

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

枯れている技術の[swiftmailer](https://swiftmailer.symfony.com/)に浮気しようか悩み中。

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