harungecit / php-email-validator
The most complete email validation and disposable email detection library for PHP: 136,000+ disposable domains (updated daily, false-positive filtered), MX/DNS, RFC & spoof checks, role-based and typo detection, SMTP verification. Framework-agnostic with first-class Laravel, Symfony, CodeIgniter 4,
Requires
- php: >=8.0
- ext-filter: *
- psr/log: ^1.0|^2.0|^3.0
- psr/simple-cache: ^1.0|^2.0|^3.0
Requires (Dev)
- cakephp/validation: ^4.4 || ^5.0
- egulias/email-validator: ^3.2 || ^4.0
- illuminate/console: ^9.0 || ^10.0 || ^11.0 || ^12.0 || ^13.0
- illuminate/support: ^9.0 || ^10.0 || ^11.0 || ^12.0 || ^13.0
- illuminate/validation: ^9.0 || ^10.0 || ^11.0 || ^12.0 || ^13.0
- phpunit/phpunit: ^10.0 || ^11.0 || ^12.0
- symfony/config: ^6.0 || ^7.0 || ^8.0
- symfony/console: ^6.0 || ^7.0 || ^8.0
- symfony/dependency-injection: ^6.0 || ^7.0 || ^8.0
- symfony/http-kernel: ^6.0 || ^7.0 || ^8.0
- symfony/validator: ^6.0 || ^7.0 || ^8.0
- yiisoft/validator: ^2.0
Suggests
- ext-curl: Faster and more robust downloads for the list synchronizer (streams are used otherwise)
- ext-intl: Internationalized domain names (IDN) and the spoof (homograph) check
- ext-memcached: For Memcached cache support
- ext-phalcon: Phalcon 5 integration (validators and DI service provider)
- ext-redis: For Redis cache support
- cakephp/cakephp: CakePHP integration (validation provider and plugin)
- codeigniter4/framework: CodeIgniter 4 integration (auto-discovered validation rules and service)
- egulias/email-validator: Format modes "rfc" and "strict" (RFC 5322 parsing)
- illuminate/support: Laravel integration (service provider, facade, rules, Blade directive, Artisan commands)
- illuminate/view: Laravel @disposableEmail Blade directive
- predis/predis: For Redis cache without ext-redis
- symfony/console: Symfony console commands (email-validator:sync, :stats, :check)
- symfony/http-kernel: Symfony bundle integration
- symfony/validator: Symfony validation constraints (#[NotDisposableEmail], #[ValidEmail])
- symfony/yaml: For YAML configuration file support
- yiisoft/validator: Yii 3 integration (#[NotDisposableEmail] and #[VerifiedEmail] rules)
- yiisoft/yii2: Yii 2 integration (application component and validators)
Provides
None
Conflicts
None
Replaces
None
README
PHP Email Validator
The most complete email validation & disposable email detection library for PHP. One package for Laravel, Symfony, CodeIgniter 4, CakePHP, Yii 2, Yii 3, Phalcon — and plain PHP.
$validator = HarunGecit\EmailValidator\EmailValidator::create(); $validator->isValid('john@gmail.com'); // true — format, disposable and MX checks $validator->isDisposable('john@mailinator.com'); // true $validator->checkDisposable('x@eu.mailinator.com') // explains *why*: ->getMatchedDomain(); // 'mailinator.com' (subdomain match) $validator->validate('john@gmial.com')->getSuggestion(); // 'john@gmail.com' (with typo suggestions on)
- 136,700+ disposable domains from multiple sources — rebuilt daily, released weekly, and false-positive filtered (no
namecheap.com, noyandex.net, no ISP domains, no public suffixes). - Explainable results — which list entry matched, whether through a subdomain, and from which list.
- A full validation pipeline, not just a blocklist — RFC format modes, MX/DNS, homograph spoofing, role accounts, plus-addressing, typo suggestions, SMTP and catch-all.
- Every PHP framework, natively — validation rules, constraints, services and commands that feel at home in each framework.
- Fast — O(1) lookups (~2.5 µs, subdomains included); with OPcache the full domain index costs zero load time and per-request memory.
- No API keys, no per-request fees — everything runs locally; network is only used for the checks you enable (MX, SMTP) and for list syncs.
Table of Contents
- Why this package?
- Installation
- Quick start
- Disposable email detection
- The disposable domain list
- Keeping the list fresh (sync)
- Full validation pipeline
- Configuration
- Framework integrations — Laravel · Symfony · CodeIgniter 4 · CakePHP · Yii 2 · Yii 3 · Phalcon · Plain PHP
- Command line
- Performance
- Caching, rate limiting & logging
- Result objects & API reference
- Testing · Upgrading · Contributing · Changelog
⭐ Why this package?
| php-email-validator | Typical disposable-list package | |
|---|---|---|
| Disposable domains | 136,700+ disposable domains, multi-source, daily rebuild | one source, a few thousand to ~100k |
| False-positive protection | Public Suffix List, popularity, typo & allowlist filters, regression tests | none — aggregated feeds are merged blindly |
Subdomain detection (x.mailinator.com) |
✅ (configurable) | sometimes |
| Explains matches (entry + source) | ✅ checkDisposable() |
rarely |
| Runtime sync between releases | ✅ append-only, atomic, locked, HTML-safe | sometimes |
| Format: filter / unicode / RFC 5322 / strict | ✅ | framework-dependent |
| MX / DNS, spoof (homograph), role-based, plus-addressing, typo suggestions | ✅ | ❌ |
| SMTP mailbox & catch-all verification | ✅ (opt-in) | ❌ |
| Frameworks | Laravel, Symfony, CodeIgniter 4, CakePHP, Yii 2, Yii 3, Phalcon, plain PHP | usually one |
| CLI | ✅ vendor/bin/email-validator |
framework-only, if any |
| Caching / rate limiting / PSR-3 logging | ✅ Memory, File, Redis, Memcached, PSR-16 | ❌ |
| PHP versions | 8.0 – 8.5 | varies |
Installation
composer require harungecit/php-email-validator
Compatibility
- PHP 8.0 – 8.5 (runtime verified on 8.0; full test suite on 8.1 – 8.5), on Linux, Windows and macOS.
- Laravel 9 – 13 · Symfony 6.4 – 8.x · CodeIgniter 4.4+ · CakePHP 4.4+ / 5.x · Yii 2.0 · Yii 3 (yiisoft/validator 2.x) · Phalcon 5.
- Optional:
ext-intl(IDN + spoof check),egulias/email-validator(rfc/strictformat modes),ext-curl(sync),ext-redis/predis/predis/ext-memcached(cache backends),symfony/yaml(YAML config).
🚀 Quick start
use HarunGecit\EmailValidator\EmailValidator; $validator = EmailValidator::create(); // format + disposable + MX checks if (!$validator->isValid($email)) { // reject } // Detailed report $result = $validator->validate('user@mailinator.com'); $result->isValid(); // false $result->getErrors(); // ['Disposable email address'] $result->getChecks(); // ['format' => true, 'disposable' => false, 'mx' => true] $result->getMetadata(); // ['disposable_match' => 'mailinator.com', 'disposable_source' => 'blocklist']
Just need a disposable check, no network?
$validator = EmailValidator::basic(); // format + disposable only $validator->isValid('user@yopmail.com'); // false
🛡 Disposable email detection
$validator->isDisposable('user@mailinator.com'); // true (email address) $validator->isDisposableDomain('mailinator.com'); // true (bare domain) $check = $validator->checkDisposable('User@EU.Mailinator.com'); $check->isDisposable(); // true $check->getDomain(); // 'eu.mailinator.com' (normalized: lowercase, trailing dot removed, IDN → punycode) $check->getMatchedDomain(); // 'mailinator.com' $check->isSubdomainMatch(); // true $check->getSource(); // 'blocklist' | 'custom' | 'synced' | 'allowlist' | null $check->isAllowlisted(); // false $check->toArray(); // JSON-serializable
How a domain is matched
The domain and each of its parent domains are looked up, most specific first —
a.b.mailinator.com, b.mailinator.com, mailinator.com — so throwaway subdomains are caught too.
On each level the lists are consulted in this order (first hit wins):
| Priority | List | Source label |
|---|---|---|
| 1 | Your allowlist — lists.custom_allowlist, custom_allowlist_paths, addToAllowlist() |
allowlist |
| 2 | Your blocklist — lists.custom_blocklist, custom_blocklist_paths, addToBlocklist() |
custom |
| 3 | Bundled allowlist (237 legitimate providers, ISPs and privacy relays) | allowlist |
| 4 | Synced blocklist (lists.synced_blocklist_path) |
synced |
| 5 | Bundled blocklist (or your replacement via lists.blocklist_path) |
blocklist |
So your own decisions always win over the bundled data — e.g. you can block a privacy relay the package allows, or allow a domain the package blocks.
use HarunGecit\EmailValidator\Config\ConfigurationBuilder; $config = ConfigurationBuilder::create() ->basic() ->withCustomBlocklist(['competitor-trials.example']) // block more ->withCustomAllowlist(['partner.mailinator.com']) // allow exceptions ->withCustomBlocklistFiles([__DIR__ . '/lists']) // *.conf, *.txt, *.list, *.json files or directories ->withoutSubdomainBlocking() // optional: exact matches only ->build(); $validator = EmailValidator::withConfig($config);
Lists can also be changed at runtime (fluent): addToBlocklist(), addToAllowlist(),
removeFromBlocklist(), removeFromAllowlist(), getBlocklist(), getBlocklistCount(),
isBlocklisted(), isAllowlisted(). Runtime changes never copy the shared index.
Privacy relays (Apple Hide My Email, Firefox Relay, DuckDuckGo, SimpleLogin, Proton Pass, addy.io) forward to a real inbox and are allowed by default. To block them, add them to your blocklist.
📚 The disposable domain list
The bundled list is rebuilt every day from multiple upstream projects and released as a patch
version every Monday, so a regular composer update keeps you current. Want daily freshness?
See sync.
| Source | Role | Treatment |
|---|---|---|
| disposable-email-domains | hand-curated, conservative | trusted — taken as-is |
| eramitgupta/disposable-email | large daily aggregator (several lists + StopForumSpam toxic domains) | untrusted — every entry is filtered |
data/blocklist.local.conf |
maintainers' additions | always included |
data/allowlist.conf |
legitimate domains | always excluded (with subdomains) |
Aggregated feeds contain real companies and ISPs that were once abused by spammers. Blindly merging
them would block e.g. namecheap.com, yandex.net, sutterhealth.org, Roadrunner/Spectrum ISP
addresses (nyc.rr.com) or entire country zones (msk.ru, pp.ua). The build therefore drops:
| Filter | Applies to | Example of what it prevents |
|---|---|---|
| Invalid syntax / upstream merge artifacts | all / untrusted | btcmail.pwguerrillamail.net (two lines glued together) |
| Public Suffix List entries | all | msk.ru, pp.ua, it.com — would block every domain below them |
| Allowlisted domains and their subdomains | all | vip.126.com, simplelogin.co, nyc.rr.com |
| Tranco top-100k popular domains | untrusted only | namecheap.com, compaq.com, cafe24.com |
| Typos of major providers | untrusted only | gamil.com → handled by typo suggestions instead of a misleading "disposable" error |
On top of that the build refuses to shrink the list by more than 5% in one run, switches to an
additive (remove-nothing) mode if an untrusted source is down, and CI keeps regression tests for
known false positives. Build details live in data/metadata.json and in the
output of email-validator stats.
Found a missing domain or a false positive? See CONTRIBUTING.md — fixes are permanent.
🔄 Keeping the list fresh (sync)
Package releases are weekly; the list on main is rebuilt daily. The synchronizer downloads
remote lists into a file in your app, and every validator configured with that file blocks those
domains too — no deploy needed.
vendor/bin/email-validator sync --path=var/email-validator/synced-blocklist.txt
$config = ConfigurationBuilder::create() ->standard() ->withSyncedBlocklist(__DIR__ . '/var/email-validator/synced-blocklist.txt') ->build();
Schedule it (cron, Laravel scheduler, Symfony scheduler, …):
15 3 * * * cd /path/to/app && vendor/bin/email-validator sync --path=var/email-validator/synced-blocklist.txt
By default the synchronizer downloads this package's own filtered list from main
(Configuration::DEFAULT_SYNC_URL), so you get the same false-positive protection.
Use --url= (repeatable) or the sync.urls setting to add other plain-text or JSON lists.
Safeguards — a bad download never damages your last good list:
- failed requests, non-2xx responses, HTML error pages, empty responses and lists without a single valid domain are rejected before anything is written;
- append-only by default;
--replaceonly drops domains when every source succeeded; - allowlisted domains (bundled + yours, including subdomains) are never written;
- only domains that are not already bundled are stored, so the file stays tiny and cheap to load;
- writes go to a temp file that is atomically renamed, under an exclusive lock; an unchanged list is not rewritten.
Programmatic use:
use HarunGecit\EmailValidator\Sync\ListSynchronizer; $result = (new ListSynchronizer($config))->sync(replace: false); $result->isSuccessful(); $result->getAdded(); $result->getTotal(); $result->getErrors();
🔬 Full validation pipeline
validate() runs every enabled check and returns a ValidationResult:
| Check | Default | What it does |
|---|---|---|
format |
✅ | Syntax — see format modes |
spoof |
❌ | Mixed-script / invisible characters per address part (needs ext-intl) |
disposable |
✅ | Bundled + custom + synced lists, subdomain-aware |
mx |
✅ | MX records (checkdnsrr), cached per domain |
role_based |
❌ | admin@, info@, support@, … (configurable prefixes) |
subaddress |
❌ | Plus addressing user+tag@ (warning + base_email metadata) |
smtp |
❌ | Live mailbox check (RCPT TO) — slow, may be blocked |
catch_all |
❌ | Detects domains that accept every address |
typo_suggestion |
❌ | "Did you mean gmail.com?" — never fails validation |
Presets
| Preset | Enabled checks |
|---|---|
minimal() |
format |
basic() |
format, disposable |
standard() |
format, mx, disposable, typo suggestion |
strict() |
format, mx, disposable, role-based, subaddress, typo suggestion, smtp, catch-all |
$validator = EmailValidator::strict(); // ConfigurationBuilder::create()->strict()->build() $validator = EmailValidator::basic();
strict()enables SMTP and catch-all verification, which open live connections and can be slow or blocked. For most applicationsstandard()is the recommended starting point.
Format modes
| Mode | Engine | Accepts |
|---|---|---|
filter (default) |
FILTER_VALIDATE_EMAIL |
ASCII addresses |
filter_unicode |
FILTER_VALIDATE_EMAIL + FILTER_FLAG_EMAIL_UNICODE |
internationalized local parts (jörg@example.com) |
rfc |
egulias/email-validator RFCValidation |
everything RFC 5322 allows |
strict |
egulias/email-validator NoRFCWarningsValidation |
RFC 5322 without warnings (no quoted local parts, …) |
ConfigurationBuilder::create()->standard()->withFormatMode('rfc')->withSpoofCheck()->build();
Spoofing (homograph) detection
$validator->isSpoofed('user@xn--gmil-63d.com'); // true — "gmаil.com" with a Cyrillic "а" $validator->isSpoofed("p\u{0430}ypal@example.com"); // true — mixed-script local part $validator->isSpoofed('ivan@почта.рф'); // false — one script per part is legitimate
Individual checks
$validator->isValidFormat('user@example.com'); $validator->hasValidMX('user@example.com'); // MX records (cached) $validator->hasValidDNS('user@example.com'); // A / AAAA records $validator->isRoleBased('info@example.com'); $validator->isSubaddressed('user+tag@gmail.com'); $validator->getBaseEmail('user+tag@gmail.com'); // 'user@gmail.com' $validator->isCatchAll('user@example.com'); $validator->validateSMTP('user@example.com'); $validator->getSuggestion('user@gmial.com'); // SuggestionResult: 'user@gmail.com', confidence 0–100
Batch validation
$emails = ['user1@gmail.com', 'user2@mailinator.com', 'invalid-email']; $validator->validateBatch($emails); // array<string, ValidationResult> $validator->filterValid($emails, false); // ['user1@gmail.com'] ($checkMX = false) $validator->filterInvalid($emails, false); $validator->getStatistics($emails, false); // ['total' => 3, 'valid' => 1, 'invalid' => 2, 'disposable' => 1, 'invalid_format' => 1, 'no_mx' => 0]
⚙ Configuration
Use the fluent builder, an array, or a PHP/JSON/YAML file — they all produce a Configuration.
$config = ConfigurationBuilder::create() ->standard() ->withRoleBasedCheck() ->withFileCache(__DIR__ . '/cache', 3600) ->withRateLimiting(100, 60) ->build(); $validator = EmailValidator::withConfig($config); $validator = EmailValidator::fromConfigFile(__DIR__ . '/config/emailvalidator.php'); // .php / .json / .yaml $config = Configuration::fromArray([...]); // same keys
Ready-made templates: config/emailvalidator.php and config/emailvalidator.json.
All configuration keys
| Key | Default | Description |
|---|---|---|
checks.format / mx / disposable |
true |
Core checks |
checks.role_based / smtp / typo_suggestion / subaddress / catch_all / spoof |
false |
Optional checks |
format.mode |
filter |
filter, filter_unicode, rfc, strict |
lists.blocklist_path / allowlist_path |
null |
Replace the bundled lists |
lists.custom_blocklist / custom_allowlist |
[] |
Domains added to the lists (take precedence over bundled lists) |
lists.custom_blocklist_paths / custom_allowlist_paths |
[] |
Extra list files or directories |
lists.synced_blocklist_path |
null |
File maintained by sync (ignored until it exists) |
lists.block_subdomains |
true |
Match parent domains (x.mailinator.com → mailinator.com) |
lists.role_based_path |
null |
Custom role-prefix list file |
sync.urls |
package list on main |
Remote lists for sync (text or JSON, http/https only) |
sync.timeout |
30 |
HTTP timeout in seconds |
cache.driver / ttl / prefix / options |
memory, 3600 |
memory, file, redis, memcached, psr16, null |
rate_limit.enabled / max_attempts / decay_seconds |
false, 100, 60 |
Token-bucket limiter |
role_based.prefixes |
36 prefixes | Role-account local parts |
typo.common_domains / typo.mappings |
major providers | Typo suggestion data |
smtp.timeout / from_email / from_domain |
10, null, null |
SMTP verification |
dns.timeout |
5 |
DNS timeout |
🧩 Framework integrations
Every integration wraps the same core, so behaviour and configuration keys are identical everywhere.
| Framework | Validation | Services | Commands / sync | Extras |
|---|---|---|---|---|
| Laravel 9–13 | rule objects + string rules | facade, container | Artisan, scheduler | Blade directive, translations, Laravel Boost |
| Symfony 6.4–8 | #[NotDisposableEmail], #[ValidEmail], #[NotRoleBasedEmail] |
autowiring | bin/console |
YAML config, Scheduler |
| CodeIgniter 4.4+ | auto-registered rules | service('emailValidator') |
CLI + cron | zero-config discovery, .env |
| CakePHP 4.4+/5 | validation provider | DI plugin | CLI + cron | Configure key |
| Yii 2 | model validators | app component | CLI + cron | aliases, Yii::t() messages |
| Yii 3 | attribute rules | DI container | CLI + cron | — |
| Phalcon 5 | validators | DI service provider | CLI + cron | config service section |
Laravel
Supports Laravel 9–13 (rule objects need Laravel 10+). The service provider and the EmailValidator facade are auto-discovered.
php artisan email-validator:install # publishes config/emailvalidator.php (add --translations for the messages) php artisan email-validator:sync # download the latest disposable domains
Validation rules
use HarunGecit\EmailValidator\Framework\Laravel\Rules\NotDisposableEmail; use HarunGecit\EmailValidator\Framework\Laravel\Rules\NotRoleBasedEmail; use HarunGecit\EmailValidator\Framework\Laravel\Rules\ValidEmail; $request->validate([ 'email' => ['required', 'email', new NotDisposableEmail()], 'work_email' => ['required', new ValidEmail()], // every check enabled in config, first failure reported 'billing' => ['required', new ValidEmail(checkMx: false)], // skip DNS for this rule 'contact' => ['required', 'email', new NotRoleBasedEmail()], 'other' => ['required', new NotDisposableEmail('Please use a permanent address.')], ]); // String rules: valid_email, not_disposable, not_role_based, has_mx, not_subaddressed, not_spoofed $request->validate(['email' => 'required|email:rfc,dns|not_disposable']);
Messages live in the emailvalidator::validation translation group (php artisan vendor:publish --tag=emailvalidator-translations).
Rule-object messages accept :attribute; NotDisposableEmail also :domain and :matched, ValidEmail also :reason and :suggestion.
Facade, Blade & commands
use EmailValidator; // HarunGecit\EmailValidator\Framework\Laravel\EmailValidatorFacade EmailValidator::isDisposable('user@mailinator.com'); // true EmailValidator::checkDisposable('user@eu.mailinator.com')->getMatchedDomain(); // 'mailinator.com' EmailValidator::validate('john@gmial.com')->getSuggestion()?->getSuggestedEmail(); // 'john@gmail.com' EmailValidator::getListStatistics();
@disposableEmail($user->email) <p>Please switch to a permanent email address.</p> @enddisposableEmail
php artisan email-validator:check user@eu.mailinator.com example.org [--json] # exit 1 if disposable php artisan email-validator:validate user@example.com [--no-mx] [--json] # exit 1 if invalid php artisan email-validator:sync [--replace] [--json] php artisan email-validator:stats [--json]
Configuration (config/emailvalidator.php, env-aware; the cache driver defaults to your Laravel cache store):
'format' => ['mode' => env('EMAIL_VALIDATOR_FORMAT_MODE', 'filter')], 'lists' => [ 'custom_blocklist' => ['my-throwaway.com'], 'custom_allowlist' => ['my-company.com'], 'synced_blocklist_path' => storage_path('app/email-validator/synced-blocklist.txt'), 'block_subdomains' => true, ], 'sync' => [ 'urls' => [Configuration::DEFAULT_SYNC_URL], // EMAIL_VALIDATOR_SYNC_URLS (comma-separated) 'schedule' => env('EMAIL_VALIDATOR_SYNC_SCHEDULE'), // null|hourly|daily|weekly|monthly|cron expression ],
Set EMAIL_VALIDATOR_SYNC_SCHEDULE=daily and the package registers email-validator:sync on Laravel's scheduler (withoutOverlapping()).
AI agents using Laravel Boost get this package's guidelines automatically.
Symfony
Supports Symfony 6.4, 7.x and 8.x.
// config/bundles.php return [ HarunGecit\EmailValidator\Framework\Symfony\EmailValidatorBundle::class => ['all' => true], ];
# config/packages/email_validator.yaml email_validator: checks: { mx: true, disposable: true, role_based: false, spoof: false } format: { mode: filter } # filter | filter_unicode | rfc | strict lists: custom_blocklist: ['my-throwaway.com'] custom_allowlist: ['my-company.com'] synced_blocklist_path: '%kernel.project_dir%/var/email-validator/synced-blocklist.txt' # default; ~ disables block_subdomains: true sync: { timeout: 30 }
use HarunGecit\EmailValidator\Framework\Symfony\Validator\NotDisposableEmail; use HarunGecit\EmailValidator\Framework\Symfony\Validator\NotRoleBasedEmail; use HarunGecit\EmailValidator\Framework\Symfony\Validator\ValidEmail; use Symfony\Component\Validator\Constraints as Assert; class SignupDto { #[Assert\NotBlank, Assert\Email, NotDisposableEmail] public string $email; #[ValidEmail(checkMx: false, message: 'Invalid email: {{ reason }}')] public string $workEmail; #[NotRoleBasedEmail(groups: ['registration'])] public string $contactEmail; }
Message parameters: {{ value }}, {{ domain }}, {{ matched }} (NotDisposableEmail); {{ check }}, {{ reason }}, {{ suggestion }} (ValidEmail), with one error code per failed check.
Services: EmailValidator (autowirable, alias email_validator), Configuration, ListSynchronizer (email_validator.synchronizer).
bin/console email-validator:check user@eu.mailinator.com [--json]
bin/console email-validator:validate user@example.com [--no-mx] [--json]
bin/console email-validator:sync [--replace] [--json] # schedule with cron or Symfony Scheduler (RunCommandMessage)
bin/console email-validator:stats [--json]
CodeIgniter 4
Zero configuration. CodeIgniter's auto-discovery picks up the package's Config/Registrar.php and Config/Services.php:
the rules are added to Config\Validation::$ruleSets and service('emailValidator') returns a shared validator.
$rules = ['email' => 'required|valid_email|not_disposable_email|not_role_based_email']; if (! $this->validate($rules)) { return redirect()->back()->withInput()->with('errors', $this->validator->getErrors()); }
| Rule | Fails when |
|---|---|
not_disposable_email |
the domain (or a parent domain) is disposable |
email_has_mx |
the domain has no MX record |
not_role_based_email |
the address is a role account (admin@, info@, …) |
not_subaddressed_email |
the address has a +tag |
not_spoofed_email |
it mixes scripts or has invisible characters (needs ext-intl) |
verified_email |
the full configured pipeline fails; the message names the failed check |
Messages ship as language lines ({field}, {value} and per-field custom errors work); override them in
app/Language/<locale>/Validation.php. Use permit_empty for optional fields.
$validator = service('emailValidator'); // shared; single_service('emailValidator') for a new instance $validator->checkDisposable('user@eu.mailinator.com')->getMatchedDomain(); // 'mailinator.com'
To configure, create app/Config/EmailValidator.php (properties mirror the configuration keys; .env overrides such as
emailvalidator.checks.mx = false work too):
namespace Config; use HarunGecit\EmailValidator\Framework\CodeIgniter\Config\EmailValidator as BaseEmailValidator; class EmailValidator extends BaseEmailValidator { public array $lists = [ 'custom_blocklist' => ['competitor-temp.com'], 'synced_blocklist_path' => WRITEPATH . 'email-validator/synced.conf', 'block_subdomains' => true, ]; }
Discovery disabled? Add \HarunGecit\EmailValidator\Framework\CodeIgniter\EmailValidatorRules::class to Config\Validation::$ruleSets.
Sync from cron: vendor/bin/email-validator sync --path=writable/email-validator/synced.conf.
CakePHP
use HarunGecit\EmailValidator\Framework\CakePHP\EmailValidationProvider; public function validationDefault(Validator $validator): Validator { // One call: registers the "emailValidator" provider and adds the rules EmailValidationProvider::attach($validator, 'email', [ 'notDisposable', 'notRoleBased' => 'Please use a personal email address.', // custom message ]); return $validator; }
Rules: notDisposable, notRoleBased, notSubaddressed, notSpoofed, hasMx, verifiedEmail (full pipeline).
Manual use works as with any provider: ->setProvider('emailValidator', new EmailValidationProvider())->add('email', 'notDisposable', ['rule' => 'notDisposable', 'provider' => 'emailValidator']).
Register the plugin to get EmailValidator/Configuration in the DI container, configured from the EmailValidator key:
// src/Application.php $this->addPlugin(\HarunGecit\EmailValidator\Framework\CakePHP\EmailValidatorPlugin::class); // config/app_local.php 'EmailValidator' => [ 'checks' => ['mx' => true, 'role_based' => true], 'lists' => ['synced_blocklist_path' => TMP . 'email-validator' . DS . 'synced.conf'], 'cache' => ['driver' => 'cakephp', 'options' => ['config' => 'default']], // optional: use a Cake cache config ],
public function signup(\HarunGecit\EmailValidator\EmailValidator $emailValidator) { /* injected */ }
Yii 2
// config/web.php (and console.php) 'components' => [ 'emailValidator' => [ 'class' => \HarunGecit\EmailValidator\Framework\Yii2\EmailValidatorComponent::class, 'config' => [ 'checks' => ['mx' => true, 'role_based' => true], 'lists' => ['synced_blocklist_path' => '@runtime/email-validator/synced.conf'], // aliases are resolved ], ], ],
use HarunGecit\EmailValidator\Framework\Yii2\NotDisposableEmailValidator; use HarunGecit\EmailValidator\Framework\Yii2\NotRoleBasedEmailValidator; use HarunGecit\EmailValidator\Framework\Yii2\VerifiedEmailValidator; public function rules() { return [ ['email', 'required'], ['email', 'email'], ['email', NotDisposableEmailValidator::class], ['email', NotRoleBasedEmailValidator::class], ['email', VerifiedEmailValidator::class, 'messages' => ['mx' => 'The domain of {attribute} cannot receive email.']], ]; } Yii::$app->emailValidator->checkDisposable('user@eu.mailinator.com')->getMatchedDomain(); // 'mailinator.com'
Messages support {attribute}, {value}, {domain}, {matched} (and {check}, {error}, {suggestion} for VerifiedEmailValidator) and go through Yii::t('app', …).
Yii 3
use HarunGecit\EmailValidator\Framework\Yii3\NotDisposableEmail; use HarunGecit\EmailValidator\Framework\Yii3\VerifiedEmail; use Yiisoft\Validator\Rule\{Email, Required}; final class SignupForm { #[Required, Email, NotDisposableEmail] public string $email = ''; #[VerifiedEmail(skipOnEmpty: true, messages: ['disposable' => 'Please use a permanent email address.'])] public string $backupEmail = ''; }
Handlers receive EmailValidator from the DI container — register it with your configuration:
// config/common/di/email-validator.php return [ EmailValidator::class => ['__construct()' => [Configuration::fromArray(['checks' => ['mx' => true]])]], ];
Phalcon 5
use HarunGecit\EmailValidator\Framework\Phalcon\EmailValidatorServiceProvider; use HarunGecit\EmailValidator\Framework\Phalcon\Validator\NotDisposableEmail; use HarunGecit\EmailValidator\Framework\Phalcon\Validator\VerifiedEmail; use Phalcon\Filter\Validation; use Phalcon\Filter\Validation\Validator\Email; $di->register(new EmailValidatorServiceProvider([ 'checks' => ['mx' => true], 'lists' => ['synced_blocklist_path' => BASE_PATH . '/storage/email-validator/synced.conf'], ])); // or no argument: reads the "emailValidator" section of the "config" service $validation = new Validation(); $validation->add('email', new Email()); $validation->add('email', new NotDisposableEmail(['message' => 'Disposable addresses (:domain) are not allowed'])); $validation->add('email', new VerifiedEmail(['messages' => ['mx' => 'The domain of :field cannot receive email']])); $messages = $validation->validate($_POST);
Plain PHP / any framework
The core has no framework dependency: create an EmailValidator, keep it in your container, and
use vendor/bin/email-validator sync from cron. Slim, Mezzio, Laminas, Lumen, Nette, Spiral,
WordPress plugins — anything with Composer works.
$validator = EmailValidator::fromConfigFile(__DIR__ . '/config/emailvalidator.php');
💻 Command line
vendor/bin/email-validator check <email|domain>... Is it disposable? Shows the matched entry and its source
vendor/bin/email-validator validate <email>... Full validation (format, disposable, MX, ...)
vendor/bin/email-validator sync Download remote lists into the synced list file
vendor/bin/email-validator stats List statistics
Options: --config=FILE --path=FILE --url=URL (repeatable) --timeout=SECONDS
--replace --no-mx --no-subdomains --json -h/--help -V/--version
$ vendor/bin/email-validator check user@eu.mailinator.com gmail.com 163.com DISPOSABLE user@eu.mailinator.com (matched mailinator.com in blocklist) OK gmail.com ALLOWED 163.com (matched 163.com in allowlist)
Exit codes: 0 = nothing flagged, 1 = disposable/invalid found or sync failed, 2 = usage error —
handy for shell scripts and CI (e.g. validating a CSV export).
⚡ Performance
- Lookups are O(1) hash lookups; a check including the parent-domain walk takes ~2.5 µs.
- With OPcache (every production PHP-FPM setup) the precompiled
data/blocklist.phpindex is an immutable array in shared memory: loading the whole list costs nothing and adds no per-request memory. - Without OPcache (CLI, workers) the list is read with a single
explode()in ~18 ms, once per process. - Runtime list changes (
addToBlocklist(), custom/synced lists) are kept in small side tables, so the shared index is never copied. - MX and catch-all results are cached through the configured cache backend.
Caching, rate limiting & logging
ConfigurationBuilder::create()->withMemoryCache(3600); // in-process (default) ConfigurationBuilder::create()->withFileCache('/tmp/ev-cache', 3600); // filesystem ConfigurationBuilder::create()->withRedisCache('127.0.0.1', 6379, null, 0); // ext-redis or Predis ConfigurationBuilder::create()->withMemcachedCache('127.0.0.1', 11211); // ext-memcached ConfigurationBuilder::create()->withPsr16Cache($anyPsr16Cache); // any PSR-16 cache ConfigurationBuilder::create()->withoutCache(); CacheManager::registerDriver('my-driver', MyCacheAdapter::class); // custom drivers
Rate limiting throttles validations per address with a token bucket that shares the cache backend (works across processes with Redis/Memcached/File):
$config = ConfigurationBuilder::create()->standard()->withRateLimiting(maxAttempts: 5, decaySeconds: 60)->build(); try { $validator->validate($email); } catch (RateLimitExceededException $e) { echo "Retry after {$e->getRetryAfter()}s"; }
PSR-3 logging: $validator->setLogger($logger) or $config->setLogger($logger) — each validate()
logs a debug entry with the outcome, checks and errors.
📖 Result objects & API reference
ValidationResult
isValid, getEmail, getDomain, getLocalPart, getChecks, passed(check), getErrors, getFirstError, getWarnings, isDisposable, isRoleBased, isSubaddressed, getMetadata, getMetadataValue, hasSuggestion, getSuggestion, toArray, toJson.
Metadata keys: disposable_match, disposable_source, subaddressed, base_email.
DisposableCheckResult
getInput, getDomain, isDisposable, getMatchedDomain, getSource, isAllowlisted, isSubdomainMatch, toArray, jsonSerialize.
Source constants: SOURCE_BLOCKLIST, SOURCE_CUSTOM, SOURCE_SYNCED, SOURCE_ALLOWLIST.
SuggestionResult
getOriginalEmail, getSuggestedEmail, getOriginalDomain, getSuggestedDomain, getReason, getConfidence, isHighConfidence, toArray, __toString.
EmailValidator
| Method | Description |
|---|---|
create() / withConfig($c) / fromConfigFile($path) / strict() / basic() |
Factories |
validate($email): ValidationResult |
Full pipeline |
isValid($email, ?bool $checkMX = null): bool |
Boolean validation |
validateBatch($emails) |
array<string, ValidationResult> |
checkDisposable($emailOrDomain): DisposableCheckResult |
Explained disposable lookup |
isDisposable($email) / isDisposableDomain($domain) |
Disposable checks |
isValidFormat / hasValidMX / hasValidDNS / isSpoofed / isRoleBased / isSubaddressed / getBaseEmail / getSuggestion / isCatchAll / validateSMTP |
Individual checks |
getListStatistics(): array |
Counts, sources, last sync, index strategy |
addToBlocklist / addToAllowlist / removeFrom* / getBlocklist / getAllowlist / get*Count / isBlocklisted / isAllowlisted |
List management |
validateMultiple / validateWithDetails / filterValid / filterInvalid / getStatistics |
2.x-compatible helpers |
extractDomain / extractLocalPart / normalize / normalizeMultiple |
Utilities |
setConfiguration / getConfiguration / setLogger / setCacheEnabled / clearCache |
Runtime |
ConfigurationBuilder
Presets strict/standard/basic/minimal; cache withMemoryCache/withFileCache/withRedisCache/withMemcachedCache/withPsr16Cache/withCache/withoutCache;
withRateLimiting/withoutRateLimiting; checks withRoleBasedCheck/withTypoSuggestion/withSmtpCheck/withSubaddressCheck/withCatchAllCheck/withSpoofCheck/withoutMxCheck/withoutDisposableCheck;
format withFormatMode; lists withBlocklist/withAllowlist/withCustomBlocklist/withCustomAllowlist/withCustomBlocklistFiles/withCustomAllowlistFiles/withSyncedBlocklist/withoutSubdomainBlocking; build().
Fetcher (list files)
loadBlocklist, loadAllowlist, loadAll, loadBlocklistIndex, loadAllowlistIndex, loadCustomBlocklist, loadCustomAllowlist, loadPaths, mergeLists, saveList, compileIndex, getMetadata, clearCache, getBlocklistPath, getBlocklistIndexPath, getAllowlistPath, getMetadataPath, blocklistExists, allowlistExists, getBlocklistCount, getAllowlistCount, isOpcacheActive.
Exceptions
All extend HarunGecit\EmailValidator\Exceptions\EmailValidatorException:
RateLimitExceededException (getRetryAfter(), getMaxAttempts()), InvalidEmailException,
ConfigurationException (invalid value, unsupported format, missing file or dependency),
CacheException, SmtpConnectionException.
Testing
composer test # PHPUnit: core, data integrity and every framework integration composer test-coverage # HTML coverage report in ./coverage composer update-lists # maintainers: rebuild data/ from all upstream sources
The library runs on PHP 8.0+; the test suite needs PHP 8.1+ (PHPUnit 10–12). CI runs PHP 8.1 – 8.5 on Ubuntu, Windows and macOS, the oldest supported framework versions on PHP 8.1, a PHP 8.0 runtime smoke test, and data-integrity checks on every push.
Upgrading
3.2 is backward compatible with 3.0/3.1 and the 2.x API. Worth knowing:
- The bundled list grew from ~8,900 to over 136,000 domains in 3.2.0. If a legitimate domain your users rely
on is now flagged, add it to
lists.custom_allowlistand report it. - Your
custom_blocklist/addToBlocklist()entries now override the bundled allowlist (previously the allowlist always won). The bundled blocklist still never overrides the allowlist. EmailValidator::create()now shares the precompiled index instead of copying the list.- List files written by
Fetcher::saveList()end with a newline.
Coming from 1.x? The namespace changed from PHPOrbit\EmailValidator to HarunGecit\EmailValidator,
and the package from phporbit/php-email-validator to harungecit/php-email-validator.
Contributing
Contributions are welcome — missing disposable domains, false-positive reports, framework integrations and docs. Please read CONTRIBUTING.md; security issues go through SECURITY.md.
License
MIT — see LICENSE. The disposable domain data is derived from disposable-email-domains (CC0) and eramitgupta/disposable-email (MIT); thanks to their maintainers and to the Public Suffix List and Tranco projects.
Author
Harun Geçit — GitHub · LinkedIn · Email
If this package saves you from fake sign-ups, please ⭐ the repository — it helps other PHP developers find it.
Changelog
Automated weekly data releases (blocklist updates) are documented on the Releases page.
v3.2.0
- 136,577 disposable domains at release (up from 8,948): the blocklist is now built from disposable-email-domains (trusted) and eramitgupta/disposable-email (filtered aggregator).
- False-positive filtering in the build: Public Suffix List, Tranco top-100k, typo domains, allowlist subdomains and upstream merge artifacts; allowlist expanded to 237 domains (ISPs, organisations, privacy relays such as SimpleLogin and Proton Pass).
- The list is rebuilt daily on
main; patch releases still ship every Monday (skipped automatically whenmainhas unreleased code). - New
checkDisposable()returningDisposableCheckResult(matched domain, source, subdomain match),isDisposableDomain(),isSpoofed()andgetListStatistics();validate()addsdisposable_match/disposable_sourcemetadata. - New configuration:
lists.custom_blocklist,custom_allowlist,custom_*_paths,synced_blocklist_path,block_subdomains,sync.urls,sync.timeout,format.mode(filter,filter_unicode,rfc,strict) and thespoofcheck. - New list synchronizer (
ListSynchronizer) with append-only/replace modes, HTML/empty-response rejection, allowlist filtering, atomic writes and locking. - New framework-agnostic CLI:
vendor/bin/email-validator check|validate|sync|stats. - Precompiled OPcache index (
data/blocklist.php): zero load cost and no per-request memory with OPcache; runtime list changes no longer copy the list. - Domain normalization (case, trailing dot, IDN → punycode) for every list lookup.
- New integrations: CodeIgniter 4 (auto-discovered rules + service), CakePHP (validation provider + plugin), Yii 2 (component + validators), Yii 3 (attribute rules), Phalcon 5 (validators + service provider).
- Laravel: rule objects,
not_spoofedrule,@disposableEmailBlade directive, Artisan commands, scheduled sync, Laravel Boost guidelines. Symfony:#[NotDisposableEmail]/#[ValidEmail]constraints and console commands. Laravel 13 and Symfony 8 support. - Application lists (
custom_*) now take precedence over the bundled allowlist. - Added CONTRIBUTING.md and SECURITY.md; leaner Composer dist archive.
v3.1.1
- Updated the disposable blocklist to 8,900+ domains (785 new domains from upstream).
isDisposable()now detects subdomains of disposable providers (user@sub.mailinator.com) by also checking parent domains; the most specific list entry wins. Lookups use a hash index instead of a linear scan.- Added a scheduled GitHub Actions workflow that refreshes the blocklist weekly and publishes a patch release automatically.
composer update-listsnow rebuilds the blocklist asupstream + data/blocklist.local.conf − allowlistinstead of a keep-everything union, so upstream false-positive removals are applied (removedgmal.com,iwi.net,mailhub.pro). Added safety checks for unattended runs (malformed lines skipped, abort on a suspiciously small upstream or a >5% shrink;--forceoverrides).
v3.1.0
- Updated the disposable blocklist to 8,100+ domains (244 new domains from the disposable-email-domains project).
- Expanded the allowlist to 190+ domains (added
proton.me,pm.me,hey.com,tuta.com,zohomail.com,icloud.com,duck.com,simplelogin.io,addy.io, and more). The upstream allowlist was discontinued; it is now maintained in this package. - Added
composer update-lists(scripts/update-lists.php) to refresh the blocklist from upstream with a single command. - Expanded the default typo mappings and common-domain list (Proton, Tuta, HEY, Zoho Mail, GMX, Yandex, and regional Yahoo/Hotmail/Outlook domains) and synchronized them across all config templates.
- Expanded the default role-based prefixes (
administrator,donotreply,security,privacy,legal,compliance,notifications, and more). Fetcher::saveList()now always writes LF line endings regardless of platform.
v3.0.1
- Fixed CI failures on PHP 8.0 and 8.1.
v3.0.0
- Requires PHP 8.0+ (dropped 7.4). Forward-compatible through PHP 8.5.
- Added a rich
ValidationResultobject returned by the newvalidate()method, plusvalidateBatch(). - Added
Configurationand a fluentConfigurationBuilderwithstrict,standard,basic, andminimalpresets. - Added configuration file loading (PHP / JSON / YAML) via
EmailValidator::fromConfigFile(). - Added new checks: role-based detection, subaddress (plus addressing), typo suggestions (
SuggestionResult), SMTP verification, and catch-all detection. - Added a pluggable cache layer with Memory, File, Redis, Memcached, PSR-16, and Null adapters.
- Added token-bucket rate limiting with
RateLimitExceededException. - Added PSR-3 logging support.
- Added Laravel (service provider, facade, publishable config, validation rules) and Symfony (bundle, DI extension) integrations.
- Updated the disposable blocklist to 7,900+ domains, synchronized with the disposable-email-domains project.
- Fully backward compatible with the 2.x public API.
v2.0.0
- Breaking: Namespace changed from
PHPOrbit\EmailValidatortoHarunGecit\EmailValidator. - Breaking: Package name changed from
phporbit/php-email-validatortoharungecit/php-email-validator. - Added batch validation (
validateMultiple,filterValid,filterInvalid),isValid(),validateWithDetails(),getStatistics(), and theEmailValidator::create()factory. - Added a fluent interface for list management, MX record caching,
hasValidDNS(), email normalization, andisAllowlisted()/isBlocklisted(). - Enhanced the
Fetcherwith caching, custom list loading, and merge capabilities. - Added multi-platform CI (Ubuntu, Windows, macOS).
v1.0.x
- Initial releases: email format validation, disposable email detection, and MX record validation.