Ask two mail servers whether an address exists and you can get two different answers. One says the user is there, the other says no such mailbox. This happens more often than most people expect, and it usually means the server is answering VRFY and RCPT TO from different code paths. When the two commands disagree, treat the RCPT TO result as the truth. It is the command that actually opens the door for a message, so it reflects the real acceptance path.
How VRFY and RCPT TO differ inside the SMTP conversation
Both commands live in the same SMTP session, and both are defined in RFC 5321, but they serve different purposes. VRFY is a diagnostic lookup. You send VRFY someuser and the server tells you whether that local part exists. It was designed for operators and debugging, and the spec explicitly lets administrators disable it. Many modern servers do exactly that, answering 252 (“cannot VRFY user, but will accept message”) for everything, or 502 (“command not implemented”).
RCPT TO is not a diagnostic. It is part of the envelope transaction. You send MAIL FROM:<sender@example.com>, then one or more RCPT TO:<target@domain.com> lines, and the receiving server has to decide whether it will accept mail for that recipient right now. A 250 means the address goes on the delivery list. A 550 means the server refuses it, and RFC 5321 specifies the 550 response for undeliverable recipients. The command exercises the same routing and mailbox lookup that a real inbound message hits, which is why it carries more weight.
An email verifier that leans on VRFY alone is trusting a hint. An email verifier that issues the RCPT TO command inside a real SMTP conversation is testing the actual door. The difference matters most on servers that answer VRFY generously and RCPT TO strictly.
The disagreement case: 250 from VRFY, 550 from RCPT TO
Picture a mail server with a legacy VRFY handler that only checks syntax. Send it VRFY madeupname123 and it returns 250 as long as the local part is a well-formed address. The same server, on the RCPT TO path, runs a real directory lookup and returns 550 for the same string because no mailbox exists. Nothing is broken. The two commands are just answering different questions: “could this be an address?” versus “will I take mail for it?”
The reverse also shows up. Some servers disable VRFY entirely and return 252 or 502, while RCPT TO gives a clean 550 for unknown users. If your verifier treats a non-250 VRFY as inconclusive and stops there, you learn nothing. Push through to RCPT TO and you get a usable answer. The lesson holds in both directions: when VRFY and RCPT TO disagree, RCPT TO is the authoritative signal because it reflects the actual mail acceptance path. VRFY describes a policy the server may not enforce at delivery time.
Why the RCPT TO answer survives contact with reality
The RCPT TO reply is produced by the same logic that will handle the message if you send it. If the server says 250 at RCPT TO, the message will be queued for that recipient. If it says 550, the message is rejected at the envelope stage and never enters the queue. There is no gap between the test and the outcome. A 250 from VRFY, by contrast, promises nothing about delivery. The server can accept the VRFY and still reject the RCPT TO five seconds later, and it will be within its rights.
This is why a verification pipeline should run VRFY as an optional extra, not as the primary signal. Issue RCPT TO first, record the reply code, and use VRFY only to add context when you want to understand server behavior. If you log both and see a conflict, the RCPT TO code wins. Every time.
Reading the replies: accept-all, unknown, and the control test
The hardest case is not the plain 550. It is the server that answers 250 to RCPT TO for every local part you throw at it. That is a catch-all domain, and the catch-all meaning is simple: the receiving server accepts mail for every address on the domain, real or invented. source covers this in more depth. A catch-all server answers 250 OK to RCPT TO for every local part, including strings no human would ever register. The result is an accept-all email status, sometimes labeled accept_all in verification output, and it tells you almost nothing about the specific mailbox.
You cannot spot a catch-all from a single probe. Verifiers detect catch-all using a control test with an invented address. The procedure looks like this:
- Open an SMTP conversation with the domain’s mail server and send an EHLO or HELO greeting.
- Send MAIL FROM with a plausible sender address the server will not reject outright.
- Issue RCPT TO for an invented local part, something like
zzq7x9-nonexistent@domain.com, that no real user would have. - Record the reply. A 550 or 551 on the invented address means the server does real recipient validation, so a 250 on your target address is meaningful.
- A 250 on the invented address means the server is catch-all or accept-all, and every RCPT TO result for that domain is suspect.
- Optionally re-run the control with a second invented address to rule out a fluke or a greylisting quirk.
Do not skip the control test. Without it, a 250 on a target address on a catch-all domain looks identical to a 250 on a normal domain, and you will mark dead mailboxes as deliverable. The control is what separates a real verification from a guess.
There is a third outcome worth handling: the accept-then-bounce server. It answers 250 to RCPT TO, takes the message text, and then sends a bounce after receiving message text because the mailbox does not exist. From the SMTP side the transaction succeeded. The failure shows up minutes or hours later as a bounce message in the sender’s inbox. RCPT TO cannot catch this pattern on its own, which is why verification results should be treated as probabilities rather than certainties and why sending practice should include bounce handling.
What to do with each result in your sending workflow
A clean 550 at RCPT TO is the easiest outcome. The address is undeliverable, and you should suppress it. Keep it on a known-bad list so you do not waste another SMTP conversation on it, and remove it from active segments. A 250 on a domain that passed the control test is a deliverable address with reasonable confidence. Send to it, watch engagement, and let bounces correct the record.
A catch-all result is not a reason to delete an address. It means you cannot confirm or deny the mailbox, so the status is unknown. Treat accept_all and unknown status the same way: lower the priority, avoid aggressive re-verification, and decide based on engagement history. If the address has opened mail recently, keep it. If it has never engaged and the domain is catch-all, it is a candidate for suppression on engagement grounds, not on verification grounds.
Watch for temporary email provider domains and disposable services too. These often run catch-all setups, and even when they do not, the addresses are short-lived. A verifier that flags the domain as a temporary email provider gives you a better reason to skip it than any single RCPT TO reply. Combine domain reputation, the control test, and the RCPT TO code before you make a call.
One more operational note: rate and pacing matter. Hammering a domain’s mail server with dozens of RCPT TO probes in a row invites throttling, greylisting, or a block. Space out probes per domain, rotate sender addresses in MAIL FROM, and respect 4xx replies as “try later” rather than “bad address.” A verifier that ignores 4xx codes and treats them as failures will generate false negatives and pollute your known-bad lists.
If you want a reference implementation to compare against, AllMail publishes blog content about email verification at allmail.one that walks through these SMTP mechanics in detail. The practical summary is short. Run the control test, issue RCPT TO, read the reply code, and when VRFY and RCPT TO disagree, believe RCPT TO. It is the command the receiving server uses to decide whether your message gets in, and that is the only answer that matters at send time.