こんにちは。
バックエンドエンジニアのほんちゃんです。
エンジニア
ほんちゃん
甘いものが好きなので小腹が空いたらお菓子等を食べてしまうことが多いです。健康のため我慢することを意識しだしました。
エンジニア
ほんちゃん
早速ですが、今回は少々特殊な対応となります。
本来はエラーとなるはずの非推奨のメールアドレスに対してエラーが発生しないようにしますので
なぜそのような対応が必要になったか、経緯からまとめました。
エンジニア
ほんちゃん
目次
経緯
数年前にLaravel5で構築したシステムを、Laravel11にアップデートすることになりました。
既存の仕様はそのままにフレームワークと関連ライブラリのみアップデートが必要になったのですが、 仕様の一つに「RFC違反のメールアドレス宛に送信時、エラーが発生しないこと」がありました。
「Laravel RFC違反 メール送信」といったキーワードでGoogle検索をすると実装例がいくつかヒットしますが、Laravel9以降はSymfony Mailerが採用されたことで、Laravel11ではそれらは使用できなくなっています。
Gmail等、一般的なメールサーバではRFC違反のメールアドレスへの送受信が禁止されていたり、キャリア側でメールアドレス変更を推奨しているケースもあり、使用者が減ってきている印象はありますが、もし対応が必要となった場合にどのような修正が必要か調査しました。
メールアドレスの対応以外にも、既存仕様と最新版フレームワークとの調整が必要になるケースがあるかもしれませんので、対応方法のひとつとして本記事で残したいと思います。
今回は非推奨のメールアドレスを許可する対応となりますが、メール送信以外でも不具合の原因になる可能性があります。
可能であればフレームワークのアップデートを機に仕様を見直すのも良いと思います。
エンジニア
ほんちゃん
環境
下記構成にて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);
ドット開始のメールアドレスを使用しているのでこれは想定内ですね。
エンジニア
ほんちゃん
どこで例外がスローされているか
エラー内容を参考に例外のスロー元を調査すると下記クラスに到達しました。
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違反のメールアドレスだからかと思います。
エンジニア
ほんちゃん
念のため、MailCatcher(http://127.0.0.1:1080/)でも確認しました。
※こちらはtoのメールアドレスが表示されました。
エンジニア
ほんちゃん
例外スローを抑制できたことが確認できました。
※本物のメールアドレスとメールサーバでも検証したかったのですが、RFC違反のメールアドレスを用意できなかったため今回は検証できませんでした。
最後に
RFC違反のメールアドレス宛にメール送信時、例外スローを抑制する方法でした。
着手前はクラスの継承やサービスコンテナの結合で対応できると想定していたのですが、メール送信の処理を追って行くうちにそれが無理だと思い、最終的にcomposer.jsonでクラスを置換することになりました。
今回はメールアドレスを例に説明させていただきましたが「Laravel11が想定していない処理を実装する方法」としても参考にしていただければと思います。
最後までお読みいただき、ありがとうございました。
ロジカルスタジオではエンジニアを募集しています。
今回使ったLaravelやPHPUnitなど、バックエンド技術を使って開発がしてみたいという方は、是非採用サイトからご応募ください!