📩

Laravel の開発時、メールのデバッグってどうやってますか?

2023/12/21に公開

はじめに

メールを送る処理の実装時うっかり誰かにメールを送ってしまうことはありませんか?
開発時のメールの誤送信事故を防止するために、ダミーのメールサーバに送るということが多いかと思います。
MailCatcher や MailHog、Mailpit などのツールで解決されている方も多いのではないでしょうか。
弊プロダクトでは、ツールは利用せずにログにメールの送信内容を出力しています。
この記事では、Laravel を利用した開発時にログにメールの送信内容を出力するアプローチでのメリットとデメリット、ログの可読性を上げるための工夫など私たちが行っていることを紹介します。

前提

  • 大量のメールを一括送信するようなケースは想定していない
  • HTML形式のメールのデバッグは想定していない

ツールを使うことの利点

代表的なツールとして、MailCatcher や MailHog、Mailpit などがよく挙げられます。
ツールを利用するメリットとしては下記が考えられます。

  • Web UI から、メールのヘッダーや本文、HTMLレンダリングされた結果などを簡単に確認することができる
  • Dockerイメージが提供されていることもあり CI/CD に容易に組み込める
  • API が提供されているものであれば、API を通じて正しく送信されたこと、内容が正しいことを検証することができる

一方で、ツールを導入するには工数がかかります。
ツールの選定や初期設定、導入後のアップグレードなどの保守コストが発生するため、導入を決断するには少し慎重になると思います。

弊社では、導入時の保守コストが少なく、よりシンプルな手法を模索しておりました。

ログに出力するメリットとデメリット

メリット

  • 実際のメールサーバを使用しないので、うっかり実際のユーザーにメールを送信してしまうリスクがない
  • メール内容が直接ログに出力されるため、リアルタイムにメールの送信内容を確認できる
  • 外部サービスやSMTPサーバに依存せずに開発が可能になる
  • 追加のサービスやツールを設定する必要がなく、開発環境がシンプルになる

ツールを利用した際のメリットとかぶるところもあるかもしれませんが、ログ出力の方がシンプルで保守コストの少ない案になったと考えております。

デメリット

  • メールの視覚的検証が難しい
  • 実際のメール送信プロセスをエミュレートすることができない

ログ出力にしたことで見やすさは確かに劣りますが、テキストメールであれば我慢できるレベルです(HTMLメールは厳しいです)。
また、ログ出力では実際のメール送信プロセスを確認することはできませんが、テスト観点を切り分けて、別のフェーズ(結合テストなど)でメール送信プロセスを確認すれば問題ないと考えています。

ログにメールの内容を出力する

ログドライバーを設定

メールをログに出力するには、メールのログドライバーを利用します。
ログドライバーを利用するには、環境変数 MAIL_MAILERlog を設定します。
標準では SMTPドライバーが選択されますが、MAIL_MAILERlog を設定することでログドライバーを選択することが可能です。

.env.local
MAIL_MAILER=log

出力結果

実際にメールの送信処理を実行してデバッグしてみます。

[2023-12-19 05:11:19] local.DEBUG: From: XXXXXXX@example.com
To: XXXXXXX@example.com
Subject: =?utf-8?Q?=E3=80=90=E3=81=88=E3=82=93=E3=81=95?=
 =?utf-8?Q?=E3=81=8C=E3=81=9D=E3=81=A3=E2=99=AA?=
 =?utf-8?Q?=E3=80=91=E4=BB=AE=E7=99=BB=E9=8C=B2?=
 =?utf-8?Q?=E3=81=AE=E5=8F=97=E4=BB=98?=
MIME-Version: 1.0
Date: Tue, 19 Dec 2023 05:11:19 +0900
Message-ID: <d9e579f978972f2ec3123617649c9eec@example.com>
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable

=E3=81=93=E3=81=AE=E5=BA=A6=E3=81=AF=E3=80=8C=E3=81=88=E3=82=93=E3=81=95=
=E3=81=8C=E3=81=9D=E3=81=A3=E2=99=AA=E3=80=8D=E3=82=92=E3=81=94=E5=88=A9=
=E7=94=A8=E3=81=84=E3=81=9F=E3=81=A0=E3=81=8D=E3=81=82=E3=82=8A=E3=81=8C=
=E3=81=A8=E3=81=86=E3=81=94=E3=81=96=E3=81=84=E3=81=BE=E3=81=99=E3=80=82
=
=E7=8F=BE=E5=9C=A8=E4=BB=AE=E7=99=BB=E9=8C=B2=E3=81=AE=E7=8A=B6=E6=85=8B=
=E3=81=A7=E3=81=99=E3=81=AE=E3=81=A7=E3=80=81=E4=B8=8B=E8=A8=98URL=E3=81=
=8B=E3=82=89=E6=9C=AC=E7=99=BB=E9=8C=B2=E3=82=92=E8=A1=8C=E3=81=A3=E3=81=
=A6=E3=81=8F=E3=81=A0=E3=81=95=E3=81=84=E3=80=82

http://localhost:8000/register/?t=xxxxxx

=E6=9C=AC=E3=83=A1=E3=83=BC=E3=83=AB=E3=81=
=AB=E8=A6=9A=E3=81=88=E3=81=8C=E3=81=AA=E3=81=84=E5=A0=B4=E5=90=88=E3=80=
=81=E4=BD=95=E3=81=8B=E3=81=94=E4=B8=8D=E6=98=8E=E3=81=AA=E7=82=B9=E3=81=
=8C=E3=81=82=E3=82=8B=E5=A0=B4=E5=90=88=E3=81=AF=E4=B8=8B=E8=A8=98=E3=82=
=88=E3=82=8A=E3=81=8A=E5=95=8F=E3=81=84=E5=90=88=E3=82=8F=E3=81=9B=E3=81=
=8F=E3=81=A0=E3=81=95=E3=81=84=E3=80=82
http://localhost:8000/contact/
=

=E2=80=BB=E6=9C=AC=E3=83=A1=E3=83=BC=E3=83=AB=E3=81=AF=E3=80=81=E3=82=
=B7=E3=82=B9=E3=83=86=E3=83=A0=E3=82=88=E3=82=8A=E8=87=AA=E5=8B=95=E9=80=
=81=E4=BF=A1=E3=81=95=E3=82=8C=E3=81=A6=E3=81=84=E3=81=BE=E3=81=99=E3=80=
=82=E3=81=93=E3=81=AE=E3=83=A1=E3=83=BC=E3=83=AB=E3=81=AB=E7=9B=B4=E6=8E=
=A5=E3=81=94=E8=BF=94=E4=BF=A1=E3=81=84=E3=81=9F=E3=81=A0=E3=81=8F=E3=81=
=93=E3=81=A8=E3=81=AF=E3=81=A7=E3=81=8D=E3=81=BE=E3=81=9B=E3=82=93=E3=80=
=82
--------------------------------------------------------
=E3=81=
=88=E3=82=93=E3=81=95=E3=81=8C=E3=81=9D=E3=81=A3=E2=99=AA=EF=BC=9Ahttp://lo=
calhost:8000

=E9=9B=BB=E8=A9=B1=E7=95=AA=E5=8F=B7=EF=BC=9A06-4862-7707=

=E3=81=8A=E5=95=8F=E3=81=84=E5=90=88=E3=82=8F=E3=81=9B=EF=BC=9Ahttp://lo=
calhost:8000/contact/
=E9=81=8B=E5=96=B6=E4=BC=9A=E7=A4=BE=EF=BC=9ABABYJO=
B=E6=A0=AA=E5=BC=8F=E4=BC=9A=E7=A4=BE
-----------------------------------=
---------------------

このように、ログドライバーを設定するだけでは quoted-printable エンコードされていてそもそも読めない。且つ、URLをクリックできない状態になってしまいます。

quoted-printable をデコードする処理を追加する

可読性を上げる工夫として quoted-printable をデコードする処理 を追加します。

サービスプロバイダーの仕組みを利用して環境変数 MAIL_MAILER=log の場合、Laravelのメールのログ出力の実装クラス \Illuminate\Mail\Transport\LogTransport を自前の実装クラス(ExtendedLogTransport)に差し替えるアプローチをします。

後述する ExtendedLogTransport クラスでログの出力チャンネルの設定のために必要ですので、環境変数 MAIL_LOG_CHANNEL を追加します。
ここでは、 stderr としています。

.env.local
MAIL_MAILER=log
MAIL_LOG_CHANNEL=stderr

ExtendedLogTransport クラスを作成します。以下のようなイメージです。

\App\Mail\ExtendedLogTransport
class ExtendedLogTransport extends LogTransport implements TransportInterface
{
    public function send(RawMessage $message, Envelope $envelope = null): ?SentMessage
    {
        $this->logger->debug(
            sprintf(
                "Logging instead of sending mail...\nFrom: %s\nTo: %s\nBcc: %s\nSubject: %s\n\n%s\n",
                $this->getAddress($message, "From"),
                $this->getAddress($message, "To"),
                $this->getAddress($message, "Bcc"),
                $this->getSubject($message),
                $this->getBody($message)
            )
        );

        return new SentMessage($message, $envelope ?? Envelope::create($message));
    }

    private function getAddress(Message $message, string $name): ?string
    {
        $header = $message->getHeaders()->get($name);
        if ($header === null) {
            return null;
        }

        return $header->getBodyAsString();
    }

    private function getSubject(Message $message): string
    {
        $header = $message->getHeaders()->get("Subject");
        return $header->getValue();
    }

    private function getBody(Message $message): string
    {
        $body = $message->getBody();
        return $body->getBody();
    }
}

afterResolving() メソッドを利用して MailManager の依存解決後に ExtendedLogTransport を依存注入します。

\App\Providers\LogTransportServiceProvider
class LogTransportServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this->app->afterResolving(MailManager::class, function (MailManager $mailManager) {
            $mailManager->extend("log", function ($config) {
                $logger = $this->app->make(LoggerInterface::class);
                $logger->channel($config['channel']);

                return new ExtendedLogTransport($logger);
            });
        });
    }
}

調整後の出力結果

[2023-12-19 05:16:55] local.DEBUG: Logging instead of sending mail...
From: XXXXXXX@example.com
To: XXXXXXX@example.com
Bcc: 
Subject: 【えんさがそっ♪】仮登録の受付

この度は「えんさがそっ♪」をご利用いただきありがとうございます。
現在仮登録の状態ですので、下記URLから本登録を行ってください。

http://localhost:8000/register/?t=xxxxxx

本メールに覚えがない場合、何かご不明な点がある場合は下記よりお問い合わせください。
http://localhost:8000/contact/

※本メールは、システムより自動送信されています。このメールに直接ご返信いただくことはできません。
--------------------------------------------------------
えんさがそっ♪:http://localhost:8000

電話番号:06-4862-7707
お問い合わせ:http://localhost:8000/contact/
運営会社:BABYJOB株式会社
--------------------------------------------------------

これで実際に送信したメールとほぼおなじ状態で表示することができるようになりました!

おわりに

簡単ではありますがログにメールの送信内容を出力するアプローチでの実装方法やメリットとデメリット、出力結果などを紹介させていただきました。
最新の Laravel10 であれば、記事中で実施していた「quoted-printable をデコードする処理」も不要で、より簡単にログでのメールのデバッグが可能です。
もし他にも「もっと良いやり方」や「こうした方がいいよ」などがありましたらコメント等で教えて頂けますと嬉しいです!

最後までお読みいただきありがとうございました!

参考リンク

BABY JOB  テックブログ

Discussion