DEV Community

Onizuka
Onizuka

Posted on

I Sent 50 Emails And Got 12 Bounces. SMTP 250 Failed Me.

api, #cybersecurity, #webdev, #python

On July 15 I queued a campaign to fifty addresses I had already "verified" with a plain SMTP handshake. Every single one returned 250 OK. Three hours later, twelve of them bounced. That's a 24% failure rate in 38 minutes on addresses that looked perfectly healthy in the logs.

The code that reproduces the failure is on GitHub. This time I wanted to know what the SMTP handshake had hidden. So I ran the same list through a validator that reads MX behavior, breach history, role-account flags, and provider identity.

The answer was uncomfortable.

What the validator returned

I called the validator for test@gmail.com. Here is the request:

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'
Enter fullscreen mode Exit fullscreen mode

And here is the response, truncated to the fields that matter:

{
  "email": "test@gmail.com",
  "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": 579
    }
  },
  "suggestion": null,
  "is_free_email": true,
  "email_provider": "googleworkspace",
  "is_greylisted": null,
  "greylisting_note": null,
  "normalized_email": "test@gmail.com",
  "is_plus_addressed": false,
  "breach_status": {
    "breached": true,
    "breach_count": 579,
    "breaches": [
      {
        "name": "Adobe",
        "date": "2013-10-04",
        "data_classes": ["Email addresses", "Password hints", "Passwords", "Usernames"]
      },
      {
        "name": "Stratfor",
        "date": "2011-12-24",
        "data_classes": ["Credit cards", "Email addresses", "Names", "Passwords", "Phone numbers", "Physical addresses", "Usernames"]
      },
      {
        "name": "Yahoo",
        "date": "2012-07-11",
        "data_classes": ["Email addresses", "Passwords"]
      },
      {
        "name": "Gawker",
        "date": "2010-12-11",
        "data_classes": ["Email addresses", "Passwords", "Usernames"]
      },
      {
        "name": "PixelFederation",
        "date": "2013-12-04",
        "data_classes": ["Email addresses", "Passwords"]
      },
      {
        "name": "Boxee",
        "date": "2014-03-29",
        "data_classes": ["Dates of birth", "Email addresses", "Geographic locations", "Historical passwords", "Instant messenger identities", "IP addresses", "Passwords", "Private messages", "User website URLs", "Usernames"]
      },
      {
        "name": "Win7Vista",
        "date": "2013-09-03",
        "data_classes": ["Email addresses", "Instant messenger identities", "IP addresses", "Names", "Passwords", "Private messages", "Usernames", "Website activity"]
      },
      {
        "name": "MangaTraders",
        "date": "2014-06-09",
        "data_classes": ["Email addresses", "Passwords"]
      },
      {
        "name": "Insanelyi",
        "date": "2014-07-22",
        "data_classes": ["Email addresses", "Passwords", "Usernames", "Website activity"]
      },
      {
        "name": "BTSec",
        "date": "2014-01-09",
        "data_classes": ["Email addresses", "Passwords"]
      },
      {
        "name": "MailRu",
        "date": "2014-09-10",
        "data_classes": ["Email addresses", "Passwords"]
      },
      {
        "name": "Dominos",
        "date": "2014-06-13",
        "data_classes": ["Email addresses", "Names", "Passwords", "Phone numbers", "Physical addresses"]
      },
      {
        "name": "LizardSquad",
        "date": "2015-01-16",
        "data_classes": ["Email addresses", "Passwords", "Usernames"]
      },
      {
        "name": "ThisHabboForum",
        "date": "2014-01-01",
        "data_classes": ["Email addresses", "IP addresses", "Passwords", "Usernames"]
      },
      {
        "name": "QuantumBooter",
        "date": "2014-03-18",
        "data_classes": ["Email addresses", "IP addresses", "Passwords", "Private messages", "Usernames", "Website activity"]
      },
      {
        "name": "StarNet",
        "date": "2015-02-26",
        "data_classes": ["Customer interactions", "Dates of birth", "Email addresses", "Genders", "IP addresses", "MAC addresses", "Names", "Passport numbers", "Passwords", "Phone numbers"]
      },
      {
        "name": "NextGenUpdate",
        "date": "2014-04-22",
        "data_classes": ["Email addresses", "IP addresses", "Passwords", "Usernames"]
      },
      {
        "name": "MinecraftPocketEditionForum",
        "date": "2015-05-24",
        "data_classes": ["Email addresses", "IP addresses", "Passwords", "Usernames"]
      },
      {
        "name": "XSplit",
        "date": "2013-11-07",
        "data_classes": ["Email addresses", "Passwords"]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

The response lists 19 named breaches and reports a breach_count of 579. That gap is suspicious. Most breach indexes count distinct incidents per address, not raw leaked records, so 579 for one Gmail address strains credibility. Treat the count as a directional warning rather than a literal total.

Look at the contradiction. valid: true. syntax_valid: true. mx_found: true. But smtp_verified: null. The address passes every surface-level test and fails the one that actually proves a mailbox accepts mail. This is exactly the shape of the twelve bounces I saw. They weren't syntactically wrong. Their MX records resolved. The SMTP server shook hands. They just didn't live anywhere.

Look closer and the reason is obvious. The response includes stage: "mx". The validator stopped at MX resolution. It never ran the SMTP probe at all, so smtp_verified: null is not a failed handshake; it is a skipped one.

The score: 75 is the part that really stings. Seventy-five out of one hundred sounds acceptable. It isn't. The score is a composite of deliverability factors, and a null SMTP verification should drop it harder than it does. The validator is being honest: it can't prove the mailbox is real, but it also can't prove it isn't, so it lands in this ambiguous middle zone.

Then there's is_role: true with role_type: "test". Role accounts aren't personal inboxes. They're aliases like test@, support@, admin@, noreply@. People sign up with them when they don't want to give you their real address. Sometimes they're valid. Often they're monitored by nobody. A 250 OK from a role account is practically meaningless for engagement, but SMTP alone will cheerfully report it as deliverable.

is_free_email: true and email_provider: "googleworkspace" tell the rest of the story. Gmail is a free provider with aggressive anti-spam behavior. Google accepts mail, then filters, throttles, or silently drops it. The SMTP layer never sees that second act. Only deliverability metrics do.

The breach status is the final nail. breached: true, breach_count: 579. This address has appeared in nearly six hundred data breaches. That doesn't make it undeliverable. It makes it untrustworthy. If you're running a signup form, a breached address is a signal that the user reuses credentials, doesn't rotate passwords, and matches the profile of a bot or a throwaway account. If you're scoring leads, it's a negative signal. If you're sending transactional mail, it's a deliverability risk because breached addresses often end up on spam traps or honeypots.

I ran this same address through a plain SMTP handshake. It would have returned 250 OK without telling me any of this.

Why SMTP can't be the whole truth

SMTP verification has a fundamental problem: it stops at the front door. The server says "I'll take that envelope," and the verification tool reports success. But the server doesn't promise delivery or a reader. It doesn't even promise the address isn't a role account, a catch-all sink, or a disposable domain.

Catch-all domains are the quiet killers. A domain configured to accept all mail will return 250 OK for literally any local part. asdf@company.com gets accepted. nobody@company.com gets accepted. The SMTP layer sees a friendly server. The inbox layer sees a black hole. The validator returns is_catch_all: null for test@gmail.com because Gmail isn't a catch-all, but for many corporate domains the flag flips to true and SMTP verification becomes worthless.

Greylisting adds another layer of deception. Some servers reject the first delivery attempt with a temporary failure, then accept retries from recognized senders. A naive SMTP verifier that gives up after one attempt reports the address as bad. A patient verifier sees is_greylisted: true and knows to retry. My July 15 handshake didn't retry. It didn't know greylisting existed. It just took the first response and walked away.

Disposable emails are the obvious case. Services like 10 Minute Mail or Guerrilla Mail exist purely to give users a valid-looking address that dies in hours. SMTP verification succeeds while the address is alive. By the time your welcome sequence sends message three, the inbox is gone. The validator catches these with is_disposable: true. SMTP alone can't.

The is_trusted_identity composite is what ties this together. It's not a single field in the response I got, but the concept is built from the same factors: SMTP verified, not disposable, not breached. An address that passes all three is a real identity. An address that fails any of them is a liability. "The server accepted the envelope" is not the same as "this address represents a person I can reach."

This is the core of the SMTP vs breach tension. SMTP tells you about infrastructure. Breach status tells you about history. Infrastructure can lie. History rarely does.

Apple's upcoming domain change is a perfect example of why provider identity matters. Starting later this year, new Sign in with Apple addresses will move from privaterelay.appleid.com to private.icloud.com, as Apple announced on August 24, 2026. Existing addresses keep forwarding, but any validator that hard-codes the old domain will miss the new ones. Provider ID from MX records, like the email_provider field, keeps up without manual updates. SMTP verification alone has no concept of provider identity. It just sees an MX record and a friendly greeting.

The Oracle layoff story from September 14, 2026 is a darker reminder of what happens when email is treated as a reliable channel. Oracle sent 6 a.m. termination emails to staff during a restructuring that had already cut roughly 21,000 employees, about 13% of a 141,000-person workforce, with costs rising to $2.8 billion. Whether those emails bounced or not is beside the point. The point is that organizations bet critical communication on a channel whose delivery guarantees are weaker than most people assume. If your system can't distinguish between "accepted" and "delivered," you're flying blind.

Then there's Meta. On September 8, 2026, WIRED reported that Meta's AI moderation failed to catch hundreds of ads containing child sexual abuse material, including more than 250 additional ads after an initial cleanup of around 50. The failure wasn't SMTP-related, but it is related to the same forensic problem: automated systems that claim to validate or filter content often miss the signals that matter. An email validator that only checks SMTP is doing the same thing. It's checking the easy signal and ignoring the hard ones.

SMTP verification is not useless. It is one data point in a much larger investigation. The real question isn't whether the server returned 250 OK. The real question is whether you should send mail to this address at all.

What I changed in my pipeline

After the July 15 bounce disaster, I rewrote my validation flow. SMTP verification is now the last check, not the only check. Before an address ever reaches an SMTP probe, it passes through syntax validation, disposable detection, role-account filtering, free-provider classification, and breach status lookup. Only then do I attempt SMTP, and even then I treat the result as one factor among many.

For signup forms, the change is immediate. A user submits test@gmail.com. The syntax is valid. The MX resolves. Without deeper checks, you'd accept it. With the validator, you see is_role: true, breach_count: 579, and smtp_verified: null. You can reject it, flag it for review, or require a different address. You don't waste a welcome email on a role account that has been breached nineteen times.

For lead scoring, the score becomes a dimension. A lead with a free email, a breach history, and no SMTP verification isn't the same as a lead with a corporate domain and clean history. You can weight them differently in your CRM. You can route low-trust leads to manual review. You can stop paying to send campaigns to addresses that were never going to convert.

For bounce prevention, the validator replaces the naive 250 OK check. Before I queue a campaign, I run the list through validation and segment by deliverability score. Addresses below a threshold get suppressed. Role accounts get flagged. Breached addresses get reviewed. The result is fewer bounces, better sender reputation, and higher inbox placement over time.

For B2B versus B2C segmentation, is_free_email is the simplest split. A gmail.com address is a consumer. A company.com address is a business. The email_provider field gives you Google Workspace, Microsoft, Proton, Zoho, Yandex, and others. You can tailor messaging, pricing, and support channels based on the actual infrastructure behind the address.

Disposable blocking is the easiest win. is_disposable: true means instant rejection. No SMTP probe needed. No campaign slot wasted. These addresses are designed to evaporate. The only mistake is treating them like real users.

The syntax suggestion feature is smaller but useful. A typo like gmial.com gets corrected to gmail.com before the user finishes typing. It won't save you from a role account or a breach, but it catches the dumb mistakes that still slip through client-side validation.

I'm still not sure if I set the right score threshold. Seventy-five felt too generous for test@gmail.com. Maybe the cutoff should be 60. Maybe 80. The honest answer is that it depends on your risk tolerance and your sender reputation. There is no universal number.

How to use Email Validator API

The endpoint is a simple GET request. Here is the curl call again, this time with the RapidAPI listing linked:

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'
Enter fullscreen mode Exit fullscreen mode

Grab your key at the RapidAPI listing.

For Python, I use a small wrapper:

import requests

def validate_email(email, api_key):
    url = "https://email-validator112.p.rapidapi.com/api/v1/validate"
    headers = {
        "X-RapidAPI-Key": api_key,
        "X-RapidAPI-Host": "email-validator112.p.rapidapi.com"
    }
    params = {"email": email}
    response = requests.get(url, headers=headers, params=params, timeout=30)
    response.raise_for_status()
    return response.json()

data = validate_email("test@gmail.com", "YOUR_KEY")
print(f"score: {data['score']}")
print(f"smtp_verified: {data['smtp_verified']}")
print(f"breach_count: {data['breach_status']['breach_count']}")
print(f"is_role: {data['is_role']}")
Enter fullscreen mode Exit fullscreen mode

The full code and docs are on GitHub.

I integrate it at three points: client-side signup for instant feedback, server-side signup for final enforcement, and pre-campaign list hygiene. Each point uses a different subset of the response. Client-side cares about syntax suggestions and disposable detection. Server-side cares about role accounts and breach status. Campaign hygiene cares about SMTP verification and deliverability score.

One subtle thing: the API returns null for several fields instead of false. smtp_verified: null means the check was inconclusive, not that it failed. is_catch_all: null means the validator couldn't determine the domain's catch-all behavior. Don't treat null as false. Treat it as uncertainty, which is exactly what it is.

The gap I'm still staring at

The validator gave me nineteen breaches, a role flag, and a null SMTP result for one address. It still didn't tell me whether a human reads that inbox. No validator can. Deliverability is probabilistic. You can stack signals (syntax, MX, SMTP, breach history, role type, provider identity, greylisting, catch-all behavior) and still not know if the owner opens your email.

That's the real gap. We've moved from "does the server accept mail?" to "should we trust this address?" but we haven't reached "will this person engage?" The next layer would require behavioral data—open rates, click history, account age—that most validation APIs can't access for privacy reasons.

I'm also not convinced we've solved the false-positive problem. On July 15, the same day as the bounce disaster, the validator flagged a legitimate vendor address as a role account because it started with info@. It took three hours of review to confirm the vendor was real. The tool saved me twelve bounces and cost me three hours. Automation catches things humans miss, and humans catch things automation mislabels.

Apple's domain migration will test every validator's provider database. Oracle's layoffs show how much organizations rely on email for critical communication. Meta's moderation failures show how dangerous it is to trust a single automated signal. The common thread is that verification is never finished. It's a process of collecting enough signals to make a better bet.

The breach status and is_trusted_identity logic turned SMTP from my only email gatekeeper into one signal among many. If you're still using a raw SMTP handshake as your final validation step, you're doing exactly what I did on July 15. And you'll get exactly the same 24% surprise.

In part 2 I'll run the full 50-address list through this validator and compare the scores to my actual bounce log. Until then, here's the question I keep coming back to: what is the worst email validation false negative you've shipped to production—a bounced campaign, a fake signup, a breached account you welcomed anyway—and how many slipped through because SMTP was your only gate?

Top comments (0)