Skip to content

Latest commit

 

History

5 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

List-Wash

List-Wash: self-hosted email verification

CI PyPI Python 3.9+ License: MIT

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 TO and resets; it never sends DATA, 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 into invalid, so you don't prune real subscribers.
pip install listwash

Quickstart

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

Compared to the paid verifiers

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.

How it works

Four tiers, cheapest first:

  1. Syntax: malformed addresses are invalid without touching the network.
  2. MX/DNS: the domain must resolve and have mail servers (falls back to the RFC 5321 implicit-MX A record). Dead domains are invalid.
  3. 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.
  4. SMTP probe: connect to the domain's best MX on port 25 and issue RCPT TO for each address, without ever sending DATA.

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.

Verdicts

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.

What SMTP probing can and can't tell you

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.1 for a bad address) but may greylist or rate-limit under volume → unknown.
  • Microsoft 365 / Outlook returns 550 5.4.1 Access denied for both unknown recipients and blocked probes; genuinely ambiguous, so listwash returns unknown rather than wrongly prune a real address.
  • Servers that reject the probing IP by policy (5.7.*, "access denied", "blocked") → unknown, never invalid.
  • Greylisting (450/451) → unknown; re-run later. unknown verdicts 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.

Requirements

  • 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-smtp still 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.

CLI reference

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.

Library use

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"])

Caching

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.

Responsible use

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.

Data lists

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.

License

MIT

About

Self-hosted email list verifier — syntax, MX, disposable detection, and polite SMTP mailbox probing with catch-all detection. pip install listwash

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages