# How to check if an e-mail address exists without sending an e-mail

> What syntax, MX, disposable-domain and mailbox checks can tell you without sending mail, why SMTP probing gives false answers, and tested code in Node, Python and PHP.

Canonical: https://mobilevalidate.com/blog/check-if-an-email-address-exists-without-sending · Last updated: 2026-11-05

![A ladder diagram between you, DNS and the mail server: an MX lookup, HELO answered 250, RCPT TO answered 250, 252 or 4XX as unknown, no DATA sent, beside syntax, domain, disposable and mailbox checks.](https://mobilevalidate.com/images/blog/check-if-an-email-address-exists-without-sending.svg)

*Syntax, domain and disposable checks work without sending; SMTP probes often can't confirm the mailbox.*


By MobileValidate team (https://mobilevalidate.com/about) · Published: 2026-11-05 · Category: Developers · Tags: Email verification, Deliverability, Mx record, Smtp, Fraud prevention

You can check an e-mail address's syntax and whether its domain accepts mail (its MX records) without sending anything. You can also check whether the domain is a known throwaway service and, for major webmail providers, whether the mailbox exists. Probing the recipient's mail server yourself with SMTP `RCPT TO` is unreliable in 2026: catch-all domains, greylisting and rate limits hide the real answer. For addresses on company domains, a confirmation e-mail is still the only proof.

The Stack Overflow question ["How to check if an email address exists without sending an email?"](https://stackoverflow.com/questions/565504) was asked in 2009 and had 278,736 views on 2026-09-26. Its best-known answers predate today's anti-abuse defences. This article gives the current picture, with code we ran on 2026-09-26.

## What can you learn about an address without sending mail?

Four things, in increasing order of cost and uncertainty. We call it the **e-mail validation ladder**:

| Step | Question | How | Cost | Can it be certain? |
|---|---|---|---|---|
| 1. Syntax | Is it shaped like an address? | Local code | Free | Yes, for "invalid" |
| 2. Domain / MX | Can the domain receive mail? | DNS lookup | Free | Yes, for "no domain" and "null MX" |
| 3. Disposable | Is it a known throwaway domain? | Blocklist | Free | No: lists lag and have false positives |
| 4. Mailbox | Does this mailbox exist? | Provider-specific check | Paid | Only for providers that give a clear answer |

Steps 1 and 2 can prove an address is bad. None of the steps proves that the person typing owns the address. That is what a confirmation e-mail is for (see below).

Here is the decision table we use. Each outcome maps to accept, confirm or block:

| Outcome | Decision | Message to the user |
|---|---|---|
| Syntax invalid | Block | "Please check your e-mail address" |
| Domain doesn't exist | Block | "We couldn't find that domain. Is it spelled correctly?" |
| Null MX (`0 .`) | Block | "That domain doesn't accept e-mail" |
| DNS timeout or error | Accept, confirm by e-mail | none |
| Known disposable domain | Confirm, or add friction | depends on your risk (see [free trials](/use-cases/otp-and-signup-fraud)) |
| Mailbox `registered: false` | Confirm | "We couldn't find this mailbox. Is the address correct?" |
| Mailbox `registered: true` | Accept | none |
| Mailbox unknown | Accept, confirm by e-mail | none |

## How does an MX lookup work, and what does "no MX" mean?

A sending server finds the receiving server by looking up the domain's MX records in DNS. [RFC 5321](https://www.rfc-editor.org/rfc/rfc5321.html) (section 5.1) sets the rules, and there are four outcomes:

- **MX records found.** The domain names at least one mail server. The address *can* be deliverable. It says nothing about the mailbox.
- **Domain doesn't exist** (DNS `NXDOMAIN`). RFC 5321 says this "MUST be reported as an error". The address can't work.
- **Domain exists, but no MX record.** This is not an error. RFC 5321 says the address "is treated as if it was associated with an implicit MX RR" pointing to the domain itself, so mail goes to the domain's A or AAAA record.
- **Null MX.** [RFC 7505](https://www.rfc-editor.org/rfc/rfc7505.html) lets a domain publish a single MX record with preference 0 and the exchange `.` "to indicate that a domain does not accept email". `example.com` does this.

We ran the Node code below against real domains on 2026-09-26:

| Domain | Result |
|---|---|
| `gmail.com` | `mx`: five MX records, preference 5 to 40 |
| `example.com` | `null_mx`: one MX with an empty exchange |
| `neverssl.com` | `implicit_mx`: no MX record, but an address record |
| `no-such-domain-mv-test.com` | `no_domain`: `ENOTFOUND` |

A common bug is to treat "no MX" as "invalid". That rejects the implicit-MX case, which RFC 5321 allows. Another is to treat a DNS timeout as "invalid". A timeout means you don't know, so let the address through and rely on the confirmation e-mail.

## Why do SMTP RCPT probes give false answers?

Because the receiving server has good reasons not to tell a stranger the truth. The classic trick is to connect to the MX host, send `HELO`, `MAIL FROM` and `RCPT TO:<address>`, read the reply, and disconnect before sending any data. It fails in several ways.

**`VRFY` is usually useless.** RFC 5321 has a command meant for this, but section 7.3 says sites "may want to disable either or both of VRFY or EXPN for security reasons". A server that does so "MUST return a 252 response". The standard wording for 252 is "Cannot VRFY user, but will accept message and attempt delivery", which is neither yes nor no.

**Some providers answer `RCPT TO` honestly, and some can't.** Gmail documents `550 5.1.1`: "The email account that you tried to reach does not exist" ([Google, 2026](https://knowledge.workspace.google.com/admin/support/troubleshooting/gmail-smtp-errors-and-codes)). Microsoft 365 rejects unknown recipients at its edge with `550 5.4.1 Recipient address rejected: Access denied` when Directory-Based Edge Blocking is in effect. But DBEB works only for domains set to "Authoritative", and Microsoft says that even then "there might be infrequent instances" where non-existent addresses are allowed through ([Microsoft Learn, 2026](https://learn.microsoft.com/en-us/exchange/mail-flow-best-practices/use-directory-based-edge-blocking)). Many servers accept first and bounce later.

**Probing looks like abuse.** Gmail's error list includes "Gmail has detected an unusual rate of email originating from your IP address" and temporary rate limits. A sign-up flow that opens SMTP sessions to check addresses, and never sends mail, has exactly the traffic pattern that anti-spam systems watch for. If your probing IP is the one that sends your password-reset e-mails, you put those at risk.

**Your server may not be allowed to connect at all.** Many cloud hosts block outbound port 25 by default, so the probe fails from your own infrastructure before it reaches anyone.

## What do catch-all and greylisting do to the result?

They turn a clear "no" into a false "yes" or a meaningless "try later".

A **catch-all** (accept-all) domain accepts `RCPT TO` for every local part, real or not. The probe answers "yes" for `anything@company.example`, and a non-existent mailbox is discovered only when a bounce arrives later, or never. No check made without sending can tell a real mailbox from a made-up one on such a domain. Our upcoming article on catch-all addresses covers how to treat them.

**Greylisting**, described in [RFC 6647](https://www.rfc-editor.org/rfc/rfc6647.html), rejects "mail from unfamiliar sources with a 'transient (soft) fail' (4xx)" and accepts it when the sender retries later. A real mail server retries. A probe that gives up after the first 4xx reply learns nothing. A probe that retries has to hold state for minutes, which doesn't fit a sign-up form.

The honest summary: from your own server, a probe can sometimes prove that a mailbox *doesn't* exist at a provider that answers at RCPT time. It can almost never prove that a mailbox *does* exist.

## When is a confirmation e-mail still required?

Whenever you need to know that the person owns the address, not just that it exists. A mailbox check tells you `jane@gmail.example` is real. It doesn't tell you that the person typing is Jane. Send a confirmation link:

- **Before any marketing or newsletter mail.** Google's [sender guidelines](https://support.google.com/a/answer/81126) tell senders to "Confirm each recipient's email address before subscribing them", and to keep the spam rate in Postmaster Tools below 0.10%, never reaching 0.30%.
- **Before an address becomes a login or recovery channel.** Otherwise someone can register with another person's address.
- **For company and custom domains**, where no check can give a reliable answer.

Pre-send checks don't replace confirmation. They make it work better: typos are fixed while the user is still on the page, dead domains never get a message, and fewer confirmation e-mails bounce.

## How do I run the checks in Node, Python and PHP?

Each version below implements steps 2 and 4. The Node version also includes steps 1 and 3. All three ran on 2026-09-26 (Node 24, Python 3 with dnspython 2.8.0, PHP 8.3) with the results shown above. The mailbox step uses our public sandbox key, which answers only the [test addresses](/docs/test-values).

```js title="ladder.mjs"
import { promises as dns } from "node:dns";

// Step 1: syntax. Deliberately simple: one @, a local part of 1–64 characters,
// a domain with at least one dot. Your mail provider decides the rest.
export function checkSyntax(address) {
  const email = address.trim().toLowerCase();
  const at = email.lastIndexOf("@");
  if (at < 1 || at !== email.indexOf("@")) return { ok: false };
  const local = email.slice(0, at), domain = email.slice(at + 1);
  if (local.length > 64 || email.length > 254 || /\s/.test(email)) return { ok: false };
  if (!/^(?=.{1,253}$)([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9-]{2,63}$/.test(domain)) return { ok: false };
  return { ok: true, email, domain };
}

// Step 2: can the domain receive mail at all? (RFC 5321 §5.1, RFC 7505)
export async function checkDomain(domain) {
  try {
    const mx = await dns.resolveMx(domain);
    if (mx.length === 1 && mx[0].exchange === "") return "null_mx"; // "0 ." = accepts no mail
    return mx.length > 0 ? "mx" : "no_mx";
  } catch (err) {
    if (err.code === "ENOTFOUND") return "no_domain";
    if (err.code !== "ENODATA") return "dns_error"; // timeout, SERVFAIL: unknown, not bad
  }
  // No MX record: RFC 5321 falls back to the domain's own A/AAAA address.
  const [a, aaaa] = await Promise.allSettled([dns.resolve4(domain), dns.resolve6(domain)]);
  return a.status === "fulfilled" || aaaa.status === "fulfilled" ? "implicit_mx" : "no_mx";
}

// Step 3: known throwaway domain? Load a maintained list once at start-up.
export function isDisposable(domain, blocklist) {
  const parts = domain.split(".");
  for (let i = 0; i < parts.length - 1; i++) if (blocklist.has(parts.slice(i).join("."))) return true;
  return false;
}

// Step 4: does the mailbox exist? Major webmail providers only.
export async function checkMailbox(email, apiKey) {
  const res = await fetch("https://api.mobilevalidate.com/v1/lookup", {
    method: "POST",
    headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
    body: JSON.stringify({ emails: [email], checks: ["email"], wait: 5 }),
  });
  if (!res.ok) return { registered: null, reason: `HTTP_${res.status}` };
  const row = (await res.json()).results[0];
  const check = row.checks?.["email.valid"];
  return { registered: check?.registered ?? null, reason: check?.reason ?? null, billed: check?.billed ?? false };
}
```

The syntax check is intentionally loose. Full RFC 5322 grammar allows quoted local parts and comments that real providers don't hand out. It also rejects non-ASCII local parts, which [RFC 6531](https://www.rfc-editor.org/rfc/rfc6531.html) allows through the SMTPUTF8 extension. If you serve users with internationalized addresses, relax that rule and convert domains to punycode before the DNS lookup.

```python title="ladder.py"
import dns.exception
import dns.resolver  # pip install dnspython
import requests      # pip install requests

def check_domain(domain: str) -> str:
    try:
        answers = dns.resolver.resolve(domain, "MX", lifetime=5)
    except dns.resolver.NXDOMAIN:
        return "no_domain"
    except dns.resolver.NoAnswer:
        answers = None
    except (dns.resolver.NoNameservers, dns.exception.Timeout):
        return "dns_error"  # unknown, not bad
    if answers is not None:
        hosts = [r.exchange.to_text() for r in answers]
        return "null_mx" if hosts == ["."] else "mx"  # RFC 7505: "0 ." accepts no mail
    for rtype in ("A", "AAAA"):  # RFC 5321 §5.1: no MX, fall back to the address record
        try:
            dns.resolver.resolve(domain, rtype, lifetime=5)
            return "implicit_mx"
        except Exception:
            pass
    return "no_mx"

def check_mailbox(email: str, api_key: str) -> dict:
    r = requests.post(
        "https://api.mobilevalidate.com/v1/lookup",
        headers={"Authorization": f"Bearer {api_key}"},
        json={"emails": [email], "checks": ["email"], "wait": 5},
        timeout=10,
    )
    if not r.ok:
        return {"registered": None, "reason": f"HTTP_{r.status_code}"}
    check = r.json()["results"][0].get("checks", {}).get("email.valid", {})
    return {"registered": check.get("registered"), "reason": check.get("reason")}
```

```php title="ladder.php"
<?php
function check_domain(string $domain): string {
    $hosts = [];
    if (getmxrr($domain, $hosts)) {
        // RFC 7505 null MX ("0 .") comes back as an empty or "." host name
        return (count($hosts) === 1 && in_array($hosts[0], ['', '.'], true)) ? 'null_mx' : 'mx';
    }
    // RFC 5321 §5.1: no MX record, fall back to the domain's address record
    return (checkdnsrr($domain, 'A') || checkdnsrr($domain, 'AAAA')) ? 'implicit_mx' : 'no_mx';
}
```

PHP's `getmxrr()` doesn't separate "domain doesn't exist" from "no MX record": for our non-existent test domain it returned `no_mx`. That's fine for a block decision, because neither can receive mail. If you need the difference, use a DNS library.

For the mailbox step with the sandbox key `mv_test_publicSandboxn9ZgneuhR1B9CRfKG3fulym`, the test addresses returned:

| Address | `registered` | `reason` | `billed` |
|---|---|---|---|
| `registered@test.mobilevalidate.com` | `true` | none | `false` |
| `not-registered@test.mobilevalidate.com` | `false` | none | `false` |
| `unknown@test.mobilevalidate.com` | `null` | `UPSTREAM_TIMEOUT` | `false` |
| `unsupported@test.mobilevalidate.com` | `null` | `UNSUPPORTED_PROVIDER` | `false` |

The last row is how a company-domain address looks in live mode. Our [mailbox check](/services/email-verification) covers major webmail providers only, and never sends mail to the address. It doesn't check MX records or disposable domains, so run steps 2 and 3 yourself. In live mode it is billed per conclusive answer, with separate real-time and bulk prices on the [pricing](/pricing) page. Unknown and unsupported answers are free, and every account can run up to 5 phone numbers and 5 e-mail addresses through the real-time checks for free on the dashboard's Try it page. The API does fix obvious domain typos for you: a mistyped provider domain comes back with a `suggestion` such as "Did you mean gmail.com?". The full response format is in [e-mail checks](/docs/emails), and the [sign-up recipe](/docs/recipes/email-signup-check) wraps it in a ready-made decision.

## What should I do with "unknown"?

Accept the address and send the confirmation e-mail. "Unknown" means the check couldn't get an answer: a DNS timeout, a provider outside coverage, a catch-all domain, a temporary failure. It says nothing bad about the person, and a large share of legitimate business users will land here.

What unknown is **not**:

- **Not a reason to block.** Blocking unknowns turns your checker's coverage gaps into lost sign-ups, and hits business users hardest.
- **Not the same as `false`.** Store it as `null`, with the reason and the time you checked. A later check, or a confirmed click, can settle it.
- **Not something to retry in a loop.** Retry once in the background if you like. Don't make the user wait.

For fraud-sensitive flows, combine an unknown e-mail with other signals rather than acting on it alone, for example the phone checks in [how to reduce fake sign-ups](/blog/how-to-reduce-fake-signups-with-phone-and-email-checks).

## Which checks are appropriate, and which aren't?

Check addresses that people give you, when they give them to you: at sign-up, checkout or a contact-detail change. Don't use any of these techniques to guess addresses, test name variations or build mailing lists. That is enumeration, and it is how spammers harvest addresses. Our API refuses requests that look like generated lists (20 or more addresses on one domain that differ only by digits or separators), and the [acceptable use policy](/legal/acceptable-use) forbids list building.

The difference between a mailbox check and an account check (is this address registered on a particular service?) is covered in [e-mail verification vs account existence checks](/blog/email-verification-vs-account-existence-checks).

## What are the key takeaways?

- Without sending mail, you can check **syntax, domain/MX, disposable domains and, for major webmail providers, the mailbox**.
- **"No MX" isn't "invalid".** RFC 5321 falls back to the domain's address record. Only a missing domain or a null MX (`0 .`, RFC 7505) proves the domain takes no mail.
- **DIY SMTP probing is unreliable.** `VRFY` returns 252 by design, catch-all domains accept everything, greylisting returns 4xx, and probe traffic can get your IP rate-limited.
- **Unknown means accept and confirm**, never block.
- **Confirmation e-mail is still required** before newsletters, for login and recovery addresses, and for company domains.
- Our [mailbox check](/services/email-verification) covers major webmail providers only. Other domains come back unknown with `UNSUPPORTED_PROVIDER`, free of charge.

## Sources

1. [RFC 5321: Simple Mail Transfer Protocol (sections 3.5, 5.1 and 7.3)](https://www.rfc-editor.org/rfc/rfc5321.html) — IETF, 2008
2. [RFC 7505: A "Null MX" No Service Resource Record for Domains That Accept No Mail](https://www.rfc-editor.org/rfc/rfc7505.html) — IETF, 2015
3. [RFC 6531: SMTP Extension for Internationalized Email](https://www.rfc-editor.org/rfc/rfc6531.html) — IETF, 2012
4. [RFC 6647: Email Greylisting: An Applicability Statement for SMTP](https://www.rfc-editor.org/rfc/rfc6647.html) — IETF, 2012
5. [Email sender guidelines](https://support.google.com/a/answer/81126) — Google (Gmail Help), 2026
6. [Gmail SMTP errors and codes (updated 2026-09-24)](https://knowledge.workspace.google.com/admin/support/troubleshooting/gmail-smtp-errors-and-codes) — Google Workspace, 2026
7. [Use Directory-Based Edge Blocking to reject messages sent to invalid recipients in Exchange Online](https://learn.microsoft.com/en-us/exchange/mail-flow-best-practices/use-directory-based-edge-blocking) — Microsoft Learn, 2026
8. [How to check if an email address exists without sending an email? (question 565504)](https://stackoverflow.com/questions/565504) — Stack Overflow, 2009

## Frequently asked questions

### Can I check if an e-mail address exists without sending an e-mail?

Partly. You can check the syntax, whether the domain accepts mail (its MX records) and whether it is a known throwaway domain. For major webmail providers, a mailbox check service can also say whether the mailbox exists. For many company domains, only a confirmation e-mail proves it.

### Does the SMTP VRFY command still work?

Rarely in a useful way. RFC 5321 lets sites disable VRFY for security reasons, and a server that does so must answer 252, which means neither yes nor no. Most large providers don't give a usable answer to it.

### What does it mean when a domain has no MX record?

If the domain doesn't exist, the address can't work. If the domain exists but has no MX record, RFC 5321 says mail goes to the domain's own address record (an implicit MX). If the domain publishes a single MX of '0 .', RFC 7505 says it accepts no mail at all.

### Is it safe to probe mailboxes with RCPT TO from my own server?

It is unreliable and risky. Catch-all domains accept every address, greylisting gives temporary failures, and repeated probes can get your IP rate-limited, which hurts the mail you actually want to deliver.

### Does the MobileValidate mailbox check work for company domains?

No. It covers major webmail providers only. Addresses on other domains come back unknown with reason UNSUPPORTED_PROVIDER, and those results are not charged.
