Diagnose why aren't my emails landing? for self-hosted SMTP in 30 seconds. Zero paid APIs. Zero runtime deps.
npx-friendly.
npx smtp-warmer test --host smtp.example.com --port 587 --user me@example.comStatus Check Summary
----------------------------------------------
PASS TLS handshake Negotiated TLSv1.3 with TLS_AES_256_GCM_SHA384.
PASS SMTP AUTH + RCPT sandbox Auth + RCPT TO:<me@example.com> accepted. Session rolled back; no mail sent.
PASS Sending IP reputation (DNSBL) 1 sending IP(s) clean across 4 DNSBLs.
PASS Reverse DNS / PTR alignment Forward-confirmed reverse DNS for all 1 sending IP(s).
Composite score: 10.0 / 10
You're running your own SMTP relay (Postfix, Haraka, Postmark-via-VPN, whatever) and your mail keeps landing in spam. There are exactly four reasons this happens at the transport layer, and you don't need a $99/mo SaaS account to check any of them.
smtp-warmer test runs the four most common transport-layer deliverability checks against your relay and gives you a 0–10 score plus actionable remediation hints — without sending real mail.
| Check | What it does |
|---|---|
| TLS handshake | Verifies cert validity, expiry, protocol version (TLS 1.2/1.3), and cipher strength. |
| SMTP AUTH + RCPT sandbox | Logs in with your credentials and verifies a RCPT TO:<your-own-address> succeeds, then RSETs. No mail sent. |
| Sending IP reputation | Reverses your sending IP and queries 4 free DNSBLs (Spamhaus ZEN, Barracuda, SpamCop, UCEPROTECT). No API key. |
| Reverse DNS / PTR alignment | Verifies the sending IP has a PTR record and that the PTR forward-resolves back to the same IP (FCrDNS). |
| Check | What it does |
|---|---|
| Bounce-loop / Return-Path | Sends one envelope, reconnects via IMAP, polls for the message, and verifies Return-Path matches MAIL FROM. Catches SRS rewrites and bounce loops. |
| Rate-limit / throttling | Pipelines N small envelopes (default 5, cap 20) and watches for 421/450/452 throttle codes and >2× monotonic RTT growth. |
Both live checks refuse to run unless --bounce-recipient is your own inbox — its domain must match --ehlo (or --user), or be in a small allow-list (Gmail, Outlook, iCloud, Fastmail, Proton).
Every check is independent. Each has a per-check timeout (default 10s), and any check can be skipped with --skip or isolated with --only.
# Run once, throw away
npx smtp-warmer test --host smtp.example.com --port 587 --user me@example.com
# Or install globally
npm i -g smtp-warmer
smtp-warmer test --host smtp.example.com --port 587 --user me@example.com
# Programmatic
npm i smtp-warmerimport { runChecks } from 'smtp-warmer'
const report = await runChecks({
target: { host: 'smtp.example.com', port: 587, user: 'me@example.com', pass: process.env.SMTP_PASS },
})
console.log(`Composite score: ${report.composite}/10`)smtp-warmer test --host <h> --port <p> [options]
--user <email> SMTP login (also used as RCPT sandbox recipient)
--pass <secret> SMTP password (or read from $SMTP_PASS)
--from <email> Override MAIL FROM (default: --user)
--rcpt <email> Override RCPT TO sandbox (default: --user)
--ehlo <hostname> Override EHLO/HELO hostname (default: os.hostname())
--skip <ids> Comma-separated check ids to skip (tls|auth|reputation|reverseDns|bounceLoop|rateLimit)
--only <ids> Run only the listed comma-separated checks
--timeout <ms> Per-check timeout in milliseconds (default: 10000)
--insecure-tls Disable server-cert verification (dev only)
--json Print machine-readable JSON
--no-color Disable ANSI colors
Live (real mail is sent — opt-in):
--live Enable bounceLoop + rateLimit checks (sends mail)
--bounce-recipient <email> Test recipient — must be your own inbox
--rate-count <N> Envelope count for rateLimit (default: 5, cap: 20)
--imap-host <hostname> IMAP server (default: smtp.* → imap.*)
--imap-port <number> IMAP port (default: 993, implicit TLS)
--imap-user <user> IMAP login (default: --user)
--imap-pass <secret> IMAP password (or read from $IMAP_PASS, then --pass)
--imap-mailbox <name> Mailbox to poll (default: INBOX)
--bounce-poll-ms <ms> How long to poll IMAP for the probe (default: 30000)
-h, --help Show this help
-v, --version Show version
# Self-hosted Postfix → your own inbox on the same domain
smtp-warmer test --host smtp.example.com --port 587 \
--user me@example.com --ehlo mail.example.com \
--live --bounce-recipient me@example.com
# Gmail SMTP relay → Gmail inbox (with an app password)
smtp-warmer test --host smtp.gmail.com --port 587 \
--user you@gmail.com --pass "$GMAIL_APP_PASS" \
--live --bounce-recipient you@gmail.com --rate-count 3The bounce-recipient validator refuses anything that doesn't look like the operator's own inbox — there's no mode that lets you point smtp-warmer at a third party.
| Code | Meaning |
|---|---|
| 0 | All checks passed (composite ≥ 8.0) |
| 1 | At least one warn or composite < 8.0 |
| 2 | At least one fail |
| 3 | Bad arguments |
CI-friendly:
- run: npx -y smtp-warmer test --host $SMTP_HOST --port 587 --user $SMTP_USER --json
env:
SMTP_PASS: ${{ secrets.SMTP_PASS }}| Tool | Layer | When to reach for it |
|---|---|---|
dmarc-doctor |
DNS records (SPF / DKIM / DMARC) | "Are my domain's auth records published correctly?" |
smtp-warmer (you are here) |
Live SMTP transport | "Why does my own relay land in spam?" |
inbox-warmer |
Ongoing reputation building | "I just spun up a new domain and need to warm it for weeks." |
coldflow |
Full cold-email platform | "I want everything wired together — sequences, replies, tracking." |
dmarc-doctor answers the DNS question once. smtp-warmer answers the live-transport question every time you change relays. inbox-warmer runs continuously for weeks.
- The auth check never sends mail. It performs
MAIL FROM+RCPT TO+RSET— the SMTP equivalent of "knock and walk away". The session is rolled back before anyDATAcommand is issued. - The default RCPT sandbox is
--user(your own address). If you pass--rcpt, you are responsible for ensuring it's a deliberate test recipient (your own inbox). - DNSBL lookups are pure DNS queries — they generate four DNS A-record lookups per sending IP and zero outbound mail.
- The live checks (
bounceLoop,rateLimit) do send real mail, but only when you pass--liveand--bounce-recipient. The recipient is validated against--ehloand a small allow-list of major mailbox providers; off-domain recipients are refused. The rateLimit check is hard-capped at 20 envelopes per run. - Live integration tests (
LIVE_SMTP_TESTS=1 npm test) are gated behind env vars so the unit suite stays fast and offline-safe.
npm install
npm run typecheck # tsc --noEmit
npm test # vitest run (offline, mocked)
npm run build # tsc → dist/Tests are split into:
- Unit (
test/): pure-function scoring, mocked SMTP/IMAP/DNS — runs in <1 second offline. 78 specs. - Live (
test/live/, gated byLIVE_SMTP_TESTS=1): hits a real SMTP relay + IMAP mailbox. Seetest/live/README.mdfor env-var requirements and provider notes (Gmail-only for v0.2).
LIVE_SMTP_TESTS=1 \
LIVE_SMTP_HOST=smtp.gmail.com \
LIVE_SMTP_USER=you@gmail.com \
LIVE_SMTP_PASS="$GMAIL_APP_PASS" \
LIVE_SMTP_RCPT=you@gmail.com \
npm testMIT © pypesdev