Developer Guides
How to Validate an Email Address in PHP (filter_var, DNS, and Beyond)
The standard way to validate an email address in PHP is filter_var($email, FILTER_VALIDATE_EMAIL). It’s built in, needs no packages, and handles the syntax rules well. Pair it with a DNS check using getmxrr() to confirm the domain can receive mail, and send a confirmation email to prove the mailbox is real. Every example here was run on PHP 8.3.
filter_var with FILTER_VALIDATE_EMAIL
<?php
$email = trim($_POST['email'] ?? '');
if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
echo 'Please enter a valid email address.';
} else {
echo 'Looks good.';
}
filter_var() returns the filtered value on success and false on failure, so compare with === false. A loose ! check works too, but the strict comparison makes intent clear.
According to the PHP manual’s validation filters page, the filter validates against the addr-spec syntax from RFC 822, except that comments, whitespace folding, and dotless domain names are rejected. The same page carries a warning worth repeating: the only true way to confirm an address is valid and exists is to send mail to it.
What filter_var accepts and rejects
These results come from running PHP 8.3:
| Input | FILTER_VALIDATE_EMAIL |
|---|---|
user@example.com | valid |
User@Example.COM | valid |
a@b.c | valid |
user@xn--bcher-kva.de (punycode domain) | valid |
user@[192.0.2.1] | valid |
user@[IPv6:2001:db8::1] | valid |
user@localhost | invalid (dotless domain) |
us..er@example.com, .user@example.com | invalid |
"john doe"@example.com | invalid |
user(comment)@example.com | invalid |
ünïcode@example.com | invalid (valid with the Unicode flag) |
user@bücher.de | invalid, even with the Unicode flag |
" user@example.com" (leading space) | invalid |
| 65-character local part | invalid |
Three things to take from that table:
- Trim first. Whitespace makes the filter fail, and users paste addresses with trailing spaces all the time.
- IP literals pass.
user@[192.0.2.1]is legal under RFC 5321 section 4.1.3, but almost no real person signs up with one. Reject them explicitly if you don’t want them. - Unicode needs a flag, and only covers the local part.
FILTER_FLAG_EMAIL_UNICODE
Since PHP 7.1, FILTER_FLAG_EMAIL_UNICODE allows Unicode characters in the local part:
<?php
var_dump(filter_var('ünïcode@example.com', FILTER_VALIDATE_EMAIL)); // bool(false)
var_dump(filter_var('ünïcode@example.com', FILTER_VALIDATE_EMAIL, FILTER_FLAG_EMAIL_UNICODE)); // string(21) "ünïcode@example.com"
It does not accept a Unicode domain like bücher.de. To validate an internationalized domain, convert it to its ASCII (punycode) form first with idn_to_ascii() from the intl extension, then validate. Note that accepting a Unicode local part only helps if your mail provider supports SMTPUTF8; we cover the whole picture in internationalized email addresses.
<?php
// Requires the intl extension.
function to_ascii_email(string $email): ?string
{
$at = strrpos($email, '@');
if ($at === false) {
return null;
}
$domain = idn_to_ascii(substr($email, $at + 1));
return $domain === false ? null : substr($email, 0, $at + 1) . $domain;
}
$ascii = to_ascii_email('user@bücher.de'); // user@xn--bcher-kva.de
var_dump(filter_var($ascii, FILTER_VALIDATE_EMAIL)); // string(21) "user@xn--bcher-kva.de"
Since PHP 7.4, idn_to_ascii() defaults to the UTS #46 variant, per the manual, and it returns false on failure.
Don’t sanitize instead of validating
FILTER_SANITIZE_EMAIL strips characters that aren’t allowed in email addresses. It’s tempting to run it first, but it can silently turn bad input into a different, valid-looking address. If a user types jane doe@example.com, sanitizing gives janedoe@example.com, which isn’t what they meant. Trim whitespace at the ends, validate, and show an error when validation fails.
Checking the domain with getmxrr
Syntax alone accepts jane@gmail.con. To catch dead domains, ask DNS. The rules are in RFC 5321 section 5.1: look up MX records, and if there are none, treat the domain’s own A or AAAA record as an implicit MX. RFC 7505 adds the null MX, a single record with preference 0 and exchange ., which means the domain accepts no mail.
Here’s a complete function using only built-in functions:
<?php
declare(strict_types=1);
/**
* Returns ['ok' => bool, 'reason' => string, 'mx' => string[]].
*/
function validate_email(string $input): array
{
$email = trim($input);
if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
return ['ok' => false, 'reason' => 'bad_syntax', 'mx' => []];
}
$domain = strtolower(substr($email, strrpos($email, '@') + 1));
// filter_var accepts IP literals like user@[192.0.2.1]; most signup forms shouldn't.
if (str_starts_with($domain, '[')) {
return ['ok' => false, 'reason' => 'ip_literal', 'mx' => []];
}
$fqdn = $domain . '.'; // trailing dot: don't let the resolver append a search domain
$hosts = [];
$weights = [];
if (getmxrr($fqdn, $hosts, $weights)) {
array_multisort($weights, SORT_ASC, SORT_NUMERIC, $hosts);
// RFC 7505 null MX: a single "0 ." record means the domain accepts no mail.
if (count($hosts) === 1 && in_array($hosts[0], ['', '.'], true)) {
return ['ok' => false, 'reason' => 'null_mx', 'mx' => []];
}
return ['ok' => true, 'reason' => 'mx_found', 'mx' => $hosts];
}
// No MX: RFC 5321 section 5.1 falls back to the domain's own A/AAAA record.
if (checkdnsrr($fqdn, 'A') || checkdnsrr($fqdn, 'AAAA')) {
return ['ok' => true, 'reason' => 'implicit_mx', 'mx' => [$domain]];
}
// getmxrr/checkdnsrr return false for "no records", "no such domain", and DNS errors alike.
return ['ok' => false, 'reason' => 'no_mail_server', 'mx' => []];
}
foreach (array_slice($argv, 1) as $address) {
echo $address, ' => ', json_encode(validate_email($address)), PHP_EOL;
}
Run it from the command line:
php validate.php someone@gmail.com someone@example.com x@github.io
someone@gmail.com => {"ok":true,"reason":"mx_found","mx":["gmail-smtp-in.l.google.com","alt1.gmail-smtp-in.l.google.com",...]}
someone@example.com => {"ok":false,"reason":"null_mx","mx":[]}
x@github.io => {"ok":true,"reason":"implicit_mx","mx":["github.io"]}
Notes on the details:
getmxrr()doesn’t sort. In testing it returned Gmail’s hosts in arbitrary order with their weights in a parallel array.array_multisort()sorts both by preference.- The trailing dot matters. A user note on the
checkdnsrr()manual page points out that without it, the system resolver may treat the name as relative and append a local search domain, which can make a nonexistent domain appear to resolve. falseis ambiguous.getmxrr()andcheckdnsrr()returnfalsewhether the domain doesn’t exist, has no records, or the lookup failed. If you need to tell a timeout apart from “no such domain” (and you should, so a flaky resolver doesn’t reject real users), use a DNS library or a verification API that reports the difference.- These calls block. Each lookup holds up the request until the resolver answers. On signup forms, that’s usually acceptable; for list imports, run them in a background job.
For background on how MX records route mail, see what an MX record is.
Should you use a regex in PHP instead?
filter_var() is better maintained than anything you’ll write by hand, so start there. A regex makes sense when you need the exact same rule in JavaScript and PHP, so both sides agree. If you do, add the D modifier: without it, PCRE’s $ also matches before a trailing newline, so "user@example.com\n" would pass. Our email validation regex guide has a tested pattern with a PHP version.
Frameworks
Laravel and Symfony both ship email validation rules with options for stricter RFC checks and DNS lookups. Check your framework version’s documentation for the available modes. They still sit at the same two layers covered here, syntax and DNS, and none of them prove a mailbox exists.
What PHP validation can’t tell you
After filter_var() and an MX check, you still don’t know whether:
- The mailbox exists. Checking it means an SMTP conversation with the recipient’s server, which many providers don’t answer reliably, and cloud hosts commonly block outbound port 25. See how SMTP email verification works.
- It’s a typo of a real address.
gmial.commay well have DNS records. See common email typos. - It’s a disposable inbox. Those have valid MX records by design.
- The person owns it. Only a confirmation link proves that.
Calling a verification API from PHP
The API on this site runs syntax, MX, disposable, role, free-provider, and typo checks in one request. Using the curl extension:
<?php
declare(strict_types=1);
const VERIFIER_BASE = 'https://bookmyspot.us/tools/email-verifier/api/v1';
function verify_email_api(string $email): array
{
$ch = curl_init(VERIFIER_BASE . '/verify?' . http_build_query(['email' => $email]));
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['X-API-Key: ' . getenv('VERIFIER_API_KEY')],
CURLOPT_CONNECTTIMEOUT => 3,
CURLOPT_TIMEOUT => 10,
]);
$body = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if ($body === false || $code !== 200) {
throw new RuntimeException("Verifier request failed (HTTP $code)");
}
return json_decode($body, true, flags: JSON_THROW_ON_ERROR);
}
$result = verify_email_api('jane@gmial.com');
echo $result['status'], ' ', $result['reason'], ' ', $result['suggestion'] ?? 'no suggestion', PHP_EOL;
// status is "valid", "risky", or "invalid"
The response includes status, reason, checks (syntax, mx, disposable, role, free_provider), the mx host list, and a typo suggestion. It does not test mailboxes over SMTP. Free keys with a daily limit are available on the API documentation page. The email verification API guide covers bulk requests and how to act on each status, and you can try any address by hand in the free email verifier.
FAQ
Is filter_var enough to validate an email in PHP?
For syntax, yes, as long as you trim the input first and decide whether to allow IP-literal domains, which it accepts. It doesn’t check that the domain has mail servers or that the mailbox exists, so add a DNS check and a confirmation email.
Does FILTER_VALIDATE_EMAIL support international characters?
Only in the local part, and only with FILTER_FLAG_EMAIL_UNICODE (PHP 7.1 and later). Unicode domains are rejected even with the flag, so convert the domain with idn_to_ascii() before validating.
How do I check if an email domain exists in PHP?
Call getmxrr() on the domain with a trailing dot. If it returns no MX records, fall back to checkdnsrr() for A and AAAA records, since RFC 5321 allows delivery to a domain’s own address when no MX exists. Also treat a single MX of . as a null MX, meaning the domain accepts no mail.
Why does filter_var reject user@localhost?
The PHP manual states that dotless domain names are not supported by the filter. That’s usually what you want on a public form. For internal tools that need dotless hosts, use a custom check instead.
Free email verifier: syntax, MX, disposable and role-based addresses, typos. Paste up to 100 at once.