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