
An email validation regex should check the format and nothing more: one @, no spaces, and a dot in the domain. Run the same check again on the server, then use a separate check to find out whether the mailbox exists, because no regex can tell you that.
This guide gives you two email regex patterns, tested in JavaScript, Python and PHP, with a table of what each one accepts and rejects. The short version:
+ or long top-level domains.For the format rules behind the patterns (local part, domain labels, length limits), see valid email address format rules and examples.
There's no single regex for valid email addresses that everyone agrees on. In practice you choose between a short pattern that catches obvious typos and a stricter one that also enforces the dot and domain label rules.
| Pattern | Accepts | Rejects |
|---|---|---|
| Practical | Anything shaped like something@something.something without spaces |
Missing @, two @, spaces, a domain without a dot |
| Strict | Addresses that follow the HTML standard's character rules, with a dot in the domain | Everything the practical pattern rejects, plus leading, trailing or double dots, hyphen-edged domain labels and non-ASCII letters |
^[^\s@]+@[^\s@]+\.[^\s@]+$
In words: one or more characters that are not whitespace or @, then @, then more of the same, a dot, and more of the same. Most other guides use the same shape as their simple email regex.
It accepts plus tags (user+tag@example.com), apostrophes (o'brien@example.com) and any top-level domain length. It also accepts things the standard doesn't allow, such as user..name@example.com, and short nonsense such as a@b.c. That's fine for a first check, as long as a second check follows.
The stricter pattern starts from the regex the HTML standard publishes for type="email", which we cover in the regex and type="email" section of our format guide. It changes two things: dots in the local part must sit between other characters (no leading, trailing or double dot), and the domain needs at least one dot.
^[a-zA-Z0-9!#$%&'*+\/=?^_`{|}~-]+(?:\.[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])?)+$
The / is escaped as \/ so the same string works in a JavaScript regex literal, a PHP pattern with / delimiters and a Python raw string. Neither pattern checks length: the local part can be up to 64 characters and the whole address up to 254 in practice, so the code below checks that separately.
Here is how both email regex patterns handle the same 15 addresses. "Format" is what the email standard says, as explained in the format guide.
| Address | Practical | Strict | Format |
|---|---|---|---|
first.last@example.com |
Accepts | Accepts | Valid |
user+tag@example.com |
Accepts | Accepts | Valid |
o'brien@example.com |
Accepts | Accepts | Valid |
USER@EXAMPLE.COM |
Accepts | Accepts | Valid |
user@sub.example.co.uk |
Accepts | Accepts | Valid |
user@example.photography |
Accepts | Accepts | Valid, long top-level domain |
a@b.c |
Accepts | Accepts | Valid format, but not a realistic address |
user.example.com |
Rejects | Rejects | Invalid, no @ |
user@example |
Rejects | Rejects | Allowed by the standard, but most sites expect a dot |
user name@example.com |
Rejects | Rejects | Invalid, space |
user..name@example.com |
Accepts | Rejects | Invalid, double dot |
.user@example.com |
Accepts | Rejects | Invalid, leading dot |
user@example..com |
Accepts | Rejects | Invalid, empty domain label |
user@-example.com |
Accepts | Rejects | Invalid, label starts with a hyphen |
josé@example.com |
Accepts | Rejects | Valid under RFC 6531 if both mail servers support it |
Both patterns accept a@b.c. A regex can check the shape of a domain, not whether it exists.
You'll find RFC 5322 email regex patterns that run to hundreds of characters and handle quoted local parts and comments. They're hard to read, hard to maintain and hard to debug when a customer reports a rejected address. Even a perfect one can only tell you that an address is written correctly, not that it receives mail. The HTML standard itself calls its own, simpler regex a "willful violation" of RFC 5322.
type="email")In JavaScript, use RegExp.test() on the trimmed value and check the length before the pattern:
const EMAIL_PATTERN = /^[a-zA-Z0-9!#$%&'*+\/=?^_`{|}~-]+(?:\.[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])?)+$/;
function isValidEmail(input) {
const email = String(input).trim();
if (email.length > 254) return false; // whole address
if (email.indexOf('@') > 64) return false; // local part
return EMAIL_PATTERN.test(email);
}
console.log(isValidEmail(' user+tag@example.com ')); // true
console.log(isValidEmail('user..name@example.com')); // false
console.log(isValidEmail('a'.repeat(65) + '@example.com')); // false
In the browser, start with an email input. It checks the value against the HTML standard's rules before the form submits. A pattern attribute adds a second rule on top of that check rather than replacing it, so you can use the practical pattern to require a dot in the domain, which type="email" alone doesn't:
<form method="post" action="/signup">
<input type="email" name="email" required
pattern="[^\s@]+@[^\s@]+\.[^\s@]+"
title="Enter an address like name@example.com">
<button type="submit">Sign up</button>
</form>
The browser matches pattern against the whole value, so it needs no ^ or $. With this form, user@example is rejected because of the pattern, while user..name@example.com still gets through both checks.
Client-side checks are a convenience for the user, and anyone can bypass them (MDN). Check the address again on the server. A minimal Node.js handler for the form above:
const http = require('node:http');
const EMAIL_PATTERN = /^[a-zA-Z0-9!#$%&'*+\/=?^_`{|}~-]+(?:\.[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])?)+$/;
function isValidEmail(input) {
const email = String(input).trim();
if (email.length > 254) return false;
if (email.indexOf('@') > 64) return false;
return EMAIL_PATTERN.test(email);
}
http.createServer((req, res) => {
if (req.method !== 'POST' || req.url !== '/signup') {
res.writeHead(404).end();
return;
}
let body = '';
req.on('data', (chunk) => { body += chunk; });
req.on('end', () => {
const email = new URLSearchParams(body).get('email') || '';
if (!isValidEmail(email)) {
res.writeHead(400, { 'Content-Type': 'text/plain' })
.end('Please enter a valid email address, like name@example.com.');
return;
}
res.writeHead(200, { 'Content-Type': 'text/plain' }).end(`Thanks, ${email.trim()}`);
});
}).listen(3000);
re)In Python, compile the pattern once and use re.fullmatch on the stripped value:
import re
EMAIL_PATTERN = re.compile(r"^[a-zA-Z0-9!#$%&'*+\/=?^_`{|}~-]+(?:\.[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])?)+$")
def is_valid_email(value: str) -> bool:
email = value.strip()
if len(email) > 254:
return False
if email.find("@") > 64:
return False
return EMAIL_PATTERN.fullmatch(email) is not None
print(is_valid_email(" user+tag@example.com ")) # True
print(is_valid_email("user..name@example.com")) # False
print(is_valid_email("a" * 65 + "@example.com")) # False
# match() vs fullmatch(): $ also matches before a trailing newline
print(bool(EMAIL_PATTERN.match("user@example.com\n"))) # True
print(bool(EMAIL_PATTERN.fullmatch("user@example.com\n"))) # False
Use fullmatch, not match. re.match only anchors at the start of the string, and $ also matches just before a final newline, so match lets "user@example.com\n" through. fullmatch requires the whole string to match.
email.utils.parseaddr is not a validator. It splits a display name from an address, so it's useful when input arrives as Jane Doe <jane@example.com>. Run the pattern on the address part afterwards:
from email.utils import parseaddr
name, address = parseaddr("Jane Doe <jane@example.com>")
print(name) # Jane Doe
print(address) # jane@example.com
filter_var vs preg_matchIn PHP, start with filter_var and FILTER_VALIDATE_EMAIL. According to the PHP manual, it validates against the RFC 822 addr-spec syntax, without support for comments, whitespace folding or dotless domain names. The manual also points out that the only way to confirm an address is to send mail to it.
With the FILTER_FLAG_EMAIL_UNICODE flag (PHP 7.1.0 and later), filter_var also accepts Unicode characters in the local part. Use preg_match when you need your own rule, for example to match what your JavaScript check does.
<?php
const EMAIL_PATTERN = '/^[a-zA-Z0-9!#$%&\'*+\/=?^_`{|}~-]+(?:\.[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])?)+$/D';
function isValidEmail(string $value): bool
{
return filter_var(trim($value), FILTER_VALIDATE_EMAIL) !== false;
}
function matchesEmailPattern(string $value): bool
{
$email = trim($value);
if (strlen($email) > 254) {
return false;
}
$at = strpos($email, '@');
if ($at !== false && $at > 64) {
return false;
}
return preg_match(EMAIL_PATTERN, $email) === 1;
}
var_dump(isValidEmail(' user+tag@example.com ')); // bool(true)
var_dump(isValidEmail('user@example')); // bool(false)
var_dump(filter_var('josé@example.com', FILTER_VALIDATE_EMAIL, FILTER_FLAG_EMAIL_UNICODE) !== false); // bool(true)
var_dump(matchesEmailPattern('user..name@example.com')); // bool(false)
// Without the D modifier, $ also matches before a trailing newline
var_dump(preg_match('/^[^\s@]+@[^\s@]+\.[^\s@]+$/', "user@example.com\n")); // int(1)
var_dump(preg_match('/^[^\s@]+@[^\s@]+\.[^\s@]+$/D', "user@example.com\n")); // int(0)
The pattern is the same string as in JavaScript and Python. Inside PHP single quotes, the two apostrophes in the character classes are written as \'.
How the three PHP checks handle the test addresses:
| Address | filter_var |
filter_var + FILTER_FLAG_EMAIL_UNICODE |
preg_match (strict pattern) |
|---|---|---|---|
first.last@example.com |
Accepts | Accepts | Accepts |
user+tag@example.com |
Accepts | Accepts | Accepts |
o'brien@example.com |
Accepts | Accepts | Accepts |
USER@EXAMPLE.COM |
Accepts | Accepts | Accepts |
user@sub.example.co.uk |
Accepts | Accepts | Accepts |
user@example.photography |
Accepts | Accepts | Accepts |
a@b.c |
Accepts | Accepts | Accepts |
user.example.com |
Rejects | Rejects | Rejects |
user@example |
Rejects | Rejects | Rejects |
user name@example.com |
Rejects | Rejects | Rejects |
user..name@example.com |
Rejects | Rejects | Rejects |
.user@example.com |
Rejects | Rejects | Rejects |
user@example..com |
Rejects | Rejects | Rejects |
user@-example.com |
Rejects | Rejects | Rejects |
josé@example.com |
Rejects | Accepts | Rejects |
On these addresses filter_var and the strict pattern agree. They differ on rarer input: filter_var accepts quoted local parts such as "john..doe"@example.com, which the pattern rejects. If you have no reason to write your own rule, filter_var is the easier one to maintain.
If your email regex rejects valid addresses, it's usually one of these:
+ tags. user+tag@example.com is a valid address, and many people use tags to sort mail.[a-z]{2,4}. Domains like .photography are longer.John.Smith@Example.com. Allow both cases in the pattern (or use a case-insensitive flag).josé@example.com. If you keep that rule, tell the user that letters like é aren't supported, instead of a generic error. The format guide's section on internationalized addresses explains why support is uneven.Here is a pattern that makes the first four mistakes:
const BAD_PATTERN = /^[a-z0-9._%-]+@[a-z0-9.-]+\.[a-z]{2,4}$/;
console.log(BAD_PATTERN.test('user+tag@example.com')); // false
console.log(BAD_PATTERN.test('user@example.photography')); // false
console.log(BAD_PATTERN.test('USER@EXAMPLE.COM')); // false
console.log(BAD_PATTERN.test(' user@example.com')); // false
console.log(BAD_PATTERN.test('user@example.com')); // true
The strict pattern from above, with a trim, accepts all four:
const EMAIL_PATTERN = /^[a-zA-Z0-9!#$%&'*+\/=?^_`{|}~-]+(?:\.[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 check = (value) => EMAIL_PATTERN.test(value.trim());
console.log(check('user+tag@example.com')); // true
console.log(check('user@example.photography')); // true
console.log(check('USER@EXAMPLE.COM')); // true
console.log(check(' user@example.com')); // true
Can a regex check if an email exists? No. All of these pass both patterns:
nobody-here@example.com: correctly formatted, but there may be no such mailbox.jane@gmial.com: a typo of a real domain, still a well-formed address.To find out whether an address can receive mail, send it to the CampaignKit email validation API after your regex check. The endpoint is POST https://api.campaignkit.cc/v1/email/validate, with your API key as a Bearer token and up to 100 addresses per request (API docs):
curl -X POST https://api.campaignkit.cc/v1/email/validate \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"emails": [
"valid@example.com",
"john@gmai.com"
]
}'
The response has one result per address. This example is adapted from the API docs, with two results:
{
"creditsUsed": 2,
"results": [
{
"email": "valid@example.com",
"contextId": "ctx_abc123",
"result": {
"syntax": "pass",
"mx": "pass",
"mailbox": "pass",
"score": 10,
"classifier": "valid",
"description": [],
"smtpResponse": "250 OK"
}
},
{
"email": "john@gmai.com",
"contextId": "ctx_typo01",
"result": {
"syntax": "pass",
"mx": "fail",
"mailbox": "n/a",
"score": 0,
"classifier": "invalid",
"description": ["domain", "didYouMean"],
"smtpResponse": "",
"didYouMean": "john@gmail.com",
"didYouMeanDomain": "gmail.com"
}
}
]
}
For a sign-up form, classifier is the field to route on. It is valid, risky or invalid, and score runs from 0 to 10 if you need finer control. When a domain looks like a typo of a well-known provider, the result includes didYouMean (here john@gmail.com), which you can show to the user as a suggestion.
Here is the regex-then-API flow in Node.js 18 or later (built-in fetch), with your key in the CAMPAIGNKIT_API_KEY environment variable:
const EMAIL_PATTERN = /^[a-zA-Z0-9!#$%&'*+\/=?^_`{|}~-]+(?:\.[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])?)+$/;
async function checkSignupEmail(input) {
const email = String(input).trim();
// 1. Format check: no API call for obvious typos
if (email.length > 254 || email.indexOf('@') > 64 || !EMAIL_PATTERN.test(email)) {
return { ok: false, message: 'Please enter a valid email address, like name@example.com.' };
}
// 2. Mailbox check with the API
const response = await fetch('https://api.campaignkit.cc/v1/email/validate', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.CAMPAIGNKIT_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ emails: [email] })
});
if (response.status === 429) {
// Rate limited: accept the address and check it again later
return { ok: true, message: 'Accepted, not checked yet' };
}
if (!response.ok) {
throw new Error(`Validation API error ${response.status}: ${await response.text()}`);
}
const data = await response.json();
const { result } = data.results[0];
if (result.classifier === 'invalid') {
const hint = result.didYouMean ? ` Did you mean ${result.didYouMean}?` : '';
return { ok: false, message: `This address can't receive mail.${hint}` };
}
if (result.classifier === 'risky') {
return { ok: true, message: 'Accepted, flagged for review', flags: result.description };
}
return { ok: true, message: 'Accepted' };
}
checkSignupEmail('john@gmai.com').then(console.log).catch(console.error);
Errors come back as JSON with an error code: invalid_request (400), unauthorized (401, invalid or missing key) or rate_limit_exceeded (429). The snippet accepts the address on a 429 rather than blocking the sign-up, and throws on the others, because those point to a problem in your code or key. Our guide to real-time email validation at the point of capture covers how to handle each result in a sign-up flow, and getting started with the email validation API walks through a first request.
Run the test addresses through both patterns:
const PRACTICAL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const STRICT = /^[a-zA-Z0-9!#$%&'*+\/=?^_`{|}~-]+(?:\.[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 addresses = [
'first.last@example.com',
'user+tag@example.com',
"o'brien@example.com",
'USER@EXAMPLE.COM',
'user@sub.example.co.uk',
'user@example.photography',
'a@b.c',
'user.example.com',
'user@example',
'user name@example.com',
'user..name@example.com',
'.user@example.com',
'user@example..com',
'user@-example.com',
'josé@example.com'
];
for (const email of addresses) {
console.log(email, 'practical:', PRACTICAL.test(email), 'strict:', STRICT.test(email));
}
Each line should match the Practical and Strict columns of the test table at the top: true for "Accepts" and false for "Rejects". The first seven addresses print true for both patterns, the next three false for both, and the last five true for the practical pattern and false for the strict one.
To test the server side, start the Node.js handler and post an address. Use --data-urlencode, because a raw + in form data is decoded as a space:
curl -s --data-urlencode 'email=user+tag@example.com' http://localhost:3000/signup
# Thanks, user+tag@example.com
curl -s --data-urlencode 'email=user@example' http://localhost:3000/signup
# Please enter a valid email address, like name@example.com.
A valid address is rejected. Check for a top-level domain cap such as {2,4}, a missing + in the character class, or a lowercase-only class. Run the address through the test snippet above to see which pattern rejects it.
The browser accepts an address, but the server rejects it (or the other way around). The two sides often run different rules. The form in this guide, for example, lets user..name@example.com through, and the Node.js handler rejects it. Use the same pattern string in your JavaScript, Python and PHP code where you can. In PHP, wrap it in delimiters (/.../), escape / inside the pattern as \/, and add the D modifier so $ doesn't match before a trailing newline. If your PHP pattern matches non-ASCII letters, add the u modifier so it works on UTF-8 characters rather than bytes.
filter_var rejects user@localhost. That's expected: per the PHP manual, FILTER_VALIDATE_EMAIL doesn't accept dotless domain names. For local testing, use an address with a dot in the domain or a separate rule.
What is the best regex for email validation?
For most forms, a short pattern such as ^[^\s@]+@[^\s@]+\.[^\s@]+$ that checks for one @, no spaces and a dot in the domain, plus a length check and a server-side check. Use the stricter pattern in this guide if you also want to reject double dots and invalid domain labels.
Is there one regex that follows RFC 5322? There are long patterns that try to, but they're hard to read and maintain, and the web's own standard deliberately uses a simpler rule. Even a complete one only checks the format, not whether the address receives mail.
Can regex tell me if an email exists?
No. A regex only checks how an address is written. nobody-here@example.com and typos like gmial.com pass. To find out whether a mailbox can receive mail, use a validation service or API.
Should I use filter_var or preg_match in PHP?
Start with filter_var($email, FILTER_VALIDATE_EMAIL), which follows a documented standard and needs no pattern of your own. Use preg_match when you need a rule filter_var doesn't have, such as the exact pattern your JavaScript check uses.
Does type="email" replace server-side validation?
No. The browser check helps users fix typos before they submit, but anyone can bypass it. Validate the address again on the server.
A regex tells you an address is well formed. The CampaignKit email validation API tells you whether it can receive mail, with a classifier, a 0 to 10 score and typo suggestions, for up to 100 addresses per request.
Get 200 free validations, no credit card needed, and use them with the API. Or try one address free in the email verifier, no signup (5 free checks per hour).
Start with 200 free validations. Upgrade only when you're ready.
No credit card required • Cancel anytime