DEV Community

Alexey D
Alexey D

Posted on

Why Your Email Validation is Probably Wrong (And How to Fix It)

Most apps validate email with a regex. Some add an MX record check. Both are necessary — neither is sufficient.

Here's what you're missing: an email address can be syntactically valid, have proper MX records, and still be completely useless. It might be a disposable inbox that expires in 10 minutes, a catch-all address that accepts everything but nobody reads, or admin@yourcompany.com — a role account that goes to a shared inbox and gets marked as spam on arrival.

The gap between "this address exists" and "a real person will read this email" is where deliverability problems live.

The real validation checklist

  1. Syntax — RFC 5322 compliant, not just regex
  2. MX records — does the domain actually receive email?
  3. Disposable detection — is this from a throwaway service?
  4. Catch-all detection — does the domain accept everything regardless?
  5. Role account detection — is this info@, noreply@, admin@?
  6. Deliverability score — given all the above, will this email actually reach someone?

The API

I built an email validation API that checks all six. Free tier available.

curl -X POST https://cardiac-nickel-eat-whether.trycloudflare.com/validate \
  -H "Content-Type: application/json" \
  -d '{"email": "test@gmail.com"}'
Enter fullscreen mode Exit fullscreen mode

Response:

{
  "email": "test@gmail.com",
  "is_valid": true,
  "is_disposable": false,
  "is_role_based": false,
  "is_catch_all": false,
  "is_premium_provider": true,
  "mx_valid": true,
  "deliverability_score": 94,
  "quality": "excellent",
  "domain": "gmail.com",
  "suggestion": null
}
Enter fullscreen mode Exit fullscreen mode

The deliverability_score (0–100) is the number you actually care about. quality maps it to a human label: excellent (85+), good (60–84), risky (30–59), invalid (below 30).

Python integration

import requests

def check_email(email: str) -> dict:
    r = requests.post(
        "https://cardiac-nickel-eat-whether.trycloudflare.com/validate",
        json={"email": email},
        timeout=5
    )
    r.raise_for_status()
    return r.json()

def should_accept_signup(email: str) -> tuple[bool, str]:
    result = check_email(email)

    if not result["is_valid"] or not result["mx_valid"]:
        return False, "Email address doesn't exist"

    if result["is_disposable"]:
        return False, "Disposable email addresses are not allowed"

    if result["deliverability_score"] < 30:
        return False, "We couldn't verify this email address"

    # Risky but not blocked — you might want to require email confirmation
    if result["deliverability_score"] < 60:
        return True, "low_confidence"  # flag for stricter onboarding

    return True, "ok"
Enter fullscreen mode Exit fullscreen mode

Batch cleaning for existing lists

If you're working with an existing user base or imported leads:

import requests

emails = [
    "user@gmail.com",
    "test123@mailinator.com",  # disposable
    "info@somecompany.com",    # role-based
    "anything@catch-all.io",  # catch-all
]

r = requests.post(
    "https://cardiac-nickel-eat-whether.trycloudflare.com/batch",
    json={"emails": emails}
)

results = r.json()["results"]

sendable = [
    r["email"] for r in results
    if r["deliverability_score"] >= 60 and not r["is_role_based"]
]

print(f"Sendable: {len(sendable)}/{len(emails)}")
# Sendable: 1/4
Enter fullscreen mode Exit fullscreen mode

Up to 10 emails per batch request.

JavaScript — real-time form validation

const validateEmail = async (email) => {
  const res = await fetch('https://cardiac-nickel-eat-whether.trycloudflare.com/validate', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ email })
  });
  return res.json();
};

// Debounced input handler
let timer;
emailInput.addEventListener('input', () => {
  clearTimeout(timer);
  timer = setTimeout(async () => {
    const result = await validateEmail(emailInput.value);

    if (result.is_disposable) {
      showError('Please use a permanent email address');
    } else if (result.suggestion) {
      showHint(`Did you mean ${result.suggestion}?`);
    } else if (result.quality === 'excellent' || result.quality === 'good') {
      showSuccess();
    }
  }, 600);
});
Enter fullscreen mode Exit fullscreen mode

Note the suggestion field — if someone types user@gmial.com, the API returns "suggestion": "user@gmail.com". Typo correction in a single API call.

What "catch-all" means and why it matters

A catch-all domain accepts every email sent to it — anything@example.com, doesnotexist@example.com, all of it — and the MX record check passes. This fools naive validators completely.

The problem: you have no idea if a real person owns john@catch-all-company.com. It might be the CEO's inbox. It might route to /dev/null.

The API flags these with "is_catch_all": true and drops the deliverability score accordingly. You can choose how to handle them — maybe allow but require confirmation, maybe just log and monitor bounce rates.

Disposable email coverage

The API checks against 300+ known disposable email providers: Mailinator, Guerrilla Mail, 10 Minute Mail, Temp Mail, and hundreds more. The list is updated regularly.

If you find a disposable service that slips through, I'm open to adding it — leave a comment with the domain.

Try it

Free tier on RapidAPI — search "Email Validator Pro." The test endpoint above works without authentication for evaluation.

Top comments (0)