【Laravel】システム内で送信されたメールの eml ファイルを残す

 Laravel は PHP のフレームワークでよく web サイトに関連する何かしらに使われます。そしてその中でメールを送信する機能を作することがあります。このメール送信機能で送ったメールを Laravel 独力で eml ファイル(電子メールの平文ファイル。メーラーでこのファイルを開くと人間に読める形でメールが読めます)としてログに残す方法を紹介します。
 使うのは Laravel のイベント機能と Swift_Message のファイルの出力先指定機能です。
イベント 8.x Laravel
swiftmailer/swiftmailer: Comprehensive mailing tools for PHP
 Laravel のメール送信機能(ここでは Laravel で用意された通知等も含むメール送信処理を実行する機能とします。mb_send_mail 等の素の関数などは app ディレクトリ以下で呼ばれていても Laravel のメール送信機能ではありません)にはイベントが紐づけられており、メール送信イベントを検知して自動で処理を発火させることができます。
メール 8.x Laravel#イベント

<?php
/* /app/Providers/EventServiceProvider.php */

namespace App\Providers;

use Illuminate\Auth\Events\Registered;
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use Illuminate\Mail\Events\MessageSending;
use Illuminate\Mail\Events\MessageSent;

class EventServiceProvider extends ServiceProvider
{
    /**
     * The event listener mappings for the application.
     *
     * @var array
     */
    protected $listen = [
        Registered::class => [
            SendEmailVerificationNotification::class,
        ],
        MessageSending::class => [
            // この様に MessageSending 以下ににメール送信時に発火する自前のメールロギングクラス名を記述
            // ここでは \App\Mail\MailLogger::class
            \App\Mail\MailLogger::class
        ],
        MessageSent::class => [
            // ここにメール送信完了時に発火する自前のメールロギングクラス名を記述
            \App\Mail\MailLogger::class
        ]
    ];
}

 これでメール送信処理の前後に一々手書きでロギング処理を書いたり、ロギング処理付きのメール送信機能ラッパーを書いたりすることなく Laravel 内のメール送信機能に連動したロギング処理のクラスを定義できます。

 ロギング処理の中でイベントを起こしたメールの Swift_Message クラスベースのメールメッセージクラスを呼べます。これを eml ファイルとして出力します。これは次のコードでできます。

<?php
/* /app/Mail/MailLogger.php */

namespace App\Mail;

use Illuminate\Mail\Events\MessageSending;
use Illuminate\Mail\Events\MessageSent;
use Illuminate\Mail\Message;

class MailLogger
{
    /**
     * @param  MessageSending|MessageSent $event
     * @throws \Swift_IoException
     */
    public function handle(MessageSending | MessageSent $event): void
    {
        if (! $event->data || ! isset($event->data['message'])) {
            \Log::info('fire '.class_basename($event).'. but not exists data');

            return;
        }

        /** @var \Swift_Message|Message $message どちらのイベントでも共通 */
        $message = $event->data['message'];

        // ログファイルの出力先パスを呼び出す
        $logPath = $this->makeLogFilePath($message, $event);

        // Swift_Message の中身をログファイルを対象に出力させる
        $stream = new \Swift_ByteStream_FileByteStream($logPath, true);
        $message->toByteStream($stream);

        // メールファイルとは別にログを残したり
        $subject = $message->getSubject();
        if ($event instanceof MessageSending) {
            \Log::info("メールを送信中。件名: {$subject}。ファイルパス: {$logPath}");
        } elseif ($event instanceof MessageSent) {
            \Log::info("メールを送信済。件名: {$subject}。ファイルパス: {$logPath}");
        }
    }

    /**
     * ログファイルの出力先パス
     * @param  \Swift_Message|Message     $message
     * @param  MessageSending|MessageSent $event
     * @return string
     */
    protected function makeLogFilePath(\Swift_Message | Message $message, MessageSending | MessageSent $event): string
    {
        // ファイルパスとして適切になる様に置換を行った件名 + メッセージオブジェクトハッシュ で程々探しやすく重複しにくいファイル名を作る
        $logNameBase = str_replace(str_split('\/:*?"><|'), '_', $message->getSubject())
            .'_'.spl_object_hash($message);
        // 送信前後でディレクトリを分ける
        // 後々 diff -qr コマンドなどで差分ファイルを見つけて探したり何なりします
        // ディスク容量が気になるなら二重保存の代わりにファイル移動を使う手もあります
        if ($event instanceof MessageSending) {
            $logPathBase = storage_path('logs/mail/send/').$logNameBase;
        } else {
            $logPathBase = storage_path('logs/mail/sent/').$logNameBase;
        }

        // ファイル名がダブった場合、連番をつける。
        $logPath     = $logPathBase.'.eml';
        $i           = 1;
        while (file_exists($logPath)) {
            $logPath = $logPathBase."_{$i}".'.eml';
            ++$i;
        }

        // 構築されたログファイルの出力先パスを返す
        return $logPath;
    }
}
>株式会社シーポイントラボ

株式会社シーポイントラボ

TEL:053-543-9889
営業時間:9:00~18:00(月〜金)
住所:〒432-8003
   静岡県浜松市中央区和地山3-1-7
   浜松イノベーションキューブ 315
※ご来社の際はインターホンで「316」をお呼びください

CTR IMG