DEV Community

Hira for LayerCall

Posted on

The unsubscribe link you can't remove is also your login

Someone can click "unsubscribe" on a sign-in code and lock themselves out of their own account. You won't find out. The send reports success, same as always.

Here's how that happens.

Our email provider is CSA-certified, which means it attaches a List-Unsubscribe header to everything it relays. Not just marketing. Everything. There's no setting to turn it off for a transactional-only account, and when I set my own List-Unsubscribe it got quietly replaced with theirs. I pulled the raw source of a delivered message to check, because I didn't believe the docs.

Mail clients turn that header into a button. Apple Mail puts an Unsubscribe banner across the top of the message. Gmail does its own version.

So the six-digit code you send someone to get into their account arrives with an unsubscribe button stapled to it.

Click it and the address goes on the provider's suppression list. After that every message to that person is dropped at the relay. The API still returns 2xx. Nothing in your app notices. They ask for a code, your login form says "check your inbox", and nothing is ever going to arrive. They can't get in, and they can't tell you either, because your support flow emails them too.

No error, no bounce, no log line. From your side everything looks fine. From theirs your product is just broken.

The clean fix is a separate provider for auth mail only, and that's where I'm heading. It's not a quick job though. Your auth provider sends its own confirmations and magic links, so pointing that at a different relay means new credentials, new DNS, a warm-up period and a second sender reputation to look after. Until that's done your sign-in codes and your newsletters live or die together.

So here's what I did in the meantime.

First, check the suppression list before triggering the code. You might not be able to ask about one address — on mine, GET /blockedContacts/{email} returns 404 whether or not the address is on the list, because that path only exists for DELETE. The list endpoint ignores an email filter too. So you fetch the whole list and match locally.

// Cached, because this sits on the login path.
let cache = null;

async function suppressed(email) {
  const key = process.env.ESP_API_KEY;
  if (!key) return null;                    // fail open, always

  if (!cache || Date.now() - cache.at > 300_000) {
    const rows = await fetchWholeSuppressionList(key);   // paginate it
    if (rows) cache = { at: Date.now(), map: rows };     // keep stale on failure
  }
  return cache?.map.get(email.toLowerCase()) ?? null;    // reason, or null
}
Enter fullscreen mode Exit fullscreen mode

I got this wrong the first time in a way worth stealing. I checked for HTTP 200 on the per-address path, and since that path 404s for suppressed and healthy addresses alike, my check reported "fine" for every customer, forever, without ever failing. I only caught it because I tried it against an address I already knew was on the list. Do that.

It also has to fail open. A login form that turns people away because a third-party API timed out is a worse outage than the one you're preventing.

Second, say which reason it is. A hard bounce and an unsubscribe are opposite situations. Telling someone "you unsubscribed" when their mailbox actually rejected us is wrong and a bit insulting — their address is fine. So: unsubscribed gets "this address was unsubscribed from our messages, which also stops sign-in codes", and a bounce gets "earlier mail to this address bounced, so our provider stopped trying — if the address is right, ask us to re-enable it."

Third, give them a way back that doesn't need email. This is the part people skip. A support form works while they're suppressed, because filing a ticket doesn't require you to email them. A support email address obviously doesn't. And say plainly on the confirmation screen that no confirmation email is coming, since that's the exact thing they're writing in about.

One thing I'd argue against: don't automate the un-suppression.

A hard bounce means the mailbox doesn't exist, and re-sending to it damages your sending reputation for every other customer. That's how a domain ends up filtered. An unsubscribe means they chose, and reversing that automatically is what GDPR and CAN-SPAM specifically prohibit — doing it systematically on a CSA-certified account puts the account itself at risk, and that account is carrying your sign-in codes. The blast radius of getting it wrong is much bigger than the problem.

"A customer says they clicked it by accident" is real and common. That's a person's call. One click in your admin tool, not a cron job.

The email specifics here are mine, but the shape isn't. Somewhere in your system is an action that reports success while doing nothing, and the only symptom is a customer who goes quiet.

Top comments (0)