Skip to main content
AI and automation

Mail scanners were confirming newsletter subscriptions nobody clicked

Moeez Ahmad · Published 16 August 2026 · Updated 8 September 2026 · 6 min read

Double opt-in exists for one reason: a human being must take a deliberate action to prove they want the email. Someone types an address, we send a confirmation link, and only when that link is followed do we treat the subscription as real. The second step is the consent record.

Ours confirmed the subscription the instant anything requested the page. Not anything human. Anything.

Who clicks a link that nobody clicked

Between our confirmation email leaving Resend and a person reading it, the URL inside it passes through machinery that opens links as a matter of routine:

  • Corporate mail security gateways that fetch every URL in every inbound message to check it against threat feeds, before the recipient sees the mail at all.
  • Inbox providers prefetching links to render previews.
  • Anti-phishing scanners, archivers, and DLP tooling doing the same.

None of these are attacks. They are the normal, expected behaviour of email infrastructure, and they are the reason the rule exists that a GET request must not change server state. A GET is a question. It is allowed to be repeated, cached, prefetched, and replayed by intermediaries that have no idea what your application means by it.

Our confirmation page was a GET that performed the mutation directly. So the sequence for a recipient inside a company running link scanning was:

  1. They enter an address, possibly by mistake, possibly not theirs.
  2. The confirmation email arrives at the gateway.
  3. The scanner fetches the link.
  4. They are subscribed.
  5. The email lands in their inbox, unread.

The record in our database says a human clicked to confirm. No human did. The one artifact the whole flow exists to produce was fabricated by a virus scanner.

BROKENmail scannerGET /confirm?token=xpage mutatessubbedno human actedFIXEDmail scannerGET /confirm?token=xrenders buttonscanner stops herehuman clicksPOSTsubbedconsent recorded

The fix, and the thing we kept

The mutation moved behind a server action, which is a POST, and the page now renders an explicit confirm button that a person has to press.

What is worth noting is what the page still does on GET:

// Only a structural pre-check for UX (skip rendering the button for an
// obviously bad link) - the real, trusted verification happens again
// server-side inside confirmNewsletterSubscription when the form submits.
const check = verifyConfirmToken(token);

The page verifies the token on load, but that check is explicitly not trusted. It exists so somebody arriving with an expired link sees "this link has expired" instead of a confirm button that will fail when they press it. The verification that actually authorises the subscription runs again inside the action, on submit.

That duplication is deliberate and we would fight to keep it. A check performed to decide what to render is a user-experience affordance. A check performed to decide whether to mutate is a security control. They look identical, they call the same function, and collapsing them into one is how an application ends up trusting a decision made before the request that mattered.

Why this sits under automation

We ship AI and automation work, and the reflex in that field is to ask how much of a workflow a machine can complete on its own. This bug is a small, unglamorous answer to the same question from the other direction: the machines were already acting. Nobody designed a system to auto-subscribe people. It happened because a link existed, and things that are not people follow links.

The design principle we apply in automation work is the same one that fixes this page. Ask what the cost of being wrong is, then decide who holds the action at that cost. In our Relay triage console, every AI-drafted reply shows its confidence, and an action above the escalation boundary hands control back to a person with an editable draft rather than sending on its own. The reason is identical: a system that can act should still not act when the record of intent is the entire point of the step.

A confirmation click is a consent record. A model's reply is a statement made in your company's name. Neither should be produced by something that cannot mean it.

The check to run on your own code

Search your application for state changes reachable from a GET. Not just newsletter confirmations. Unsubscribe links, invitation acceptances, "mark as read", one-click approvals in notification emails, anything with a token in a query string that does something when visited.

For each one, ask whether you would be comfortable if it fired every time the recipient's employer scanned their mail, because that is the actual behaviour you have shipped. If the answer is no, the mutation belongs behind a POST and a button.

We had exactly one. It took a line of code to fix and it had been quietly manufacturing consent for as long as the form had been live.