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

Email Checker Laravel Package

mattketmo/email-checker

PHP library that validates email addresses beyond a simple regex: it checks RFC compliance and DNS MX records, and can connect to the SMTP server to confirm the mailbox actually exists. Framework-agnostic, so it drops into any PHP or Laravel app.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the library and confirm which API your pinned version exposes — the package has changed shape across majors (older releases centered on a check()/checkEmail() call returning status constants, current ones expose isDisposable() on EmailChecker\EmailChecker).

composer require mattketmo/email-checker
use EmailChecker\EmailChecker;

$checker = new EmailChecker();          // loads the bundled disposable-domain list
$checker->isDisposable('[email protected]');   // true
$checker->isDisposable('[email protected]');     // false

Places to look first in vendor/mattketmo/email-checker:

  1. src/EmailChecker.php — the public surface you will actually call.
  2. src/Adapter/ — the adapter interface plus array/JSON-file implementations; this is how you supply your own domain list instead of the bundled one.
  3. data/ (or resources/) — the shipped JSON file of disposable domains. Skim it; it tells you exactly what counts as "disposable" and where false positives will come from.

First real use case: gate the registration endpoint. Bind a singleton in a service provider so the JSON list is parsed once per request/worker, not once per validation call:

// app/Providers/AppServiceProvider.php
use EmailChecker\EmailChecker;

public function register(): void
{
    $this->app->singleton(EmailChecker::class, fn () => new EmailChecker());
}

Then a Laravel 10+ validation rule:

namespace App\Rules;

use Closure;
use EmailChecker\EmailChecker;
use Illuminate\Contracts\Validation\ValidationRule;

final class NotDisposable implements ValidationRule
{
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        if (! is_string($value)) {
            return;
        }

        $email = mb_strtolower(trim($value));

        if (app(EmailChecker::class)->isDisposable($email)) {
            $fail('Please use a permanent email address — disposable inboxes are not accepted.');
        }
    }
}
$request->validate([
    'email' => ['required', 'string', 'email', 'max:255', 'unique:users,email', new NotDisposable()],
]);

Implementation Patterns

Tiered policy instead of a blanket block. Hard-blocking disposable addresses on free signups is fine; blocking them on a paid checkout or a B2B lead form costs conversions. Practical split:

// app/Support/EmailPolicy.php
public function handle(string $email, string $context): string
{
    $disposable = app(EmailChecker::class)->isDisposable(mb_strtolower($email));

    return match (true) {
        ! $disposable               => 'allow',
        $context === 'checkout'     => 'allow',     // take the money, flag the account
        $context === 'newsletter'   => 'shadow',    // accept silently, low-value list
        default                     => 'reject',
    };
}

For shadow/allow cases, persist a boolean (users.disposable_email) so support and fraud review can filter later. You get the signal without the bounce.

Pair it with a syntax rule and, optionally, an MX check. This library is a domain blocklist, nothing more — it does not validate format or deliverability. Laravel's email rule (which uses egulias/email-validator) plus dns_get_record($domain, DNS_MX) covers the rest:

public function passes(string $email): bool
{
    $domain = Str::afterLast($email, '@');

    return checkdnsrr($domain, 'MX') || checkdnsrr($domain, 'A');
}

Cache MX results for a day — DNS lookups on every registration will hurt.

Allow your own overrides on top of the shipped list. Wrap the library rather than calling it directly everywhere, so you can subtract the domains your users legitimately use and add the ones that keep slipping through:

// config/email-checker.php
return [
    'allow'     => ['my-privacy-provider.example'],   // never treat as disposable
    'disposable' => [],                               // extra domains you've discovered
];
final class DisposableDomains
{
    public function __construct(private EmailChecker $checker) {}

    public function isDisposable(string $email): bool
    {
        $domain = Str::lower(Str::afterLast(trim($email), '@'));

        if (in_array($domain, config('email-checker.allow'), true)) {
            return false;
        }

        if (in_array($domain, config('email-checker.disposable'), true)) {
            return true;
        }

        return $this->checker->isDisposable($email);
    }
}

This wrapper is also your escape hatch if the package goes stale — the rest of the app only ever sees DisposableDomains::isDisposable().

Custom adapters for a bigger list. The list file is the package's real value, and it's a plain JSON array of domains. If you maintain a merged blocklist (yours + theirs), the adapter interface lets you feed it in without forking:

use EmailChecker\Adapter\ArrayAdapter;

$domains = json_decode(file_get_contents(storage_path('app/disposable.json')), true);
$this->app->singleton(EmailChecker::class, fn () => new EmailChecker(new ArrayAdapter($domains)));

Bulk/backfill work belongs in a job. Auditing an existing user table, or re-scanning after a list refresh, should be a queued command that chunks records — don't loop the checker over 500k rows inside a request.

Testing. Never let tests depend on the shipped list contents — a composer update will break them. Bind a fake in TestCase::setUp():

$this->app->instance(EmailChecker::class, new class extends EmailChecker {
    public function isDisposable(string $email): bool
    {
        return str_ends_with($email, '@disposable.test');
    }
});

With constructor-promoted EmailChecker in the rule, resolve the rule from the container (app(NotDisposable::class)) rather than new NotDisposable() so the fake is injected.

Surfacing it to the user. Return a distinct error message and a 422 with a specific code (email_disposable) so the frontend can suggest a different address or an "I use this provider, let me in" support path.

Gotchas and Tips

  • The list is the product, and it drifts. New disposable services appear constantly. Pin the package version, then either bump it regularly or maintain your own supplement list. A rejection list frozen for a year is close to useless.
  • Case, whitespace, and plus-addressing. [email protected], [email protected] and [email protected] must all be normalized before the check. mb_strtolower(trim($email)) first; verify your version's behavior on subdomains ([email protected]) empirically rather than assuming — if it doesn't match parent domains, strip to the registrable domain yourself with a PSL-aware helper.
  • It is not a validator. No format check, no MX, no SMTP. Don't let a passing isDisposable() result stand in for 'email' in your ruleset.
  • False positives target real users. Privacy-forward providers and small self-hosted domains occasionally land on these lists. Always provide an appeal path or an allowlist — a hard, silent rejection with no recourse is the worst outcome.
  • Don't advertise the blocklist. Error strings like "disposable emails are blocked" or "this domain is on our blocklist" invite trivial bypass (forwarding addresses, catch-all custom domains). Keep the message generic and log the real reason server-side.
  • Path and deployment assumptions. The bundled JSON is read from disk relative to the package. With composer install --no-dev --optimize-autoloader, vendor pruning, or unusual opcache/preload setups, confirm the data file is actually present — a missing file may throw or silently behave unexpectedly in production but not locally.
  • Singleton or you'll pay for it. A fresh EmailChecker per validation reads and decodes the JSON every time. Bind it, and be aware that long-running workers (Octane, queue consumers) hold that instance — if you reload the list at runtime, refresh the container binding too.
  • Watch the API across majors. isValid(), checkEmail() and status constants exist in some versions and not others. Pin an exact constraint in composer.json and read the changelog before upgrading; a minor-looking bump can remove the method your rule calls.
  • Small-package risk. ~274 stars, one maintainer. That's fine for a data-plus-lookup library, but don't scatter use EmailChecker\... across fifty files — the wrapper interface above costs an hour and makes replacement trivial.
  • Where to extend: the adapter interface (custom list sources), your wrapper service (allowlist/denylist merging and context policy), and a custom validation rule (user-facing behavior). Those three cover essentially everything you'd want to change.
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.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin