# Validate an email address in Laravel

_2026-10-14 · Boundstone (https://boundstone.io/blog/validate-email-laravel)_


You want to validate an email in Laravel, and you're wondering whether the framework already does enough. Mostly, it does. Laravel ships three email validators, and one of them goes further than a regex — it resolves the domain and checks for a mail server. This post covers what each rule actually does, where the built-in rules stop, and how to add the one check Laravel has no rule for: whether the address is disposable or a shared role inbox.

## What Laravel validates out of the box

Laravel's `email` rule takes a comma-separated list of validators, backed by the `egulias/email-validator` package:

```php
$request->validate([
    'email' => 'required|email:rfc,dns',
]);
```

The three you'll reach for:

- `email:rfc` — RFC 5322 syntax. Structure only. `bob@nowhere.invalid` passes because it's shaped like an address.
- `email:filter` — PHP's `filter_var($value, FILTER_VALIDATE_EMAIL)`. Also syntax only, using PHP's own rules for the edge cases.
- `email:dns` — this one does real work. It resolves the domain and confirms it has a mail (MX) record, so the address at least points at a domain that can receive mail. `bob@nowhere.invalid` fails here.

If you're wondering what that MX record is and why it matters, we wrote a short explainer: [what is an MX record](/blog/what-is-an-mx-record).

So `email:rfc,dns` gives you syntax plus a live MX check in one line. For a lot of forms, that is genuinely all you need — don't bolt an API call onto a signup form that a stdlib rule already covers.

## What the built-in rules can't catch

`email:dns` tells you a domain can receive mail. It doesn't tell you what kind of address you've been handed. Two gaps matter for signups:

- **Disposable addresses.** `mailinator.com` and thousands of throwaway domains have perfectly valid MX records. `email:dns` passes them.
- **Role accounts.** `info@`, `support@`, `sales@` — shared inboxes, not people. Valid syntax, valid MX, wrong thing to write into a "notify this user" column.

Laravel has no built-in rule for either. You'd be hand-maintaining a domain blocklist forever. That's the layer Boundstone's email endpoint covers.

## Add a Boundstone check as a custom rule

`POST /v1/verify/email` returns the flags Laravel doesn't: `disposable`, `role_account`, and a `free_provider` flag, alongside `valid_syntax`, `domain`, and `mx_found`. Wrap it in a custom rule.

First, store your key:

```php
// config/services.php
'boundstone' => [
    'key' => env('BOUNDSTONE_API_KEY'),
],
```

Then the rule (Laravel 10+ `ValidationRule` contract):

```php
<?php

namespace App\Rules;

use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Support\Facades\Http;

class SignupEmail implements ValidationRule
{
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        $response = Http::withToken(config('services.boundstone.key'))
            ->acceptJson()
            ->post('https://api.boundstone.io/v1/verify/email', [
                'email' => $value,
            ]);

        // Fail open: don't block a real user because a third party is down.
        if ($response->failed()) {
            return;
        }

        $result = $response->json();

        if ($result['disposable'] ?? false) {
            $fail('Use a permanent email address, not a disposable one.');
        }

        if ($result['role_account'] ?? false) {
            $fail('Use a personal address rather than a shared inbox like info@ or sales@.');
        }
    }
}
```

`Http::withToken()` sends the `Authorization: Bearer bs_live_YOUR_KEY` header for you. The fail-open branch is deliberate — an outage on our side should never lock a legitimate customer out of your signup form.

The rule is named `SignupEmail`, not `DeliverableEmail`, on purpose: it screens out disposable and role addresses, but it does not confirm the mailbox exists. Neither Laravel's MX check nor this call establishes deliverability, so the name shouldn't promise it.

## Wire it into a FormRequest

Keep validation out of the controller. A FormRequest reads cleanly and puts the two layers side by side:

```php
<?php

namespace App\Http\Requests;

use App\Rules\SignupEmail;
use Illuminate\Foundation\Http\FormRequest;

class StoreSubscriberRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'email' => [
                'required',
                'string',
                'email:rfc,dns',    // Laravel: syntax + MX
                new SignupEmail(),  // Boundstone: disposable + role
            ],
        ];
    }
}
```

Order matters: `email:rfc,dns` runs first and cheaply, so a malformed address never reaches the API call. If you'd rather run the check across several fields at once, `withValidator()` and `$validator->after(...)` give you the same result from a hook instead of a rule.

## Read what was — and wasn't — checked

Every response carries a `checks` object. For a valid address it reads:

```json
{
  "valid_syntax": true,
  "domain": "gmail.com",
  "mx_found": true,
  "disposable": false,
  "role_account": false,
  "free_provider": true,
  "checks": {
    "performed": ["syntax", "mx", "disposable_list", "role_list"],
    "not_performed": ["smtp_mailbox", "catch_all"]
  }
}
```

Read `not_performed` before you trust `performed`. Boundstone does **not** open an SMTP connection to the mailbox, and does not detect catch-all domains — `smtp_mailbox` and `catch_all` are listed as not performed, plainly, on every response. So `disposable: false` here means the domain isn't on a disposable list; it does not mean a human reads that inbox. We'd rather you know the boundary than sell you a "valid" that quietly guessed.

(`free_provider` is returned as information — gmail.com is a free provider — but it is not one of the `performed` checks, and it isn't a reason to reject anyone.)

## The short version

- Syntax only: `email:rfc` and `email:filter`. Don't ship these alone for signups.
- Syntax + a real MX lookup: `email:dns`. Often all you need — use it before you reach for an API.
- Disposable and role-account detection: no Laravel rule exists. That's Boundstone's `/v1/verify/email`, wrapped in a custom rule.
- Trust the `valid` because of `not_performed`: no SMTP probe, no catch-all guess, and the response says so.

Paste an address into the free [email validator](/tools/email-validator) to see the exact response first — no signup, no card. On plain PHP without Laravel? Same idea, different wiring: [validate an email in PHP](/blog/validate-email-php).
