Suppression list includes all required sources
Why it matters
A suppression list missing any of its four required sources — unsubscribes, hard bounces, spam complaints, or manual admin adds — has gaps that will cause sends to addresses that should never receive email. CAN-SPAM § 5 mandates honoring opt-outs (unsubscribes); GDPR Art. 21 covers the right to object (maps to unsubscribes and complaints); GDPR Art. 17 covers erasure (maps to manual admin removes); CASL § 6 mirrors CAN-SPAM's unsubscribe requirement. Spam complaints are the most commonly missing source: ESPs surface them via feedback loops that must be explicitly wired to write to the suppression table — they do not suppress automatically unless configured.
Severity rationale
Medium because each missing suppression source creates a specific class of non-compliance (unsubscribes → CAN-SPAM; complaints → GDPR Art. 21) rather than a blanket deliverability failure, but the legal exposure is real.
Remediation
Centralize all four suppression sources through a single addToSuppression function so no source can bypass the global table:
type SuppressionReason = 'unsubscribe' | 'hard_bounce' | 'spam_complaint' | 'manual_add'
async function addToSuppression(email: string, reason: SuppressionReason, addedBy = 'system') {
await db.suppression.upsert({
where: { email: email.toLowerCase().trim() },
create: { email: email.toLowerCase().trim(), reason, added_by: addedBy, suppressed_at: new Date() },
update: { reason, updated_at: new Date() }
})
}
Wire this to: your POST /api/unsubscribe endpoint, your bounce webhook handler (hard bounces only — see ab-000870), your ESP complaint feedback-loop webhook, and your admin contact management UI. Verify all four by querying SELECT DISTINCT reason FROM suppressions — all four values must appear.
Detection
-
ID:
suppression-sources -
Severity:
medium -
What to look for: Enumerate all code paths that write to the suppression table. Count and classify each by source type. The suppression list must include all of: (1) unsubscribes, (2) hard bounces, (3) spam complaints, and (4) manual admin adds. Check whether each of these 4 sources has a code path that writes to the suppression table with an appropriate reason code. Report the ratio: "N of 4 required sources present."
-
Pass criteria: At least 4 of 4 required sources write to the suppression table. The suppression table has records with
reasonvalues covering unsubscribe, hard_bounce, spam_complaint, and manual_add. All four sources write to the same table. -
Fail criteria: Fewer than 4 of the required sources are present. For example: spam complaints are logged in an analytics table but never added to the suppression list, or manual admin adds bypass the suppression table.
-
Skip (N/A) when: Never — all four sources are universally required.
-
Detail on fail: List the missing sources. Example:
"Suppression table only populated from unsubscribes and hard bounces — spam complaints from ESP not written to suppression list"or"Manual admin suppression goes to a separate blocklist table, not the global suppression table" -
Remediation: Ensure all four sources write to the same suppression table:
type SuppressionReason = 'unsubscribe' | 'hard_bounce' | 'spam_complaint' | 'manual_add' async function addToSuppression(email: string, reason: SuppressionReason, addedBy?: string) { await db.suppression.upsert({ where: { email: email.toLowerCase().trim() }, create: { email: email.toLowerCase().trim(), reason, added_by: addedBy ?? 'system', suppressed_at: new Date() }, update: { // If already suppressed for a less serious reason, escalate reason, updated_at: new Date() } }) }Wire this function into: your unsubscribe endpoint, your bounce webhook handler (hard bounces only), your complaint webhook handler, and your admin tools.
External references
- external · CAN-SPAM-Sec5 — CAN-SPAM Act §5 — requires functioning opt-out mechanism honored within 10 business days
- gdpr · Art. 17 — GDPR Article 17 — Right to erasure obliges ceasing processing on valid request
- gdpr · Art. 21 — GDPR Article 21 — Right to object requires cessation of processing
- external · CASL-S6 — CASL Section 6 — unsubscribe mechanism required; withdrawal of consent must be processed within 10 business days
- iso-25010:2011 · functional-correctness — Functional Correctness (functional suitability)
Taxons
History
- 2026-04-18·v1.0.0·Initial import from data-quality-list-hygiene·automated