Skip to main content

listwash

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

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

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

listwash-0.1.0.tar.gz (67.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

listwash-0.1.0-py3-none-any.whl (65.7 kB view details)

Uploaded Python 3

File details

Details for the file listwash-0.1.0.tar.gz.

File metadata

  • Download URL: listwash-0.1.0.tar.gz
  • Upload date:
  • Size: 67.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.14.5

File hashes

Hashes for listwash-0.1.0.tar.gz
Algorithm Hash digest
SHA256 4047ad470230432e948168611cea3f4af652f19ccd38d30faf81b32402aa1dbf
MD5 d760af52be2ad64ec9eae8fb8f097069
BLAKE2b-256 cffad9648610d6f0de5378af7aecc6ccfd4f77fac957e6d7516b40138d83032c

See more details on using hashes here.

File details

Details for the file listwash-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: listwash-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 65.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.14.5

File hashes

Hashes for listwash-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 4829c5ffbbc63e34a351464cfef95fb743a16f6d4f299279e1c36125169a3a02
MD5 db04c4db54c1df185542f5206d99c242
BLAKE2b-256 4ea6604cff725f7bd0ff31a853ee0e4fd95c4d2ef70f4436130d1f06f0176ae0

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page