List Hygiene as a Control Loop: Re-Engagement and Sunset Policies That Protect Deliverability
Dead addresses are not neutral ballast — Gmail, Yahoo, and Microsoft weaponize them against your sending reputation. Here is the engineer's playbook: a subscriber state machine, an engagement-scoring recipe that survives Apple MPP, a re-engagement probe that produces a hard signal, and the SQL and cron to sunset dead addresses before they push you past the 0.3% complaint cliff.
EvilMail TeamJuly 15, 202612 min read
A dead address is not idle — it is working against you
Mailbox providers do not score you on what your ESP dashboard shows. Gmail, Yahoo, and Microsoft score you on recipient-side behavior: opens, replies, folder moves, and the "report spam" button. An address that never engages produces exactly one class of signal over time — the absence of anything positive, and eventually a complaint or a bounce. That address does not fail quietly in isolation. It drags down the reputation of every good subscriber sitting in the same send.
The math is unforgiving. Since February 2024, any domain sending 5,000+ messages a day to Gmail must authenticate with SPF and DKIM and DMARC, honor one-click unsubscribe within two days (RFC 8058), and keep its spam complaint rate below 0.3% as reported in Google Postmaster Tools. Yahoo shipped the same requirements the same month. A 0.3% cap buys you exactly 3 complaints per 1,000 delivered before you are over the line — and dormant recipients complain at wildly disproportionate rates, because the person who forgot they ever signed up is the person who hits "spam" instead of scrolling to your unsubscribe link. An aging, unpruned list does not hold still. It drifts toward that cliff on its own.
So treat hygiene as what it actually is: a control loop. You send, you measure recipient behavior, you recompute each subscriber's state, and you actuate — the actuator is your sunset policy. Re-engagement is the last diagnostic gate before you conclude an address is provably dead and prune it. Reject the vanity metric while you're at it: a 40k list that reaches the inbox beats a 200k list that rots in spam, every single send.
Re-Engagement & Sunset Policies: List Hygiene for Deliverability — EvilMail Blog
Classify before you prune: the four states of an address
Every subscriber moves through a state machine. Model it explicitly, because the entire automation layer downstream depends on these transitions being well defined.
The states are Active, Cooling, Dormant, and Sunset/Suppressed. Positive engagement — a click, a reply, a login — resets a subscriber to Active from anywhere upstream of suppression. Absence of engagement walks them right, one bucket at a time.
What matters more than the state names is refusing to treat distinct failure classes identically:
Hard bounce (5.1.1, no such mailbox): the address is dead. Suppress immediately. There is nothing to re-engage.
Repeated soft bounce (5.2.2, mailbox full; 4.x transient): retry on a backoff, but a mailbox that has been full for weeks is functionally abandoned — sunset it.
Pristine spam trap: an address that was never a real user, seeded by the provider on scraped or purchased lists. If you have these, your acquisition is broken upstream; no hygiene job saves you.
Recycled spam trap: an address that *was* a real person, went dead, hard-bounced for a dormancy period, and then got re-armed by the provider as a trap. This is the specific monster sunset policies exist to kill. It poisons every good address that shares the send.
Role accounts (info@, sales@) and disposable/temp addresses: structurally low-engagement — gate them at signup rather than mailing them for months.
The recycled trap is why last-engagement date is not optional telemetry. It is the only signal that catches an address *before* the provider turns it into a weapon.
Instrument the signals you'll actually sunset on
Engagement is an event stream, not a boolean. You cannot sunset intelligently off is_active. Log every meaningful interaction as a row.
sql
CREATE TABLE engagement_events (
id BIGSERIAL PRIMARY KEY,
recipient TEXT NOT NULL,
message_id TEXT,
event_type TEXT NOT NULL, -- open|click|reply|login|bounce|complaint|unsub
smtp_code TEXT, -- e.g. '5.1.1' for bounces
occurred_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX ON engagement_events (recipient, occurred_at);
-- Per-subscriber rollup you actually query against
CREATE MATERIALIZED VIEW subscriber_state AS
SELECT recipient,
MAX(occurred_at) FILTER (WHERE event_type='open') AS last_open_at,
MAX(occurred_at) FILTER (WHERE event_type IN ('click','reply','login')) AS last_hard_at,
COUNT(*) FILTER (WHERE event_type='complaint') AS complaints
FROM engagement_events
GROUP BY recipient;
Now the open-tracking caveat, and it is not a footnote. Since iOS 15 (September 2021), Apple Mail Privacy Protection pre-fetches your tracking pixel through an Apple proxy whether or not a human ever looked at the message. Roughly half of consumer opens are MPP-inflated and detached from any real read. Never sunset on opens alone, and never count an open as proof of life. Weight the signals by how hard they are to fake:
score(event) by type: reply=8 site_login=6 click=5 open=1 complaint=-100
time decay: weight * 0.5 ^ (age_days / 30)
A click, a reply, and a login are human acts a proxy cannot manufacture. An open is worth a single discounted point that halves every 30 days. Sum it per subscriber and you have a defensible engagement score that survives the MPP era.
For bounces, parse the DSN and map the SMTP enhanced status code (RFC 3463) into the state machine rather than treating every non-delivery as equal:
Final-Recipient: rfc822; [email protected]
Action: failed
Status: 5.1.1 ← bad mailbox → suppress now
Diagnostic-Code: smtp; 550 5.1.1 The email account does not exist
5.1.1/5.1.2 are hard — suppress. 5.2.2 (mailbox full) is soft — retry, then sunset. Anything 4.x.x is transient — retry with backoff, do not touch state yet.
The re-engagement campaign: your last diagnostic probe
A win-back is not a nag sequence. It is a controlled experiment with one job: extract a hard signal from a subscriber who has gone quiet, so you can decide whether they are alive or dead. Design it like a probe, not a drip.
Trigger: no meaningful engagement (click/reply/login, not open) for a window scaled to your cadence — 90 days for daily senders, 180+ for monthly senders.
Cadence: a tight 2–3 message sequence over 14–21 days. Not a six-week drip that keeps pounding a dead box and manufacturing complaints.
Content: a single unambiguous CTA — "Confirm you still want these emails" — that *requires a click* to register. A preference-center link works; a passive "we miss you" with no interaction to capture is useless as a diagnostic.
Isolation: send the whole re-engagement stream from a separate IP or subdomain pool. These are your lowest-engagement recipients by definition; if the send tanks, you do not want that reputation damage bleeding into your primary stream.
Exit rule, set in advance: anyone who does not produce a hard click by the end of the window is sunset. Full stop, no exceptions, no "one more try."
Every message still carries a working RFC 8058 one-click unsubscribe header — a clean unsubscribe is a *good* outcome here, far better than a complaint:
Writing the sunset policy: thresholds, cadence, exceptions
Here is the opinionated, numeric version. Adapt the numbers to your cadence, but ship *numbers*, not vibes.
Sunset after N days of zero hard engagement, scaled to send frequency: daily senders ~90d, weekly ~120–150d, monthly ~180–270d.
Hard bounce → immediate suppression. No re-engagement attempt on a dead mailbox.
Complaint → immediate global suppression. One "report spam" and they never receive another marketing send, ever.
Carve-outs that must never sunset on engagement: transactional mail — receipts, password resets, security alerts. Recipients act on these without clicking a tracked link, so engagement scoring does not apply. Also exempt recent purchasers and customers with an active subscription.
Suppress, do not delete. Keep a permanent suppression table. Re-importing a burned address next quarter is the single most common self-inflicted reputation wound in this whole discipline.
On consent and GDPR: the suppression list is itself a legitimate-interest record — you are storing an address specifically to *not* contact it. Keep the minimum needed to enforce that, and document why.
Automate it or it will never happen
Hygiene that depends on someone remembering to run a cleanup is hygiene that does not exist. Build a nightly job that recomputes states and moves subscribers between them idempotently. Run it in --dry-run first and eyeball the report before you ever flip a live subscriber to suppressed.
sql
-- Flag dormant subscribers: no HARD engagement in 90 days, not already suppressed
WITH dead AS (
SELECT s.recipient
FROM subscriber_state s
LEFT JOIN suppression x USING (recipient)
WHERE x.recipient IS NULL
AND (s.last_hard_at IS NULL OR s.last_hard_at < now() - interval '90 days')
)
INSERT INTO suppression (recipient, reason, created_at)
SELECT recipient, 'sunset_no_engagement_90d', now() FROM dead
ON CONFLICT (recipient) DO NOTHING; -- idempotent
bash
# /etc/cron.d/list-hygiene — recompute state + report every night at 03:15
15 3 * * * mailer /opt/evilmail/bin/sunset-job --dry-run --report=/var/log/sunset/$(date +\%F).json
The send path then checks the suppression table on every message — that is the actuator closing the loop. Nothing leaves the queue to a suppressed address, no matter which campaign queued it.
Watch the loop from the outside, too. Google Postmaster Tools and Microsoft SNDS are the provider's own mirror of your reputation. When your hygiene loop is working, the Postmaster spam-rate panel trends down and holds flat below 0.1%, and your domain/IP reputation stays at "High." Those panels are the closed-loop feedback that tells you the actuator is doing its job.
Gate the front door too
Everything above is downstream cleanup, and it is wasted effort if you let garbage in at the top. Validate at signup, before an address ever reaches the events table:
Syntax + MX check. Reject addresses whose domain has no MX record — there is no mailbox to deliver to.
Reject disposable/temp domains at registration for anything that needs a durable relationship. Consume a maintained disposable-domain blocklist and refuse those signups for account and marketing lists.
Double opt-in. Store only confirmed addresses. A confirmed-opt-in list never accumulates typo'd or hostile addresses, and it is the cleanest possible defense against pristine traps.
There is a nuance specific to running mail infrastructure like evilmail: temp mail is a legitimate product, and disposable addresses are a feature for the people who use them. But your *own* marketing and account list must never be built from disposable addresses — that is exactly the failure class your sunset job spends its nights pruning. Gate your own front door even while you provide the throwaway mailboxes on the other side. Reach for real-time verification (an API call at signup) when the signup path can absorb the latency, and periodic batch verification for lists you inherited or imported.
Checklist: ship a sunset policy this quarter
Log engagement as an event stream (open/click/reply/login), not a boolean flag.
Weight click/reply/login far above opens; never sunset on opens — MPP makes them lie.