Skip to content

Suppression list primitive (bounce + complaint + unsubscribe store) #59

Description

@productdevbook

Problem

No transactional ESP SDK ships a suppression layer, but every production team builds one. SES, SendGrid, Postmark, Mailgun all auto-suppress bounces, but each exposes a different API. When you use multiple providers (we support 13!) you need a unified store.

Evidence: sendgrid-docs #5177, #6194, SES SNS bounce tutorials, "no `sg_message_id` on webhook" complaints. Recurring pattern across all providers.

Proposal

  1. New `SuppressionStore` interface (parallels `IdempotencyStore`):
    ```ts
    interface SuppressionStore {
    has(recipient: string): Promise<SuppressionRecord | null>
    add(recipient: string, reason: SuppressionReason, source?: string): Promise
    remove(recipient: string): Promise
    }
    ```
  2. Built-ins: `memorySuppressionStore()`, `unstorageSuppressionStore(storage)`.
  3. `withSuppression(store, { onBlocked?: 'drop' | 'error' })` middleware — checks before driver.send.
  4. `unemail/webhooks` auto-registers bounces/complaints into the store (wire up existing normalized events).
  5. Docs: drop-in recipe for SES+SNS, Resend webhook, SendGrid event webhook.

Acceptance

  • SuppressionStore interface + 2 built-ins + tests
  • Middleware blocks sends with `EmailError.code = 'SUPPRESSED'`
  • Bounce webhook auto-adds to store (opt-in)

Metadata

Metadata

Assignees

No one assigned

    Labels

    deliverabilityDKIM, ARC, DMARC, MTA-STS, BIMImiddlewareComposable middleware (retry, rate-limit, …)priority-criticalMust landv1.1v1.1 milestone (shipped)

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions