Global suppression list shared across campaigns
Why it matters
A per-campaign suppression list means an unsubscribe from Campaign A has no effect on Campaign B, C, or triggered automations. This is a direct CAN-SPAM § 5 violation: the law requires that opt-outs apply to all commercial email from the sender within 10 business days, not just the campaign the unsubscribe link appeared in. GDPR Art. 21 right to object similarly applies globally — an objection to marketing email must stop all marketing email, not one drip sequence. Per-campaign suppression is architecturally convenient (each campaign manages its own list) but legally and operationally incorrect.
Severity rationale
High because per-campaign-only suppression is a structural CAN-SPAM § 5 violation that allows continued sends to unsubscribed contacts across campaigns, exposing the sender to FTC enforcement action.
Remediation
Enforce a single global suppression table consulted before every send path — marketing, transactional, triggered, and drip:
async function isSuppressed(email: string): Promise<boolean> {
const row = await db.suppression.findFirst({
where: { email: email.toLowerCase().trim() }
})
return row !== null
}
// Apply in every dispatch path:
async function dispatchEmail(to: string, template: string, data: object) {
if (await isSuppressed(to)) {
await db.sendLog.create({ data: { to, template, outcome: 'suppressed' } })
return
}
await esp.send({ to, template, data })
}
Audit every send path (campaign scheduler, transactional trigger, automation worker, drip sequence processor) and add the isSuppressed call if it is missing. Per-campaign exclusion lists can remain for content targeting, but the global suppression check must precede all of them.
Detection
-
ID:
global-suppression-list -
Severity:
high -
What to look for: Enumerate all email send paths in the system (marketing campaigns, transactional sends, triggered automations, drip sequences). Count every distinct send path found. For each, check whether it consults a global suppression table before dispatch. A per-campaign suppression list means an address suppressed from one campaign can still receive sends from another — this is a compliance and deliverability risk. Per-campaign-only suppression does not count as pass.
-
Pass criteria: A single suppression table is consulted before any email is dispatched. 100% of send paths — at least 1 — check global suppression. Count all send paths and report the ratio: "N of N send paths check global suppression." Unsubscribes, hard bounces, and complaints are suppressed globally.
-
Fail criteria: Suppression is per-campaign only, or the global suppression check is bypassed for 1 or more send paths (transactional sends, triggered automations).
-
Skip (N/A) when: Never — global suppression is a universal requirement.
-
Detail on fail: Example:
"Suppression table scoped by campaign_id — unsubscribe from Campaign A does not prevent Campaign B from sending"or"Transactional send path does not check global suppression list before dispatch" -
Remediation: Enforce a global suppression check in all send paths:
async function isSuppressed(email: string): Promise<boolean> { const suppression = await db.suppression.findFirst({ where: { email: email.toLowerCase().trim() } }) return suppression !== null } // In every send dispatch path: async function dispatchEmail(to: string, template: string, data: object) { if (await isSuppressed(to)) { await db.sendLog.create({ data: { to, template, outcome: 'suppressed', suppressed_at: new Date() } }) return // Do not send } await esp.send({ to, template, data }) }
External references
- external · CAN-SPAM-Sec5 — CAN-SPAM Act §5 — opt-out must be honored across all commercial messages from the sender
- gdpr · Art. 21 — GDPR Article 21 — Right to object must be effective for all processing activities
- external · CASL-S6 — CASL Section 6 — unsubscribe mechanism must apply to all commercial electronic messages from sender
- 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