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 (7,900+ 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) — 7,900+ disposable/temporary email domains. - Allowlist (
allowlist.conf) — 180+ domains that should always be considered valid.
The blocklist is synchronized with the community-maintained disposable-email-domains project (plus a few local additions). 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.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.