Self-hosted email list verification. Wash a mailing list on your own machine (syntax checks, MX lookups, disposable-domain detection, and a polite SMTP mailbox probe with catch-all detection) instead of paying a per-address fee to upload your list to a third party.
- One dependency (dnspython). CSV in, CSV out, stdlib everywhere else.
- Nothing leaves your machine except the SMTP probe itself; no list ever gets uploaded anywhere.
- No mail is ever sent. The probe issues
RCPT TOand resets; it never sendsDATA, so no message is delivered and nobody gets a mysterious email. - Honest verdicts. Servers that block probes, greylist, or answer
ambiguously are reported as
unknown, never guessed intoinvalid, so you don't prune real subscribers.
pip install listwash
Check a few addresses (--helo is the domain the probe identifies itself as;
use one you control):
listwash check ceo@example.com info@example.org --helo yourdomain.com
Wash a whole list:
listwash verify leads.csv --helo yourdomain.com
1988 rows, 1642 unique addresses, verifying 1642
probing 511 domains on port 25...
25/511 domains
...
-> leads_verified.csv
verdict breakdown:
valid 1103
invalid 201
catch-all 187
unknown 127
do_not_mail 24
mailable (valid + catch-all): 1290 / 1988
The output CSV is your input with five columns appended: verdict,
smtp_code, detail, flags, mx_domain. Everything else (column order,
extra columns, duplicate rows) is preserved.
No outbound port 25? Run the passive tiers only (no probe identity needed):
listwash verify leads.csv --no-smtp
| listwash | ZeroBounce | NeverBounce | |
|---|---|---|---|
| Price | free, MIT | credit-based, about $4-10 / 1,000¹ | credit-based, about $3-8 / 1,000¹ |
| Your list | never leaves your machine | uploaded to their servers | uploaded to their servers |
| Syntax / MX / disposable / role checks | yes | yes | yes |
| SMTP mailbox probe + catch-all detection | yes | yes | yes |
| Probes from | your IP, politely, over port 25 | their managed IP pools | their managed IP pools |
| Works where port 25 is blocked | passive tiers only (--no-smtp) |
yes | yes |
| Engagement scoring, inbox-placement tests | no | yes (add-ons) | no |
| Web dashboard, integrations, support SLA | no (CLI + Python API) | yes | yes |
| Data retention | yours; nothing to retain | per their policy | per their policy |
¹ list prices at typical volumes, mid-2026; check their sites.
When a paid service is the right call: you have no machine with outbound port 25 anywhere (the passive tiers still work, but you lose the mailbox probe), you want engagement/activity scoring on top of deliverability, or you need an SLA and someone else's compliance paperwork. What they're selling beyond that is mostly the same DNS and SMTP mechanics this tool runs, minus the upload, the invoice, and the retention policy.
Four tiers, cheapest first:
- Syntax: malformed addresses are
invalidwithout touching the network. - MX/DNS: the domain must resolve and have mail servers (falls back to
the RFC 5321 implicit-MX A record). Dead domains are
invalid. - Disposable & flags: throwaway-mail domains (8,000+ bundled) are
do_not_mail. Role addresses (info@,sales@, …) and free providers (gmail, outlook, …) are flagged but not failed. - SMTP probe: connect to the domain's best MX on port 25 and issue
RCPT TOfor each address, without ever sendingDATA.
Before trusting any per-address answer, tier 4 probes a control address, a random local part that cannot exist:
- Control accepted → the domain is catch-all; acceptance is meaningless,
every address there is marked
catch-all(deliverable but risky). - Control refused by policy or greylisted → the server won't talk to us
honestly; every address there is
unknown, not guessed. - Control rejected as unknown user → the server distinguishes mailboxes, so its per-address answers are trustworthy.
| verdict | meaning |
|---|---|
valid |
MX ok, server distinguishes recipients, mailbox accepted |
invalid |
bad syntax, dead domain / no MX, or a hard "user unknown" reject |
catch-all |
server accepts every recipient; deliverable but unverifiable, bounce risk |
unknown |
greylisted, timeout, probe blocked, or port 25 unreachable |
do_not_mail |
disposable domain; suppress regardless of deliverability |
The independent flags column may contain role, free, and/or
disposable: informational only, never a verdict.
Every verifier that probes from a single IP (including the paid ones) hits the same walls. listwash tells you when it hit one instead of guessing:
- Gmail usually distinguishes mailboxes (
550 5.1.1for a bad address) but may greylist or rate-limit under volume →unknown. - Microsoft 365 / Outlook returns
550 5.4.1 Access deniedfor both unknown recipients and blocked probes; genuinely ambiguous, so listwash returnsunknownrather than wrongly prune a real address. - Servers that reject the probing IP by policy (
5.7.*, "access denied", "blocked") →unknown, neverinvalid. - Greylisting (
450/451) →unknown; re-run later.unknownverdicts are deliberately not served from cache on a later day.
What it is reliable for: killing dead domains, hard bounces, disposable and role addresses, and flagging catch-all domains as risky before they damage your sender reputation.
It cannot check SPF/DKIM/DMARC alignment or inbox placement: those are properties of your sending, not of the recipient address.
-
Python ≥ 3.9.
-
Outbound TCP port 25 for the SMTP tier. Most residential ISPs and cloud providers (AWS, GCP, Azure, Hetzner by default) block it. Test yours:
python -c "import socket; socket.create_connection(('gmail-smtp-in.l.google.com', 25), 10); print('port 25 open')"If it's blocked,
--no-smtpstill gives you tiers 1-3 (dead domains, syntax, disposables) from anywhere. -
A domain you control for
--helo. The probe identifies itself with it; probing with a made-up identity gets you blocked and is rude.
listwash verify INPUT.csv [--out PATH] [--email-col NAME] [--limit N] [shared options]
listwash check EMAIL [EMAIL...] [shared options]
shared options:
--no-smtp passive tiers only (no port 25, no identity needed)
--helo DOMAIN probe HELO name (required unless --no-smtp)
--mail-from ADDR probe MAIL FROM (default: verify@<helo>)
--workers N domains probed in parallel (default 8)
--fresh ignore the verdict cache
--cache-days N max age for cached definitive verdicts (default 7)
--resolvers IP[,IP] DNS servers for MX lookups (default: system resolver;
useful when a home-router DNS mishandles MX queries)
--disposable-file P replace the bundled disposable-domain list
--timeout SEC SMTP connect timeout per MX (default 12)
--pause SEC delay between RCPTs to the same server (default 0.4)
The email column is auto-detected (common header names, else the first column
where most values contain @); use --email-col to override.
from listwash import verify
results = verify(
["ceo@example.com", "info@example.org"],
helo="yourdomain.com", # or smtp=False for passive-only
)
for email, r in results.items():
print(email, r["verdict"], r["detail"], r["flags"])SMTP verdicts are cached per address (%LOCALAPPDATA%\listwash\ on Windows,
~/.cache/listwash/ elsewhere) so re-running a list is cheap. Definitive
verdicts are served for up to --cache-days days; unknown verdicts are
only served same-day, so a next-day re-run automatically retries greylisted
and blocked domains. --fresh ignores the cache entirely.
The probe is deliberately polite: one connection per domain, sequential RCPTs
with a delay, RSET between them, QUIT when done, and never DATA. Even
so, verify lists you have a legitimate reason to hold: cleaning your own
subscriber or CRM data. Don't use it to validate scraped or purchased lists
you have no right to mail, and don't crank the politeness settings down to
hammer other people's mail servers. Mail server operators can and do block
IPs that probe abusively; the conservative defaults are what keep this tool
working.
disposable_domains.txt (8,000+ domains), free_providers.txt, and
role_prefixes.txt are bundled and easy to extend: one entry per line, #
comments. PRs adding domains welcome. The disposable list can also be swapped
at runtime (--disposable-file) for e.g. the CC0
disposable-email-domains
list.
MIT
