For email validation in PHP, use filter_var($email, FILTER_VALIDATE_EMAIL) for syntax, convert the domain part to punycode with idn_to_ascii() first, and check via DNS that the domain accepts mail: an MX or A record and no null MX. Whether the mailbox exists can only be confirmed by sending a confirmation email.
Nearly every PHP application has an email field, and nearly every one validates it differently. I’ve seen patterns that reject [email protected], checks that cap TLDs at four letters and quietly break .email, and forms that turn away every address on an internationalised domain. A solid check isn’t hard to build once you know where PHP’s built-ins stop.
What filter_var() covers in PHP email validation
filter_var($email, FILTER_VALIDATE_EMAIL) checks syntax and returns the address or false. That handles what real users type almost all of the time. Its limits are worth knowing, though. Ten cases on PHP 8.5, plain filter_var() on the left, the extended function from the next section on the right:
| Input | filter_var() | validateEmail() |
|---|---|---|
[email protected] | valid | valid |
[email protected] | valid | valid |
jörg@müller.de | invalid | valid, as jö[email protected] |
user@localhost | invalid | rejected |
a@b | invalid | rejected |
[email protected] | valid | rejected (domain doesn’t exist) |
"john doe"@example.com | invalid | rejected |
ü[email protected] | invalid | rejected (null MX) |
[email protected] | invalid | rejected |
[email protected]. | invalid | rejected |
Two rows stand out. [email protected] is syntactically perfect, but the domain doesn’t exist. And example.com publishes a null MX, meaning it explicitly accepts no mail. filter_var() catches neither, because it only looks at characters.
Internationalised domains need a punycode step
filter_var() doesn’t understand IDNs, so jörg@müller.de fails even though the domain exists and receives mail. Split off the domain, convert it with idn_to_ascii(), then validate. FILTER_FLAG_EMAIL_UNICODE additionally allows UTF-8 in the local part.
Split at the last @, not the first. Quoted local parts may contain an @; domains never do. Conversion details, including the ß trap, are in the punycode guide.
A complete validation function with a DNS check
<?php
declare(strict_types=1);
function acceptsMail(string $asciiDomain): bool
{
$mx = @dns_get_record($asciiDomain, DNS_MX);
if ($mx === false) {
return true; // DNS trouble: don't reject on uncertainty
}
if ($mx !== []) {
// null MX (RFC 7505): exactly one record with an empty target
return (count($mx) === 1 && $mx[0]['target'] === '') === false;
}
return checkdnsrr($asciiDomain, 'A') || checkdnsrr($asciiDomain, 'AAAA');
}
function validateEmail(string $input, bool $checkDns = true): ?string
{
$email = trim($input);
$at = strrpos($email, '@');
if ($at === false || strlen($email) > 254) {
return null;
}
$local = substr($email, 0, $at);
$domain = substr($email, $at + 1);
// punycode the domain so jörg@müller.de can be validated
$asciiDomain = idn_to_ascii($domain, IDNA_NONTRANSITIONAL_TO_ASCII | IDNA_USE_STD3_RULES, INTL_IDNA_VARIANT_UTS46);
if ($asciiDomain === false) {
return null;
}
$normalized = $local . '@' . $asciiDomain;
if (filter_var($normalized, FILTER_VALIDATE_EMAIL, FILTER_FLAG_EMAIL_UNICODE) === false) {
return null;
}
if ($checkDns && acceptsMail($asciiDomain) === false) {
return null;
}
return $normalized;
}
It returns the normalised address rather than true, so you always store the same spelling of the domain and avoid duplicate accounts for the Unicode and punycode forms.
On a DNS failure, acceptsMail() lets the address through on purpose. A brief resolver outage shouldn’t block signups; the confirmation email catches genuine problems later. If you’d rather be strict, return false there. Why the null MX check is needed and how the A-record fallback works is covered in checking MX records in PHP.
Stricter checks with egulias/email-validator
For closer RFC compliance, egulias/email-validator is the standard package; Laravel and Symfony build on it. Validations can be combined:
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use Egulias\EmailValidator\EmailValidator;
use Egulias\EmailValidator\Validation\DNSCheckValidation;
use Egulias\EmailValidator\Validation\MultipleValidationWithAnd;
use Egulias\EmailValidator\Validation\RFCValidation;
$validator = new EmailValidator();
$rules = new MultipleValidationWithAnd([new RFCValidation(), new DNSCheckValidation()]);
foreach (['[email protected]', 'jörg@müller.de', '"john doe"@php.net', '[email protected]', '[email protected]'] as $email) {
$ok = $validator->isValid($email, $rules);
printf("%-34s %s\n", $email, $ok ? 'valid' : 'invalid: ' . str_replace("\n", ' | ', trim((string) $validator->getError()?->description())));
}
With version 4.0.4:
[email protected] valid
jörg@müller.de valid
"john doe"@php.net valid
[email protected] invalid: No MX or A DNS record was found for this email
[email protected] invalid: No MX or A DNS record was found for this email | Domain accepts no mail (Null MX, RFC7505)
Two differences from filter_var(): the package accepts IDN domains without your own conversion, and it allows a quoted local part with a space, which RFC 5322 permits. Whether you want those in your database is a product decision. Some downstream systems, CRMs and mailing tools among them, choke on quoted addresses.
Normalising without changing meaning
The domain part is case-insensitive (RFC 4343), so lowercase it freely. The local part is, strictly speaking, case-sensitive under RFC 5321. Hardly any server treats Jane@ and jane@ differently, but keep the user’s spelling for delivery and use a separate lowercased column for login and duplicate checks.
Avoid aggressive “cleanup”. Removing dots or cutting everything after + matches one provider’s behaviour and breaks others, where jane.doe@ and janedoe@ are different people. Plus addressing is also a deliberate tool: someone signing up as jane+shop@ wants to know where mail comes from. Rejecting the plus sign annoys exactly the users who think hardest about their address.
What no validation can tell you
None of this proves the mailbox exists or that anyone reads it. Typos like gmial.com pass if the typo domain accepts mail, and some do. What’s left:
- a confirmation email with a link, the only real proof
- optionally a “did you mean gmail.com?” hint for common typos, without rejecting input
- no SMTP
RCPT TOprobing, which is unreliable and looks like harvesting
Length limits are fixed: 254 characters for the full address, 64 for the local part. The function above checks the first; filter_var() enforces the second.
A word on the browser side: <input type="email"> applies the WHATWG definition, which is deliberately simpler than RFC 5322 and rejects quoted local parts outright. That’s fine as a first hint for users, but it disagrees with egulias on edge cases, so never treat the browser’s verdict as the final one. Validate again on the server with the same rules you use everywhere else, and make sure the error messages from both layers say the same thing.
Syntax checks belong in every form; DNS checks belong in the server-side request after submission, since dns_get_record() has no timeout. The domain logic is reusable for plain domain fields, see validating domain names with regex.
Looking for a mail domain of your own? The betadomain search shows instantly whether a name is free. An .email domain costs €29.99 a year, a .com €14.99.
Found your domain? You can check and register it directly at buy a domain – yearly price from €2.99.
FAQ
Is FILTER_VALIDATE_EMAIL reliable?
For nearly all real-world addresses, yes. It rejects a few exotic but valid forms, such as quoted local parts containing spaces, and doesn't handle internationalised domains without help. For signup forms that trade-off is usually fine.
Should I verify mailboxes over SMTP?
No. Many servers accept any address at RCPT TO or use greylisting, so the result is unreliable. Doing it at volume also looks like address harvesting and gets your IP blocklisted. A confirmation link is the only clean method.
Should I write my own email regex?
Usually not. The full RFC 5322 grammar is too complex for a readable regex, and hand-written patterns tend to reject valid addresses like plus addressing or long TLDs. filter_var() or egulias/email-validator are better tested.
What is egulias/email-validator?
A Composer package that validates addresses strictly against the RFCs, with optional DNS checks. It detects null MX (RFC 7505) and gives readable error descriptions. Laravel and Symfony use it for their email rules.