Custom Email Validation in Laravel
In August 2020 I opened laravel/framework#33835, and it was merged into the 7.x branch the same day. This is the story behind it.
The gap
Laravel's email validation rule is genuinely good. You can stack built-in checks — rfc, dns, spoof, filter — and get solid validation with a single rule string. Under the hood it uses egulias/email-validator, which is a proper email parser rather than a regex.
But egulias/email-validator already supports custom validators through its EmailValidation interface. You can plug in any class to run additional checks on top of the standard ones. Laravel just had no way to use that extension point from a rule definition.
So every project that needed custom email logic — blocking disposable providers, restricting to specific domains, enforcing an internal deny-list — had to write a separate custom rule class, wire it up, and maintain it alongside the standard email rule. Two separate things doing one conceptually unified job.
The fix was small: let the email rule accept a fully qualified class name alongside the built-in mode strings.
What the PR added
One new form of the email rule:
'email' => 'email:rfc,dns,App\\Validation\\Email\\MyCustomValidator'
Laravel resolves the class through the container and executes it as part of the existing validation chain. The built-in modes and your custom class run together, in order, as a single rule. If any of them fail, validation fails.
That means you keep the familiar syntax, you keep all the built-in checks you were already using, and you add your own logic on top without a separate rule or a separate field in your request class.
A real example: domain allowlist
Say you're building a B2B product where only users with a corporate email address can sign up — no Gmail, no Outlook, no disposable providers. You want RFC-valid, DNS-verified, and from your client's domain.
<?php
namespace App\Validation\Email;
use Egulias\EmailValidator\EmailLexer;
use Egulias\EmailValidator\Validation\EmailValidation;
class CorporateDomainValidation implements EmailValidation
{
public function __construct(private readonly string $domain) {}
public function isValid(string $email, EmailLexer $emailLexer): bool
{
return str_ends_with(strtolower($email), '@' . $this->domain);
}
public function getError(): ?InvalidEmail
{
return null;
}
public function getWarnings(): array
{
return [];
}
}
Because Laravel resolves the class through the container, you can bind a specific instance in a service provider:
$this->app->bind(CorporateDomainValidation::class, fn () => new CorporateDomainValidation(
config('services.client.email_domain')
));
And then use it in any form request:
use App\Validation\Email\CorporateDomainValidation;
public function rules(): array
{
return [
'email' => ['required', 'email:rfc,dns,' . CorporateDomainValidation::class],
];
}
The rfc and dns checks run first. If those pass, CorporateDomainValidation runs. One rule string, three checks, zero separate rule classes to register.
Another example: blocking disposable providers
The same pattern works for blocking throwaway email services. There are open-source lists of disposable email domains you can pull in as a package or maintain yourself:
class NoDisposableEmailValidation implements EmailValidation
{
public function isValid(string $email, EmailLexer $emailLexer): bool
{
$domain = strtolower(substr(strrchr($email, '@'), 1));
return ! in_array($domain, $this->getDisposableDomains(), strict: true);
}
public function getError(): ?InvalidEmail
{
return null;
}
public function getWarnings(): array
{
return [];
}
private function getDisposableDomains(): array
{
// load from config, cache, or a package
return config('validation.disposable_email_domains', []);
}
}
Stack it with your domain check if needed:
'email' => [
'required',
'email:rfc,dns,' . NoDisposableEmailValidation::class . ',' . CorporateDomainValidation::class,
],
Why not a custom Rule class instead?
You could. Laravel's Rule interface works fine for this. But there are two reasons EmailValidation is a better fit here specifically.
First, it's semantically correct. Email format checks belong together. Putting a domain restriction in a separate rule class separates things that are conceptually one check — "is this a valid email address for this application?" — into multiple validation entries with separate error messages and separate failure modes.
Second, EmailValidation implementations are composable in a way that Rule classes aren't. You can stack multiple validators in one rule string, and the email validator library handles the execution order. Each class is small and does one thing. You can reuse NoDisposableEmailValidation across every project that needs it without coupling it to Laravel's validation system at all.
One current gap worth knowing about
Laravel's newer fluent rule builder — Rule::email() — doesn't support custom validators yet. You can do this:
use Illuminate\Validation\Rule;
'email' => Rule::email()->rfcCompliant()->validateMxRecord(),
But there's no ->validator(MyCustomValidator::class) equivalent. The string-based form is still the only way to plug in a custom EmailValidation class today.
This is a good candidate for a PR. The fluent builder sits in Illuminate\Validation\Rules\Email, it already chains through the same underlying check, and adding a validator(string $class) method that appends the class name to the modes array would be a small, focused, well-scoped change — exactly the kind that gets reviewed and merged quickly. If you're looking for a first open source contribution, this is it.
The open source part
The PR itself is about 50 lines of code. It modifies the validateEmail method in Illuminate\Validation\Validator to check whether each segment of the rule string is a class name and, if so, resolve it from the container rather than treating it as a built-in mode string.
That's the part I find most satisfying about this kind of contribution. The feature isn't complicated — the complexity is already in egulias/email-validator, which has supported custom validators for years. The PR just surfaces an existing capability through a familiar interface.
If you've been putting off contributing to an open source project because you're waiting for a big enough idea: this is what most merged PRs look like. A real pain point, a narrow fix, tests, done.