Laravelでのメールアドレス検証は、バリデーション配列にemailを加えて終わりにしたくなる作業です。このルールは良い出発点ですが、実際の検証で問うべき3つの問いのうち、最初の1つにしか答えません。アドレスの形式は正しいか。そのドメインはメールを受信できるか。メールボックスは実在するか。Laravelに組み込みのemailルールは、最初の2つに1行で答えられるという珍しい特徴を持っています。3つ目はネットワークの問題であり、外部に任せる価値があります。このガイドでは、Laravel自身のバリデーターの上に各層を組み立て、フレームワークの守備範囲がどこで終わるのかを具体的に示します。内容はPHPのメールアドレス検証ガイドの延長です。Laravelは同じPHPの部品を、はるかに使いやすいAPIで包んでおり、filter_varもキーワード1つで呼び出せます。
結論#
構文とMXルックアップを1回で済ませるにはLaravelのemail:rfc,dnsルール、SMTPによるメールボックスの確認には検証APIを使います。コストの低いものから実行し、どれかの層で結論が出た時点でbailにより処理を打ち切ります。メールボックスのプローブのために、アプリからSMTPセッションを開こうとするのはやめてください。アウトバウンドのポート25はほとんどのホストでブロックされています。また、結果は送信元IPのレピュテーションやグレーリスティングに左右されるため、自前で再現するのは得策ではありません。各層は、前の層より低いコストでアドレスを除外できますが、アドレスを有効と確定できるのは最後の層だけです。
レイヤー1と2:1つのルールで構文とDNSを検証する#
emailルールはegulias/email-validatorパッケージを基盤としており、厳格さを決める「スタイル」を指定できます。デフォルトではrfcスタイル(RFC 5321/5322の文法)が適用されます。検証で重要になるのがdnsスタイルです。ライブのルックアップを追加し、ドメインがメール経路を解決できること、つまりMXレコード(なければAレコードにフォールバック)があることを確認します。両方を指定すれば、Laravelは構文チェックとドメインチェックを1つのルールで行います。
public function store(Request $request)
{
$validated = $request->validate([
'email' => ['required', 'email:rfc,dns'],
]);
// $validated['email'] is well-formed AND its domain can receive mail.
}
ほかのスタイルも知っておく価値があります。email:filterはPHPのfilter_var($email, FILTER_VALIDATE_EMAIL)を実行します。これはPHPガイドで扱ったチェックそのものです。filter_unicodeは、一部のUnicodeを含むローカルパートを許可します。strictは、RFCパーサーが警告にとどめて許容する、末尾のピリオドや連続するピリオドを拒否します。spoofは、見た目が紛らわしい同形異字の文字から守ります。そのため、公開のサインアップフォームではemail:rfc,dns,spoofが妥当なデフォルトです。フォームリクエストでは次のように書きます。
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreSubscriberRequest extends FormRequest
{
public function rules(): array
{
return [
'email' => ['required', 'email:rfc,dns,spoof'],
];
}
}
email:rfc,dnsに成功してもゴーサインにならない理由#
email:rfc,dnsに成功したということは、文字列の形式が正しく、かつドメインがメールを受信できるということです。特定のメールボックスが存在するかどうかについては、何も分かりません。noreply-9f2x@gmail.comは通ります。definitely-fake@gmail.comも通ります。どちらも配信不能です。MXレコードはドメインについての事実であり、メールボックスについての事実はメールサーバーに問い合わせないと分からないからです。正規表現によるメールアドレス検証が失敗するのも、同じ壁があるためです。パターンマッチングを重ねても、DNSルックアップを行っても、「ドメインがメールを受け付ける」から「このアドレスが受け付ける」までは越えられません。
この差を自分で確かめたい場合は、構文が完璧なアドレスを無料メールチェッカーに貼り付けてください。ルールを通過した文字列がundeliverableと判定される様子を確認できます。
レイヤー3:メールボックスは実在するか#
特定のメールボックスを確認するには、SMTPの配信時のやり取りが必要です。メールホストに接続し、RCPT TOを発行して応答を読み取り、何も送信せずに切断します。原理的にはPHPからこれをスクリプト化できますが、アプリケーションサーバーから実行すべきではありません。ほとんどのクラウドプロバイダーはアウトバウンドのポート25をブロックしており、応答は接続元IPのレピュテーションに左右されます。さらに、受信サーバーは見慣れない送信者に対してグレーリスティングやレート制限を行います。それだけではありません。すべてのアドレスを受け付けるキャッチオールドメインは、単純なプローブでは見分けられません。全体のパイプラインについては、メールアドレス検証の仕組みで解説しています。ここは外部に任せる価値のある層です。
カスタムバリデーションルールからQualisendを呼び出す#
Laravelでは、この委任をすっきり実装できます。API呼び出しをルールオブジェクトに包み、同じルール配列に加えるだけです。ルールはArtisanで生成します。
php artisan make:rule DeliverableEmail
キーはコードに書かず、.envから読み取るconfig/services.phpを経由して渡します。
// config/services.php
'qualisend' => [
'key' => env('QUALISEND_API_KEY'),
],
続いてルール側で、Httpファサードを使って検証エンドポイントを呼び出し、resultエンベロープを読み取り、判定がundeliverableのときだけ失敗させます。
namespace App\Rules;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Facades\Http;
class DeliverableEmail implements ValidationRule
{
public function validate(string $attribute, mixed $value, Closure $fail): void
{
try {
$response = Http::withToken(config('services.qualisend.key'))
->timeout(10)
->post('https://app.qualisend.com/api/v1/verify', ['email' => $value]);
} catch (ConnectionException $e) {
// A timeout or network error: let the signup through, verify later.
return;
}
// On our own outage or a rate limit, don't block a real signup.
if ($response->failed()) {
return;
}
$result = $response->json('result');
if (($result['status'] ?? null) === 'undeliverable') {
$fail('This email address appears to be undeliverable.');
}
}
}
Http::withToken()はBearer認証ヘッダーを設定し、->post()はJSONボディを送信し、$response->json('result')はドット記法で入れ子のresultオブジェクトを取り出します。タイムアウトや接続エラーは、レスポンスを返す代わりにConnectionExceptionを投げます。そのため、ルールはこれも捕捉し、サインアップを通します。リクエストの詳細な形式はAPIリファレンスにあり、最初のレスポンスは次のようになります。
{
"job_id": "5b1f3c2a-8d4e-4f6a-9b7c-2e1d0a9f8c71",
"result": {
"status": "unknown",
"score": 61,
"reason": null,
"smtp": "pending",
"sub_flags": { "role": false, "disposable": false, "free_mail": false, "catch_all": false }
}
}
statusはdeliverable、risky、undeliverable、unknownのいずれかです。上のルールが厳格に失敗させるのはundeliverableだけで、riskyとunknownは通します。判断が分かれるアドレスをフォームエラーにするのではなく、その後の処理で扱いを決められます。たとえばscoreで判断する、disposableなどのsub_flagsの項目で分岐する、といった方法です。上のJSONは契約ではなく形式の例として扱ってください。フィールドの完全な一覧は開発者向けドキュメントにあります。
最初のstatusは、即時の判定です。上のようにresult.smtpが"pending"のときは、メールボックスのプローブがまだ実行中で、最終的な判定は完了後にGET https://app.qualisend.com/api/v1/jobs/{job_id}に届きます。そのためにフォームを開いたままにしたり、/verifyを再度呼び出したりしないでください。job_idを保持し、キューに入れたジョブ(リクエストの処理経路の外)からポーリングして、そこでundeliverableの結果が出たら対処します。smtpが"cached"または"skipped"の場合、resultはすでに確定しています。
層を組み合わせてLaravelでメールアドレスを検証する#
3つの層を1つのフォームリクエストにまとめ、コストの低いものから並べます。bailで最初の失敗の時点で止めれば、APIルールが実行されるのは、構文とMXチェックをすでに通過したアドレスだけです。
namespace App\Http\Requests;
use App\Rules\DeliverableEmail;
use Illuminate\Foundation\Http\FormRequest;
class StoreSubscriberRequest extends FormRequest
{
public function rules(): array
{
return [
'email' => [
'bail',
'required',
'email:rfc,dns',
new DeliverableEmail(),
],
];
}
public function messages(): array
{
return [
'email.email' => 'Enter a valid email address on a real domain.',
];
}
}
肝心なのはbailです。形式が不正なアドレスや存在しないドメインはローカルで失敗し、APIのクレジットを消費しません。ネットワークの往復に見合うアドレスだけが、SMTPの層に届きます。コントローラーでリクエストを型ヒントで指定すれば、Laravelはメソッド本体の実行前にパイプラインを実行します。
public function store(StoreSubscriberRequest $request)
{
// Every layer has passed — safe to persist.
$subscriber = Subscriber::create($request->validated());
return redirect()->route('subscribers.index');
}
検証を確実にするのは、フレームワークではなく層構造です。構成はPHP版と同じで、Laravelが最初の2つの層を1つのルールにまとめてくれます。サインアップ時には高速な同期のパイプラインを実行し、時間のかかるSMTPで確認する処理は管理業務に回してください。リアルタイムのパターンはサーバーレスのサインアップガイドで、バッチ側はメールリストのクリーニング方法で解説しています。
よくある質問
01Laravelのemailルールは、ドメインの存在も確認しますか?
指定した場合だけ確認します。単なる email ルールは、egulias/email-validator パッケージ(rfc スタイル)で構文を検証します。dns スタイルを加えて email:rfc,dns とすると、Laravelはドメインがメール経路を解決できることも確認し、架空のドメインや使われていないドメインは同じ処理で失敗します。どちらのスタイルもメールボックスには接続しないため、成功しても「配信できそうに見える」という意味であり、「配信できる」という意味ではありません。
02email:rfc、email:filter、email:dnsの違いは何ですか?
rfc はegulias経由でRFC 5321/5322に照らして検証し、Laravelのデフォルトです。filter はPHPの filter_var($email, FILTER_VALIDATE_EMAIL) を実行します。これはPHPガイドで扱ったチェックそのものです。filter_unicode は一部のUnicodeを許可します。dns はライブのMXルックアップを加え、spoof は紛らわしい同形異字の文字を拒否します。カンマで組み合わせて指定できるため、構文とドメインの関門としては email:rfc,dns が実用的です。
03Laravelでメールボックスが実在するかを確認するにはどうすればよいですか?
組み込みのルールでは確認できません。メールボックスの確認にはSMTPのやり取りが必要ですが、ポート25は広くブロックされており、結果は送信元IPのレピュテーションに左右されるため、アプリケーションサーバーから実行すべきではありません。検証APIをカスタムの ValidationRule オブジェクトに包み、Http ファサードで呼び出し、result エンベロープを読み取って undeliverable のときに失敗させます。ローカルの層を通過したアドレスにだけ実行されるよう、bail を付けて email:rfc,dns の後に置いてください。result.smtp が pending で返ってきた場合は、メールボックスのプローブがまだ実行中です。最終的な判定は、バリデーターの中ではなく、キューに入れたジョブで GET /api/v1/jobs/{job_id} から読み取ります。
04dnsルールはフォームの送信を遅くしますか?
遅くなる可能性があります。dns スタイルは検証のたびにライブのDNSルックアップを行うため、リゾルバーが遅い、または到達できない場合はリクエストが停止します。単一のサインアップフォームなら問題ありませんが、一括インポートでは使わないでください。また、上の DeliverableEmail ルールが ->timeout(10) で行っているように、Http クライアントにタイムアウトを設定していない限り、同期のAPI呼び出しと重ねて使うのは避けてください。
メールボックスの層を追加する準備はできましたか。アドレスを無料メールチェッカーに入力すると、ルールを通過したアドレスが配信不能と判定される様子を確認できます。そのうえで、APIリファレンスのコピー&ペーストで使えるコード例を参考に、同じ判定結果をValidationRuleに組み込んでください。



