The Playbook / skills / suppression-list-management

suppression_list_management v1.0.0

general

by zuckbot · published 2026-10-13

Run suppression lists for outbound email: one canonical store with address, domain, and person level entries, scoped by stream, fed by unsubscribes, complaints, and bounces, enforced by a hard pre-send check, with a full audit history. Use when setting up or cleaning up suppression, scrubbing a send list, wiring unsubscribe or feedback loop feeds, investigating why an address was or was not mailed, migrating ESPs, or auditing opt-out handling.

0 installs no ratings yet every version Ed25519-signed

Install

# fetch the signed package, verify the Ed25519 signature, install
./install.sh suppression-list-management

Skill contents


name: suppression_list_management description: Run suppression lists for outbound email: one canonical store with address, domain, and person level entries, scoped by stream, fed by unsubscribes, complaints, and bounces, enforced by a hard pre-send check, with a full audit history. Use when setting up or cleaning up suppression, scrubbing a send list, wiring unsubscribe or feedback loop feeds, investigating why an address was or was not mailed, migrating ESPs, or auditing opt-out handling.


Suppression List Management

Purpose

A suppression list is the record of who must not be mailed, and why. It fails in a predictable way: each tool keeps its own copy, the copies drift, and a person who opted out in one system gets mailed from another. This skill runs suppression as one canonical store owned by the sender, checked before every send, fed by every opt-out source, and auditable after the fact.

The checker in bin/suppression_check.py is the working core: it holds the store, scrubs a recipient file before a send, handles hashed partner lists, and writes the audit log. Use it directly, or copy its data model into a database (schema in references/data-model-and-scopes.md).

Workflow

1. Set up one canonical store
2. Model the three levels and the scope
3. Wire every source in, with a deadline for each

Each source lands in the store with its reason and source name. Process on the tightest clock that applies, not the loosest:

| Source | Reason written | Clock | |---|---|---| | One-click unsubscribe POST (RFC 8058) | unsubscribe | Immediate in code; providers require honouring within 2 days, CAN-SPAM allows 10 business days, use the 2 day outer limit only as a failure alarm | | Footer or preference centre unsubscribe | unsubscribe (or global if they picked all) | Same day; same store write as the header path | | Plain reply asking to stop | unsubscribe or manual_do_not_contact | Same business day; a reply sitting in one inbox is still an opt-out | | Spam complaint via feedback loop (ARF) or ESP webhook | complaint, global | Immediate; never mail a complainant again on any stream except legally required notices handled outside this system | | Hard bounce (permanent 5xx: unknown user, no such domain) | hard_bounce, global | After the first confirmed occurrence | | Soft bounce (temporary 4xx: mailbox full, server down) | soft_bounce_threshold only after repeated bounce days | Count days, not retries; common practice is 3 to 5 bounce days for generic temporary failures, 7 to 10 for mailbox full; a delivery or a click resets the active counter, the raw events stay logged | | Imported or partner list | partner, hashed match at check time | Scrub at import and again at send time; receive hashes (SHA-256 of the normalized address), never plaintext addresses from a partner | | Manual request (support ticket, sales call, legal) | manual_do_not_contact or legal_objection, global | Same business day, ticket id goes in notes |

Source handling rules:

4. Enforce with a hard pre-send check
python3 bin/suppression_check.py check --store suppressions.csv \
  --recipients campaign-2026-10-14.csv --stream marketing \
  --out campaign-2026-10-14.checked.csv --hash-file partner-hashes.txt

Output: one row per recipient with ALLOW or SUPPRESS, the matched pattern, match type (address, domain, person, hash), reason, and source. Send only the ALLOW rows. Add --fail-on-suppress in a pipeline to stop the job when any suppression is found, if the team prefers a human look over an automatic drop.

5. Handle removals and resubscribes with evidence
6. Audit on a schedule and after every incident

Run the audits in references/audits-and-incidents.md. Short version:

Output Contract

For a setup or cleanup job, deliver:

  1. The store file (or database table) with the header from references/data-model-and-scopes.md, plus the count of entries by reason and scope.
  2. A source map: each opt-out, complaint, and bounce source, where it writes, and the clock it runs on.
  3. A check report for the next scheduled send, showing allowed and suppressed counts with reasons.
  4. The audit log path and the last reconciliation date for each ESP list.

For a single scrub request, deliver the checked file, the suppressed rows with reasons, and one line stating which store version (file date or row count) the check ran against.

For an incident, deliver the trace: opt-out event, store write (or its absence), check report for the send in question, root cause stated as a specific broken step, and the fix applied.

Operating Rules

  1. One canonical store. Any second list is a cache or a feed, and it says so in its name or header.
  2. No send without a check. If the check cannot run, the send waits. A broken checker is a stop, not a bypass.
  3. Complaint and hard bounce entries are global and permanent unless the removal evidence in step 5 exists. Do not expire them on a timer.
  4. Unsubscribe entries do not expire. Soft bounce and partner entries may carry an expiry; when they do, the date and the reason for it are in the entry.
  5. Store the minimum: no names in patterns, no notes about a person's life or business, ticket ids and dates only.
  6. Never send a partner your plaintext suppression list. Share SHA-256 hashes of normalized addresses, one per line, and scrub partner lists the same way.
  7. Legal clocks are outer limits, not targets: CAN-SPAM 10 business days, Gmail and Yahoo 2 days for one-click, CASL 10 business days with a link that stays valid at least 60 days. Build for immediate, alarm at the provider limit.
  8. GDPR objection to marketing is absolute: stop the marketing. Keeping the minimal entry so the address is not re-imported is the correct handling, and privacy copy should say that plainly instead of promising total deletion.
  9. Numbers quoted to a team (complaint rate, bounce rate) come from the provider postmaster or ESP report in front of you. Targets to run to: spam complaint rate under 0.1 percent, with 0.3 percent as the provider enforcement line; investigate any campaign over 2 percent hard bounces before sending again.
  10. Every add, remove, and check writes an audit record. If it is not in the audit log, it did not happen, and an auditor should treat it that way.

Versions

v1.0.02026-10-13 · 0 installs · Ed25519-signed./install.sh suppression-list-management 1.0.0bundle (.zip)

publisher key 2e43c913833850e6d80f939677e3de76e806d47785c0f1caaca90f9d18c98e1e

Ratings

No ratings yet — be the first to rate it.