Email validation is one of those tasks that looks simple but has hidden depth. There is no perfect regex that catches every invalid email while accepting every valid one — but there are practical patterns that work well for the 99.9% of real-world inputs you will encounter. This guide explains what email validation can and cannot do, gives you code in four languages, and covers the mistakes that cause subtle bugs.
What makes an email address valid?
An email address has the format local@domain. The full specification (RFC 5322) allows an astonishing range of formats — quoted strings, comments in parentheses, even spaces inside quotes. In practice, almost all real-world email addresses follow a much simpler subset:
- Local part: letters, digits, dots, hyphens, underscores, plus signs — no consecutive dots, no leading or trailing dot
- @ symbol: exactly one, mandatory
- Domain: letters, digits, hyphens, at least one dot, a TLD of two or more characters
A pattern that covers this practical subset will accept every email your users will realistically type, while rejecting obvious typos.
The validation layers
There are three distinct levels of email validation, each catching different problems:
| Layer | What it checks | Example caught |
|---|---|---|
| Syntax (regex) | Format looks like an email | user@ (no domain) |
| DNS lookup | Domain exists, has MX records | user@thisdoesnotexist.xyz |
| SMTP probe | Mailbox accepts connections | nonexistent@gmail.com |
For most web forms, syntax validation is sufficient. DNS and SMTP checks add latency and complexity; use them only when delivery failures are very costly (transactional email, account confirmation flows).
A practical regex pattern
This pattern covers the practical subset of valid emails:
^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$
Breaking it down:
| Part | Pattern | Meaning |
|---|---|---|
| Local part | [a-zA-Z0-9._%+\-]+ |
One or more: letters, digits, . _ % + - |
| Separator | @ |
Literal at-sign |
| Domain | [a-zA-Z0-9.\-]+ |
Letters, digits, dots, hyphens |
| Dot | \. |
Literal dot before TLD |
| TLD | [a-zA-Z]{2,} |
Two or more letters (covers .io, .museum, .photography) |
| Anchors | ^ $ |
Must match the entire string |
This pattern intentionally rejects edge cases like IP-literal addresses (user@[192.168.1.1]) and quoted local parts ("user name"@example.com). These are technically valid per RFC 5322 but are never used in practice.
Code examples
JavaScript
// Browser and Node — covers async form validation
function isValidEmail(email) {
if (typeof email !== 'string') return false;
const trimmed = email.trim().toLowerCase();
const pattern = /^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,}$/;
return pattern.test(trimmed);
}
// Usage
console.log(isValidEmail('alice@example.com')); // true
console.log(isValidEmail('alice@example')); // false — no TLD
console.log(isValidEmail('alice @example.com')); // false — space
console.log(isValidEmail('alice+filter@sub.io')); // true — plus addressing
// HTML5 also provides built-in validation:
// <input type="email"> — browser handles basic format check
// Access programmatically: input.validity.valid
Python
import re
EMAIL_PATTERN = re.compile(
r'^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,}$',
re.IGNORECASE
)
def is_valid_email(email: str) -> bool:
if not isinstance(email, str):
return False
return bool(EMAIL_PATTERN.match(email.strip()))
# Usage
print(is_valid_email('alice@example.com')) # True
print(is_valid_email('alice@example')) # False
print(is_valid_email('alice..double@example.com')) # True — pattern allows it
# add extra check if needed
# For stricter validation, the email-validator package is battle-tested:
# pip install email-validator
# from email_validator import validate_email, EmailNotValidError
# try:
# valid = validate_email("alice@example.com")
# email = valid.email # normalised form
# except EmailNotValidError as e:
# print(str(e))
Go
package main
import (
"regexp"
"strings"
)
var emailPattern = regexp.MustCompile(
`(?i)^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,}$`,
)
func isValidEmail(email string) bool {
trimmed := strings.TrimSpace(email)
return emailPattern.MatchString(trimmed)
}
func main() {
tests := []string{
"alice@example.com", // valid
"alice+tag@sub.example.io", // valid — plus and subdomain
"alice@example", // invalid — no TLD
"@example.com", // invalid — empty local part
"alice @example.com", // invalid — space
}
for _, email := range tests {
println(email, "->", isValidEmail(email))
}
}
PHP
<?php
function isValidEmail(string $email): bool {
$trimmed = trim($email);
// PHP has a built-in filter — use it as the first check
if (filter_var($trimmed, FILTER_VALIDATE_EMAIL) === false) {
return false;
}
// filter_var handles RFC 5321 syntax; add TLD length check
$domain = substr(strrchr($trimmed, '@'), 1);
$tld = substr(strrchr($domain, '.'), 1);
return strlen($tld) >= 2;
}
// Usage
var_dump(isValidEmail('alice@example.com')); // bool(true)
var_dump(isValidEmail('alice@example')); // bool(false)
var_dump(isValidEmail('alice @example.com')); // bool(false) — space
// For DNS validation (optional):
// $domain = substr(strrchr($email, '@'), 1);
// $hasMX = checkdnsrr($domain, 'MX');
What regex cannot validate
Even a perfect regex cannot detect these problems:
- Domain does not exist —
user@thisdoesnotexist12345.compasses any syntax check - Mailbox does not exist —
nobody@gmail.comis syntactically valid but unreachable - Disposable email —
abc@mailinator.comis valid but often used for throwaway signups - Typos in real domains —
user@gmial.comlooks valid to any regex
For catching typos in popular domains, libraries like mailcheck.js suggest corrections ("Did you mean gmail.com?"). For blocking disposable addresses, maintain or subscribe to a blocklist.
Quick reference: common patterns by use case
| Use case | Recommended approach |
|---|---|
| Web form syntax check | Regex pattern above or type="email" in HTML |
| Python backend | email-validator package |
| PHP backend | FILTER_VALIDATE_EMAIL filter |
| Confirm delivery is possible | DNS MX record lookup |
| Block disposable addresses | Blocklist (e.g. disposable-email-domains) |
| Confirm the inbox exists | Send a confirmation email — only reliable method |
6 common mistakes
1. Not trimming whitespace. Users often paste email addresses with a leading or trailing space. Always trim() / strip() before validating. The examples above all do this.
2. Case-sensitive comparison. The domain part of an email is case-insensitive by spec. Store and compare emails lowercased to prevent Alice@Example.com and alice@example.com being treated as different accounts.
3. Trusting validation as deliverability proof. A valid format does not mean the email will be delivered. Build your registration flow to handle bounces.
4. Blocking legitimate emails with over-strict regex. Patterns that forbid + signs break plus-addressing (Alice uses alice+shopping@gmail.com to filter). Patterns that require exactly one dot in the domain break .io, .co, and similar TLDs. Use the permissive pattern above rather than inventing your own restrictive one.
5. Validating only on the client. Always re-validate on the server. Client-side validation is a UX convenience; it is not a security control.
6. Not re-validating before sending. If your system stores emails and sends to them later, re-validate the format before each send. Data quality degrades over time through manual edits, imports, and migrations.
FAQ
Q: What is the maximum length of an email address? RFC 5321 sets the maximum at 254 characters total (64 for the local part, 255 for the domain). In practice, check that the value fits in your database column and reject anything over 254 characters.
Q: Can I use HTML <input type="email"> instead of writing regex?
Yes, for client-side UX. Browsers implement their own email validation for type="email" inputs, which covers the common cases. You still need server-side validation — never rely solely on client-side checks.
Q: Should I send a verification email or validate with DNS? For critical flows (account creation, newsletter signup), always send a verification email. It is the only way to confirm that a human controls the inbox. DNS validation is a useful extra step but not a substitute for email confirmation.
Q: My regex rejects user.name+tag@example.co.uk — what's wrong?
The plus sign (+) and the two-dot TLD (.co.uk) are both valid. Make sure your character class includes + in the local part ([a-zA-Z0-9._%+\-]+) and that your domain pattern allows multiple dots ([a-zA-Z0-9.\-]+).
Q: Is user@localhost a valid email?
Technically yes per RFC 5321, for sending mail on a local network. For public web forms, you almost certainly want to reject it. Add a check that the domain contains at least one dot.
Q: How do I test an email validation regex interactively? Use Toolmingo's Regex Tester — paste the pattern and a list of test addresses, and see which match instantly in your browser with no data sent to a server.