# How to validate an email address in PHP

_2026-07-22 · Boundstone (https://boundstone.io/blog/validate-email-php)_


You typed a query into a search box that reads something like "validate email in PHP," and you are hoping the answer is not a 40-character regex someone pasted from a forum in 2009. Good news: it is not. PHP ships with the right tool built in, and for the syntax half of the job you need zero dependencies. The other half — does this address point at a domain that can actually receive mail, and is it a throwaway — takes one more built-in and, if you want it, one HTTP call. Let's do all three, honestly, and be clear about where each one stops.

## Use `filter_var`, not a regex

The single most common mistake is writing your own pattern. Don't. Email syntax is defined by RFC 5322, the grammar is genuinely awful, and PHP already implements a pragmatic version of it in C:

```php
$email = 'you@example.com';

if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
    // Not a syntactically valid address. Reject it.
    exit('Bad email format.');
}
```

`FILTER_VALIDATE_EMAIL` returns the address on success and `false` on failure, so a strict `=== false` check is the safe read. This is the right built-in. It catches the missing `@`, the double dots, the trailing space, the whole class of typos that a naive regex either misses or over-rejects. If all you need is "did the user fill this field in with something shaped like an email," you are already done — ship it.

What `filter_var` does not tell you: whether the domain exists, whether it accepts mail, or whether `you@example.com` is a real inbox versus a plausible-looking string. Syntax and reachability are different questions.

## Add an MX check with `checkdnsrr`

To find out whether the domain can receive mail at all, pull the domain off the address and ask DNS whether it publishes MX records:

```php
$domain = substr(strrchr($email, '@'), 1);

if (checkdnsrr($domain, 'MX') === false) {
    // Domain publishes no MX records — it can't receive mail.
    exit('That domain does not accept email.');
}
```

`checkdnsrr($domain, 'MX')` returns `true` if any MX record exists and `false` otherwise. It performs a live DNS lookup, so it costs a network round trip — do it after the syntax check has already thrown out the garbage, and expect it to occasionally be slow or to time out on a flaky resolver. If you want the actual mail hosts rather than a boolean, `getmxrr($domain, $hosts)` fills an array by reference. Either way, you now know two real things: the address is well-formed, and its domain can, in principle, take mail.

Between `filter_var` and `checkdnsrr` you have covered syntax and MX with nothing but the standard library. For a contact form, that is a completely reasonable place to stop.

## Where the built-ins stop

Here is what neither built-in can tell you. Is `user@mailinator.com` a disposable address that will evaporate in an hour? Is `info@` a role account that goes to a shared mailbox nobody reads? Those judgments need a maintained list of throwaway domains and a list of role prefixes — data that goes stale and is not shipping inside PHP. You can hand-roll and hand-maintain both. Most people would rather not.

There is also the question of a *live* mailbox. Neither `filter_var` nor `checkdnsrr` opens an SMTP conversation to confirm the specific inbox exists, and honestly, you should be wary of anything that claims to — mailbox-level SMTP probing is unreliable, rate-limited, and often blocked outright.

## The layer past: disposable and role, one contract

This is where an API earns its place — not by doing anything magic, but by folding the disposable list, the role list, and a live MX check into one response you get the same way from every language. Here is the call in plain PHP with cURL:

```php
$ch = curl_init('https://api.boundstone.io/v1/verify/email');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer bs_live_YOUR_KEY',
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS     => json_encode(['email' => $email]),
]);

$response = curl_exec($ch);
curl_close($ch);

$result = json_decode($response, true);
```

The response gives you `valid_syntax`, `domain`, `mx_found`, `disposable`, `role_account`, `free_provider`, and a `checks` object. So you can act on the parts the built-ins can't reach:

```php
if ($result['disposable']) {
    exit('Disposable address — please use a permanent inbox.');
}

if ($result['role_account']) {
    // Warn, don't block: role accounts are real, just shared.
    $warn = 'That looks like a shared inbox (info@, sales@).';
}
```

One note so nothing surprises you: `free_provider` is returned as a flag — `true` for a gmail.com or outlook.com address — but it is *not* one of the checks the API counts as performed. It is context, not a verdict. Blocking free providers is almost always the wrong call.

## Read the `checks` arrays before you trust `valid`

The reason to trust a "valid" from this API is that it tells you exactly what "valid" covered. For a well-formed address, the response reports:

```json
"checks": {
  "performed":     ["syntax", "mx", "disposable_list", "role_list"],
  "not_performed": ["smtp_mailbox", "catch_all"]
}
```

Read `not_performed` as a feature, not fine print. `smtp_mailbox` and `catch_all` are listed because the API did **not** open an SMTP session to confirm the specific inbox exists, and did **not** detect whether the domain accepts every address (catch-all). Boundstone does not do those today, and says so in the response rather than implying a green checkmark means "this human reads mail here." A "valid" you can audit beats a "valid" you have to take on faith.

## The short version

- Validating email syntax in PHP? Use `filter_var($email, FILTER_VALIDATE_EMAIL)`. Never a regex.
- **Want to know the domain can receive mail?** Add `checkdnsrr($domain, 'MX')`. Two built-ins, zero dependencies — genuinely enough for many forms.
- **Need disposable and role detection too?** That is the API's job — one contract across your stack, with a `checks.not_performed` list telling you the exact edges of the answer.

Kick the tires with no signup at the [free email validator](/tools/email-validator), see the same pattern in [Python](/blog/validate-email-python), or read the field-by-field contract in the [docs](/docs).
