Newsletter

Email regex: validate an email address in JavaScript, PHP and HTML

An email regex ready to copy, tested on real cases, with the JavaScript function, the PHP equivalent and the HTML attribute: what it checks, what it lets through, and how to go further.

Email regex: validate an email address in JavaScript and PHP
Quick answer

To validate the format of an email address, test it against the regex below (test() in JavaScript, preg_match() in PHP). It accepts prenom.nom+tag@sous.domaine.fr and rejects double dots, spaces and domains without a top-level domain. It does not guarantee that the mailbox exists: for that, you have to send a message or query the domain.

An email regex does one thing: it checks that a string has the shape of an email address. That is not much, and it is indispensable in every sign-up, contact or order form. Here is the regular expression I have used for years, with its JavaScript, PHP and HTML versions, and above all a detailed look at what it accepts and what it rejects.

The email regex, ready to copy

javascript
^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$

Read from left to right:

  • ^ and $: the whole string has to be an address, not merely contain one.
  • [^<>()[\]\\.,;:\s@"]+: the local part (before the @) is made of permitted characters, with no spaces or reserved punctuation. Groups separated by a dot (prenom.nom) are accepted; a double dot is not.
  • |(".+"): the quoted alternative, provided for by the standard, allows "jean dupont"@example.com.
  • @, then the domain: either an IP address in square brackets ([192.168.1.1]), or alphanumeric labels separated by dots and ending in a top-level domain of at least two letters.
History

In October 2022 this rule replaced a version whose backslashes had gone missing when the site was migrated in 2021. If you copied the old one, replace it: it let malformed addresses through.

What the regex checks, and what it does not

The table below is the actual result of test() under Node.js 22 and of preg_match() under PHP 8.5 on sixteen addresses. Both engines return the same verdict.

Address tested Result Why
jean.dupont@example.com accepted the standard case
jean+news@sub.example.co.uk accepted the + and subdomains are legitimate
JEAN@EXAMPLE.COM accepted the i flag ignores case
"jean dupont"@example.com accepted quoted local part, provided for by RFC 5322
jean@[192.168.1.1] accepted domain given as an IP address
jéan@exemple.fr accepted the local part accepts non-ASCII characters
jean@localhost rejected no top-level domain after the dot
jean@example rejected same reason
jean..dupont@example.com rejected double dot in the local part
jean@exa_mple.com rejected underscores are not allowed in a domain name
jean@exemple.café rejected accented top-level domain: see the limits
a@b.c rejected single-letter top-level domain
@example.com rejected empty local part
jean@.com rejected empty domain before the top-level domain
jean dupont@example.com rejected space
jean@example.com (trailing space) rejected the $ anchor rejects any character after the top-level domain: remember to trim()

What no regex can check: that the domain exists, that it accepts mail, and that the mailbox is active. personne@gmail.com has a perfect shape and most likely receives nothing.

JavaScript version

The function takes the address as a parameter, strips the surrounding whitespace and returns true or false. The i flag makes the match case-insensitive.

validateEmail.js
/**
 * Checks the format of an email address.
 * @param {string} email
 * @returns {boolean}
 */
function validateEmail(email) {
  const emailReg = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/i;
  return emailReg.test(String(email).trim());
}

console.log(validateEmail('jean.dupont@example.com')); // true
console.log(validateEmail('jean..dupont@example.com')); // false
console.log(validateEmail(' jean@example.com '));      // true, thanks to trim

In a form

The typical use case: block submission and show a message for as long as the address is malformed.

formulaire.js
const form = document.querySelector('#inscription');
const champ = document.querySelector('#email');
const erreur = document.querySelector('#email-erreur');

form.addEventListener('submit', (event) => {
  if (!validateEmail(champ.value)) {
    event.preventDefault();
    erreur.textContent = 'Adresse e-mail invalide';
    champ.setAttribute('aria-invalid', 'true');
    champ.focus();
  }
});

The error field is a visible element next to the input, not an alert() box: the user keeps the context and can correct the address straight away.

PHP version

Client-side validation is an aid to data entry, not a security measure. The server has to run the check again: JavaScript can be disabled, and a form can be called directly. The same regex works with preg_match(), give or take the quotes.

isValidEmail.php
/**
 * Checks the format of an email address.
 */
function isValidEmail(string $email): bool
{
    $pattern = '/^(([^<>()[\]\\\\.,;:\s@"]+(\.[^<>()[\]\\\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/i';

    return preg_match($pattern, trim($email)) === 1;
}

var_dump(isValidEmail('jean.dupont@example.com')); // bool(true)
var_dump(isValidEmail('jean@localhost'));          // bool(false)

Note the double escaping of the backslashes in the single-quoted PHP string: \\\\ in the code produces \\ in the pattern, which means “a literal backslash” to the regex engine.

Regex or filter_var?

PHP provides filter_var($email, FILTER_VALIDATE_EMAIL), which follows the standard more closely. Both approaches are valid; they do not rule in quite the same way:

Address This article’s regex filter_var
a@b.c rejected accepted
"jean dupont"@example.com accepted rejected
jéan@exemple.fr accepted rejected without FILTER_FLAG_EMAIL_UNICODE, accepted with it
jean+news@sub.example.co.uk accepted accepted

For a public form, filter_var with the Unicode flag is a sound default; the regex remains useful when you want exactly the same rule in JavaScript and in PHP.

HTML version

The browser already knows how to validate an email: type="email" refuses to submit the form if the format is wrong, and displays its own message. The pattern attribute lets you add a stricter rule, this article’s for instance.

formulaire.html
<label for="email">Adresse e-mail</label>
<input
  id="email"
  name="email"
  type="email"
  required
  autocomplete="email"
  pattern="(([^<>\(\)\[\]\\.,;:\s@&quot;]+(\.[^<>\(\)\[\]\\.,;:\s@&quot;]+)*)|(&quot;.+&quot;))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))"
  title="Une adresse de la forme prenom.nom@domaine.fr"
>

Four details: pattern is implicitly anchored, so you must not write ^ and $; the quotes in the pattern are written &quot; so as not to close the attribute; browsers compile pattern with the v flag, which requires parentheses and square brackets inside a character class to be escaped (\(\)\[\]), otherwise the pattern is silently ignored; and HTML validation can be bypassed with one line in the console. It improves the user experience; it does not replace the server-side check.

Edge cases worth knowing

  • The +: jean+boutique@gmail.com is a valid address that many people use to sort their mail. A regex that rejects it loses sign-ups.
  • Long top-level domains: .photography, .paris and .technology exist. That is why the pattern ends in [a-zA-Z]{2,} rather than a closed list of extensions.
  • Accents: the local part accepts them; the domain does not in this regex (internationalised domain names are actually written in Punycode, xn--…, before being resolved).
  • Surrounding whitespace: copying and pasting from a spreadsheet often adds a trailing space. Call trim() before the test, as both functions above do.
  • Case: the domain is case-insensitive; the local part is case-sensitive in theory, but no mainstream provider makes the distinction. Comparing in lower case is the expected behaviour.

Testing the regex before deploying it

A test set beats proofreading. In JavaScript, a loop over known addresses is enough:

test.js
const cas = {
  'jean.dupont@example.com': true,
  'jean+news@sub.example.co.uk': true,
  'jean@localhost': false,
  'jean..dupont@example.com': false,
  'jean dupont@example.com': false,
};

for (const [email, attendu] of Object.entries(cas)) {
  const ok = validateEmail(email) === attendu;
  console.log(`${ok ? 'OK ' : 'KO '} ${email}`);
}

To explore a pattern, regex101 colours each group and explains what it captures. Pick the ECMAScript flavour for JavaScript and PCRE2 for PHP: both accept this regex without modification.

Going further: checking that the address exists

Once the shape is right, three further checks cut down on fake addresses, from the cheapest to the most reliable:

  1. Reject disposable domains (temporary addresses created to get around a sign-up) using a maintained list.
  2. Query the domain’s MX records on the server: a domain with no mail server will never receive a message. In PHP, checkdnsrr($domaine, 'MX') answers in one line.
  3. Send a confirmation email with a link to click. It is the only proof that someone reads that mailbox, and it is what every serious service does.

The first two checks are the ones I run on Foliade’s public forms; the third remains the reference as soon as an account is created.

The other validation regexes

Same approach, other formats: validating a date, an IP address, a postcode or a password.

Common errors

Forgetting the ^ and $ anchors Without them, test() accepts “bonjour jean@example.com merci” because a valid address appears somewhere in the string. The function in this article anchors the regex at both ends.
Validating on the client side only JavaScript can be disabled and bypassed. The same check has to exist on the server, in PHP here, before anything is saved or sent.
Assuming that a valid address exists The regex checks a shape, not a mailbox. “nimportequoi@gmail.com” passes. Only a confirmation email proves that the address receives mail.
Rejecting legitimate addresses Over-strict regexes reject the “+” (jean+newsletter@…) or new long top-level domains such as .photography. Always test with real addresses before tightening the rule.

JavaScriptPHP

Damien Flandrin Web developer since 2010, creator of Gekkode and Email Impact. Every article is tested on a real project before publication. Contact
Newsletter

New tests, tutorials and projects, by e-mail.

Reproducible tests, versioned code, dated results. Never any spam.