-
Notifications
You must be signed in to change notification settings - Fork 11.5k
feat: Add advanced email validation methods to Email rule #56580
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Add advanced email validation methods to Email rule #56580
Conversation
- Add validateMxRecord() for DNS/MX record validation - Add preventSpoofing() for unicode spoofing protection - Add noDots() to block Gmail dot aliases - Add noAliases() to block email aliasing techniques - Add trustedDomains() to whitelist specific domains - Add noDisposable() to block temporary email services - Add noForwarding() to block email forwarding services - Add noSuspiciousPatterns() to block suspicious email patterns - Add strictAdvanced() for comprehensive protection - Maintain backward compatibility with existing Email rule - Add comprehensive test coverage for new methods
$this->trustedDomainsOnly = true; | ||
if ($domains !== null) { | ||
$this->customTrustedDomains = $domains; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could shorten this if wanted
$this->trustedDomainsOnly = true; | |
if ($domains !== null) { | |
$this->customTrustedDomains = $domains; | |
} | |
$this->trustedDomainsOnly = $domains !== null | |
? $domains | |
: true; |
or even
$this->trustedDomainsOnly = true; | |
if ($domains !== null) { | |
$this->customTrustedDomains = $domains; | |
} | |
$this->trustedDomainsOnly = $domains === null || $domains; |
$parts = explode(':', $rule); | ||
if (isset($parts[1])) { | ||
$flags = explode(',', $parts[1]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could go without array indexes
$parts = explode(':', $rule); | |
if (isset($parts[1])) { | |
$flags = explode(',', $parts[1]); | |
[, $flagList] = explode(':', $rule); | |
if (isset($flagList)) { | |
$flags = explode(',', $flagList); |
or simply
$parts = explode(':', $rule); | |
if (isset($parts[1])) { | |
$flags = explode(',', $parts[1]); | |
$flagList = strstr($rule, ':', true); | |
if (isset(flagList)) { | |
$flags = explode(',', flagList); |
if (str_starts_with($rule, 'email:')) { | ||
$parts = explode(':', $rule); | ||
if (isset($parts[1])) { | ||
$flags = explode(',', $parts[1]); | ||
$flags = array_filter($flags, fn($f) => $f !== 'dns'); | ||
return $flags ? 'email:' . implode(',', $flags) : 'email'; | ||
} | ||
} | ||
return $rule; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You might use early returns here:
if (str_starts_with($rule, 'email:')) { | |
$parts = explode(':', $rule); | |
if (isset($parts[1])) { | |
$flags = explode(',', $parts[1]); | |
$flags = array_filter($flags, fn($f) => $f !== 'dns'); | |
return $flags ? 'email:' . implode(',', $flags) : 'email'; | |
} | |
} | |
return $rule; | |
if (! str_starts_with($rule, 'email:')) { | |
return $rule; | |
} | |
$parts = explode(':', $rule); | |
if (isset($parts[1])) { | |
return $rule; | |
} | |
$flags = explode(',', $parts[1]); | |
$flags = array_filter($flags, fn($f) => $f !== 'dns'); | |
return $flags ? 'email:' . implode(',', $flags) : 'email'; |
$rules = array_map(function ($rule) { | ||
if (str_starts_with($rule, 'email:')) { | ||
$parts = explode(':', $rule); | ||
if (isset($parts[1])) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will this ever not be true?
if ($this->blockGmailDots && $this->hasGmailDots($localPart, $domain)) { | ||
$this->messages[] = 'Gmail addresses with dots are not allowed.'; | ||
return false; | ||
} | ||
|
||
if ($this->blockAliases && $this->hasAliases($localPart)) { | ||
$this->messages[] = 'Email aliases are not allowed.'; | ||
return false; | ||
} | ||
|
||
if ($this->blockDisposable && $this->isDisposableEmail($domain)) { | ||
$this->messages[] = 'Temporary or disposable email services are not allowed.'; | ||
return false; | ||
} | ||
|
||
if ($this->blockForwarding && $this->isForwardingService($domain)) { | ||
$this->messages[] = 'Email forwarding services are not allowed.'; | ||
return false; | ||
} | ||
|
||
if ($this->blockSuspiciousPatterns && $this->hasSuspiciousPatterns($localPart)) { | ||
$this->messages[] = 'This email format appears to be temporary or invalid.'; | ||
return false; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can't you use the messages from the validation translation file here?
return [ | ||
'gmail.com', | ||
'googlemail.com', | ||
'outlook.com', | ||
'hotmail.com', | ||
'live.com', | ||
'msn.com', | ||
'icloud.com', | ||
'me.com', | ||
'mac.com', | ||
'yahoo.com', | ||
'yahoo.co.uk', | ||
'yahoo.ca', | ||
'yahoo.com.au', | ||
'yahoo.de', | ||
'yahoo.fr', | ||
'yahoo.es', | ||
'yahoo.it', | ||
'ymail.com', | ||
'rocketmail.com', | ||
'aol.com', | ||
'aim.com', | ||
]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What makes them especially trusted? Is this a custom list or does it come from somewhere official?
/** | ||
* Check for email aliases. | ||
* | ||
* @param string $localPart | ||
* @return bool | ||
*/ | ||
protected function hasAliases(string $localPart): bool | ||
{ | ||
return str_contains($localPart, '+') || | ||
preg_match('/\.{2,}/', $localPart) || | ||
str_starts_with($localPart, '.') || | ||
str_ends_with($localPart, '.'); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure if we want the gmail dots to be provider-specific while we don't apply the aliases solely to providers that actually implement that part of the spec
'/^test\d*$/', // Test emails | ||
'/^temp\d*$/', // Temporary emails | ||
'/^noreply/', // No-reply emails | ||
'/^admin\d*$/', // Admin emails |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is [email protected]
suspicious?
protected function hasSuspiciousPatterns(string $localPart): bool | ||
{ | ||
$suspiciousPatterns = [ | ||
'/^[a-z]\d{8,}$/', // Random letters + numbers |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why specifically 8?
public function testNoAliases() | ||
{ | ||
$this->fails( | ||
(new Email())->noAliases(), | ||
'[email protected]', | ||
['Email aliases are not allowed.'] | ||
); | ||
|
||
$this->fails( | ||
Rule::email()->noAliases(), | ||
'[email protected]', | ||
['Email aliases are not allowed.'] | ||
); | ||
|
||
$this->fails( | ||
(new Email())->noAliases(), | ||
'[email protected]', | ||
['Email aliases are not allowed.'] | ||
); | ||
|
||
$this->fails( | ||
(new Email())->noAliases(), | ||
'[email protected]', | ||
['Email aliases are not allowed.'] | ||
); | ||
|
||
$this->fails( | ||
(new Email())->noAliases(), | ||
'[email protected]', | ||
['Email aliases are not allowed.'] | ||
); | ||
|
||
$this->passes( | ||
(new Email())->noAliases(), | ||
'[email protected]' | ||
); | ||
|
||
$this->passes( | ||
Rule::email()->noAliases(), | ||
'[email protected]' | ||
); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could use a data provider here
public function testNoAliases() | |
{ | |
$this->fails( | |
(new Email())->noAliases(), | |
'[email protected]', | |
['Email aliases are not allowed.'] | |
); | |
$this->fails( | |
Rule::email()->noAliases(), | |
'[email protected]', | |
['Email aliases are not allowed.'] | |
); | |
$this->fails( | |
(new Email())->noAliases(), | |
'[email protected]', | |
['Email aliases are not allowed.'] | |
); | |
$this->fails( | |
(new Email())->noAliases(), | |
'[email protected]', | |
['Email aliases are not allowed.'] | |
); | |
$this->fails( | |
(new Email())->noAliases(), | |
'[email protected]', | |
['Email aliases are not allowed.'] | |
); | |
$this->passes( | |
(new Email())->noAliases(), | |
'[email protected]' | |
); | |
$this->passes( | |
Rule::email()->noAliases(), | |
'[email protected]' | |
); | |
} | |
#[DataProvider('provideDotAliasRules')] | |
public function testNoAliases(Email $rule, string $emailAddress, ?array $errorMessages = null) | |
{ | |
if ($errorMessages === null) { | |
$this->passes($rule, $emailAddress); | |
} else { | |
$this->fails($rule, $emailAddress, $errorMessages); | |
} | |
} | |
public static function provideDotAliasRules() | |
{ | |
return [ | |
'email class and gmail address with plus' => [ | |
(new Email())->noAliases(), | |
'[email protected]', | |
['Email aliases are not allowed.'], | |
}, | |
'rule method and gmail address with plus' => [ | |
(new Email())->noAliases(), | |
'[email protected]', | |
['Email aliases are not allowed.'] | |
], | |
'email class and gmail address with dots within' => [ | |
(new Email())->noAliases(), | |
'[email protected]', | |
['Email aliases are not allowed.'] | |
], | |
'email class and gmail address with leading dots' => [ | |
(new Email())->noAliases(), | |
'[email protected]', | |
['Email aliases are not allowed.'] | |
], | |
'email class and gmail address with trailing dots' => [ | |
(new Email())->noAliases(), | |
'[email protected]', | |
['Email aliases are not allowed.'] | |
], | |
'email class and gmail address without pluses and dots' => [ | |
(new Email())->noAliases(), | |
'[email protected]', | |
], | |
'rule method and gmail address without pluses and dots' => [ | |
Rule::email()->noAliases(), | |
'[email protected]' | |
], | |
]; | |
} |
or
public function testNoAliases() | |
{ | |
$this->fails( | |
(new Email())->noAliases(), | |
'[email protected]', | |
['Email aliases are not allowed.'] | |
); | |
$this->fails( | |
Rule::email()->noAliases(), | |
'[email protected]', | |
['Email aliases are not allowed.'] | |
); | |
$this->fails( | |
(new Email())->noAliases(), | |
'[email protected]', | |
['Email aliases are not allowed.'] | |
); | |
$this->fails( | |
(new Email())->noAliases(), | |
'[email protected]', | |
['Email aliases are not allowed.'] | |
); | |
$this->fails( | |
(new Email())->noAliases(), | |
'[email protected]', | |
['Email aliases are not allowed.'] | |
); | |
$this->passes( | |
(new Email())->noAliases(), | |
'[email protected]' | |
); | |
$this->passes( | |
Rule::email()->noAliases(), | |
'[email protected]' | |
); | |
} | |
#[DataProvider('providePassingDotAliasRules')] | |
public function testNoAliases(Email $rule, string $emailAddress, array $errorMessages = null) | |
{ | |
$this->passes($rule, $emailAddress); | |
} | |
public static function providePassingDotAliasRules() | |
{ | |
return [ | |
'email class and gmail address without pluses and dots' => [ | |
(new Email())->noAliases(), | |
'[email protected]', | |
], | |
'rule method and gmail address without pluses and dots' => [ | |
Rule::email()->noAliases(), | |
'[email protected]' | |
], | |
]; | |
} | |
#[DataProvider('provideFailingDotAliasRules')] | |
public function testNoAliases(Email $rule, string $emailAddress, array $errorMessages = null) | |
{ | |
$this->fails($rule, $emailAddress, $errorMessages); | |
} | |
public static function provideFailingDotAliasRules() | |
{ | |
return [ | |
'email class and gmail address with plus' => [ | |
(new Email())->noAliases(), | |
'[email protected]', | |
['Email aliases are not allowed.'], | |
}, | |
'rule method and gmail address with plus' => [ | |
(new Email())->noAliases(), | |
'[email protected]', | |
['Email aliases are not allowed.'] | |
], | |
'email class and gmail address with dots within' => [ | |
(new Email())->noAliases(), | |
'[email protected]', | |
['Email aliases are not allowed.'] | |
], | |
'email class and gmail address with leading dots' => [ | |
(new Email())->noAliases(), | |
'[email protected]', | |
['Email aliases are not allowed.'] | |
], | |
'email class and gmail address with trailing dots' => [ | |
(new Email())->noAliases(), | |
'[email protected]', | |
['Email aliases are not allowed.'] | |
], | |
]; | |
} |
Any further email validation should just be a package imo. |
Description
This PR enhances the Laravel Email validation rule by adding advanced email validation methods while maintaining full backward compatibility. These methods address common real-world email validation challenges that developers face when building secure web applications.
Problem Statement
Current Laravel email validation provides basic functionality, but many applications require advanced email validation to:
Solution
Added 9 new optional methods to the
Illuminate\Validation\Rules\Email
class:Core Validation Methods
validateMxRecord()
- Validates DNS/MX records to ensure email deliverabilitypreventSpoofing()
- Prevents unicode spoofing attacks in email addressesrfcCompliant(bool $strict = false)
- Enhanced RFC compliance validationwithNativeValidation(bool $allowUnicode = false)
- PHP native validation integrationAdvanced Protection Methods
noDots()
- Blocks Gmail dot aliases ([email protected] → [email protected])noAliases()
- Blocks common aliasing techniques ([email protected])trustedDomains(?array $domains = null)
- Restricts to whitelisted domains onlynoDisposable()
- Blocks temporary email services (tempmail.org, guerrillamail.com, etc.)noForwarding()
- Blocks email forwarding services (simplelogin.io, anonaddy.com, etc.)noSuspiciousPatterns()
- Blocks suspicious patterns (test123, temp456, etc.)Convenience Methods
strictAdvanced()
- Enables all advanced protections with one methodstrict()
- Shorthand for strict RFC complianceBenefits to End Users
Real-World Use Cases
Why It Doesn't Break Existing Features
Email
rule usage continues unchangedRule
,DataAwareRule
,ValidatorAwareRule
)How It Makes Building Web Applications Easier
Implementation Details
Testing
Examples
Supported Use Cases
This enhancement makes Laravel's email validation more powerful while maintaining the framework's principle of elegant, expressive code.