Bulk Email Verifier: syntax, MX, disposable, role and typo
Pricing
from $0.60 / 1,000 email verifieds
Bulk Email Verifier: syntax, MX, disposable, role and typo
Check email lists without sending anything: syntax, mail server (MX) lookup, disposable domains, role accounts, free providers and typo suggestions. One row per address with a status and reasons. No login, no API key. Charged per address that gets a verdict.
Pricing
from $0.60 / 1,000 email verifieds
Rating
0.0
(0)
Developer
Edge
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
a day ago
Last modified
Categories
Security
Limited permissionsShare
Bulk Email Verifier: syntax, MX, disposable, role and typo checks
Paste a list of email addresses and get one row per address with a status (valid, invalid, risky or unknown), the reasons behind it, and flags for disposable domains, role accounts, free providers and likely typos. Nothing is sent to any address. No login, no API key, no website is scraped: the Actor asks DNS which mail server takes mail for each domain and compares the address with built-in lists.
By default this Actor does not check that the mailbox exists. It tells you whether the address is well formed, whether its domain exists and accepts mail, and whether it looks risky. An optional mailbox probe over SMTP (checkSmtp) is described below, with its limits.
What a run does, for each address
- Cleans the entry: removes a display name (
Name <a@b.com>),mailto:, brackets and surrounding punctuation, splits entries that hold several addresses, removes duplicates ignoring upper and lower case, keeps the input order. - Checks the syntax (a practical subset of the email address standard: local part up to 64 characters, whole address up to 254, valid domain labels, a top level domain of letters (whether it exists is settled by the DNS lookup); quoted local parts and IP-literal domains are rejected).
- Looks up the MX records of the domain over DNS. If there are none, it falls back to the domain's own A or AAAA record, as mail systems do. It checks that at least one mail host (of the first five by priority) resolves to an address. A domain with a "null MX" (a published statement that it takes no mail) is
invalid. - Compares with the built-in lists: 277 disposable domains (and their subdomains), 92 free provider domains, 68 role names, 116 known typo domains.
- Optionally probes the mailbox (see below).
The lists are written by hand and are not complete: new disposable services appear every week. A domain missing from the list is not a guarantee that it is a real mailbox provider.
Input
| Field | Type | Default | What it does |
|---|---|---|---|
emails | list of text | three sample addresses | One address per line. Also accepted: Name <address>, mailto: links, several addresses in one line separated by commas or semicolons. Up to 100,000 entries; needed unless datasetId is set. An entry that is not an address becomes a row with status invalid. |
checkSmtp | true or false | false | Optional mailbox probe, see below. |
timeoutSeconds | whole number, 2 to 60 | 10 | How long to wait for one DNS answer or one mail server reply. DNS lookups are tried twice. |
maxEmails | whole number, 1 to 100,000 | 10,000 | Stops reading the list after this many different addresses. Duplicates do not count. |
datasetId | dataset ID | none | A dataset to read the addresses from, such as a scraper's results. See below. |
emailField | text | email | The field of each dataset row that holds the address. |
Run it after a scraper
Ran a lead scraper? Pass its results instead of pasting a list.
- Open the scraper's page in the Console, select the Integrations tab and add this Actor, to run when the scraper's run succeeds.
- In this Actor's input there, put this line in the Dataset field (
datasetId):{{resource.defaultDatasetId}} - In Email field (
emailField), name the field of the scraper's rows that holds the address. The default isemail.
Each run then reads up to 100,000 rows of that dataset. A field that holds a list of addresses gives one address per item. A row with no address in that field is skipped and not charged, and the dataset part of RUN_SUMMARY counts these rows. Addresses pasted in emails are checked too, except the three sample ones of the form. Duplicates are removed and maxEmails caps the total, as for a pasted list. For a single run you can also put the ID of a dataset you own in datasetId. The Actor gets read access to that one dataset only.
Output
One dataset row per unique address, in input order. Every row has every field.
| Field | Meaning |
|---|---|
email | The address as read from the input, after the cleanup in step 1. |
normalized | Lower case, domain in ASCII (punycode). Null when the syntax is invalid. |
domain | The domain in lower case ASCII. Null when the syntax is invalid. |
status | See the table below. |
reasons | List of short codes, in the order found. See the table below. |
mxHost | The mail server that would receive mail (the first one by priority that resolves). The domain itself when it has no MX record but an address record. Null otherwise. |
isDisposable | The domain, or a parent of it, is on the disposable list. |
isRole | The part before the @ (without a +tag) is a role name such as info, support or sales. |
isFreeProvider | The domain is a free mailbox provider such as gmail.com. Information only: it does not make a row risky. |
didYouMean | The full address with the domain a known typo most likely stands for (john@gmial.com gives john@gmail.com), or null. |
smtpResult, smtpDetail | Outcome of the mailbox probe and the server's reply, or null when the probe is off or not tried for the row. |
Status
status | Meaning | Charged |
|---|---|---|
valid | Syntax fine, the domain accepts mail, nothing risky found. | yes |
invalid | Bad syntax; the domain does not exist or accepts no mail; or (probe on) the mail server refused the mailbox. | yes |
risky | Deliverable in principle, but the domain is disposable, the address is a role account, the domain looks like a typo of a big provider, the local part is not plain ASCII, or (probe on) the server accepts every address. | yes |
unknown | DNS did not answer in time, so no verdict is given. A temporary DNS failure is never reported as invalid. | no |
Reasons
| Code | Meaning |
|---|---|
bad_syntax | The address is malformed; the next code says how: empty, missing_at, multiple_at, empty_local_part, empty_domain, bad_local_part, quoted_local_part, local_part_too_long, address_too_long, bad_domain, domain_without_dot, bad_tld, ip_literal_domain. |
domain_not_found | The domain does not exist (DNS answered "no such domain"). |
domain_accepts_no_mail | The domain has a null MX, or no MX and no address record at all. |
mail_host_has_no_address | The domain names mail hosts but none of the first five has an address. |
dns_temporary_failure | DNS did not answer after the retries (status unknown). |
mx_found | A mail host for the domain was found. |
no_mx_uses_address_record | No MX record, but the domain has an address record, so mail would go there. |
disposable_domain | On the disposable list. |
role_account | A role name such as info or admin. |
possible_typo | The domain is on the typo list; see didYouMean. |
non_ascii_local_part | The part before the @ has non-ASCII characters, which many mail servers refuse. |
check_timeout | One address took longer than 12 times timeoutSeconds (status unknown). |
smtp_mailbox_accepted | Probe on: the server accepted this address and refused a random one on the same domain. |
smtp_mailbox_rejected | Probe on: the server said this mailbox does not exist (status invalid). |
smtp_catch_all | Probe on: the server accepts any address on the domain (status risky). |
smtp_blocked | Probe on: the server refused for a reason about us (blocklist, policy), not about the mailbox. The status is not changed. |
smtp_temporary | Probe on: a temporary reply such as greylisting. The status is not changed. |
smtp_unreachable | Probe on: no connection could be made. The status is not changed. |
smtp_protocol_error | Probe on: the server sent something unexpected or hung up. The status is not changed. |
Example row (shape only, probe off; the values are made up):
{"email": "john@gmial.com", "normalized": "john@gmial.com", "domain": "gmial.com", "status": "risky","reasons": ["mx_found", "possible_typo"], "mxHost": "mx1.gmial.com", "isDisposable": false, "isRole": false,"isFreeProvider": false, "didYouMean": "john@gmail.com", "smtpResult": null, "smtpDetail": null}
The mailbox probe (checkSmtp, off by default)
When on, for each address that passed the DNS check (and is not on a disposable domain) the Actor connects to its mail host on port 25, says hello, names an empty sender, asks whether it would accept mail for the address, then asks the same about one random address on the same domain, and says goodbye. No message is ever sent (no DATA command is issued). At most two connections are open to one mail host at a time.
What to expect:
- Outbound port 25 is blocked on many platforms and networks, and many mail servers refuse or delay probes from shared addresses. If 5 connections fail and none has succeeded, probing is switched off for the rest of the run, the rows say
smtp_unreachable, and the run summary says so. The DNS result of each row is kept. - A server may answer in ways that do not tell you about the mailbox. Those replies are recorded (
smtp_blocked,smtp_temporary) and leave the status alone. - A mailbox probe is not a delivery guarantee: servers can accept an address and bounce later.
This behavior is tested against a scripted mail server in memory. It has not been tried against real mail servers from the Apify platform, so treat the first probe run as a test with a short list.
Pricing
Pay per event. One event, charged once for each row with status valid, invalid or risky:
| Event | Price | When |
|---|---|---|
email-verified | 0.0006 USD (0.60 USD per 1,000 addresses), the same on every plan | per dataset row with status valid, invalid or risky |
Rows with status unknown are delivered and never charged. Duplicates are removed before checking and are not charged. Apify also applies its standard run start event once per run, shown on the pricing tab of this Actor. Set a maximum cost per run in the Console and the Actor stops with the results it has when that limit is reached.
Reliability
- DNS lookups use the name servers of the container the Actor runs in. A lookup is tried twice over every configured server, with a pause between the tries; a truncated UDP answer is asked again over TCP; answers that do not match the question are ignored.
- Each address has a deadline of 12 times
timeoutSeconds; past it the row isunknownand is not charged. - Up to 50 addresses are checked at once. A domain shared by many addresses is looked up once.
- DNS stop: if 20 addresses in a row end as
unknownbecause DNS does not answer, the run stops with a clear message instead of spending compute on a dead network. The rest are not checked and not charged. - Each run writes a
RUN_SUMMARYrecord to the key-value store: results by status, charged results, reason counts, DNS queries and retries, list sizes, timing and why a run stopped.
Privacy
The Actor contacts no website and no third-party service: it sends DNS queries about the domain part of each address to the container's name servers, and, only with checkSmtp on, talks to the mail host of the address. Addresses are written to your run's dataset (the platform also keeps your input with the run), never to the run log, and the Actor keeps nothing between runs. Make sure you have a lawful basis to process the addresses you submit.
Support
If a verdict looks wrong, open an issue on the Actor page with the address's domain (not the whole list) and the reasons the row shows. Edge