X Xerobit

Regex Email Validation — Patterns, Edge Cases, and Best Practices

Email validation regex needs to balance strictness with real-world email formats. Learn why the perfect email regex is impossible, pragmatic patterns that work, RFC 5322...

Mian Ali Khalid · · 4 min read
Use the tool
Regex Tester
Test regular expressions with live match highlighting and explanation.
Open Regex Tester →

Email validation regex is famously tricky — the RFC 5322 specification for valid email addresses is more complex than most developers realize. Here’s a pragmatic approach that catches obvious mistakes without blocking valid addresses.

Use the Regex Tester to test email validation patterns.

Why “perfect” email regex is impractical

The full RFC 5322 valid local-part can contain:

  • Quoted strings: "user name"@example.com
  • Comments: user(comment)@example.com
  • IP addresses: user@[192.168.1.1]
  • Internationalized domain names: user@münchen.de

A regex that handles all of this is 6KB long and nearly unmaintainable. For practical use, reject the theoretical edge cases.

Pragmatic email regex

// Covers 99.9% of real email addresses:
const EMAIL_REGEX = /^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$/;

EMAIL_REGEX.test('user@example.com')          // true
EMAIL_REGEX.test('user.name+tag@example.co.uk') // true
EMAIL_REGEX.test('user@subdomain.example.com')  // true
EMAIL_REGEX.test('@example.com')              // false (no local part)
EMAIL_REGEX.test('user@')                     // false (no domain)
EMAIL_REGEX.test('user@.com')                 // false (domain starts with dot)
EMAIL_REGEX.test('user @example.com')         // false (space)
EMAIL_REGEX.test('user@example.c')            // false (TLD too short)

What it misses (intentional trade-offs)

// These are valid per RFC 5322 but rejected by the pragmatic regex:
'user name@example.com'    // Space in unquoted local part
'"user name"@example.com'  // Quoted local part
'user@[192.168.1.1]'       // IP address literal
'user@localhost'           // No TLD (valid in some internal systems)

// Usually you WANT to reject these for web forms
// Exception: if building email servers or strict RFC compliance

More permissive pattern (fewer false negatives)

// Looser — accepts more edge cases:
const LOOSE_EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

LOOSE_EMAIL.test('user@example.com')     // true
LOOSE_EMAIL.test('"user"@example.com')   // true (quoted local part)
LOOSE_EMAIL.test('user@localhost')       // true (no TLD required)

HTML5 email input validation

The browser’s built-in email validation uses a simplified RFC 5321 pattern:

<!-- Browser validates format on submit: -->
<input type="email" name="email" required>

<!-- Custom pattern: -->
<input type="email" name="email"
  pattern="[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}"
  title="Enter a valid email address">

The browser pattern is intentionally lenient to accommodate international addresses.

JavaScript form validation

function validateEmail(email) {
  const regex = /^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$/;
  
  if (!email) return { valid: false, error: 'Email is required' };
  if (!regex.test(email)) return { valid: false, error: 'Invalid email format' };
  if (email.length > 254) return { valid: false, error: 'Email too long' };
  
  const [local, domain] = email.split('@');
  if (local.length > 64) return { valid: false, error: 'Local part too long' };
  
  return { valid: true };
}

Python email validation

import re

EMAIL_PATTERN = re.compile(
    r'^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$'
)

def is_valid_email(email: str) -> bool:
    if not email or len(email) > 254:
        return False
    return bool(EMAIL_PATTERN.match(email))

# For production: use email-validator library
# pip install email-validator
from email_validator import validate_email, EmailNotValidError

def validate_email_address(email: str) -> str:
    """Validates and normalizes an email address."""
    try:
        # Validates format AND DNS MX record
        valid = validate_email(email, check_deliverability=True)
        return valid.email  # Normalized email
    except EmailNotValidError as e:
        raise ValueError(str(e))

Server-side: the definitive check

Regex catches format errors. For definitive validation:

// Check DNS MX records (server-side only):
import dns from 'dns/promises';

async function isDeliverableEmail(email) {
  const domain = email.split('@')[1];
  try {
    const records = await dns.resolveMx(domain);
    return records.length > 0;
  } catch {
    return false;  // Domain has no MX records
  }
}

// But the ONLY definitive check: send a confirmation email
// If they click the link, the email is valid and they own it

Related posts

Related tool

Regex Tester

Test regular expressions with live match highlighting and explanation.

Written by Mian Ali Khalid. Part of the Dev Productivity pillar.