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.
- 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
| 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 |
composer require harungecit/php-email-validator- 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).
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$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-serializableThe 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 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.
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.txtBy 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();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 |
| 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.
| 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();$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$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$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]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 keysReady-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 |
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 |
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 domainsValidation 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>
@enddisposableEmailphp 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.
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]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.
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 */ }// 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', β¦).
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]])]],
];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);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');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).
- 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.
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 driversRate 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.
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.
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 sourcesThe 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.
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.
Contributions are welcome β missing disposable domains, false-positive reports, framework integrations and docs. Please read CONTRIBUTING.md; security issues go through SECURITY.md.
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.
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.
Automated weekly data releases (blocklist updates) are documented on the Releases page.
- 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.
- 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).
- 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.
- Fixed CI failures on PHP 8.0 and 8.1.
- 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.
- 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).
- Initial releases: email format validation, disposable email detection, and MX record validation.