Email Validation Regex: Patterns for JavaScript, Python and PHP

email validationEmail Validation Regex: Patterns for JavaScript, Python and PHP

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:

  1. Pick a pattern: a practical one that only checks the shape, or a stricter one that also checks dots and domain labels.
  2. Use it in your language, after trimming spaces, and add a length check.
  3. Avoid the common mistakes that reject real customers, such as blocking + or long top-level domains.
  4. Verify the address itself with an API call, because a correctly formatted address can still bounce.

For the format rules behind the patterns (local part, domain labels, length limits), see valid email address format rules and examples.

Email validation regex: a practical pattern and a strict one

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

Practical pattern (simple email regex)

^[^\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.

Stricter pattern

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.

Why not the full RFC 5322 regex

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.

JavaScript email regex (with HTML 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);

Email regex in Python (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

Validate email in PHP: filter_var vs preg_match

In 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.

Common mistakes that reject real addresses

If your email regex rejects valid addresses, it's usually one of these:

  • Rejecting + tags. user+tag@example.com is a valid address, and many people use tags to sort mail.
  • Capping the top-level domain at 3 or 4 letters, as in [a-z]{2,4}. Domains like .photography are longer.
  • Matching lowercase only. People type John.Smith@Example.com. Allow both cases in the pattern (or use a case-insensitive flag).
  • Not trimming spaces. A space pasted before or after the address fails the check. Trim first, then validate.
  • Rejecting non-ASCII addresses without saying why. The strict pattern rejects 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.
  • No length check. A regex doesn't enforce the 64-character local part or the 254-character address. Check the length in code, as in the snippets above.
  • One giant pattern. Trim, length check, a readable pattern and then a mailbox check are easier to maintain than a single regex that tries to do everything.
  • Validating on the client only. Anyone can skip the browser check, so repeat it on the server.

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

Where regex stops and an API check starts

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.
  • A disposable email address, which often stops working after a short time.
  • An address on a catch-all domain, where the server accepts mail for any address, so a single mailbox can't be confirmed.

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.

Check it works

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.

Troubleshooting

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.

FAQ

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.

Next step: check the mailbox, not just the format

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).

Join 1,000+ CompaniesImproving Email Deliverability

Start with 200 free validations. Upgrade only when you're ready.

No credit card required • Cancel anytime