Dev.to WebDev 🛠 Dev 👁 0 📖 5 min read

I tested 6 popular email regexes against 46 addresses. None passed.

Every few months I end up pasting an email regex into a project. This time I wanted to know whether the one I was about to paste was any good. So I took six of the most-copied patterns and ran them all against the same 4

Every few months I end up pasting an email regex into a project. This time I wanted to know whether the one I was about to paste was any good. So I took six of the most-copied patterns and ran them all against the same 46 addresses.

Short version: none of them got everything right, all six accepted a 260-character address, and the accepted Stack Overflow answer (6,668 votes) has a bug.

Full write-up with charts and every result: Email Validation Regex: We Tested 6 Popular Patterns.

The contenders

Each one copied exactly as published:

  1. Simple – ^[^\s@]+@[^\s@]+\.[^\s@]+$ (Stack Overflow, 1,385 votes)
  2. regular-expressions.info – ^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$, case-insensitive (source)
  3. OWASP – from the OWASP Validation Regex Repository
  4. Stack Overflow accepted answer – from "How can I validate an email address in JavaScript?"
  5. WHATWG – the pattern the HTML spec uses for <input type="email">
  6. "RFC 5322" – the long one from this answer and emailregex.com

The test set

16 addresses that should pass (plus addressing, apostrophes, long TLDs, punycode, a 64-character local part), 25 that should fail (bad dots, spaces, hyphens at label edges, underscores in the domain, one-letter TLDs, addresses over the RFC 5321 length limits), and 5 rare-but-legal edge cases I didn't score.

I ran everything in Chrome 155 and Python 3.14. Both gave identical results.

Results

Pattern Valid accepted (of 16) Invalid rejected (of 25)
Simple 16 9
regular-expressions.info 12 15
OWASP 13 20
SO accepted 15 20
WHATWG 16 18
RFC 5322 16 20

A missed valid address is a user who can't sign up, so I'd rule out anything below 16 in the first column.

Things I didn't expect

OWASP and regular-expressions.info reject o'[email protected]. The apostrophe is legal in the local part, but neither pattern's character class includes it. regular-expressions.info also rejects & and =, and both reject punycode TLDs like .xn--p1ai.

The accepted Stack Overflow answer has a stray dot. The quoted-string branch is .(".+") instead of (".+"). So it rejects "john doe"@example.com and accepts x"john doe"@example.com, which isn't a valid address.

No pattern checks length. All six accepted a 65-character local part (the limit is 64) and a 260-character address (the limit is 254). Regex is the wrong tool for that anyway; do it in code.

The "RFC 5322" regex isn't quite RFC 5322. It rejects "john doe"@example.com, because its quoted-string branch doesn't allow a plain space. The RFC allows it through folding whitespace.

Python's $ matches before a trailing newline. re.match(pattern, "[email protected]\n") passes with all six patterns. Use re.fullmatch or \Z. JavaScript's $ doesn't have this problem, and Chrome's <input type="email"> strips the newline (and surrounding spaces) before validating.

What I use now

The WHATWG pattern with one change (* → +, so the domain needs a dot), plus three checks in code. It passed all 41 scored tests in both languages. I wrote it after seeing the tests, so treat that as "passes this set", not "perfect".

const EMAIL_RE = /^[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])?)+$/;
const TLD_RE = /^(?:[a-zA-Z]{2,63}|xn--[a-zA-Z0-9-]{1,59})$/;

function isValidEmail(email) {
  if (email.length > 254 || !EMAIL_RE.test(email)) return false;
  const at = email.lastIndexOf('@');
  const local = email.slice(0, at);
  const domain = email.slice(at + 1);
  if (local.length > 64 || local.startsWith('.') || local.endsWith('.') || local.includes('..')) return false;
  return TLD_RE.test(domain.slice(domain.lastIndexOf('.') + 1));
}
import re

EMAIL_RE = re.compile(
    r"[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+"
    r"@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?"
    r"(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+"
)
TLD_RE = re.compile(r"[a-zA-Z]{2,63}|xn--[a-zA-Z0-9-]{1,59}")


def is_valid_email(email: str) -> bool:
    if len(email) > 254 or not EMAIL_RE.fullmatch(email):
        return False
    local, domain = email.rsplit("@", 1)
    if len(local) > 64 or local.startswith(".") or local.endswith(".") or ".." in local:
        return False
    return bool(TLD_RE.fullmatch(domain.rsplit(".", 1)[1]))

Trim the input before calling either one. It deliberately rejects quoted local parts, IP literals, localhost and non-ASCII addresses, which is what most signup forms want.

Test your own regex

Paste this into your browser console, swap in your pattern, and see what it gets wrong:

const myRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; // ← your pattern here

const shouldPass = [
  "[email protected]", "[email protected]", "[email protected]",
  "[email protected]", "o'[email protected]", "[email protected]",
  "[email protected]", "[email protected]", "[email protected]",
  "[email protected]", "[email protected]", "[email protected]",
  "[email protected]", "a".repeat(64) + "@example.com",
  "a&[email protected]", "[email protected]",
];
const shouldFail = [
  "plainaddress", "@example.com", "user@", "user@@example.com", "user@[email protected]",
  "[email protected]", "[email protected]", "[email protected]",
  "user [email protected]", "user@exam ple.com", " [email protected]", "[email protected] ",
  "[email protected]", "[email protected]", "[email protected]", "[email protected]",
  "[email protected].", "user@exa_mple.com", "user,[email protected]",
  "user<script>@example.com", "[email protected]", "[email protected]",
  "a".repeat(65) + "@example.com", "user@" + "b".repeat(64) + ".com",
  "a".repeat(64) + "@" + Array(3).fill("c".repeat(63)).join(".") + ".com",
];

const test = (re) => ({
  wronglyRejected: shouldPass.filter((e) => !re.test(e)),
  wronglyAccepted: shouldFail.filter((e) => re.test(e)),
});
console.log(test(myRegex));

The example pattern in the first line, "Simple", wrongly accepts 16 of the 25 bad addresses.

The part a regex can't do

Most of my "valid" test addresses use example.com, a domain reserved for documentation that will never receive mail. They all pass. A regex tells you an address is shaped like an email, not that anyone reads it. Typo domains, deleted mailboxes and throwaway inboxes all have perfectly good syntax.

For that you need DNS (MX) and SMTP checks, and ideally a disposable-domain check on top. I work at MailValid, which sells email verification, so I'm biased about that last step. The regex part above is free either way.

What does your project use? If you run your pattern through the test above, I'd like to know how it does.

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.