Check whether your own email addresses are actually valid

Paste the addresses you actually have — a signup list, a bug report, a CSV export — into the tester below and see immediately which ones this pattern accepts and which it rejects, and why. The sample already loaded mixes three valid addresses with four that fail for four different reasons. The copy-paste pattern, explained token by token, and an honest account of what no regex can prove about an email address, follow underneath.

/ / gm
Flags

    Your pattern and test text never leave this tab. Matching runs in a same-origin Web Worker with a timeout, so nothing you paste — including a real address from a bug report — is ever sent anywhere to be tested. That protection matters more than it looks: a single regular expression once caused a real, global outage — the timeout above is what keeps this page safe from the same failure mode.

    ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$

    Say this plainly before anything else

    No regular expression fully validates an email address against RFC 5322, the specification that defines what an email address legally is. RFC 5322’s address grammar permits quoted local parts containing spaces and punctuation, parenthesised comments inside the address, folding whitespace across line breaks, and domains written as bracketed IP literals. A regex that implements all of that runs to hundreds of characters, accepts addresses no real mail server on the public internet will deliver to, and still cannot tell you whether the mailbox behind it exists. The only real validation is sending a message and seeing what comes back — in practice, a confirmation link the person has to click. Everything below is a typo filter, not a proof of anything.

    Token by token

    TokenMeaning
    ^Start of the string — without it, the pattern could match partway through a longer string
    [a-zA-Z0-9._%+-]+The local part, before the @: one or more letters, digits, dots, underscores, percent signs, plus signs or hyphens
    @Literal @ — exactly one, required
    [a-zA-Z0-9.-]+The domain, up to the last dot: letters, digits, dots or hyphens
    \.A literal dot — escaped, because unescaped . means “any character”
    [a-zA-Z]{2,}The top-level label: two or more letters (com, org, museum, io)
    $End of the string — without it, trailing garbage after a valid-looking prefix would still match

    Both anchors matter more than they look. Drop ^ and $ and the pattern will happily report a match inside <a href="mailto:user@example.com" onclick="evil()"> because somewhere in that string is text shaped like an address. Anchoring both ends is what makes this a validator rather than a search.

    What it matches

    • ada@example.com — the ordinary case.
    • first.last+newsletter@sub.example.co.uk — dots and a plus-tag in the local part, a subdomain, a two-part top-level label.
    • a_b-c@x-y.io — underscore and hyphen in both halves.

    What it deliberately does not match, and why

    InputVerdictWhy
    not-an-emailRejectedNo @ at all
    user@.comRejectedDomain part cannot start with a dot — nothing between @ and .
    @nodomain.comRejectedEmpty local part — + requires at least one character
    user@domainRejectedNo dot at all, so no top-level label — technically a valid hostname on a private network, but not a deliverable public address
    "quoted string"@example.comRejectedRFC 5322 permits a quoted local part; this pattern’s character class has no " or space in it, on purpose — accepting quoted strings opens the door to local parts containing almost anything
    user@[192.168.1.1]RejectedRFC 5322 allows an IP-literal domain in brackets; essentially no real-world signup form needs to accept one
    a@b.coAcceptedSyntactically fine. Nothing stops anyone registering a domain like this, but nothing here proves it receives mail either

    The last row is the important one. This pattern will accept plenty of addresses that bounce, and reject a small number of addresses that are technically legal and would deliver. Both are intentional trade-offs for a typo filter, not defects to fix.

    Unicode and internationalised addresses

    RFC 5322’s ASCII grammar is not the end of the story: Unicode Technical Standard #18 governs how a regex engine should treat non-ASCII text, and modern mail systems increasingly support internationalised addresses under RFC 6531, where both the local part and the domain may contain non-Latin characters — 用户@例子.广告 is a legal address on a system that supports it. The pattern above rejects it outright, because [a-zA-Z0-9._%+-] is an ASCII-only character class. Extending it to Unicode script letters is possible with the u flag and a property escape such as \p{L}, but very few consumer mail providers accept these addresses in practice yet, so most production forms are still better served rejecting them with a clear message than half-supporting them with a pattern that has not been tested against a real mail transfer agent.

    Flavour: this is JavaScript’s regex

    The pattern above targets JavaScript’s built-in RegExp, as defined by the grammar in ECMA-262’s text-processing chapter. It uses nothing but a literal @, character classes and quantifiers, so it is identical in PCRE (PHP, most grep-style tools) and Python’s re without changing a single character — there is no lookaround, no named group and no possessive quantifier here for the flavours to disagree about. That will not stay true if you extend it with Unicode property escapes or lookbehind for a stricter variant; see the general flavour-honesty note on the cookbook page for where those constructs diverge between engines.

    Worked examples

    1. Why + deserves handling in your code, not just your regex

    Gmail and many other providers treat everything after a + in the local part as an ignorable tag: ada+newsletter@example.com and ada+receipts@example.com both deliver to the same inbox as ada@example.com. The pattern above correctly accepts all three as valid email shapes — that is its job. Whether your application should treat them as the same account is a business-logic decision the regex cannot make for you. A signup form that does not strip or canonicalise the plus-tag before checking for an existing account will let one person register three times.

    2. Catching the typo a user cannot see

    Paste ada@gmail,com into the tester — a comma where the dot should be, invisible at a glance in most fonts. It is rejected, because the pattern requires a literal \. before the top-level label. This is the pattern’s actual job: catching the fat-fingered mistake before a form submits, not proving the address is real.

    3. The false confidence of a stricter-looking pattern

    It is tempting to reach for a longer, scarier-looking regex that claims to be “RFC 5322 compliant” off a search result. Most of them still are not, and the ones that come close are difficult to audit by eye and slower to run for no practical gain. A short pattern you can read in ten seconds, backed by a confirmation email, beats a 400-character pattern nobody on the team fully understands.

    4. One address per line, filtering a mailing list export

    A CSV export from a mailing-list tool sometimes contains a handful of rows where the email column is empty, contains a name instead ("Ada Lovelace" with no @), or has been truncated by a spreadsheet program helpfully “fixing” what looked like a formula. With the g flag off and this pattern anchored on each line individually (splitting the file on newlines first, then testing each line), the bad rows fail cleanly and can be routed to a manual-review bucket instead of silently breaking whatever downstream system expects a real address in every row.

    Using this pattern in your own code

    The pattern is plain enough to drop into any of the three environments a validation rule like this typically has to live in — a browser form, a server-side script, and a one-off command-line check — without rewriting it for each one.

    // JavaScript, in the browser or Node.js
    const EMAIL_RE = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
    EMAIL_RE.test('ada@example.com');        // true
    EMAIL_RE.test('not-an-email');           // false
    # Python's re module — the pattern text is identical, character for character
    import re
    EMAIL_RE = re.compile(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$')
    bool(EMAIL_RE.match('ada@example.com'))  # True
    # note: re.match anchors at position 0 by itself, but the trailing $ still
    # matters — without it, re.match would accept "ada@example.com and then junk"
    # grep -P, for a quick command-line pass over a file of candidate addresses
    grep -P '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' addresses.txt

    All three run the same characters through a PCRE-compatible engine (Python’s re and GNU grep’s -P mode both implement a PCRE-like dialect for exactly this class of pattern) with identical results, precisely because this pattern avoids every construct that the three flavours disagree about — no named groups, no lookaround, nothing possessive. That is not an accident; it is why the pattern was written this way rather than with a more “modern”-looking construct that would have fractured across environments for no real gain in accuracy.

    Common mistakes when adapting this pattern

    • Forgetting the anchors when reusing the character classes elsewhere. Copying just [a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,} without ^/$ into a larger pattern (say, one that also expects surrounding quotes) changes it from “the whole string is an email” to “somewhere in the string is something email-shaped” — usually not the intended change, and an easy one to miss in review since the pattern still looks correct at a glance.
    • Adding i and assuming it changes the character classes. [a-zA-Z0-9._%+-] already covers both cases explicitly, so the i flag adds nothing here and its absence changes nothing either — a common misconception is that dropping the explicit uppercase range and adding i instead is equivalent; it is, for this pattern, but only because every other literal character in it (@, .) has no case to begin with.
    • Trying to add quoted-string support by hand. A tempting-looking fix for rejecting "quoted string"@example.com is adding "[^"]*" as an alternative local part. This technically accepts the common case, but RFC 5322’s actual quoted-string grammar allows escaped quotes and a wider character set inside the quotes than a simple [^"]* captures — a partial implementation of a grammar is often worse than no implementation, because it looks complete without being complete.

    Frequently asked questions

    Is there a regex that fully validates RFC 5322?

    Technically yes — the grammar is finite, so it can be encoded as a (very long) regular expression. Practically, no one should use one. Such a pattern accepts quoted strings, comments and IP-literal domains that no production mail system expects, is hundreds of characters long, and still says nothing about deliverability. Every serious style guide on this topic converges on the same advice: validate the shape loosely, then confirm by sending mail.

    Should I allow a + in the local part?

    Yes. It is standard for tagged addressing on Gmail, Fastmail, Outlook and most other providers, and RFC 5322 permits it unconditionally. Rejecting it blocks a real, common use of email you have no business blocking.

    Does this pattern support internationalised (non-Latin) addresses?

    No, by design — see the Unicode section above. Its character classes are ASCII-only. Supporting RFC 6531 addresses needs Unicode property escapes and, more importantly, testing against a mail system that actually delivers to them.

    Why does my form accept an address that later bounces?

    Because syntax and deliverability are different questions. A regex can only ever confirm the first. A domain can be syntactically perfect and have no mail server behind it, a full inbox, or a typo in a real provider’s name (gmial.com). Only a confirmation message, or an MX-record lookup on your server as a weaker proxy, tests the second.

    Can I check the domain has a valid MX record from this page?

    No, and no client-side JavaScript can — a DNS lookup needs a server. This page tests syntax only, entirely in your browser; an MX check would require a network request this tool deliberately does not make.

    What is the difference between this and the pattern in the main cookbook?

    None — it is the same “Email address (practical)” entry from the cookbook’s library. This page exists to explain that one pattern in depth; the cookbook page is the place to browse it alongside the other eight.

    Should I trim whitespace before testing the pattern?

    Yes. A pasted address commonly carries a leading or trailing space or a stray newline, and because the pattern is anchored with ^ and $, either one causes a false rejection. Call value.trim() before testing, in your own code as much as in this page’s test-text box.

    Limitations

    • Ascii-only local part and domain; no internationalised addresses (see above).
    • No quoted local parts, no comments, no IP-literal domains — all legal under RFC 5322, all rejected here on purpose.
    • Accepts syntactically valid but non-deliverable domains; cannot check MX records, mailbox existence, or catch-all versus rejecting mail servers.
    • Does not canonicalise +-tags or case-fold the domain; that is application logic, not a regex’s job.

    Related