DEV Community

PennyData
PennyData

Posted on

How to validate an email list in Python without SMTP (syntax, MX, disposable)

Disclosure: I publish the Apify actor used below (pennydata/bulk-email-validator-no-smtp) and it was built with AI assistance (Claude). This is a how-to, not a benchmark.

Short answer: this runs syntax, DNS/MX, disposable and role-address checks on a list of emails, without ever opening a connection to a mail server.

from apify_client import ApifyClient

client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("pennydata/bulk-email-validator-no-smtp").call(
    run_input={"emails": ["test@gmail.com", "info@example.com", "bob@mailinator.com"]}
)
rows = list(client.dataset(run["defaultDatasetId"]).iterate_items())
keep = [r["email"] for r in rows if r["valid"] and not r["isRole"]]
Enter fullscreen mode Exit fullscreen mode

Before a newsletter send you want to drop malformed addresses, dead domains and throwaway inboxes. Many validators do this by opening SMTP connections to the recipient's mail server, which is slow and can get your IP flagged. This actor never contacts a mail server: it checks syntax and DNS only.

1. Input

{
  "emails": ["test@gmail.com", "info@example.com"]
}
Enter fullscreen mode Exit fullscreen mode

You can also pass csvUrl (a public CSV; it uses the column whose header contains "email"), maxEmails and concurrency. Duplicates are validated once.

(Full call shown at the top.)

Actor page: https://apify.com/pennydata/bulk-email-validator-no-smtp

3. Output

Real rows from running the validation code locally (MX lists shortened, checkedAt omitted):

{"email": "test@gmail.com", "valid": true, "status": "deliverable_domain", "reason": null,
 "domain": "gmail.com", "hasMx": true, "hasARecord": true,
 "isDisposable": false, "isRole": false, "isFreeProvider": true, "didYouMean": null}

{"email": "info@example.com", "valid": false, "status": "invalid_domain", "reason": "null_mx",
 "domain": "example.com", "mxRecords": [], "hasMx": false, "hasARecord": true,
 "isDisposable": false, "isRole": true, "isFreeProvider": false, "didYouMean": null}

{"email": "bob@mailinator.com", "valid": false, "status": "disposable", "reason": "disposable_domain",
 "domain": "mailinator.com", "hasMx": true, "isDisposable": true, "isRole": false}
Enter fullscreen mode Exit fullscreen mode

Statuses: deliverable_domain, fallback_a_record, disposable, invalid_domain, invalid_syntax, unknown (DNS failed after retries). A SUMMARY record holds counts per status. isRole flags info@, admin@, noreply@ style addresses and didYouMean suggests fixes for typos of big providers.

Honest limits

  • It cannot tell whether a specific mailbox exists. valid: true means well-formed with a domain that accepts mail. Catch-all domains and non-existent users on real domains pass.
  • Disposable detection uses the open disposable-email-domains list (CC0); brand-new throwaway services are missed until it updates.
  • Role, free-provider and typo lists are short, hand-curated lists.
  • DNS answers change over time, so rerun before a big send.

Pricing is pay per event ($0.0008 per validated email at the time of writing; unknown rows are not charged). Check the actor page for the current price.

Written with AI (Claude).

Top comments (0)