Regex for Email Validation: A Practical Pattern and Its Limits

No regex can tell you whether an address works. A short pattern catches typos; a confirmation email proves the rest.

Try it: Regex Tester

Search for an email regex and you will find everything from eight characters to several thousand. The long ones try to follow the standards exactly and still cannot tell you whether a mailbox exists. The short ones are easy to read and, used for the right job, are good enough. That job is catching obvious mistakes such as a missing @ or a space, before you send a confirmation email that does the real check.

Why the full RFC 5322 grammar is the wrong target

RFC 5322 defines the syntax of addresses in email messages, and it allows far more than any sign-up form wants. All of these are syntactically valid:

  • "john doe"@example.com: a quoted local part containing a space.
  • john(comment)@example.com: a comment inside the address.
  • user@[192.168.0.1]: an IP address instead of a domain name.
  • admin@localhost: a domain without a dot.

A regex that implements the whole grammar, comments and folding whitespace included, is huge, hard to review and accepts addresses that many mail systems reject. It also still passes jane@gmial.com. SMTP (RFC 5321) adds length limits, 64 octets for the part before the @ and 255 for the domain, which together with the path syntax give a practical maximum of 254 characters. A length check is simpler as code than as regex.

A pragmatic pattern

This is the pattern most applications actually need: something without spaces or @, one @, then a domain containing at least one dot.

^[^\s@]+@[^\s@]+\.[^\s@]+$

It has one weakness worth fixing. Because [^\s@] also matches dots, it accepts empty domain labels such as user@example..com. Excluding the dot from each label closes that gap:

^[^\s@]+@[^\s@.]+(?:\.[^\s@.]+)+$

Here is what each pattern did with a set of test addresses in Node 24, next to the HTML specification's pattern described in the next section:

AddressShort patternLabel patternHTML type=email
first.last+news@example.co.ukAcceptedAcceptedAccepted
o'brien@example.ieAcceptedAcceptedAccepted
user@bücher.deAcceptedAcceptedRejected
δοκιμή@παράδειγμα.δοκιμήAcceptedAcceptedRejected
"john doe"@example.comRejectedRejectedRejected
admin@localhostRejectedRejectedAccepted
user @example.comRejectedRejectedRejected
user@@example.comRejectedRejectedRejected
user@example..comAcceptedRejectedRejected
user@example.com.AcceptedRejectedRejected
us..er@example.comAcceptedAcceptedAccepted
user@-example.comAcceptedAcceptedRejected
user@example.cAcceptedAcceptedAccepted

None of the three is perfect, and none has to be. Consecutive dots in the local part and one-letter top-level domains get through all of them; a confirmation email catches those. What matters is that no real, commonly used address is rejected.

There is also a performance reason to prefer the label version. On a crafted input of 40,000 characters that ends in a space, the short pattern took about 2.9 seconds in Node because of backtracking, while the label pattern finished in under a millisecond. Checking the 254-character limit before running any pattern removes the problem for both.

The HTML5 input type=email pattern

An <input type="email"> is validated by the browser against a definition in the WHATWG HTML standard. The specification calls it a willful violation of RFC 5322 and gives this JavaScript regular expression as an implementation:

/^[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 allows the usual punctuation before the @, requires domain labels of at most 63 letters, digits and hyphens that do not start or end with a hyphen, and does not require a dot, so admin@localhost passes. It rejects any non-ASCII character. You can reuse the browser's check in script with input.checkValidity() or input.validity.typeMismatch, but anything a browser checks can be bypassed, so validate again on the server.

International addresses

Domains can contain non-ASCII characters. bücher.de travels through DNS as xn--bcher-kva.de, and new URL('http://bücher.de').hostname in JavaScript, idn_to_ascii() in PHP and 'bücher.de'.encode('idna') in Python all produce that form. The part before the @ can contain non-ASCII characters too, but only mail servers that support SMTPUTF8 (RFC 6531) can deliver to such addresses.

Decide based on your mail provider, not on a regex. The patterns built on [^\s@] accept international addresses, while the HTML pattern rejects them. So does PHP's filter_var($email, FILTER_VALIDATE_EMAIL): in our tests it rejected both user@bücher.de and δοκιμή@example.com, and with the FILTER_FLAG_EMAIL_UNICODE flag it accepted the second but still rejected the Unicode domain.

The same check in JavaScript, Python and PHP

Each function trims surrounding whitespace, enforces the length limit and requires the whole string to match. The three returned identical results for the addresses above plus a few edge cases: surrounding whitespace, a newline in the middle of the value, a non-breaking space in the local part and a 256-character address.

// JavaScript
function isValidEmail(value) {
  const email = value.trim();
  return email.length <= 254 && /^[^\s@]+@[^\s@.]+(?:\.[^\s@.]+)+$/.test(email);
}

# Python
import re
EMAIL_RE = re.compile(r"[^\s@]+@[^\s@.]+(?:\.[^\s@.]+)+")

def is_valid_email(value: str) -> bool:
    email = value.strip()
    return len(email) <= 254 and EMAIL_RE.fullmatch(email) is not None

// PHP
function isValidEmail(string $value): bool
{
    $email = trim($value);
    return strlen($email) <= 254
        && preg_match('/^[^\s@]+@[^\s@.]+(?:\.[^\s@.]+)+$/Du', $email) === 1;
}

Two details in those snippets prevent real bugs. In Python, re.match() with $ accepted "user@example.com\n" because $ also matches before a final newline; re.fullmatch() rejected it. PHP behaves the same way unless you add the D modifier. The u modifier matters in PHP too: without it, \s did not match a non-breaking space, and user\u{00A0}x@example.com was accepted. In JavaScript, leave off the g flag when you only call test().

The real validation is a confirmation email

A pattern can only say that an address looks plausible. It cannot tell whether the mailbox exists, whether it accepts mail, or whether it belongs to the person typing it. Looking up MX records shows that a domain is set up to receive mail, but not that a particular mailbox exists. The only reliable test is to send a message:

  1. Store the address as unverified.
  2. Email a link containing a random, single-use token that expires, for example after 24 hours.
  3. Mark the address verified only when the link is opened.
  4. Until then, avoid sending anything else to that address.

Keep normalisation light: trim whitespace and lower-case the domain. RFC 5321 treats the part before the @ as case-sensitive, even though most providers ignore case, so store it as entered.

Try the patterns on your own list

Paste one of the patterns into the Regex Tester without the surrounding slashes, set the flags to gm, and put your sample addresses one per line. With the m flag, ^ and $ apply to each line, so every accepted address is highlighted on its own. We checked that this per-line view gives the same results as testing each address separately for all three patterns above. In application code, test one address at a time without m, or a multi-line value could pass because one of its lines matches.

Frequently Asked Questions

What is the best regex for email validation?

For most forms, ^[^\s@]+@[^\s@.]+(?:\.[^\s@.]+)+$ combined with a 254-character length check. It rejects spaces, missing or doubled @ signs and empty domain labels without rejecting international addresses. Then confirm the address by email.

Why not use a full RFC 5322 regex?

Because it accepts quoted strings, comments and IP-address domains that most systems reject, is very hard to maintain, and still cannot detect typos like gmial.com or tell whether the mailbox exists.

Is input type=email enough?

It is a good first check in the browser, but it rejects non-ASCII addresses, accepts domains without a dot, and can be bypassed. Always validate on the server as well.

How do I check that an email address really exists?

Send a confirmation email with a single-use link and treat the address as verified only after the link is opened. Syntax checks and MX lookups cannot prove that a mailbox exists or belongs to the user.
Try it: Regex Tester