After my transactional email checklist, a reader left a comment I keep thinking about. They had to decide what happens when they reply to someone's support message, and that person has switched all email off. Their answer: the reply still goes out, because the person wrote first. A bounced address still stops it.
That's the right call, and it points at something most apps get wrong. They keep one "do not email" list and check it before every send. But an unsubscribe, a bounce and a spam complaint don't mean the same thing, so they shouldn't stop the same emails.
Here's the rulebook I'd use.
Three kinds of email
- Marketing: newsletters, launches, promotions. You decide to send it.
- Transactional: password resets, receipts, verification codes. The person's action triggers it, and they're waiting for it.
- Conversational: replies to a message the person sent you, such as a support ticket. They started the conversation.
Five signals
| Signal | What it tells you | Marketing | Transactional | Reply to their message |
|---|---|---|---|---|
| Unsubscribe | "Stop sending me marketing" | Stop | Send | Send |
| Hard bounce | The address doesn't exist | Stop | Stop | Stop |
| Repeated soft bounces | The address keeps failing | Stop | Stop | Stop |
| Spam complaint | "I don't want email from you" | Stop | Stop | Stop |
| Manual block | You, or they, asked for it | Stop | Stop | Stop |
A single soft bounce, such as a full mailbox, isn't on the list. Retry it. Only suppress the address when it keeps failing.
Rule 1: an unsubscribe is about the content, not the address
When someone clicks unsubscribe on your newsletter, they're saying "stop sending me this kind of email". They aren't saying "never let me reset my password".
If an unsubscribe blocks transactional email, the person who unsubscribed last year can't log in today, and they'll think your product is broken. So check unsubscribes only before marketing sends.
The rules agree. In the US, CAN-SPAM looks at an email's primary purpose: transactional and relationship messages aren't treated like commercial ones. And Gmail and Yahoo's bulk-sender rules ask for one-click unsubscribe on marketing and subscribed email, not on a password reset.
Rule 2: a bounce is about the address
A hard bounce means the mailbox doesn't exist. That's true whatever you're sending, so stop everything to that address: campaigns, receipts and replies.
Sending again doesn't just fail. Mailbox providers watch how often you send to addresses that don't exist, and a high bounce rate hurts the delivery of every other email you send.
Rule 3: a complaint is about you
A spam complaint is the strongest signal you can get. It doesn't mean the address is bad. It means this person doesn't want email from you. Stop everything from your sender to them, including transactional email, even though that feels harsh.
If you run a platform that sends for many brands, keep complaints scoped to the brand that was reported. A complaint about one sender shouldn't block a different company's password reset.
Rule 4: check twice
Most apps check suppression when they queue an email. But an address can bounce or complain between queueing and sending, especially for scheduled emails. Check again right before the email goes out, and drop any recipient who was suppressed in the meantime.
Rule 5: don't let a transactional email turn into marketing
This one came straight from the comment. A support reply or a receipt keeps its transactional status only while it's about the thing the person asked for. Add a newsletter footer, a "you might also like" block or a promo code, and you've made it marketing. Then the unsubscribe rules apply again, and you'll want an unsubscribe link in it.
Keep transactional emails boring. That's a feature.
In code
The whole rulebook fits in one small function. Store each suppression with its reason, then decide per email type:
// reason: 'unsubscribe' | 'hard_bounce' | 'soft_bounce_repeated' | 'complaint' | 'manual'
// type: 'marketing' | 'transactional' | 'reply'
function canSend(type, suppression) {
if (!suppression) return true;
if (suppression.reason === 'unsubscribe') return type !== 'marketing';
return false; // bounces, complaints and manual blocks stop everything
}
canSend('transactional', { reason: 'unsubscribe' }); // true: the password reset goes out
canSend('marketing', { reason: 'unsubscribe' }); // false: no newsletter
canSend('reply', { reason: 'hard_bounce' }); // false: the address doesn't exist
The important part isn't the function. It's storing the reason with every suppression. If your table only says "suppressed: true", you can't make any of these decisions later.
How we do it in Cresca
These are the rules we follow in Cresca. Campaigns skip every suppressed address. Email sent through the transactional API ignores marketing unsubscribes, but hard bounces, repeated soft bounces (3 in 30 days), spam complaints and manual suppressions still block it. Complaints only block the account that was reported. And we check again right before each email is sent. The suppression docs have the details.
Your turn
Thanks again to the reader who brought up support replies: that's now rule 5. What other email in your app doesn't fit neatly into "marketing" or "transactional"? I'd like to hear the edge cases.












