BookMySpot

Developer Guides

Email Validation Regex: A Practical Pattern and Why Regex Alone Isn't Enough

An email validation regex can tell you whether a string is shaped like an email address. It cannot tell you whether the domain accepts mail or whether the mailbox exists. Use a simple, well-tested pattern to catch typos and garbage at input time, then hand off to DNS checks and, ultimately, a confirmation email. This guide gives you a pattern that is tested in JavaScript, Python, and PHP, explains each piece, and covers the traps that make “perfect” email regexes a bad idea.

What a regex can and can’t check

A regular expression works on characters. It can confirm that there is exactly one @, that the part before it uses allowed characters, and that the part after it looks like a hostname. That’s useful: it blocks entries like john.gmail.com, john@, or a pasted phone number before they reach your database.

What a regex cannot do:

  • Confirm the domain exists. user@exampel.com is perfectly well formed. Only a DNS lookup tells you whether that domain has mail servers. See what an MX record is for how that works.
  • Confirm the mailbox exists. no-such-person@gmail.com passes every pattern ever written. Knowing whether a mailbox is real requires the receiving server’s cooperation, and many servers won’t give a straight answer (more on that in how SMTP email verification works).
  • Catch plausible typos. jane@gmial.com is syntactically valid. Typo detection needs a list of common domains and a distance check, not a pattern. We cover this in common email typos.

The difference between “looks right” and “works” is the whole subject of email validation vs email verification. Regex lives firmly on the validation side.

Why “RFC-compliant” regexes are a trap

The address grammar lives in two standards. RFC 5322 section 3.4.1 defines addr-spec as local-part "@" domain, where the local part can be a dot-atom, a quoted string, or obsolete syntax, and the domain can be a dot-atom or a bracketed domain literal. The grammar also allows comments in parentheses, which can nest, and folding whitespace (section 3.2.2). RFC 5321 section 4.1.2, which governs what actually travels in SMTP commands, is narrower: no comments, but quoted strings and IP address literals are still allowed.

That means all of these are syntactically legal in some reading of the standards:

"john smith"@example.com
"very.(),:;<>[]\".VERY.\"very@\\ \"very\".unusual"@example.com
user@[192.0.2.10]
user@[IPv6:2001:db8::1]

You’ll find regexes online that try to match every one of these. They run to thousands of characters, they’re impossible to review, and they accept addresses that no consumer mailbox provider will ever issue. RFC 5321 itself says that hosts expecting to receive mail SHOULD avoid mailboxes whose local part needs the quoted-string form. Matching the full grammar buys you almost nothing and costs you readability and, sometimes, performance.

The browser vendors reached the same conclusion. The WHATWG HTML standard defines a “valid email address” for <input type="email"> and openly calls it a willful violation of RFC 5322, because the RFC syntax is too strict before the @, too vague after it, and too permissive about comments, whitespace, and quoted strings to be practical.

The HTML spec regex (a good baseline)

The WHATWG spec publishes the pattern browsers use. In JavaScript form:

const WHATWG_EMAIL =
  /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;

It’s a solid starting point because it matches what users already see in their browser. It has a few gaps you may care about:

  • It accepts dotless domains such as user@localhost. That’s intentional for intranet use but rarely what a public signup form wants.
  • It accepts a leading dot, a trailing dot, or consecutive dots in the local part (.user@, user.@, us..er@). RFC 5322’s dot-atom forbids all three.
  • It has no length limits on the whole address.

A practical email validation regex

Here’s a stricter pattern that keeps the same character set as the HTML spec but fixes those gaps. It was tested in Node.js, Python, and PHP 8.3 against the cases in the table below.

^(?=.{1,254}$)(?=[^@]{1,64}@)(?!\.)(?!.*\.\.)[A-Za-z0-9.!#$%&'*+/=?^_`{|}~-]+(?<!\.)@(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+[A-Za-z](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$

Piece by piece:

PartWhat it enforces
(?=.{1,254}$)Whole address is at most 254 characters
(?=[^@]{1,64}@)Local part is 1 to 64 characters
(?!\.) and (?<!\.)@Local part doesn’t start or end with a dot
(?!.*\.\.)No consecutive dots anywhere
[A-Za-z0-9.!#$%&'*+/=?^_`{|}~-]+RFC 5322 atext characters plus dots
(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+One or more domain labels, each 1 to 63 characters, no leading or trailing hyphen
[A-Za-z](?:...)?$A final label (the TLD) that starts with a letter, which also allows punycode TLDs like xn--p1ai

The length numbers come from the standards. RFC 5321 section 4.5.3.1.1 caps the local part at 64 octets, and the path in MAIL and RCPT commands is capped at 256 octets including the angle brackets, which leaves 254 for the address itself. That 254 figure is spelled out in verified erratum 1690 to RFC 3696.

What it accepts and rejects

InputResult
user@example.comvalid
first.last+tag@sub.example.co.ukvalid
o'brien@example.ievalid
user@example.xn--p1aivalid
user@localhostinvalid (no dot in domain)
.user@example.com, user.@example.com, us..er@example.cominvalid
"john doe"@example.cominvalid (quoted local part)
user@exa_mple.cominvalid (underscore in hostname)
user@-example.cominvalid (label starts with hyphen)
65-character local partinvalid

Rejecting quoted local parts and IP literals is a deliberate trade-off. If you run a mail client or MTA you need the full grammar, and you should use a real parser instead of a regex. For signup forms, the trade-off is sound.

Using the pattern in JavaScript, Python, and PHP

JavaScript

const EMAIL_RE =
  /^(?=.{1,254}$)(?=[^@]{1,64}@)(?!\.)(?!.*\.\.)[A-Za-z0-9.!#$%&'*+\/=?^_`{|}~-]+(?<!\.)@(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+[A-Za-z](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$/;

export function looksLikeEmail(input) {
  return EMAIL_RE.test(input.trim());
}

console.log(looksLikeEmail('user@example.com'));   // true
console.log(looksLikeEmail('us..er@example.com')); // false

Inside a regex literal the / in the character class must be escaped as \/. For more JavaScript-specific options, including the browser’s built-in check, see how to validate an email address in JavaScript.

Python

import re

EMAIL_RE = re.compile(
    r"^(?=.{1,254}$)(?=[^@]{1,64}@)(?!\.)(?!.*\.\.)"
    r"[A-Za-z0-9.!#$%&'*+/=?^_`{|}~-]+(?<!\.)@"
    r"(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+"
    r"[A-Za-z](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$"
)

def looks_like_email(value: str) -> bool:
    return EMAIL_RE.fullmatch(value.strip()) is not None

print(looks_like_email("user@example.com"))    # True
print(looks_like_email("user@example.com\n"))  # True, because strip() removes the newline

Use fullmatch(). In Python’s re, $ also matches just before a trailing newline, so re.match() with this pattern would accept "user@example.com\n" untrimmed. For server-side Python, the email-validator package is a better choice than any regex.

PHP

<?php
const EMAIL_RE = '/^(?=.{1,254}$)(?=[^@]{1,64}@)(?!\.)(?!.*\.\.)[A-Za-z0-9.!#$%&\'*+\/=?^_`{|}~-]+(?<!\.)@(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+[A-Za-z](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$/D';

function looks_like_email(string $value): bool
{
    return preg_match(EMAIL_RE, trim($value)) === 1;
}

var_dump(looks_like_email('user@example.com'));   // bool(true)
var_dump(looks_like_email('us..er@example.com')); // bool(false)

Note the D modifier at the end. Without it, PCRE’s $ also matches before a final newline, the same trap as Python. In most PHP code you’d reach for filter_var() first and keep the regex for cases where PHP and JavaScript must agree exactly.

Common regex mistakes

Restricting the TLD to 2 to 4 letters. Patterns like \.[a-z]{2,4}$ reject .museum, .photography, and every internationalized TLD in punycode form. Allow up to 63 characters.

Forgetting +. Plus addressing (name+newsletter@gmail.com) is common, and blocking it annoys exactly the careful users you want.

Forgetting the apostrophe. Surnames like O’Brien show up in real local parts. ' is in RFC 5322’s atext.

Case-sensitive character classes. [a-z] without the i flag rejects John@Example.com. Domains are case-insensitive. Local parts are technically case-sensitive under RFC 5321 section 2.4, but your validator shouldn’t reject uppercase.

Catastrophic backtracking. Patterns with nested quantifiers over overlapping character sets, such as ^([a-zA-Z0-9]+\.?)+@, can take exponential time on crafted input. OWASP’s ReDoS page uses an email-validation regex from a public regex library as one of its examples. On a Node server, a slow regex blocks the event loop for every request. The pattern above puts the length lookaheads first, so oversized input fails immediately, and each domain label must end in a literal dot, which limits how much the engine can backtrack. Still, cap input length before matching anything.

Validating the trimmed value but storing the raw one. Trim, then validate, then store the same trimmed string.

Where regex fits in a real validation pipeline

A sensible order of checks for a signup form:

  1. Length and regex in the browser and again on the server. Instant, free, and catches garbage.
  2. Typo suggestions for the most common domains (gmial.com to gmail.com).
  3. DNS check: does the domain publish MX records, or at least an A/AAAA record that RFC 5321 treats as an implicit MX? Is it a null MX (RFC 7505) that explicitly refuses mail?
  4. Disposable and role-address checks, depending on your use case.
  5. A confirmation email with a link. This is the only check that proves a person controls the inbox.

Steps 2 through 4 are what our free email verifier runs. It returns a status of valid, risky, or invalid with the reason, MX hosts, and typo suggestions, and it’s also available as an API for your own forms; see the API documentation. It doesn’t open SMTP connections to check mailboxes, so a confirmation email remains the final word.

FAQ

What is the best regex for email validation?

There isn’t a single correct one. For web forms, the WHATWG pattern that browsers use for type="email" is a good baseline, and the stricter version in this post adds length limits and blocks leading, trailing, and doubled dots. Either one should be paired with a DNS check and a confirmation email.

Can a regex match every valid email address?

Not in any practical way. RFC 5322 allows quoted local parts, nested comments, and IP address literals, and fully matching that grammar takes a very long, fragile pattern. Most applications are better off rejecting those rare forms.

Why does my regex accept addresses that bounce?

A regex only checks the format. A well-formed address can point to a domain with no mail servers, a mailbox that was deleted, or a typo of a real domain. Those failures need DNS lookups, typo detection, and real sending to catch.

Should I validate email with regex on the client or the server?

Both. Client-side checks give users instant feedback, but anyone can bypass them, so the server must repeat the check before storing or using the address. Use the same pattern in both places so they agree.

Does the regex handle internationalized email addresses?

No. The pattern here is ASCII-only, as is the HTML spec’s. Addresses with Unicode local parts or non-ASCII domains need different handling, which is covered in internationalized email addresses.

Check your list before you send

Free email verifier: syntax, MX, disposable and role-based addresses, typos. Paste up to 100 at once.

Verify emails free