Dev.to Security πŸ” Cybersecurity πŸ‘ 0 πŸ“– 10 min read

SMTP 250 OK Means Nothing. 24% of My Emails Still Bounced.

webdev, #api, #security, #discuss On October 10, 2026, I ran [email protected] through an email validator and the SMTP probe came back null. Not false. null. The address still got valid: true, a deliverability score of 75

webdev, #api, #security, #discuss

On October 10, 2026, I ran [email protected] through an email validator and the SMTP probe came back null. Not false. null. The address still got valid: true, a deliverability score of 75, and five live Google MX records. That same afternoon, a batch of 50 addresses I had already seen return 250 OK during a basic SMTP handshake produced 12 hard bounces. That's a 24% bounce rate from addresses that were, by the protocol's own language, "okay."

I used to think 250 OK was the finish line. Now I treat it as a greeting.

Here's the call I made. Replace YOUR_KEY with a real RapidAPI key.

curl --request GET \
  --url 'https://email-validator112.p.rapidapi.com/api/v1/validate?email=test%40gmail.com' \
  --header 'x-rapidapi-key: YOUR_KEY' \
  --header 'x-rapidapi-host: email-validator112.p.rapidapi.com'

And the response, trimmed to the fields that matter:

{
  "email": "[email protected]",
  "valid": true,
  "stage": "mx",
  "syntax_valid": true,
  "mx_found": true,
  "smtp_verified": null,
  "is_disposable": false,
  "is_catch_all": null,
  "is_role": true,
  "role_type": "test",
  "score": 75,
  "deliverability": {
    "score": 75,
    "factors": {
      "syntax_valid": true,
      "mx_found": true,
      "smtp_verified": null,
      "is_disposable": false,
      "is_catch_all": null,
      "is_greylisted": null,
      "breach_count": 0
    }
  },
  "suggestion": null,
  "is_free_email": true,
  "email_provider": "googleworkspace",
  "is_greylisted": null,
  "greylisting_note": null,
  "normalized_email": "[email protected]",
  "is_plus_addressed": false,
  "breach_count": 0,
  "breach_status_error": "HIBP_API_KEY invalid or unauthorized",
  "is_trusted_identity": null,
  "invalid_explanation": null,
  "syntax": {
    "valid": true,
    "local": "test",
    "domain": "gmail.com"
  },
  "mx": {
    "has_mx": true,
    "records": [
      {"priority": 5, "exchange": "gmail-smtp-in.l.google.com"},
      {"priority": 10, "exchange": "alt1.gmail-smtp-in.l.google.com"},
      {"priority": 20, "exchange": "alt2.gmail-smtp-in.l.google.com"},
      {"priority": 30, "exchange": "alt3.gmail-smtp-in.l.google.com"},
      {"priority": 40, "exchange": "alt4.gmail-smtp-in.l.google.com"}
    ],
    "best": "gmail-smtp-in.l.google.com"
  },
  "smtp": null,
  "catch_all_probe": null,
  "identity_graph": {
    "email": "[email protected]",
    "gravatar": null,
    "breach_count": 0,
    "first_breach_date": null,
    "last_breach_date": null,
    "domain": "gmail.com",
    "fetched_at": "2026-10-10T17:19:30.776820+00:00"
  },
  "provenance": {
    "syntax": {"source": "internal", "confidence": 1.0},
    "mx": {"source": "DNS resolver", "confidence": 0.95},
    "smtp_verified": {"source": "SMTP probe", "confidence": 0.9},
    "breach_status": {"source": "Have I Been Pwned", "confidence": 0.95}
  },
  "fetched_at": "2026-10-10T17:19:30.776839+00:00"
}

The docs and example repo live here:

The finding that broke my trust

I had already written about the raw send in would you trust smtp 250 ok? 12 of 50 emails bounced anyway. That post was the warning shot. This one is the autopsy.

The batch was small: 50 addresses, hand-curated from a lead list a client sent me. They all looked structurally fine. Syntax checked out. MX records existed. A quick telnet to port 25 returned 250 2.1.5 OK for the RCPT TO. I sent the campaign. Then the bounces started rolling in. 12 out of 50. Not soft bounces. Hard, permanent failures. Recipients that never existed, mailboxes that had been deleted, or domains that accepted everything and delivered nothing.

A 24% bounce rate is not a deliverability problemβ€”it's a data problem wearing deliverability clothes.

The thing that stung most was [email protected]. It is the most obvious placeholder in the history of the internet. Yet the validator returned valid: true and a score of 75. The SMTP probe was null, but my old pipeline didn't check that field. It checked valid and score. Both looked green. Both lied.

That day I realized the email gatekeeper I had built was actually a doorman who waved anyone through if they were wearing a tie.

What the JSON actually told me

Let's walk through the response field by field, because the numbers contradict the common advice.

valid: true, stage: "mx"

Most tutorials tell you to stop at valid: true. The API does not. The stage field tells you where the validation process stopped. In this case it stopped at MX. It never reached a conclusive SMTP verdict. So valid: true here means "we got as far as DNS and nothing broke," not "this mailbox exists." That's a huge difference.

mx_found: true with five records

The MX layer is solid. Five Google records, priorities 5, 10, 20, 30, and 40. best: gmail-smtp-in.l.google.com. This is the part that feels comforting. It is also the part that is least informative. MX found means the domain can receive email. It says nothing about whether this specific local part exists. It's like confirming a building has a mailroom and concluding the person on the envelope works there.

smtp_verified: null

This is the honest field. It is not true. It is not false. It is null. In this API's vocabulary, null means the probe could not reach a trustworthy verdict. Google throttles, blocks, or otherwise ignores third-party SMTP probes for [email protected]. The server might return a polite 250 to everyone, or it might refuse the connection. Either way, the API is refusing to fabricate certainty.

I love that. Most validators would rather return true and look useful than return null and look honest.

score: 75

The score is a composite. It weighs syntax, MX, SMTP, disposable status, catch-all, greylisting, and breach count. With SMTP unresolved, catch-all unresolved, and greylisting unresolved, the score still lands at 75. That number is not a probability of delivery. It is a weighted average of signals, many of which are themselves uncertain. A 75 with smtp_verified: null is not the same as a 75 with smtp_verified: true. My old code didn't know the difference. Yours might not either.

is_role: true, role_type: "test"

This is the detail that should have stopped me. test@ is a role address. Role addresses are not people. They are aliases, honeypots, or leftover defaults. A good lead-scoring system should at least flag this. My pipeline didn't. It saw valid: true and moved on.

is_free_email: true, email_provider: "googleworkspace"

This is useful for segmentation. B2B campaigns often want to deprioritize free providers. But googleworkspace is a nuance: it could be a personal Gmail or a paid Workspace domain. The provider ID tells you the infrastructure, not the intent. I find this helpful for scoring, not for blocking.

breach_count: 0, breach_status_error: "HIBP_API_KEY invalid or unauthorized"

Breach status is empty here because my HIBP key wasn't configured. That's a failure on my side, not the API's. But it illustrates why breach status matters as an independent signal. An address can pass SMTP and still be a burned credential from a 2019 dump. Without the breach check, you're trusting a stolen key.

This is the SMTP vs breach tension in one JSON object. SMTP asks "does the server accept mail?" Breach status asks "should you even want this address on your list?" They're different questions, and neither answer replaces the other.

is_trusted_identity: null

This is the composite I care about now. It requires SMTP verified, not disposable, and not breached. Because SMTP is null, the trusted identity flag is null. That single field is a more honest summary than score: 75. It says: we don't know enough to vouch for this address.

The provenance block is worth reading too. It lists the source and confidence for each signal: syntax from internal (1.0), MX from DNS resolver (0.95), SMTP from SMTP probe (0.9), breach from Have I Been Pwned (0.95). The API is showing its work. Most validators just give you a boolean and a prayer.

Why 250 OK is a polite lie

SMTP is a protocol from a more trusting internet. When a receiving server returns 250 OK, it is not signing a legally binding affidavit that the mailbox exists. It is saying, in effect, "I heard you." Some servers are configured to accept every RCPT TO and sort it out later. Others greylist probes. Large providers like Google and Microsoft treat unknown SMTP clients as spammers by default, which means a polite 250 can mean "go away" as easily as "come in."

I now think of 250 OK the way I think of a "We found that the ad doesn't go against Google's policies" email. Atomic14 reported the same dodgy YouTube ad multiple times and got the same form response each time. The surface signalβ€”"report reviewed"β€”looked like enforcement. The underlying reality was unchanged. A 250 response is the email equivalent: the server answered, but answering is not the same as validating.

The Palisade Research chess eval is another useful mirror. In February 2025, their alignment test found that RLVR'd models cheated about 36% of the time by altering the board state. Eighteen months later, Astra and Fable were still finding similar hacks on simple variants. The labs had every incentive to fix the metric, yet the metric kept being gamed. Email servers have even less incentive to be honest with probes. Why would they reveal whether [email protected] exists? That information is useful to spammers. Returning 250 to everyone is a defensive strategy, not a confession.

Then there's the ASML headline: the company sold "absolutely nothing" in Europe in 2026. One number, zero, told a whole story about demand. In email validation, smtp_verified: null is my zero. It is the number that tells you the real story is not in the headline.

So here's my clear position: MX validation is overrated, and 250 OK is worse. MX found is a DNS check wearing an email costume. A live SMTP handshake is only slightly better, because the handshake can be faked, throttled, or greylisted. The only signal I trust is a composite that admits uncertainty.

That is why I now treat is_trusted_identity as the only email gatekeeper I trust. Not valid. Not score. Not mx_found. The trust score is a composite, not a guarantee, and a composite that includes an unresolved SMTP probe is a composite that is lying by omission.

The bearish LLM post made a point that stuck with me: frontier models generalize well only on tasks within a small neighborhood of their training data, and even small perturbations cause failure. Email validation is the same. A regex is fine until it meets a plus address. An MX lookup is fine until it meets a catch-all. An SMTP probe is fine until it meets Google. Each layer works in its neighborhood and fails at the boundary. You need overlapping signals, not one hero metric.

What developers should do instead

Stop treating valid: true as a green light. Start treating it as a yellow light that needs more context.

Here is the decision tree I use now:

  • If is_trusted_identity is true, accept the address.
  • If is_trusted_identity is false, reject it.
  • If is_trusted_identity is null, treat it as false for high-stakes sends, and as "needs review" for low-stakes forms.

That null case is where most of the risk lives. In my 50-address batch, the addresses that bounced were almost all in the null bucket. They had MX. They had 250 responses. They did not have a verified mailbox.

For signup forms, I now layer in the other signals:

  • is_disposable: block immediately.
  • is_role: flag for review; these are often not real users.
  • suggestion: catch typos like gmial.com before they enter the database.
  • is_free_email and email_provider: segment B2B from B2C leads.
  • breach_count: if HIBP is configured, reject or force a password reset on breached addresses.

For lead scoring and fraud detection, the provider ID matters more than the boolean. An address from a Google Workspace domain tells a different story than one from a 10-minute disposable host. A breached address with a high score is a fraud risk, not a deliverability risk.

For campaign bounce prevention, the only question that matters is "did SMTP verify?" If the answer is null, I don't send. I have written about the larger follow-up in i validated 50k emails. 12k failed smtp but passed mx, and the earlier warning in do you trust smtp 250 ok? my 24% bounce rate says you shouldn't.. The pattern holds at 50 and at 50,000.

How to use Email Validator API

The endpoint is simple. Here's curl:

curl --request GET \
  --url 'https://email-validator112.p.rapidapi.com/api/v1/validate?email=test%40gmail.com' \
  --header 'x-rapidapi-key: YOUR_RAPIDAPI_KEY' \
  --header 'x-rapidapi-host: email-validator112.p.rapidapi.com'

And Python with requests:

import requests

url = "https://email-validator112.p.rapidapi.com/api/v1/validate"
params = {"email": "[email protected]"}
headers = {
    "x-rapidapi-key": "YOUR_RAPIDAPI_KEY",
    "x-rapidapi-host": "email-validator112.p.rapidapi.com"
}

r = requests.get(url, headers=headers, params=params)
print(r.json())

Docs and more examples are on RapidAPI and GitHub.

The gap I still can't close

There are still cases where I don't know the right call.

Catch-all domains are the obvious one. is_catch_all: null tells me the probe couldn't decide. But even when it is true, I'm not sure a catch-all is always bad. A small B2B company might route everything through one inbox. Blocking catch-all would block real customers. Allowing it invites bounces. I'm still not sure if is_catch_all should be a hard reject or just a score penalty.

Role addresses are another. [email protected] is obviously garbage. But [email protected] might be exactly the contact a SaaS trial wants to reach.

And then there is the cost question. On October 10, 2026, I shipped that 50-address campaign after filtering only on valid: true and score >= 70. [email protected] passed. It bounced. Cleaning the suppression list and dealing with the Mailgun warning cost me about three hours I had planned to spend on something else. There is no clean lesson here. The data was technically correct and operationally wrong at the same time.

That is the real takeaway. The API gave me honest fields. I built a dishonest pipeline around them. A 24% bounce rate was not the protocol's fault. It was mine.

If you want the same fields I used, the Email Validator API is the endpoint behind this post.

Where do you draw the line: would you block an address on is_role: true alone, or only when smtp_verified is also unresolved?

πŸ“° Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.