2026-08-19 · 4 MIN

A mail scanner can complete your double opt-in: making both mail links refuse to act on a visit

— WRITING

+

2026-08-19 · 4 MIN

A message left at 23:55:10.548. An opt-out for it arrived at 23:55:12.701. Two point one seconds. Nobody opens an inbox, reads a footer and decides they are done in two seconds.

That was corporate mail security doing its job. Microsoft Defender Safe Links, Proofpoint URL Defense and the rest sit in front of a recipient and fetch every address in an incoming message to see where it leads. They do it to the link in the footer, they do it to the one in the unsubscribe header, and they do it on delivery, before the person it was sent to has opened anything.

So if your unsubscribe link removes someone the moment it is visited, every reader behind one of those gateways is removed on the day their first issue lands. Nothing throws. No log says why. The list just quietly gets smaller. The Standup's unsubscribe route worked that way until it was changed on 2026-08-13.

The rule is already written down

RFC 8058 is built around this exact problem. One-click unsubscribe lives on a form submission rather than a plain visit, because a scanner issues the plain visit and never the submission. So a visit now renders a page with a single button, and only pressing the button writes anything.

The Standup article on GitHub notification setup, showing headline and first instruction section — Professional published guide with structured numbered instructions and clear hierarchy.

The Standup article on GitHub notification setup, showing headline and first instruction section — Professional published guide with structured numbered instructions and clear hierarchy.

The same door, walked the other way

The interesting part came a day later, in the route nobody thinks of as dangerous.

The confirmation email for a new subscriber ends with this: "You received this because someone entered this address at standup.kynth.studio. If that wasn't you, ignore this email." The rest of that sentence promises no list is joined without confirming.

While the confirm route completed on a visit, that promise was false. A scanner in front of the recipient would mark the record confirmed on delivery. Someone who mistyped their address, or a stranger who typed yours, ends up on a list neither of them agreed to, and the only signal that they wanted it is a fetch made by a security appliance.

The record is the whole point. Double opt-in exists to produce evidence that a human being asked. A confirmation a machine can finish is evidence of nothing, and it is the thing you have to stand behind when a complaint arrives.

the unsubscribe linkthe confirmation link
what a scanner's visit used to dostamp unsubscribed_atset confirmed to true
who it hurtsa reader removed before they reada person added who never agreed
standard that covers itRFC 8058, one-click on POSTnone, nothing but a browser ever completes a confirmation
changed on2026-08-132026-08-14
what a bare visit does nowrenders one buttonrenders one button
what the write answers witha bare 200 for a one-click client, a page for a browseralways a page

Both halves ended up as nine lines and one shared module:

/** Human click on the link in the confirmation email. CONFIRMS the intent, does not act on it. */
export async function GET(req: Request) {
  const token = mailToken(req);
  if (!token) return mailPage("That link didn't work", BAD_LINK, 400);
  const action = `/api/subscribe/confirm?token=${encodeURIComponent(token)}`;
  return mailPage('One more click', confirmForm(action, 'Confirm my subscription'));
}

/** The only path that writes. */
export async function POST(req: Request) { /* ... */ }

The error message that blamed the wrong party

One more thing surfaced while moving both routes onto shared code. The token in those links is a uuid column, confirm_token, and it is the only token column the subscriber table has. A mail client that breaks a long line in the middle of that token does not send a value that fails to match. It sends a value the database refuses to read at all, which comes back as invalid input syntax, error 22P02.

Both routes caught that and told the reader "Something went wrong on our side." The problem was in their hand, not on the server, and both routes already carried the right sentence for it. A shape check now runs before the query, so a link mangled in transit gets the page that tells them to ask for a fresh one.

The footer under both buttons says why the button is there: automated mail scanners follow links in email, and a subscription nobody chose is worse than one extra click.

That product is The Standup, and this is how we built it: https://standup.kynth.studio/?utm_source=kynth-hashnode&utm_medium=social&utm_campaign=kynth


One shipped product, taken apart, once a month. What it does, what it cost to build, what the pipeline behind it looks like, and what the numbers did — read off the repository and the live site, not written from memory. Join the list.

← All writing