Email Verifier & Validator (No SMTP):無需 SMTP 驗證 10,000 個電郵地址,費用約 $10
Validate a list of 10,000 emails without SMTP for about $10
作者製作的 Apify actor Email Verifier & Validator (No SMTP) 可免用 SMTP,按地址格式及 DNS 檢查最多 10,000 個電郵地址,費用約 $10。
Before you import a lead list or send a campaign, you want to drop the junk: typos, fake domains, throwaway inboxes. Many verifiers do this by opening SMTP connections to each mail server, which is slow and often gets IPs blocked. A cheaper first pass skips SMTP and checks everything you can know from the address and DNS alone.
This post shows how to run that pass on a CSV or a list of up to 10,000 addresses with an Apify actor I built, Email Verifier & Validator (No SMTP). Disclosure: it's my actor and I earn a little when it's used. The checks themselves are plain DNS, so you could also reimplement them yourself.
What it checks (and what it doesn't)
Per address, with no SMTP probing:
- Syntax (length limits, bad characters, internationalized domains)
- DNS: MX records, with A/AAAA fallback, and null-MX detection
- Disposable-domain list, role accounts (
info@,sales@), free providers (gmail etc.) - Typo suggestions for common providers (
yaho.com->yahoo.com)
What it can't do: it cannot tell if the mailbox jane@company.com exists. valid: true means "well-formed, and the domain accepts mail". Catch-all domains, full mailboxes and made-up users on real domains all pass. If you need mailbox-level verification, you need an SMTP-based verifier (more expensive, and results are often "unknown" anyway). Use this as a cheap first filter.
Price
Pay-per-event: $0.001 per email checked (at time of writing), so 10,000 emails is about $10, 1,000 is about $1. Duplicates in your list are removed before billing. Addresses whose DNS lookup errored out (status unknown) are not charged; invalid-syntax rows are charged like any other row. Compute is included in the per-event price.
Input
{
"emails": ["jane.doe@gmail.com", "bob@yaho.com", "temp@10minutemail.com", "bad@@example.com"],
"concurrency": 10
}
For a CSV, give a public URL instead (it uses the column whose header contains "email", or csvColumn):
{ "csvUrl": "https://example.com/leads.csv", "csvColumn": "email", "maxEmails": 10000 }
maxEmails defaults to 10,000 (maximum 100,000) and is a safety cap: you can also set a max-cost limit on the run in Apify, and the actor stops when it's reached.
Real output
I ran the actor's source locally on a 15-address test list (7 deliverable-domain, 3 disposable, 2 bad domain, 3 bad syntax). Four of the rows (mxRecords shortened):
[
{
"input": "jane.doe@gmail.com",
"email": "jane.doe@gmail.com",
"valid": true,
"status": "deliverable_domain",
"reason": null,
"domain": "gmail.com",
"mxRecords": [
"gmail-smtp-in.l.google.com",
"alt1.gmail-smtp-in.l.google.com",
"..."
],
"hasMx": true,
"hasARecord": true,
"isDisposable": false,
"isRole": false,
"isFreeProvider": true,
"didYouMean": null,
"checkedAt": "2026-10-16T05:53:24Z"
},
{
"input": "bob@yaho.com",
"email": "bob@yaho.com",
"valid": false,
"status": "invalid_domain",
"reason": "likely_typo_no_mx",
"domain": "yaho.com",
"mxRecords": [],
"hasMx": false,
"hasARecord": true,
"isDisposable": false,
"isRole": false,
"isFreeProvider": false,
"didYouMean": "bob@yahoo.com",
"checkedAt": "2026-10-16T05:53:24Z"
},
{
"input": "temp@10minutemail.com",
"email": "temp@10minutemail.com",
"valid": false,
"status": "disposable",
"reason": "disposable_domain",
"domain": "10minutemail.com",
"mxRecords": [],
"hasMx": false,
"hasARecord": true,
"isDisposable": true,
"isRole": false,
"isFreeProvider": false,
"didYouMean": null,
"checkedAt": "2026-10-16T05:53:24Z"
},
{
"input": "bad@@example.com",
"email": null,
"valid": false,
"status": "invalid_syntax",
"reason": "invalid_local_part",
"domain": null,
"mxRecords": [],
"hasMx": false,
"hasARecord": false,
"isDisposable": false,
"isRole": false,
"isFreeProvider": false,
"didYouMean": null,
"checkedAt": "2026-10-16T05:53:24Z"
}
]
Note the statuses: deliverable_domain, disposable, invalid_domain (with a didYouMean hint) and invalid_syntax. Filter on valid == true to keep the good ones. I did not run a 10,000-row batch for this post, so I won't quote timing for one. Run time depends mostly on how many distinct domains you have, since DNS results are cached per domain.
Call it from code
Run synchronously and get the rows back in one request (fine for small lists; use async runs for big ones):
curl -X POST \
"https://api.apify.com/v2/acts/pennydata~bulk-email-validator-no-smtp/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"emails":["jane.doe@gmail.com","bob@yaho.com"]}'
Python, with the official client:
from apify_client import ApifyClient
client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("pennydata/bulk-email-validator-no-smtp").call(
run_input={"csvUrl": "https://example.com/leads.csv", "csvColumn": "email"}
)
rows = client.dataset(run["defaultDatasetId"]).list_items().items
good = [r["input"] for r in rows if r["valid"] and not r["isDisposable"]]
print(len(good), "of", len(rows), "kept")
Pipeline suggestion
- Run this pass over the whole list (about $0.001 each).
- Drop
invalid_syntax,invalid_domain,disposable; applydidYouMeanfixes if you like. - Optionally treat
isRoleaddresses separately for cold outreach. - Send only the survivors to a more expensive SMTP verifier, if you need one at all.
That keeps the pricey step small. Try it on a few hundred addresses first and compare against your own bounce data before trusting it on the full list.
Written with AI (Claude).
來源:DEV Community · dev.to