Email validation regex that will not reject real users (and what a regex cannot catch)
A tested email regex for JavaScript, Python and PHP that accepts plus signs and long domains, and the throwaway and dead domains no regex can catch.
Use the pattern the HTML standard gives for email fields, tightened in three small ways. Then check the length: at most 254 characters, with at most 64 before the @. In our test of 33 forms, it rejected no real address and let no broken one through. No regex can tell a throwaway domain from a real one, so check the domain and then confirm the inbox.
/^(?!\.)(?!.*\.\.)[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])?$/This page is for developers who add an email field to a signup, checkout or contact form. The pattern stops typos before they reach your database. Throwaway inboxes and dead domains need the two steps after it.
#Which email regex should you use?
Start from the HTML standard. Browsers run its rule for every <input type="email">. The standard calls that rule a willful violation of RFC 5322, whose syntax it finds too strict before the @ and too vague after it (WHATWG).
Its own pattern lets a few broken forms through, so ours adds three rules. The part before the @ may not start or end with a dot or hold two dots in a row, as RFC 5322 requires. The domain needs at least one dot, which RFC 3696 advises checking. The last label must start with a letter, as every top-level domain on IANA's list does.
Two length rules come from the mail standards. The part before the @ holds at most 64 characters (RFC 5321). The whole address holds at most 254, because the 256-octet SMTP path also carries two angle brackets (RFC 3696, erratum 1690).
| Pattern | Real forms rejected | Bad forms accepted |
|---|---|---|
| This page's pattern | 0 of 14 | 0 of 15 |
| PHP filter_var | 0 | 0 |
| HTML standard | 0 | 7 |
| Microsoft Learn | 0 | 9 |
| uibakery.io, practical | 0 | 10 |
| Zod | 1 | 2 |
| emailregex.com, JavaScript | 1 | 3 |
| emailregex.com, RFC 5322 | 1 | 3 |
| emailregex.com, HTML5 | 1 | 8 |
| Classic, TLD of 2+ letters | 2 | 7 |
| Classic, TLD of 2 to 4 | 6 | 7 |
We ran each pattern as its page publishes it on October 25, 2026, in Node.js 22, PHP 8.3 and Python 3.13. The real forms include plus tags, an apostrophe, capitals, 11- and 18-letter top-level domains and domains in punycode. The bad forms include a missing @, double dots, a space, a trailing line break and a 260-character address.
#Why do common email regexes reject real users?
The usual culprit is the ending. Many older patterns end in [a-zA-Z]{2,4}, because every top-level domain once had two, three or four letters (RFC 3696). Today more than half of them are longer or internationalized.
- [a-zA-Z]{2,4}768 TLDs
- [a-zA-Z]{2,6}455 TLDs
- [a-zA-Z]{2,}151 TLDs
- HTML standard0 TLDs
Source: IANA list of top-level domains, counted by FFraud on October 25, 2026
Even {2,} rejects 151 of them: the internationalized ones, which travel in punycode with digits and hyphens, such as xn--vermgensberatung-pwb. The longest plain top-level domain today is travelersinsurance, at 18 letters.
The other misses in our test were small and easy to copy. emailregex.com's HTML5 pattern holds a typographic apostrophe (’) where ' belongs, so it refused o'brien@example.com. Its RFC 5322 pattern uses lowercase letters only and refused capitals. Test any pattern you copy before you ship it.
#How do you validate an email address in JavaScript, Python and PHP?
- Trim spaces and line breaks from the input, as browsers do for email fields (MDN).
- Check the two lengths, then match the whole string against the pattern. In PHP,
filter_vardoes both, and it passed the same test. - Ask the free FFraud API about the address. Refuse a throwaway inbox, and accept a relay such as Hide My Email.
- Send a confirmation email, and activate the account only after the click.
const EMAIL = /^(?!\.)(?!.*\.\.)[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) {
const email = input.trim();
const at = email.lastIndexOf("@");
return email.length <= 254 && at >= 1 && at <= 64 && EMAIL.test(email);
}
export async function checkEmail(input, timeoutMs = 2000) {
const email = input.trim();
if (!looksLikeEmail(email)) return { accept: false, why: "not an email address" };
try {
const res = await fetch("https://api.ffraud.com/public/email/check", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email }),
signal: AbortSignal.timeout(timeoutMs),
});
if (!res.ok) return { accept: true, why: "check unavailable" };
const r = await res.json();
if (r.email_type === "temp") return { accept: false, why: "disposable domain" };
return { accept: true, why: r.is_role_address ? "role address" : "ok" };
} catch {
return { accept: true, why: "check unavailable" };
}
}Each snippet refuses only an email_type of temp. The API answers alias for relay services such as Apple's Hide My Email, which forward to an inbox a real person reads, and our
Each snippet waits 2 seconds for the API. If the API fails or times out, it accepts the address and leaves the decision to the confirmation email. We ran all three against the live API on October 26, 2026:
JavaScript, Node 22.19.0
"test@mailinator.com" refused disposable domain
"test@privaterelay.appleid.com" accepted ok
"user@@example.com" refused not an email address
" team@ffraud.com " accepted role address
"test@mailinator.com" accepted check unavailable (timeout set to 1 ms)
Python 3.13.7
"test@privaterelay.appleid.com" accepted ok
"us..er@mailinator.com" refused not an email address
" team@ffraud.com\n" accepted role address
PHP 8.3.25
"test@privaterelay.appleid.com" accepted ok
"o'brien@mailinator.com" refused disposable domain
"user@example" refused not an email addressThe trailing line break trap
In PHP and Python, $ also matches just before a final line break. Our test caught it: preg_match without the D modifier and re.match with $ both accepted user@example.com followed by a line break. Use /D in PHP and re.fullmatch in Python, as the snippets do. JavaScript's $ matches only at the true end of the string.
#What does a regex let through?
Format is all a regex can judge. We ran every pattern on all 224,282 throwaway domains in FFraud's user@ plus the domain. The patterns accepted between 92.7 and 100 percent of them, and ours accepted 99.95 percent.
A pattern cannot see a misspelled provider or a domain that takes no mail either. The free API knows the throwaway domains on its list. It flags test@gmial.com, for example, because that domain is on the list:
curl https://api.ffraud.com/public/email/check \
-H "Content-Type: application/json" \
-d '{"email":"test@mailinator.com"}'The API has one limit to know. It does not test whether a domain can receive mail: a made-up domain we tried came back as normal. The
#What should each answer do to the signup?
| Answer | What it means | What to do |
|---|---|---|
| Bad format | Not a usable address | Ask the user to fix it |
| Disposable | A throwaway inbox | Refuse, and ask for a lasting address |
| Relay | An alias such as Hide My Email, read by a real person | Accept |
| Role address | A shared inbox such as admin@ | Accept, and keep an eye on it |
| Reported | Reported for abuse by others | Review it or add a challenge |
| Major provider | Gmail, Outlook and the like | Accept |
| No answer | The check is down or slow | Accept, and rely on the confirmation email |
The fields behind these answers are valid_format, email_type, is_role_address, community_blacklisted and safe_domain (email_type is temp for a throwaway inbox and alias for a relay, while is_disposable is true for both. Major providers such as Gmail and Outlook are never returned as disposable.
Any list can be wrong about one domain. Show a plain message that asks for another address and offers a way to reach you, rather than a silent block. Our guide to
#How do you test your validator?
Run your pattern on forms like these before you ship it. They are the ones that told good patterns from bad ones in our test:
should pass should fail
user+tag@example.com userexample.com
o'brien@example.com user@@example.com
User.Name@Example.COM .user@example.com
user@mail.example.co.uk us..er@example.com
user@example.photography user@-example.com
user@example.travelersinsurance user name@example.com
user@xn--bcher-kva.example user@example
user@example.xn--vermgensberatung-pwb user@192.0.2.1
(64 letters)@example.com (65 letters)@example.com
user@example.com + line breakSpeed was no problem. On hostile inputs of 20,000 characters, no pattern took more than 0.4 milliseconds in Node.js 22, and the length check in front keeps every input short.
Our pattern rejects four rare forms that the RFCs allow: a Unicode domain, a Unicode local part, a quoted local part and an address literal such as user@[192.0.2.1]. RFC 3696 calls quoted forms rarely used, though it advises passing them to the mail system instead of rejecting them. Unicode addresses need a mail system with SMTPUTF8 (RFC 6531). In PHP, FILTER_FLAG_EMAIL_UNICODE accepts a Unicode local part.
#Questions people ask
What is the maximum length of a valid email address?
254 characters in total, with at most 64 before the @. The 254 comes from the SMTP path limit of 256 octets, which includes two angle brackets (RFC 5321, RFC 3696 erratum 1690).
How can I validate an email address using a regular expression?
Trim the input, check the two lengths, and match the whole string against a pattern built on the HTML standard, such as the one at the top of this page. Then confirm the inbox with an email.
Should I use a regex or filter_var to validate an email in PHP?
Use filter_var with FILTER_VALIDATE_EMAIL, after trim. In our test it rejected none of 14 real forms and accepted none of 15 bad ones, the same result as the pattern on this page.
Why does a form reject my email address with a plus sign?
Its pattern leaves + out of the characters it allows before the @. The plus sign is valid there (RFC 5322), and every pattern in our table accepted user+tag@example.com.
Why does input type=email accept an address with no dot in the domain?
The HTML standard allows a domain of one label, such as user@localhost. A public signup form should not, which is why our pattern needs a dot. PHP's filter_var rejects dotless domains too.
Does FILTER_VALIDATE_EMAIL accept user@localhost?
No. The PHP manual says dotless domain names are not supported and are rejected, and our test agreed.
To check one address by hand, paste it into the free
Sources
HTML Living Standard: the input element, email state )RFC 5321: Simple Mail Transfer Protocol RFC 5322: Internet Message Format RFC 3696: Application Techniques for Checking and Transformation of Names RFC 3696 erratum 1690 (verified) RFC 6531: SMTP Extension for Internationalized Email List of top-level domains, version 2026102500 <input type="email"> Validation filters: FILTER_VALIDATE_EMAIL How to verify that strings are in valid email format Email checks API disposable-email-domains, 224,282 domains RFC 7505: A "Null MX" No Service Resource Record for Domains That Accept No Mail