Warmup Bounce Classification — how WarmySender protects mailboxes from cascade-pauses

WarmySender protects your mailboxes from being unfairly paused due to misclassified bounces (NDRs). A bounce — also called a non-delivery report (NDR) — is a message a mail server sends back when delivery to a recipient fails. Some bounces are real (the address doesn't exist, the mailbox is full), and some are misleading (a retry-in-progress notice, a sender-side problem misreported as a recipient problem, a transient delay). The platform applies a 4-layer defense so that a single misclassified bounce cannot poison the global blocklist or cascade-pause warmup across many customer mailboxes that share the same recipient domain in their peer pool.

Why this exists — the bounce-cascade bug class

Before the 4-layer defense landed in May 2026, a single misclassified bounce reported by one sender mailbox could trigger this chain: (1) the inbox scanner sees a mailer-daemon NDR and routes it to the bounce classifier, (2) the classifier defaults the unrecognized severity to soft, (3) the soft-bounce tracker eventually promotes the recipient domain to the global blocklist, (4) every warmup mailbox at WarmySender that has the bouncing recipient domain in its peer pool is auto-paused with pause_reason='recipient_domain_bounce'. A retry-warning email (e.g. "warning: message X delayed 24 hours, still being retried") that should have been ignored could thus pause dozens of paying-customer mailboxes overnight. On May 15, 2026 we found 96 paying-customer mailboxes across 26 workspaces silently paused over 14 days from exactly this fingerprint. The 4-layer defense closes the bug class structurally.

Layer 1 — Sender-side error classifier (LL#434)

A send-time guard in server/warmup-engine.ts's recordRecipientBounce gate. Before any bounce is attributed to the recipient domain, the code asks: "is this error actually about the recipient, or is it about the sender's own setup?" Sender-side errors like "sender domain not verified", "DKIM signature missing", DNS-failures on the sender's own SMTP host (ENOTFOUND for mail.<senderdomain>), or rate-limit hits on the sender's own provider are classified as sender-side and NEVER added to the recipient-domain blocklist. The classifier is centralized inside recordRecipientBounce so every caller (send-time, NDR scanner, future call sites) is protected — no caller can accidentally bypass it.

Layer 2 — Retry-warning skip in the inbox scanner (LL#446)

A gate in server/warmup-inbox-scanner.ts (the IMAP fan-out that detects bounces by reading mailer-daemon messages in the sender's own inbox). Before a mailer-daemon message reaches the bounce classifier, the scanner checks the subject and the first 5KB of body text for retry-warning markers: "delayed N hours", "still being retried", "warning: message ... delayed", "delivery delayed", "still trying to deliver". If matched, the message is logged as [InboxScanner][retry-warning-skip] and SKIPPED — it never reaches the classifier, never increments the bounce counter, never can promote to the blocklist. Retry-warnings are transient delivery delays that almost always succeed on the next retry — they are NOT real bounces.

Layer 3 — N≥2 distinct-sender corroboration before any blocklist add (LL#435)

The structural evergreen fix. Even when a bounce IS terminal (recipient mailbox truly rejected delivery, e.g. 550 5.1.1 user unknown), the platform now requires N≥2 distinct sender mailboxes to report a bounce for the same recipient domain within a 60-minute window before any global blocklist entry is added. Single-sender bounces are TRACKED in admin_config (so the second sender's bounce can corroborate later) but are NEVER acted on alone. This means a single misclassified bounce — any error pattern, any severity, from any single sender — structurally CANNOT add a recipient domain to the global blocklist. Config: SMTP_BOUNCE_CORROBORATION_THRESHOLD (default 2, clamp [1,10]), SMTP_BOUNCE_CORROBORATION_WINDOW_MIN (default 60, clamp [10,1440]). The tracker is persisted to admin_config['warmupRecipientBounceTracker'] so it survives process restarts; an LL#447 sweeper prunes stale per-domain reporter entries every 15 minutes to prevent stale corroboration after a process restart.

Layer 4 — Cascade-pause sentinel firing within 5 minutes (LL#448)

Every 5 minutes a sentinel in server/warmup-scheduler.ts runs sentinelBouncePauseStorm: COUNT(*) FROM mailboxes WHERE pause_reason='recipient_domain_bounce' AND warmup_enabled=false AND last_error_at > now() - interval '5 minutes'. If the count exceeds 5, an admin alert is written to admin_alerts + system_events_log + stderr (grep target [WarmupScheduler][BOUNCE-PAUSE-STORM] count=N domains=...). The sentinel is throttled to once per 30 minutes via admin_config to avoid alert-spam during legitimate large cleanups. Observation-only — it never auto-unpauses mailboxes; user-initiated re-enable is still required (account-safety: LL#171). The sentinel guarantees that even if all three preceding layers somehow fail and a cascade-pause storm begins, ops sees it within 5 minutes and can intervene.

What this means for you

Companion docs

Back to all documentation | Contact support