How to validate email addresses (and why regex isn’t enough)
What the standards allow in an email address, the 64/254 length limits, quoted and internationalized addresses, and a practical strategy for web forms.
By Exmoor Software6 min read
Validating an email address answers one narrow question: could this string be an address? It cannot tell you whether the mailbox exists or whether the person typing it owns it — only a message sent to it can. Good validation is therefore lenient about syntax, strict about a few hard limits, and ends with a confirmation email.
Which standard defines “valid”?
Two RFCs describe address syntax, and they serve different purposes:
RFC 5322 addr-spec | RFC 5321 Mailbox | |
|---|---|---|
| Used for | addresses in message headers | addresses in SMTP commands (MAIL FROM, RCPT TO) |
| Comments and folding white space | allowed, e.g. john(work)@example.com | not allowed |
| Obsolete forms | accepted when parsing | not part of the grammar |
| Domain | dot-atom or a loosely defined domain literal | host name labels, or an address literal |
For a form field, RFC 5321 is the useful reference: it describes what can actually be delivered. RFC 5322 itself says its domain syntax is liberal and that implementations must follow the syntax of the context in which addresses are used. The core of the RFC 5321 grammar:
Mailbox = Local-part "@" ( Domain / address-literal )
Local-part = Dot-string / Quoted-string
Dot-string = Atom *("." Atom)
Domain = sub-domain *("." sub-domain)
An Atom is one or more letters, digits or any of these characters:
! # $ % & ' * + - / = ? ^ _ ` { | } ~
So o'connor@example.com, user+news@example.com and first.last@mail.example are all valid. Without quotes, dots can’t start or end the local part or appear twice in a row. Domain labels contain letters, digits and hyphens, and can’t start or end with a hyphen.
Length limits: 64 and 254
RFC 5321 (section 4.5.3.1) sets maximum sizes in octets:
- local part: 64 octets;
- domain: 255 octets;
- path — the address inside
MAIL FROM:<…>orRCPT TO:<…>, including the angle brackets: 256 octets.
Because the path is <, the address and >, the address itself can be at most 256 − 2 = 254 octets. The figure of 320 (64 + 1 + 255) that still circulates comes from the original text of RFC 3696; its verified erratum 1690 sets the practical limit at 254 for exactly this reason.
Octets are not characters. In UTF-8, ż takes two octets, so the 10-character użytkownik is 11 octets long. Count bytes, not string length.
Quoted local parts
Inside double quotes, a local part may contain spaces and characters such as @ or ,:
"john smith"@example.com
"a@b"@example.com
Both are valid under RFC 5321, but the same RFC says hosts should avoid creating mailboxes that need quoting, and that all quoted forms must be treated as equivalent — "john"@example.com is the same address as john@example.com. Decide consciously whether your system accepts them; if it does, make sure storage and sending handle them. Either way, never split an address on the first @. The domain starts after the last one.
Address literals
Instead of a domain name, an address may contain an IP address in square brackets:
user@[192.0.2.10]
user@[IPv6:2001:db8::25]
RFC 5321 keeps them for hosts that are not in DNS. They bypass MX lookup entirely, and a sign-up form can reasonably reject them with a clear message.
Internationalized addresses
Two separate mechanisms are involved.
Internationalized domain names. A domain such as bücher.example exists in DNS in its ASCII form, the A-label xn--bcher-kva.example (IDNA, RFC 5890). Any mail system can deliver to it once the domain is converted. Validate the domain by converting it to ASCII and checking the result, and use the A-label for DNS lookups.
UTF-8 local parts. RFC 6531 (SMTPUTF8) extends the RFC 5321 grammar so the local part can contain non-ASCII characters, as in użytkownik@przykład.example. Delivery works only between servers that announce the SMTPUTF8 extension; without it, the client must not transmit an internationalized address. Such an address can be valid and still undeliverable from your system, so check that your sending software and provider support SMTPUTF8 before accepting it. Normalize Unicode to NFC, as RFC 6532 recommends; NFKC should not be used.
Case sensitivity
RFC 5321 says the local part must be treated as case-sensitive — for some hosts, smith and Smith are different users — and in the same paragraph that exploiting this “impedes interoperability and is discouraged”. Domain names are not case-sensitive.
In practice:
- Store and send the address as typed. Lowercasing the domain is safe.
- Compare case-insensitively when looking for duplicates or logging someone in, because people don’t reliably capitalize the same way twice.
- Don’t normalize further without knowing the provider. Gmail ignores dots in personal Gmail addresses, but Google says dots do matter in work and school accounts; tags after
+are a convention of some providers, not a rule. Removing either can produce a different address.
Why a regex isn’t enough
Typical patterns fail in both directions:
- a TLD check like
[a-z]{2,4}$rejects longer and internationalized top-level domains; - a character class like
[A-Za-z0-9._-]rejects+and', both valid and common; - loose patterns like
\S+@\S+accepta@b@cand similar strings; - a regex can’t count octets, convert an IDN to its A-label, or apply length limits per part without becoming unreadable;
- complex patterns can be slow on crafted input — OWASP’s page on ReDoS lists an email-validation regex as an example.
And no pattern can tell whether a mailbox exists. Use a few explicit checks instead.
What input type="email" does
The HTML email input checks values against the WHATWG definition of a valid email address, which the specification itself calls “a willful violation of RFC 5322”. According to the spec:
- before validation, the browser strips line breaks and leading and trailing whitespace;
- the local part is one or more
atextcharacters or dots — soa..b@example.comand.a@example.compass, although SMTP doesn’t allow them; - the domain is one or more labels of letters, digits and hyphens, at most 63 characters each, and no dot is required —
user@localhostpasses; - quoted local parts, address literals and non-ASCII local parts are rejected;
- browsers should display punycode domain labels as Unicode and convert them back in the value — test how your target browsers handle IDN input;
- with the
multipleattribute, the field accepts a comma-separated list.
Treat it as a convenience — quick feedback, often an email keyboard on phones — not as validation. Requests can bypass the browser, so the server must check again.
A practical strategy for web forms
- Trim surrounding whitespace; reject line breaks and control characters.
- Split at the last
@. - Check lengths in octets: local part ≤ 64, whole address ≤ 254.
- Convert the domain to ASCII (IDNA) and check its labels.
- Decide a policy for rare forms — quoted local parts, address literals, UTF-8 local parts — and apply it with a clear message.
- Suggest fixes for common domain typos; let the user accept them.
- Optionally, check that the domain exists and doesn’t publish a null MX, using MX records (MX lookup). Don’t reject on DNS timeouts.
- Send a confirmation email. That is the real validation.
In pseudocode:
function checkAddress(input):
s = trim(input)
if s is empty or contains a line break or control character:
return invalid("empty or invalid characters")
at = lastIndexOf(s, "@")
if at < 1 or at == length(s) - 1:
return invalid("missing local part or domain")
local = s[0 .. at-1]
domain = s[at+1 ..]
if utf8Length(local) > 64 or utf8Length(s) > 254:
return invalid("too long")
if domain starts with "[":
return applyPolicy("address literal") # usually reject
ascii = idnaToAscii(domain) # bücher.example -> xn--bcher-kva.example
if ascii failed or not validHostLabels(ascii):
return invalid("domain is not a valid host name")
if isQuoted(local) or hasNonAscii(local):
return applyPolicy("quoted or UTF-8 local part")
if not isDotAtom(local): # atext characters, single dots inside
return invalid("local part has invalid characters")
return valid(suggestion = typoSuggestion(ascii)) # gmial.com -> gmail.com
The email syntax validator implements these checks — RFC 5321 with RFC 6531 and IDNA — explains each problem it finds and suggests typo fixes, so you can compare edge cases with your own implementation. It does not check whether a mailbox exists; no syntax check can.
After validation
A valid address is only a candidate. Send a confirmation message, add the address to mailing lists only after it is confirmed, and let bounces tell you when it stops working — see email list hygiene. If your application sends the confirmation itself, sending transactional email from your app covers authentication and setup.
References
- RFC 5321 — Simple Mail Transfer Protocol
- RFC 5322 — Internet Message Format
- RFC 6531 — SMTP Extension for Internationalized Email
- RFC 6532 — Internationalized Email Headers
- RFC 5890 — IDNA: Definitions and Document Framework
- RFC 3696 erratum 1690 — maximum address length
- WHATWG HTML — valid email address
- Google — Dots don’t matter in Gmail addresses
- OWASP — Regular expression Denial of Service (ReDoS)