harungecit / php-email-validator
A comprehensive PHP email validation library to check format, detect disposable emails, validate MX records, role-based detection, typo suggestions, and support batch validation with configurable cache and rate limiting.
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)
- illuminate/support: ^9.0 || ^10.0 || ^11.0 || ^12.0
- phpunit/phpunit: ^10.0 || ^11.0 || ^12.0
- symfony/config: ^6.0 || ^7.0
- symfony/dependency-injection: ^6.0 || ^7.0
- symfony/http-kernel: ^6.0 || ^7.0
Suggests
- ext-intl: For internationalized domain name (IDN) support
- ext-memcached: For Memcached cache support
- ext-redis: For Redis cache support
- illuminate/support: To use the Laravel service provider and facade integration
- predis/predis: For Redis cache without ext-redis
- symfony/dependency-injection: To use the Symfony bundle integration
- symfony/http-kernel: To use the Symfony bundle integration
- symfony/yaml: For YAML configuration file support
README
A comprehensive, framework-friendly PHP email validation library: format and RFC checks, disposable-domain detection, MX/DNS verification, role-based detection, plus-addressing (subaddress) handling, typo suggestions, SMTP and catch-all verification, batch validation, pluggable caching and rate limiting — all configurable through a fluent builder.
Features
- Format Validation — RFC-compliant format checks via PHP's built-in filters.
- Disposable Email Detection — an extensive bundled blocklist (8,100+ domains, sourced from disposable-email-domains) with an allowlist override.
- MX & DNS Verification — verify that the domain has mail servers (MX) or A/AAAA records.
- Role-Based Detection — flag addresses like
info@,admin@,support@. - Subaddress (Plus Addressing) — detect
user+tag@gmail.com, extract the base address, compare equivalence. - Typo Suggestions — "did you mean
gmail.com?" for common provider typos, with a confidence score. - SMTP & Catch-All Verification — optional live mailbox and catch-all domain checks.
- Rich Result Object —
ValidationResultwith per-check status, errors, warnings, metadata, and JSON export. - Pluggable Cache — Memory, File, Redis, Memcached, PSR-16, or Null adapters.
- Rate Limiting — token-bucket limiter to throttle validation attempts.
- Fluent Configuration —
ConfigurationBuilderwithstrict,standard,basic, andminimalpresets, or load from PHP/JSON/YAML files. - Framework Integration — first-class Laravel and Symfony support.
- PSR-3 Logging — inject any PSR-3 logger.
- Batch Tools — validate lists, filter valid/invalid, gather statistics.
- Backward Compatible — the 2.x API (
isValid,validateWithDetails,validateMultiple, list management) still works unchanged.
Installation
composer require harungecit/php-email-validator
Compatibility
- Requires PHP 8.0 or newer. Verified on PHP 8.0, 8.1, 8.2, 8.3, 8.4 and 8.5.
- Tested on Ubuntu, Windows, and macOS.
- Optional integrations (install as needed):
illuminate/support(Laravel),symfony/http-kernel+symfony/dependency-injection(Symfony),ext-redis/predis/predis(Redis cache),ext-memcached(Memcached cache),ext-intl(IDN),symfony/yamlorext-yaml(YAML config).
The library itself runs on PHP 8.0+. Running the test suite requires PHP 8.1+ (PHPUnit 10+), since the tests use attribute-based data providers.
Quick Start
use HarunGecit\EmailValidator\EmailValidator; $validator = EmailValidator::create(); // Simple boolean check (format + disposable + MX by default) if ($validator->isValid('user@example.com')) { echo "Email is valid!"; }
Need a detailed report instead of a boolean? Use validate():
$result = $validator->validate('user@example.com'); $result->isValid(); // bool $result->getErrors(); // string[] $result->getChecks(); // ['format' => true, 'disposable' => true, 'mx' => true, ...]
Configuration
Everything is driven by a Configuration object. The easiest way to build one is the fluent ConfigurationBuilder.
use HarunGecit\EmailValidator\EmailValidator; use HarunGecit\EmailValidator\Config\ConfigurationBuilder; $config = ConfigurationBuilder::create() ->standard() // start from a preset ->withRoleBasedCheck() // then customize ->withTypoSuggestion() ->withFileCache(__DIR__ . '/cache', 3600) ->withRateLimiting(100, 60) ->build(); $validator = EmailValidator::withConfig($config);
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(); // shortcut for ConfigurationBuilder::create()->strict()->build() $validator = EmailValidator::basic(); // shortcut for the basic() preset
Note:
strict()enables SMTP and catch-all verification, which perform live network connections and can be slow or blocked by mail servers. For most applicationsstandard()is the recommended starting point.
Individual toggles
Every check has an enable* method on Configuration and a with*/without* method on the builder:
$config = ConfigurationBuilder::create() ->withoutMxCheck() // disable MX lookups ->withoutDisposableCheck() // allow disposable domains ->withSubaddressCheck() // detect plus addressing ->withCatchAllCheck() // detect catch-all domains ->withSmtpCheck(10, 'verify@your-domain.com') ->build();
Check defaults: format, mx, disposable are on; role_based, smtp, typo_suggestion, subaddress, catch_all are off (a bare new Configuration()). Presets change these as shown above.
Loading configuration from a file
EmailValidator::fromConfigFile() supports .php, .json, and .yaml/.yml:
$validator = EmailValidator::fromConfigFile(__DIR__ . '/config/emailvalidator.php');
A ready-made template lives in config/emailvalidator.php (and .json). Top-level keys: checks, cache, rate_limit, lists, role_based, typo, smtp, dns.
// config/emailvalidator.php return [ 'checks' => [ 'format' => true, 'mx' => true, 'disposable' => true, 'role_based' => false, 'smtp' => false, 'typo_suggestion' => true, 'subaddress' => false, 'catch_all' => false, ], 'cache' => ['driver' => 'memory', 'ttl' => 3600, 'prefix' => 'email_validator_', 'options' => []], 'rate_limit' => ['enabled' => false, 'max_attempts' => 100, 'decay_seconds' => 60], 'lists' => ['blocklist_path' => null, 'allowlist_path' => null, 'role_based_path' => null], 'smtp' => ['timeout' => 10, 'from_email' => null, 'from_domain' => null], 'dns' => ['timeout' => 5], ];
The ValidationResult object
validate() returns a rich result you can inspect:
use HarunGecit\EmailValidator\EmailValidator; use HarunGecit\EmailValidator\Config\ConfigurationBuilder; $config = ConfigurationBuilder::create()->standard()->withRoleBasedCheck()->build(); $validator = EmailValidator::withConfig($config); $result = $validator->validate('info@gmial.com'); $result->isValid(); // bool — true only if every enabled check passed $result->getEmail(); // 'info@gmial.com' $result->getDomain(); // 'gmial.com' $result->getLocalPart(); // 'info' $result->getChecks(); // ['format' => true, 'disposable' => true, 'role_based' => false, ...] $result->passed('mx'); // ?bool — null if the check was not run $result->getErrors(); // ['Role-based email address'] $result->getFirstError(); // 'Role-based email address' $result->getWarnings(); // string[] $result->isDisposable(); // ?bool $result->isRoleBased(); // ?bool $result->isSubaddressed(); // ?bool $result->getMetadata(); // array (e.g. base_email for subaddressed emails) $result->hasSuggestion(); // bool $result->getSuggestion(); // ?SuggestionResult $result->toArray(); // full array representation $result->toJson(JSON_PRETTY_PRINT); // JSON string
Individual Checks
Each check is also available as a standalone method:
$validator->isValidFormat('user@example.com'); // format only $validator->isDisposable('user@mailinator.com'); // blocklist (allowlist wins) $validator->hasValidMX('user@example.com'); // MX records (cached) $validator->hasValidDNS('user@example.com'); // A / AAAA records $validator->isRoleBased('info@example.com'); // role-based prefix $validator->isSubaddressed('user+tag@gmail.com');// plus addressing $validator->getBaseEmail('user+tag@gmail.com'); // 'user@gmail.com' $validator->isCatchAll('user@example.com'); // catch-all domain (SMTP) $validator->validateSMTP('user@example.com'); // live SMTP mailbox check
Typo suggestions
$suggestion = $validator->getSuggestion('user@gmial.com'); if ($suggestion !== null) { $suggestion->getSuggestedEmail(); // 'user@gmail.com' $suggestion->getSuggestedDomain(); // 'gmail.com' $suggestion->getOriginalEmail(); // 'user@gmial.com' $suggestion->getReason(); // 'known_typo' | 'similar_domain' $suggestion->getConfidence(); // 0–100 $suggestion->isHighConfidence(80); // bool (string) $suggestion; // 'user@gmail.com' }
Batch Validation
$emails = ['user1@gmail.com', 'user2@mailinator.com', 'invalid-email']; // Rich results keyed by email $results = $validator->validateBatch($emails); // array<string, ValidationResult> // Backward-compatible array results $results = $validator->validateMultiple($emails, false); // $checkMX = false // Filtering $valid = $validator->filterValid($emails, false); // string[] $invalid = $validator->filterInvalid($emails, false); // string[] // Statistics $stats = $validator->getStatistics($emails, false); // ['total' => 3, 'valid' => 1, 'invalid' => 2, 'disposable' => 1, 'invalid_format' => 1, 'no_mx' => 0]
Caching
MX lookups and catch-all results are cached through a pluggable adapter. Choose a driver via the builder:
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); // wrap a PSR-16 cache ConfigurationBuilder::create()->withoutCache(); // disable caching
Supported driver names: memory, file, redis, memcached, psr16, null. You can also register a custom driver:
use HarunGecit\EmailValidator\Cache\CacheManager; CacheManager::registerDriver('my-driver', MyCacheAdapter::class); CacheManager::getAvailableDrivers(); // string[] CacheManager::isDriverAvailable('redis'); // bool
The legacy setCacheEnabled(false) / clearCache() helpers from 2.x still work.
Rate Limiting
Throttle how many times an identifier may be validated within a decay window. When the limit is exceeded, validate()/isValid() throw RateLimitExceededException.
use HarunGecit\EmailValidator\Config\ConfigurationBuilder; use HarunGecit\EmailValidator\Exceptions\RateLimitExceededException; $config = ConfigurationBuilder::create() ->standard() ->withRateLimiting(maxAttempts: 5, decaySeconds: 60) ->build(); $validator = EmailValidator::withConfig($config); try { $result = $validator->validate($email); } catch (RateLimitExceededException $e) { echo "Too many attempts. Retry after {$e->getRetryAfter()}s (max {$e->getMaxAttempts()})."; }
The limiter shares the configured cache backend, so it works across processes when backed by Redis/Memcached/File.
Logging (PSR-3)
$validator->setLogger($psr3Logger); // or via configuration $config = ConfigurationBuilder::create()->standard()->build(); $config->setLogger($psr3Logger);
Each validate() call logs a debug entry with the email, outcome, checks, and errors.
Custom Blocklist / Allowlist
$validator = new EmailValidator([], []); // start empty $validator->addToBlocklist('custom-disposable.com') ->addMultipleToBlocklist(['temp1.com', 'temp2.com']) ->addToAllowlist('trusted-domain.com'); // allowlist always wins over blocklist $validator->removeFromBlocklist('temp1.com'); $validator->getBlocklist(); // string[] $validator->getBlocklistCount(); // int $validator->isBlocklisted('a@temp2.com'); $validator->isAllowlisted('a@trusted-domain.com');
You can also point the configuration at your own list files:
$config = ConfigurationBuilder::create() ->withBlocklist('/path/to/blocklist.conf') ->withAllowlist('/path/to/allowlist.conf') ->build();
Framework Integration
Laravel
The package ships with auto-discovery (service provider + EmailValidator facade). Publish the config if you want to customize it:
php artisan vendor:publish --tag=emailvalidator-config
Resolve it from the container, or use the facade:
use HarunGecit\EmailValidator\Framework\Laravel\EmailValidatorFacade as EmailValidator; EmailValidator::isValid('user@example.com'); EmailValidator::validate('user@example.com'); // or inject the concrete service public function store(\HarunGecit\EmailValidator\EmailValidator $validator) { /* ... */ }
Config-driven validation rules are registered automatically:
$request->validate([ 'email' => ['required', 'valid_email', 'not_disposable', 'not_role_based', 'has_mx', 'not_subaddressed'], ]);
The published config (config/emailvalidator.php) is env-aware (MAIL_FROM_ADDRESS, LOG_CHANNEL, etc.) and defaults the cache driver to laravel, reusing your application's cache store.
Symfony
Register the bundle:
// config/bundles.php return [ HarunGecit\EmailValidator\Framework\Symfony\EmailValidatorBundle::class => ['all' => true], ];
Configure it under the email_validator key:
# config/packages/email_validator.yaml email_validator: checks: format: true mx: true disposable: true typo_suggestion: true cache: driver: memory # memory | file | redis | memcached | psr16 | null ttl: 3600 rate_limit: enabled: false max_attempts: 100 decay_seconds: 60
The EmailValidator service is public and autowirable (service id email_validator, or type-hint the class).
How It Works
- Format —
filter_var($email, FILTER_VALIDATE_EMAIL). - Disposable — the domain is checked against the bundled blocklist; any domain in the allowlist is never treated as disposable.
- MX / DNS —
checkdnsrr()for MX (or A/AAAA) records, with results cached per domain. - Role-based / Subaddress / Typo / SMTP / Catch-all — each runs only when enabled in the configuration and contributes to the final
ValidationResult.
Blocklist and Allowlist
Bundled data lives in the data/ directory:
- Blocklist (
blocklist.conf) — 8,100+ disposable/temporary email domains. - Allowlist (
allowlist.conf) — 190+ domains that should always be considered valid. The upstream project no longer ships an allowlist, so this list is maintained as part of this package.
The blocklist is synchronized with the community-maintained disposable-email-domains project (plus a few local additions). To refresh it from upstream, run:
composer update-lists
One lowercase domain per line:
mailinator.com
guerrillamail.com
tempmail.com
Load, merge, or persist lists with the Fetcher utility:
use HarunGecit\EmailValidator\Fetcher; $lists = Fetcher::loadAll(); // ['blocklist' => [...], 'allowlist' => [...]] $custom = Fetcher::loadCustomBlocklist('/path/to/custom.conf'); $merged = Fetcher::mergeLists(['/a.conf', '/b.conf']); Fetcher::saveList('/out.conf', ['domain1.com', 'domain2.com']);
API Reference
EmailValidator
| Method | Description |
|---|---|
create(): self |
Factory with the default bundled lists |
withConfig(Configuration $c): self |
Factory from a configuration object |
fromConfigFile(string $path): self |
Factory from a PHP/JSON/YAML config file |
strict(): self / basic(): self |
Factory from a preset |
validate(string $email): ValidationResult |
Full validation, rich result |
isValid(string $email, ?bool $checkMX = null): bool |
Boolean validation |
validateBatch(array $emails): array |
Batch → ValidationResult[] |
validateMultiple/validateWithDetails/filterValid/filterInvalid/getStatistics |
2.x-compatible batch helpers |
isValidFormat/isDisposable/hasValidMX/hasValidDNS |
Individual checks |
isRoleBased/isSubaddressed/getBaseEmail/getSuggestion/isCatchAll/validateSMTP |
Advanced checks |
extractDomain/extractLocalPart/normalize/normalizeMultiple |
Utilities |
addToBlocklist/addToAllowlist/removeFrom*/getBlocklist/getAllowlist/… |
List management (fluent) |
setConfiguration/getConfiguration/setLogger/setCacheEnabled/clearCache |
Configuration & runtime |
ConfigurationBuilder
create(), presets strict()/standard()/basic()/minimal(), cache withMemoryCache/withFileCache/withRedisCache/withMemcachedCache/withPsr16Cache/withCache/withoutCache, withRateLimiting/withoutRateLimiting, checks withRoleBasedCheck/withTypoSuggestion/withSmtpCheck/withSubaddressCheck/withCatchAllCheck/withoutMxCheck/withoutDisposableCheck, lists withBlocklist/withAllowlist, and build().
ValidationResult
isValid, getEmail, getDomain, getLocalPart, getChecks, passed, getErrors, getFirstError, getWarnings, isDisposable, isRoleBased, isSubaddressed, getMetadata, getMetadataValue, hasSuggestion, getSuggestion, toArray, toJson.
SuggestionResult
getOriginalEmail, getSuggestedEmail, getOriginalDomain, getSuggestedDomain, getReason, getConfidence, isHighConfidence, toArray, __toString.
Fetcher
loadBlocklist, loadAllowlist, loadAll, loadCustomBlocklist, loadCustomAllowlist, mergeLists, saveList, clearCache, getBlocklistPath, getAllowlistPath, blocklistExists, allowlistExists, getBlocklistCount, getAllowlistCount.
Exceptions
All exceptions extend HarunGecit\EmailValidator\Exceptions\EmailValidatorException:
| Exception | When |
|---|---|
RateLimitExceededException |
Rate limit exceeded (getRetryAfter(), getMaxAttempts()) |
InvalidEmailException |
Semantic validation failures (factory helpers per reason) |
ConfigurationException |
Invalid config value, unsupported format, missing file |
CacheException |
Cache driver/backend errors |
SmtpConnectionException |
SMTP connection/timeout/unexpected-response errors |
Testing
composer test # run the suite composer test-coverage # HTML coverage report in ./coverage
The test toolchain uses PHPUnit 10+ and therefore requires PHP 8.1+. The library runtime supports PHP 8.0+ (verified by a dedicated CI job).
Upgrading to 3.0
3.0 is backward compatible with the 2.x public API — existing code using isValid(), validateWithDetails(), validateMultiple(), filterValid(), list management, and EmailValidator::create() continues to work unchanged.
What's new / worth knowing:
- Minimum PHP is now 8.0 (2.x supported 7.4). Language features used in 3.0 require 8.0+.
- New rich API:
validate()returns aValidationResultobject instead of an array; the array-basedvalidateWithDetails()is still available. - Configuration & presets: prefer
ConfigurationBuilderand thestrict/standard/basic/minimalpresets over ad-hoc setters. - New checks: role-based, subaddress, typo suggestion, SMTP, and catch-all (all opt-in).
- Pluggable cache & rate limiting replace the simple 2.x MX cache toggle (which still works).
- Framework packages: Laravel and Symfony integrations are now included.
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:
- Fork the repository and create a feature branch.
- Make your changes with tests.
- Ensure
composer testpasses and code follows PSR-12 with PHPDoc comments. - Submit a pull request.
License
Licensed under the MIT License. See LICENSE for details.
Author
Harun Geçit
Changelog
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.