# Capability Spec: suppression **Package**: `@suaveplan/suppression` **Version**: 0.0.0 (genesis) **Status**: ADDED --- ## ADDED Requirements ### SUP-01 — Bounce classification `classifyBounce(report)` MUST map SMTP enhanced status codes (RFC 3463) to `BounceType` (`'hard' | 'soft' | 'technical' | 'policy' | 'unknown'`). Hard and policy bounces MUST add permanent suppressions; soft bounces MUST increment a soft-bounce counter and auto-suppress after the configured threshold. ### SUP-02 — Store contract `SuppressionStore` MUST provide `add`, `remove`, `has`, `get`, `list`, `purgeExpired`. `MemorySuppressionStore` MUST implement all methods synchronously. Alternative stores (DB, Redis) adhere to the same contract via async variants. ### SUP-03 — Manager API `SuppressionManager` MUST normalise email addresses (lowercase, trim) before storage. Invalid addresses MUST surface as `SUPPRESSION.INVALID_ADDRESS`. Entries MUST carry `{ address, reason: SuppressionReason, bounceType?, createdAt, expiresAt?, source? }`. ### SUP-04 — Filter contract `SuppressionFilter.filter(addresses)` MUST return `FilterResult { allowed, suppressed }`. Addresses MUST be looked up in a single batched pass. ### SUP-05 — Unsubscribe links `UnsubscribeLink.generateLink({ address, list, ttlSeconds })` MUST produce a URL carrying an HMAC-SHA256 signature over `address|list|expiry`. `verifyLink(url, now?)` MUST reject expired or tampered links with `SUPPRESSION.LINK_EXPIRED` / `SUPPRESSION.LINK_INVALID_SIGNATURE`. `rotateSecret(newSecret, gracePeriod?)` MUST accept both old and new signatures during the grace period. ### SUP-06 — RFC 8058 one-click payload The link MUST include the List-Unsubscribe-Post header form (`List-Unsubscribe=One-Click`) and accept a POST that honours the unsubscribe without further user interaction. ### SUP-07 — Observability Every filter call MUST record `suppression.filter.total{outcome}`. Every suppression MUST record `suppression.suppressed.total{reason}`.