The canonical email regex appears in Stack Overflow answers, open-source libraries, and production systems around the world:
^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$
It looks complete. It is not. This lesson catalogs exactly what it gets wrong and why the gap matters.
What This Regex Rejects That RFC 5322 Allows
Missing atext characters
The character class [a-zA-Z0-9._%+-] includes only a subset of valid atext characters. Missing:
!→user!name@example.comrejected#→user#name@example.comrejected$→user$name@example.comrejected&→user&name@example.comrejected'→user'name@example.comrejected*→user*name@example.comrejected/→user/name@example.comrejected=→user=name@example.comrejected?→user?name@example.comrejected^→user^name@example.comrejected`→user`name@example.comrejected{|}~→ rejected
Quoted strings
The regex has no mechanism to recognize quoted strings. "user name"@example.com is rejected because spaces are not in the character class.
IP address literals
user@[192.168.1.1] is valid per RFC. The domain character class [a-zA-Z0-9.-]+ does not include [ or ], so IP literals are rejected.
What This Regex Accepts That RFC 5322 Forbids
Consecutive dots in local part
The + quantifier means "one or more characters from the class." The class includes .. So user..name is accepted — the two dots are just two characters in the sequence. The RFC rule "no consecutive dots" is not enforced.
Leading dot in local part
^[a-zA-Z0-9._%+-]+ — the ^ is followed by [...] which includes .. Nothing prevents a leading dot. .user@example.com is accepted.
Trailing dot in local part
Similarly, [...]+@ — nothing prevents a trailing dot before @. user.@example.com is accepted.
Technically invalid TLD lengths
The {2,} means "two or more letters." This is not a constraint from the RFC. The RFC imposes no minimum TLD length (single-letter TLDs exist) and the practical maximum of about 24 characters is not enforced either.
The TLD Requirement Is Wrong
\.[a-zA-Z]{2,}$
This requires at least one dot in the domain and at least two letters after the last dot. Both of these are practical constraints but neither is from the RFC:
- The RFC allows
user@localhost(no TLD required) - The RFC allows numeric-only labels:
user@1.2.3.4would be rejected even though[IPv6::1]should be an IP literal
The regex is encoding the popular mental model of email, not the actual specification.
Why "Good Enough" Is a Real Problem
You might argue: this regex handles 99% of real email addresses correctly. Why does it matter?
Real user impact: user+tag@example.com (Gmail tagging) and user'name@example.com (Irish names) are real addresses in active use. Rejecting them turns away real users.
Moving goalposts: TLD restrictions become wrong as new TLDs are added. A regex that required {2,4} was wrong when .museum launched. A regex that requires {2,6} is wrong now for some long TLDs.
Security implications: Over-restrictive validation can be used to block specific accounts. An attacker who knows the service rejects + in emails can register with user+block@example.com knowing that the legitimate owner user@example.com who tries the plus variant will fail.
What Proper Validation Looks Like
Option 1: RFC-compliant parser
Use a library that implements the full RFC 5322 grammar. In Java: the Apache Commons Email Validator (though it has known gaps). In Python: the email-validator library. These parse rather than regex-match.
Option 2: Practical compromise
Use a permissive check (does the string contain exactly one @ with non-empty parts on each side?) and verify deliverability by sending a confirmation email. This is the approach used by Google, Microsoft, and most major services.
Option 3: Informed regex
If regex is required, derive it from the actual atext definition. This still can't handle quoted strings correctly, but at least the character set is right:
^[a-zA-Z0-9!#$%&'*+/=?^_`{|}~.-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{1,}$
This still has the consecutive-dot problem and still can't handle quoted strings, but at least it doesn't reject + or '.
Testing Recommendation
When testing an email validator, always include:
user+tag@example.com— if rejected, classic regex was useduser'name@example.com— if rejected, regex missing apostropheuser!name@example.com— if rejected, regex missing exclamationuser..name@example.com— if accepted, regex doesn't check consecutive dots.user@example.com— if accepted, regex doesn't check leading dotuser.@example.com— if accepted, regex doesn't check trailing dot