ToolKitSphere IconToolKitSphere
Developer Utilities

How to Validate an Email Address with Regex (and Why Not To)

Online Tools Platform Team7 min read

Every developer writes an email validation regex eventually, and almost every one of them is subtly wrong. The pattern either rejects an address a real person actually owns, or it accepts something that could never be delivered, and usually both. This post gives you a pattern that works, walks through exactly what it accepts and rejects, and is honest about the ceiling: no regex validates email addresses correctly, and chasing one is a bad use of your afternoon. For the broader syntax this pattern is built from, see The Complete Guide to Regular Expressions.

The Pattern

^[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,}$

Apply it with the case-insensitive flag (/i in JavaScript, re.I in Python) if you prefer to drop the A-Z halves of each class. Piece by piece:

  • ^ — anchor to the start of the string. Without it the pattern matches a substring, so nonsense bob@example.com nonsense would pass.
  • [A-Za-z0-9._%+-]+ — the local part. One or more letters, digits, or the punctuation people actually use before the @. Note that + and % are literal inside a character class, and the - sits last where it cannot be read as a range.
  • @ — a literal at sign.
  • [A-Za-z0-9-]+ — the first domain label, such as mail in mail.example.co.uk.
  • (?:\.[A-Za-z0-9-]+)* — zero or more further labels, each a dot plus a label. The (?:...) is non-capturing, since there is nothing here worth capturing.
  • \.[A-Za-z]{2,} — the final dot and a top-level domain of at least two letters. This is what forces a dot into the domain at all.
  • $ — anchor to the end.

Trace it against a few inputs. first.last+tag@sub.example.co.uk matches: the local part takes first.last+tag, the first label takes sub, the repeated group takes .example and .co, and the tail takes .uk. bob@example fails, because after example there is no dot-plus-TLD left to match. bob@@x.com fails, because @ is not in the local-part class, so the engine has consumed bob, matched the first @, and then needs a label where the second @ sits. bob example.com fails on the space.

Paste it into the Regex Tester & Matcher with a column of good and bad addresses and watch which side each one lands on. Everything runs in your browser, so a list of real customer addresses never leaves your machine — which matters more here than for most patterns.

What This Pattern Deliberately Gets Wrong

It is not RFC-correct, and it is not trying to be. Three known compromises:

It accepts some invalid addresses. bob..smith@example.com passes, though consecutive dots are illegal in an unquoted local part. bob@-example.com passes, though a domain label may not begin with a hyphen. Both are typos a user is unlikely to make, and both would simply bounce. Tightening the pattern to catch them roughly doubles its length and its ability to confuse whoever reads it next.

It rejects some valid addresses. RFC 5322 allows a quoted local part — "john doe"@example.com is legal — as well as comments in parentheses, backslash-escaped characters, and domains given as IP literals like user@[192.0.2.1]. It also permits characters such as !, #, $, &, ', *, /, =, ?, ^, `, {, |, }, and ~ in the local part, none of which are in the class above. In practice, essentially nobody signs up with weird!chars#here@example.com. If your users might, widen the local-part class rather than the whole grammar.

It says nothing about deliverability. definitely.not.real@example.com is perfectly well-formed and will never receive mail.

Why No Regex Can Do This Properly

The RFC 5322 address grammar is recursive. Comments may nest — bob(a(nested)comment)@example.com is a legal address — and nesting to arbitrary depth is exactly the thing a regular language cannot express. Modern engines have recursion extensions that could technically encode it, but the well-known "RFC-compliant" pattern that circulates online is a few thousand characters long, unmaintainable, and still incomplete because it ignores the folding-whitespace rules.

More to the point, syntactic validity was never the question you cared about. You wanted to know whether mail sent to this address will arrive. Only one thing answers that: send a message with a confirmation link and see if it gets clicked. Every other check is a typo catcher.

Practical Additions

Length limits. RFC 5321 caps the full address at 254 characters and the local part at 64. A zero-width lookahead adds the total-length check without touching the rest of the pattern:

^(?=.{6,254}$)[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,}$

The (?=.{6,254}$) asserts, from position zero, that the string is between 6 and 254 characters, then hands the cursor back unmoved so the real pattern starts from the beginning. If assertions like this are new to you, Regex Lookahead and Lookbehind Made Simple covers them properly.

The HTML5 baseline. An <input type="email"> gets you free browser-side validation against the WHATWG grammar, which is a deliberate, documented narrowing of RFC 5322. It is a reasonable first gate. Note that it accepts bob@localhost, since it does not require a dot in the domain — the pattern above is stricter on exactly that point.

Normalise before comparing. Lowercase the address for uniqueness checks and lookups, but store what the user typed. Do not strip +tag suffixes: they are valid mailbox addresses, and users notice when you mangle them.

Patterns to Avoid

Two shapes show up constantly in copy-pasted validators and both are bad:

^([A-Za-z0-9]+\.?)+@(...)$

The + on a group whose contents are themselves quantified means a long, nearly-matching local part can be partitioned in exponentially many ways, and the engine will try all of them before giving up. That is catastrophic backtracking, and on user-supplied input it is a denial-of-service vector. The pattern at the top of this post avoids it because its repeated group starts with a literal . that the inner class [A-Za-z0-9-] cannot match — there is exactly one way to split any input, so there is nothing to backtrack through.

.+@.+\..+

Too loose in a way that is easy to miss. . matches almost anything, including spaces and additional at-signs, and greedy quantifiers here mean a b@c d@e f.g h sails straight through. If you want a deliberately permissive check, build it from negated classes instead: ^[^@\s]+@[^@\s]+\.[^@\s]+$. Why the greedy version behaves this way is worth understanding on its own — see Greedy vs Lazy Matching in Regex.

Testing It

Build a fixture list before you ship. A useful minimum: a plain address, one with a plus tag, one with dots in the local part, a multi-label domain, a long TLD such as .photography, an uppercase address, and then the failures — no @, two @, no TLD, leading space, trailing space, empty string.

If you need a hundred realistic addresses rather than seven, the Mock & Fake Data Generator produces them locally, which is safer than pulling a sample out of production. And when you inherit a 300-character validator from a previous developer, run it through the Regex Explainer & Pattern Breakdown before you trust it — the decomposition usually makes it obvious within seconds whether the thing is merely ugly or actively dangerous.

The takeaway is small and freeing: pick a pattern that catches typos, anchor it, cap the length, and let the confirmation email do the real validation. That is the whole job.

Frequently asked questions

What is a good email validation regex?

A practical, anchored pattern is ^[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,}$ with the case-insensitive flag. It accepts plus-addressing, dotted local parts, and multi-label domains such as sub.example.co.uk, while rejecting missing at-signs, missing domains, and bare hostnames with no dot.

Why can't regex fully validate an email address?

RFC 5322 permits quoted local parts like "john doe"@example.com, comments in parentheses, escaped characters, and bracketed IP-literal domains such as user@[192.0.2.1]. A grammar that broad cannot be expressed readably in a single pattern, and matching it would still not tell you whether the mailbox exists.

Should I use the HTML5 email input instead of regex?

Use both. type="email" gives you free client-side checking against the WHATWG willful-violation grammar, which is deliberately narrower than RFC 5322. It is a good first line of defence, but it accepts addresses with no dot in the domain, such as bob@localhost, so most sites still add a server-side check.

Does a valid email regex mean the address exists?

No. Syntax and deliverability are unrelated. valid.but.nonexistent@example.com passes every pattern in this article and will still bounce. The only authoritative test is sending a message with a confirmation link and waiting for the click.

How do I limit the length of an email address in regex?

Add a length lookahead at the start: ^(?=.{6,254}$) followed by the rest of the pattern. The lookahead is zero-width, so it checks the total length without consuming characters. RFC 5321 caps a path at 254 characters and a local part at 64.

Is an email validation regex a ReDoS risk?

Some published ones are. Patterns like ^([A-Za-z0-9]+\.?)+@ nest a quantifier inside a quantified group and can blow up on a long non-matching string. The pattern in this article is safe because the repeated group begins with a literal dot that the inner character class cannot match, so there is only one way to split the input.

Should email validation be case-insensitive?

Yes for the pattern, no for storage. Domains are case-insensitive by definition, and every mail provider in practice treats the local part that way too, so match with the i flag. Store the address as the user typed it, and compare using a normalised lowercase copy.

Try the related tools

Related articles