Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Email Domain Validation Laravel Package

madeitbelgium/laravel-email-domain-validation

A Laravel validation package that checks email domains via DNS/MX records, helping reject invalid or undeliverable addresses. It adds a simple rule for form requests and validators to improve signup and contact form data quality.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and confirm the wiring

composer require madeitbelgium/laravel-email-domain-validation

The service provider is auto-discovered on Laravel 5.5+. If you need to override defaults (resolver, allowlist, timeouts), publish the config:

php artisan vendor:publish --provider="MadeITBelgium\EmailDomainValidation\EmailDomainValidationServiceProvider"

Package tag/config filename varies by version — run php artisan vendor:publish and pick the entry from the list rather than guessing the tag.

Where to look first

  1. README.md — the usage snippet is short; the exact rule FQCN and whether a string alias (e.g. email_domain) is registered is the one thing you must confirm for your installed version.
  2. src/Rules/ — the validation rule class is the entire public surface; read it before trusting it in production, it's ~100 lines.
  3. config/ — allowlist/blocklist and any DNS settings.

First use case: stop typo and dead domains at registration

use MadeITBelgium\EmailDomainValidation\Rules\EmailDomain;

$request->validate([
    'email' => ['required', 'string', 'email', new EmailDomain()],
]);

This does what Laravel's built-in email:dns does — resolves the domain part — but with a controllable, testable implementation you can extend. That control is the whole reason to add a dependency here; the DNS check itself is one dns_get_record() call.

Implementation Patterns

FormRequest is the natural home. Put the rule in the rules() method of your registration/profile FormRequest so controllers and Livewire components stay clean:

public function rules(): array
{
    return [
        'email' => ['required', 'string', 'email', 'max:255', 'unique:users', new EmailDomain()],
    ];
}

Order matters — put unique and max before the DNS rule so you don't burn a network round-trip on input that fails cheaply.

Pair with email but never with email:dns. Stacking both doubles the lookup and gives you two different failure messages for the same problem.

Wrap it in your own rule for app-wide policy. The package validates "domain resolves"; most teams actually want "domain resolves AND isn't a disposable provider AND isn't on our blocklist". Compose once:

class WorkEmail implements ValidationRule
{
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        if (in_array(Str::after($value, '@'), config('app.free_email_domains'))) {
            $fail('Please use your work email address.');
        }

        (new EmailDomain())->validate($attribute, $value, $fail);
    }
}

Then every FormRequest, API endpoint, and Fortify/Breeze registration flow uses new WorkEmail.

Registration vs. login. Validate the domain on registration and email change only. Running DNS on every login attempt puts a network call on your hottest auth path and locks users out during a resolver blip.

Bulk/import flows. For CSV imports or admin-created users, resolve once and cache per domain rather than per email — a 5,000-row import typically hits 200 distinct domains. Wrap the check in Cache::remember("email-domain:{$domain}", 3600, ...) at the call site; the package won't do this for you.

API surface. Return a field-level 422 rather than a generic error — clients need to distinguish "bad syntax" from "domain doesn't exist" so they can prompt correctly.

Gotchas and Tips

Tests will break the moment you enable this. User::factory()->create() uses example.com/example.net, which have no MX records. Either add those to the package's allowlist in config/… for the testing environment, or — better — bind a fake check in TestCase::setUp():

$this->app->bind(EmailDomainChecker::class, fn () => new AlwaysValidChecker());

If the rule instantiates its resolver directly with new, you can't swap it; in that case, guard the rule with app()->environment('production') or use Validator::extend stubs. Read the constructor before assuming it's injectable.

dns_get_record() is blocking and un-timeoutable in PHP. A slow or blackholed resolver stalls the request for the OS default (often 5s+, 2× for retries). Mitigations: only run the rule on low-traffic endpoints, set default_socket_timeout, or front it with your own resolver class behind an interface so you can move it to a queued job later.

No MX ≠ undeliverable. RFC 5321 permits delivery to the A/AAAA record when no MX exists. If the package only checks MX, a handful of small-business domains will be rejected. Verify the behaviour and, if needed, fall back to dns_get_record($domain, DNS_A) yourself.

Punycode and subdomains. bücher.de must be converted to xn--bcher-kva.de before DNS. Str::after($email, '@') is fine for the domain but won't IDN-encode it. idn_to_ascii() first.

Catch-all domains always pass. This rule validates the domain, not the mailbox — [email protected] succeeds. Don't sell it to stakeholders as "verifies the email exists"; it catches typos, dead domains, and dead TLDs, nothing more.

Debugging failures. Reproduce outside Laravel first: dig MX example.com +short or dns_get_record('example.com', DNS_MX). If that works in your shell but the app fails, you're looking at a container DNS problem — check /etc/resolv.conf inside the PHP container, and remember php-fpm and CLI may resolve differently.

Small-package risk. 19 stars and one maintainer means no community safety net. Pin the version in composer.json, and accept that the LGPL-3.0 license (vs. the MIT you'll find on most Laravel packages) is fine to use in closed-source SaaS but has obligations if you fork and redistribute the package itself. Budget an hour to read the source — it's short enough that you can vendor the logic into app/Rules/ and drop the dependency if it ever goes stale or the Laravel constraint lags a major release.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package