The short answer: a practical email regex
This pattern accepts the addresses people actually use, including plus tags, apostrophes and long top-level domains, and rejects the mistakes that make an address undeliverable:
/^(?=.{1,254}$)(?=.{1,64}@)[A-Za-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[A-Za-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+(?:[A-Za-z]{2,63}|xn--[A-Za-z0-9-]{2,59})$/
It isn’t the full RFC 5322 grammar on purpose: it leaves out quoted local parts ("jane doe"@example.com) and IP addresses as domains, which are valid on paper but rejected by almost every mail service and form. Trim the input before you test it, since people paste addresses with spaces.
What the pattern checks
| Part | What it does |
|---|---|
(?=.{1,254}$) | The whole address has at most 254 characters (RFC 5321). |
(?=.{1,64}@) | The part before the @ has at most 64 characters. |
[A-Za-z0-9! | The local part: letters, digits and the special characters RFC 5322 allows. |
(?:\.[…]+)* | Dots only between characters: no dot at the start or end, never two in a row. |
(?: | One or more domain labels of up to 63 letters, digits and hyphens, not starting or ending with a hyphen. |
(?: | A top-level domain of letters, or an internationalized one in punycode (xn--). |
The rules behind each part are explained in the guide to the email address format.
Email regex in JavaScript
const EMAIL_REGEX =
/^(?=.{1,254}$)(?=.{1,64}@)[A-Za-z0-9!#$%&'*+\/=?^_`{|}~-]+(?:\.[A-Za-z0-9!#$%&'*+\/=?^_`{|}~-]+)*@(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+(?:[A-Za-z]{2,63}|xn--[A-Za-z0-9-]{2,59})$/;
function isValidEmail(value) {
return EMAIL_REGEX.test(value);
}
isValidEmail("jane+news@example.com"); // true
isValidEmail("jane..doe@example.com"); // false
In JavaScript, ^ and $ mark the start and end of the whole string as long as you don’t add the m flag. Don’t add g either: a global regex remembers its position between test() calls and returns alternating results. Step-by-step form validation is in the guide to validating email in JavaScript.
Email regex in Python
import re
EMAIL_REGEX = re.compile(
r"(?=.{1,254}\Z)(?=.{1,64}@)"
r"[A-Za-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[A-Za-z0-9!#$%&'*+/=?^_`{|}~-]+)*"
r"@(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+"
r"(?:[A-Za-z]{2,63}|xn--[A-Za-z0-9-]{2,59})"
)
def is_valid_email(value: str) -> bool:
return EMAIL_REGEX.fullmatch(value) is not None
Use fullmatch(). With re.match() and a pattern ending in $, Python also accepts "jane@example.com\n", because $ matches before a trailing line break. More in validating email in Python.
Email regex in PHP
const EMAIL_REGEX = '/\A(?=.{1,254}\z)(?=.{1,64}@)[A-Za-z0-9!#$%&\'*+\/=?^_`{|}~-]+(?:\.[A-Za-z0-9!#$%&\'*+\/=?^_`{|}~-]+)*@(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+(?:[A-Za-z]{2,63}|xn--[A-Za-z0-9-]{2,59})\z/';
function isValidEmail(string $value): bool
{
return preg_match(EMAIL_REGEX, $value) === 1;
}
Use \A and \z (or the D modifier): with ^ and $ alone, preg_match accepts an address followed by a line break. PHP also has a built-in check, filter_var($email, FILTER_VALIDATE_EMAIL); the test table below shows where it differs. More in validating email in PHP.
Email regex in Java
import java.util.regex.Pattern;
public final class EmailValidator {
private static final Pattern EMAIL_REGEX = Pattern.compile(
"(?=.{1,254}\\z)(?=.{1,64}@)"
+ "[A-Za-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[A-Za-z0-9!#$%&'*+/=?^_`{|}~-]+)*"
+ "@(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\\.)+"
+ "(?:[A-Za-z]{2,63}|xn--[A-Za-z0-9-]{2,59})");
public static boolean isValidEmail(String value) {
return EMAIL_REGEX.matcher(value).matches();
}
}
matches() requires the whole string to match, so the pattern needs no ^ and $. Backslashes are doubled inside Java strings. Library checks, an MX lookup and an API call in Java are in the guide to validating email in Java; the same steps for .NET are in the C# email validation guide.
Email regex in Go
import (
"regexp"
"strings"
)
// Go's regexp package (RE2) has no lookaheads, so the length limits are checked in code.
var emailRegex = regexp.MustCompile(
"^[A-Za-z0-9!#$%&'*+/=?^_\x60{|}~-]+(?:\\.[A-Za-z0-9!#$%&'*+/=?^_\x60{|}~-]+)*" +
"@(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\\.)+" +
"(?:[A-Za-z]{2,63}|xn--[A-Za-z0-9-]{2,59})$")
func isValidEmail(value string) bool {
at := strings.LastIndex(value, "@")
return len(value) <= 254 && at >= 1 && at <= 64 && emailRegex.MatchString(value)
}
The pattern contains a backtick, which can’t appear in a Go raw string, so it’s written as \x60 in a normal string. net/mail, MX lookups and an API check are in the guide to validating email in Go.
Email regex in Ruby
EMAIL_REGEX = /\A(?=.{1,254}\z)(?=.{1,64}@)[A-Za-z0-9!\#$%&'*+\/=?^_`{|}~-]+(?:\.[A-Za-z0-9!\#$%&'*+\/=?^_`{|}~-]+)*@(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+(?:[A-Za-z]{2,63}|xn--[A-Za-z0-9-]{2,59})\z/
def valid_email?(value)
EMAIL_REGEX.match?(value)
end
In Ruby, ^ and $ match at every line break, so "<script>\njane@example.com" passes a pattern written with them. Always use \A and \z. The # is escaped as a precaution: in Ruby regex literals, #$name interpolates a global variable. Model validations, an MX lookup and a mailbox check for Rails are in the guide for Ruby.
The HTML5 pattern: type="email"
Browsers validate <input type="email"> with a pattern from the HTML standard:
/^[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 standard calls it a deliberate violation of RFC 5322: it is simple on purpose. As the test table shows, it accepts dots at the start or end of the local part and domains without a top-level domain (jane@example), so use it for instant feedback and check again on the server. Tested results for type="email", plus pattern and custom error messages, are in the guide to the HTML email validator. The same idea works in schemas and spreadsheets: see JSON Schema email validation and Google Forms and Excel email validation.
Why not a full RFC 5322 regex?
A regex that implements the complete RFC 5322 address grammar, with quoted strings, comments and IP addresses, runs to hundreds of characters. It is hard to read, impossible to review, and accepts addresses like "jane doe"@[192.0.2.10] that no email service will deliver to. It also still can’t tell you whether the address exists. The practical pattern above trades those rare edge cases for clarity.
Test table: 25 tricky addresses
Each address was run through every pattern on this page. The six language versions of our pattern return identical results, shown in the “Our pattern” column. ✓ means the check accepts the address, ✗ that it rejects it; a check is right when it agrees with “Should pass?”.
| Address | Should pass? | Our pattern | Simple | HTML5 | PHP filter_var | Old pattern |
|---|---|---|---|---|---|---|
jane@example.com | Yes | ✓ | ✓ | ✓ | ✓ | ✓ |
jane.doe@ | Yes | ✓ | ✓ | ✓ | ✓ | ✓ |
jane+news@ | Yes | ✓ | ✓ | ✓ | ✓ | ✗ |
o'brien@ | Yes | ✓ | ✓ | ✓ | ✓ | ✗ |
jane_ | Yes | ✓ | ✓ | ✓ | ✓ | ✓ |
x@example.co | Yes | ✓ | ✓ | ✓ | ✓ | ✓ |
jane@ | Yes | ✓ | ✓ | ✓ | ✓ | ✗ |
JANE@EXAMPLE.COM | Yes | ✓ | ✓ | ✓ | ✓ | ✓ |
jane@ | Yes | ✓ | ✓ | ✓ | ✓ | ✗ |
jane@ | Yes | ✓ | ✓ | ✓ | ✓ | ✗ |
jane | No | ✗ | ✓ | ✓ | ✗ | ✓ |
.jane@ | No | ✗ | ✓ | ✓ | ✗ | ✓ |
jane..doe@ | No | ✗ | ✓ | ✓ | ✗ | ✓ |
jane@example | No | ✗ | ✗ | ✓ | ✗ | ✗ |
jane@-example.com | No | ✗ | ✓ | ✗ | ✗ | ✓ |
jane@ | No | ✗ | ✓ | ✗ | ✗ | ✓ |
jane@ | No | ✗ | ✓ | ✗ | ✗ | ✗ |
jane doe@example.com | No | ✗ | ✗ | ✗ | ✗ | ✗ |
jane@ | No | ✗ | ✗ | ✗ | ✗ | ✗ |
jane@example.c | No | ✗ | ✗ | ✓ | ✓ | ✗ |
"jane doe"@example.com | No (RFC-valid) | ✗ | ✗ | ✗ | ✗ | ✗ |
jane@[192 | No (RFC-valid) | ✗ | ✓ | ✗ | ✓ | ✗ |
josé@example.com | No (needs SMTPUTF8) | ✗ | ✓ | ✗ | ✗ | ✗ |
aaa…a@ (65 a’s) | No | ✗ | ✓ | ✓ | ✗ | ✓ |
jane@example.com + line break | No | ✗ | ✗ | ✗ | ✗ | ✗ |
What the columns show:
- Simple is
^[^\s@]+@[^\s@]+\. It only checks for an @ and a dot. It is fine as a first hint while someone types, but accepts double dots, underscores in domains and much more..[^\s@]{2,}$ - HTML5 is the browser’s
type="email"check shown above. It accepts dots at the edges of the local part, domains without a dot, one-letter top-level domains and over-long local parts. - PHP
filter_varisfilter_var($email, FILTER_VALIDATE_EMAIL). It accepts one-letter top-level domains and IP addresses as domains. - The old pattern (
^[a-z, which this page used to recommend) is too strict and too loose at the same time: it rejects plus tags, apostrophes and long top-level domains, and accepts double dots.A-Z0-9 ._ -]+@[a-z A-Z0-9 .-]+\ .[a-z A-Z]{2,6}$
Find email addresses in a text
To extract addresses from a block of text, drop the ^ and $ anchors and search for every match. A simpler pattern works better here, since you’ll validate what you find anyway:
const EMAIL_IN_TEXT = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g;
const text = "Write to jane.doe@example.com or sales@example.co.uk. Old address: (bob@old-domain.org)";
text.match(EMAIL_IN_TEXT); // ["jane.doe@example.com", "sales@example.co.uk", "bob@old-domain.org"]
import re
re.findall(r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}", text)
Both stop at punctuation after an address (the period after co.uk) and skip strings without a top-level domain such as name@localhost. Run the results through the validation pattern, or through the bulk email verifier to see which addresses actually work. To skip the code, paste the text into the free email extractor: it removes duplicates, sorts the addresses and filters them by domain.
Why regex is not email verification
A regex answers one question: is this string shaped like an email address? It can’t tell you whether:
- the domain exists and has mail servers (MX records),
- the mailbox exists on that server,
- the domain accepts every address (catch-all),
- the address is a disposable inbox that stops working in an hour.
jane.doe@example.com passes every pattern on this page and may still bounce. Use regex for instant feedback in the form, then check the address itself: with the free email checker by hand, or from your code with the email validation API, which runs the syntax, MX, mailbox and disposable checks in one request.