Suppression list
Your do-not-send list: everyone who unsubscribed, hard bounced, or complained, with the reason and scope that says which mail is blocked.
See it
What it is
A suppression list is the set of addresses your system checks before sending and refuses to mail. Three things land on it: people who unsubscribed, addresses that produced a permanent recipient failure (hard bounce, mailbox does not exist), and anyone who hit 'report spam'. It is not a segment or a filter, it is a veto that runs after every other targeting decision.
Each row needs a reason and a scope, because those three causes do not mean the same thing. An unsubscribe blocks marketing to that person and can be lifted by fresh, recorded consent if they sign up again. A dead address blocks every stream, transactional included, because nothing you send will arrive. A complaint is a policy call you should make deliberately and write down, since suppressing all mail after a complaint can strand somebody's password reset. Store the source and the consent history next to the reason so 'why can we not email this person' always has an answer.
Every real sending platform (Resend, Postmark, SendGrid, Mailgun, SES) keeps one for you automatically, and that is usually the right default. You reach for your own copy when mail leaves through more than one provider, or when the same human exists in your app database and your campaign tool and you need one answer to 'may we email this person'. Store the address hashed or normalized, plus the reason and the timestamp, because auditors and angry users both ask 'when did you get this'.
The classic mistake is scoping suppression too narrowly or too widely. Too narrow: you suppress per campaign, so someone who unsubscribed from the newsletter starts getting the product digest and reports you. Too wide: you suppress globally on an unsubscribe, so the person who left the marketing list stops receiving password resets and receipts. The working rule is that a marketing unsubscribe is global across all marketing, and transactional email they explicitly asked for (receipts, resets, security alerts) keeps flowing unless the reason says the address itself is dead. Also normalize before you compare, or 'Sam@Example.com ' with a trailing space sails right past the check.
Ask AI for it
Add an email suppression system. Model a suppression as a normalized address (lowercased, trimmed), a reason ('unsubscribed', 'hard_bounce', 'spam_complaint', 'manual'), a scope ('marketing' or 'all_mail'), and a source, so scope follows from reason rather than from one global flag: unsubscribes suppress marketing only, permanent recipient failures suppress all mail, and complaints follow an explicit policy I choose and you state in the code. Record consent as immutable audit events (subscribed, unsubscribed, re-consented, bounced, complained) with timestamps, and resolve the current state from them, so a recorded re-consent can supersede an earlier marketing unsubscribe while nothing ever overrides a dead address. Wrap all outbound mail in a single sendEmail() function that takes the message stream, normalizes the recipient, checks suppressions in scope for that stream, and drops plus logs instead of sending on a hit. Write suppressions automatically from unsubscribe clicks, List-Unsubscribe-Post requests, and provider bounce and complaint webhooks. Never delete audit rows.