【Laravel11】RFC違反メールアドレス使用時の例外スローを抑制する

こんにちは。

バックエンドエンジニアのほんちゃんです。

honchan

エンジニア

ほんちゃん

甘いものが好きなので小腹が空いたらお菓子等を食べてしまうことが多いです。健康のため我慢することを意識しだしました。

honchan

エンジニア

ほんちゃん

早速ですが、今回は少々特殊な対応となります。
本来はエラーとなるはずの非推奨のメールアドレスに対してエラーが発生しないようにしますので
なぜそのような対応が必要になったか、経緯からまとめました。

honchan

エンジニア

ほんちゃん

経緯

数年前にLaravel5で構築したシステムを、Laravel11にアップデートすることになりました。

既存の仕様はそのままにフレームワークと関連ライブラリのみアップデートが必要になったのですが、 仕様の一つに「RFC違反のメールアドレス宛に送信時、エラーが発生しないこと」がありました。

 

「Laravel RFC違反 メール送信」といったキーワードでGoogle検索をすると実装例がいくつかヒットしますが、Laravel9以降はSymfony Mailerが採用されたことで、Laravel11ではそれらは使用できなくなっています。

 

Gmail等、一般的なメールサーバではRFC違反のメールアドレスへの送受信が禁止されていたり、キャリア側でメールアドレス変更を推奨しているケースもあり、使用者が減ってきている印象はありますが、もし対応が必要となった場合にどのような修正が必要か調査しました。

メールアドレスの対応以外にも、既存仕様と最新版フレームワークとの調整が必要になるケースがあるかもしれませんので、対応方法のひとつとして本記事で残したいと思います。

今回は非推奨のメールアドレスを許可する対応となりますが、メール送信以外でも不具合の原因になる可能性があります。
可能であればフレームワークのアップデートを機に仕様を見直すのも良いと思います。

honchan

エンジニア

ほんちゃん

環境

下記構成にてLaravel11が動く環境を作成済みであることが前提です。

・Ubuntu22.04.2 LTS(Windows11のWSL2上に構築)

・PHP 8.3.4

・Laravel Framework 11.0.8

・Mailpit、MailCatcher

・適当なMailableを作成済み(こちらの手順と同様にOrderShippedを作成しました)

RFC違反のメールアドレスを使用時のエラーを確認

まず、構築直後のLaravel11でRFC違反のメールアドレス宛にメール送信時の結果を確認します。

適当なController等を用意して下記のコードを実行すると例外がスローされます。

// RFC違反のメールアドレスに対してメールを送信する。
\Illuminate\Support\Facades\Mail::to('.sample@example.com')
    ->send(new \App\Mail\OrderShipped);

ドット開始のメールアドレスを使用しているのでこれは想定内ですね。

honchan

エンジニア

ほんちゃん

どこで例外がスローされているか

エラー内容を参考に例外のスロー元を調査すると下記クラスに到達しました。

13行目のself::$validator->isValid()で、falseが返っているためRfcComplianceExceptionがスローされます。

// vendor/symfony/mime/Address.php
    public function __construct(string $address, string $name = '')
    {
        if (!class_exists(EmailValidator::class)) {
            throw new LogicException(sprintf('The "%s" class cannot be used as it needs "%s". Try running "composer require egulias/email-validator".', __CLASS__, EmailValidator::class));
        }

        self::$validator ??= new EmailValidator();

        $this->address = trim($address);
        $this->name = trim(str_replace(["\n", "\r"], '', $name));

        if (!self::$validator->isValid($this->address, class_exists(MessageIDValidation::class) ? new MessageIDValidation() : new RFCValidation())) {
            throw new RfcComplianceException(sprintf('Email "%s" does not comply with addr-spec of RFC 2822.', $address));
        }
    }

self::$validator->isValid()の中身は下記のようになっています。

6行目で$parser->parse()でメールアドレス形式をチェックしているようです。

// vendor/egulias/email-validator/src/Validation/MessageIDValidation.php
    public function isValid(string $email, EmailLexer $emailLexer): bool
    {
        $parser = new MessageIDParser($emailLexer);
        try {
            $result = $parser->parse($email);
            $this->warnings = $parser->getWarnings();
            if ($result->isInvalid()) {
                /** @psalm-suppress PropertyTypeCoercion */
                $this->error = $result;
                return false;
            }
        } catch (\Exception $invalid) {
            $this->error = new InvalidEmail(new ExceptionFound($invalid), '');
            return false;
        }

        return true;
    }

メールアドレス自体を加工する案

例外がスローされている処理は判明しましたが、修正に着手する前に他の案が無いか考えてみます。

メールアドレスのローカルパート部分をダブルクォーテーションで囲むと、例外はスローされずメール送信は成功しました。

ただ、案件やシステムによっては、メールアドレスの加工を避けたい場合もありそうな気はします。

// RFC違反のメールアドレスに対してメールを送信する。
\Illuminate\Support\Facades\Mail::to('".sample"@example.com')
    ->send(new \App\Mail\OrderShipped);

※メールサーバによってはこの対応で送信できないケースもあるようです。

例外スローを回避する方法は無いか

上記以外の方法となるとAddress.phpを継承したクラスを用意して差し替えれないか挑戦しましたが

クラスにfinalキーワードが付いているので、継承やサービスコンテナの結合は厳しそうです。

// vendor/symfony/mime/Address.php
/**
 * @author Fabien Potencier <fabien@symfony.com>
 */
final class Address
{

また、MessageIDValidation.phpにはnew MessageIDParserがハードコーディングされているのでこれを置換するのも厳しそうです。

// vendor/egulias/email-validator/src/Validation/MessageIDValidation.php
    public function isValid(string $email, EmailLexer $emailLexer): bool
    {
        $parser = new MessageIDParser($emailLexer);

何とか上記クラスを置換できないか調査や試行錯誤を繰り返しているうちに、composer.jsonでオートロードするクラスを置換する方法に行き着きました。

MessageIDValidation.phpの処理でメールアドレス形式のチェックが通らないため例外スローに繋がっているため、これを置換すれば良いと考えcomposer.jsonの一部分を下記のように編集しました。

// composer.json
    "autoload": {
        "psr-4": {
            "App\\": "app/",
            "Database\\Factories\\": "database/factories/",
            "Database\\Seeders\\": "database/seeders/"
        },
        "exclude-from-classmap": [
            "vendor/egulias/email-validator/src/Validation/MessageIDValidation.php"
        ],
        "files": [
            "vendor-overrides/egulias/email-validator/src/Validation/MessageIDValidation.php"
        ]
    },

filesに指定したファイルは下記を用意しました。

例外スローの原因となっているisValid()で常にtrueを返しています。

※今回は簡易的に対応するため下記ソースとなりましたが、実際に使用する時は不要な処理だけ削除等を検討した方が良さそうです。

// vendor-overrides/egulias/email-validator/src/Validation/MessageIDValidation.php
namespace Egulias\EmailValidator\Validation;

use Egulias\EmailValidator\EmailLexer;
use Egulias\EmailValidator\Result\InvalidEmail;
use Egulias\EmailValidator\Warning\Warning;

class MessageIDValidation implements EmailValidation
{
    private $warnings = [];

    private $error;

    public function isValid(string $email, EmailLexer $emailLexer): bool
    {
        return true;
    }

    public function getWarnings(): array
    {
        return $this->warnings;
    }

    public function getError(): ?InvalidEmail
    {
        return $this->error;
    }
}

下記を実行すればオートロードするクラスを変更できます。

$ composer dump-autoload

再度RFC違反のメールアドレス宛にメールを送信する

再度メール送信実行してみますと、今度はエラー画面が表示されずに終了します。

// RFC違反のメールアドレスに対してメールを送信する。
\Illuminate\Support\Facades\Mail::to('.sample@example.com')
    ->send(new \App\Mail\OrderShipped);

Mailpit(http://127.0.0.1:8025/)を見てみると受信メールが存在します。

※toのメールアドレスが[Undisclosed recipients]となっているのはRFC違反のメールアドレスだからかと思います。

honchan

エンジニア

ほんちゃん

念のため、MailCatcher(http://127.0.0.1:1080/)でも確認しました。

※こちらはtoのメールアドレスが表示されました。

honchan

エンジニア

ほんちゃん

例外スローを抑制できたことが確認できました。

※本物のメールアドレスとメールサーバでも検証したかったのですが、RFC違反のメールアドレスを用意できなかったため今回は検証できませんでした。

最後に

RFC違反のメールアドレス宛にメール送信時、例外スローを抑制する方法でした。

着手前はクラスの継承やサービスコンテナの結合で対応できると想定していたのですが、メール送信の処理を追って行くうちにそれが無理だと思い、最終的にcomposer.jsonでクラスを置換することになりました。

今回はメールアドレスを例に説明させていただきましたが「Laravel11が想定していない処理を実装する方法」としても参考にしていただければと思います。

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

 

ロジカルスタジオではエンジニアを募集しています。

今回使ったLaravelやPHPUnitなど、バックエンド技術を使って開発がしてみたいという方は、是非採用サイトからご応募ください!