Technical
A step by step order for cleaning an email list
List cleaning goes faster and costs less when the cheap steps run before the expensive ones. Each step removes rows that the next step would otherwise spend time on. This post gives an order that works, the standards behind the syntax rules, and an outline for running the domain step as a bulk job from the dashboard.
Step 1: syntax and length
Start with rules that need no network. The SMTP specification sets the maximum length of a local-part at 64 octets and of a domain name at 255 octets 1. The Internet Message Format defines the local-part as a dot-atom or a quoted string, and says the dot-atom form should be used and the quoted-string form should not 2. A dot-atom is a run of allowed characters separated by single dots, so a local part with a doubled dot, a leading dot or a space is not a normal address.
Our own input check is stricter than the grammar in two ways worth knowing: it rejects an address longer than 254 characters and any local part over 64, and it does not accept quoted local parts or domain literals such as a@[192.0.2.1]. Those rows come back as invalid_input, and a row that fails this way is not charged.
Step 2: normalize, then deduplicate
Deduplicate only after normalizing, or near-duplicates survive. Trim whitespace and lowercase the domain. The local part needs a decision: the SMTP specification says a local-part must be treated as case sensitive, and also says that exploiting that is discouraged 1. In a marketing list, lowercasing the local part is the practical choice, because two addresses that differ only by case almost certainly reach the same person. Our check lowercases both parts before it looks anything up.
- Keep the first occurrence of each normalized address and record where the others came from.
- Do not strip plus tags or dots from the local part: that is a rule of some providers, not of the standard.
- Keep the original value in a separate column so you can trace a result back.
Steps 3 and 4: domains, then role accounts
The domain step asks whether the part after the @ can receive mail at all. A domain that publishes a null MX is announcing that it accepts no mail 3. The email-domain-check product reads the domain's MX, SPF and DMARC records and open disposable-domain and free-provider lists, and sets deliverability_signal to good, risky or undeliverable. It does not contact the mail server, so it cannot confirm that a mailbox exists.
Role accounts such as info@ or support@ belong to a team rather than a person. The same check sets role_account when the local part matches its role list, which lets you route those rows to a separate segment instead of deleting them.
| Step | Check | Needs network | Action on a hit |
|---|---|---|---|
| 1 | Syntax and length limits | No | Remove or send back for correction |
| 2 | Normalize and deduplicate | No | Keep one row per address |
| 3 | Domain exists and accepts mail | Yes, DNS | Remove undeliverable |
| 4 | Disposable domain, role account | Yes, DNS and lists | Segment or remove |
| 5 | First send in small batches | Your mail system | Remove addresses that bounce |
Cleaning order and what each step removes
Run steps 3 and 4 as a bulk job
The dashboard runs a CSV through a product in the background. The limits below are read from the same configuration the service enforces. See the bulk CSV jobs guide for the full list.
| Limit | Value |
|---|---|
| Rows in one file | 10,000 |
| File size | 5 MB |
| Jobs queued or running at once | 3 |
| Jobs started per day | 30 |
Bulk job limits that matter for a list
- Prepare a CSV with one
emailcolumn that has already passed steps 1 and 2. - Split the file if it is over the row or size limit.
- Open jobs in the dashboard, upload the file, choose
email-domain-checkand the email column. - Start the job and download the results when it shows completed.
- Filter on
deliverability_signalandrole_account, then send to the rest in small batches.
const seen = new Set();
const keep = [];
for (const raw of rows) {
const email = raw.trim().toLowerCase();
const at = email.lastIndexOf('@');
if (at < 1 || at === email.length - 1) continue; // no local part or no domain
if (email.slice(0, at).length > 64) continue; // local-part limit
if (email.slice(at + 1).length > 255) continue; // domain limit
if (seen.has(email)) continue; // duplicate after normalizing
seen.add(email);
keep.push(email);
}
Steps 1 and 2 in a few lines, before you upload
Check one address before you run the job
Before you spend a job on a file, try the check on a single address in the free email domain checker to see the fields you will filter on. When the list is clean, set up the same call for new signups through the email domain check product page.
Sources
- RFC 5321: Simple Mail Transfer Protocol, read 2026-10-19
- RFC 5322: Internet Message Format, read 2026-10-19
- RFC 7505: A "Null MX" No Service Resource Record for Domains That Accept No Mail, read 2026-10-19