emailverifier.dev
Posts

List-Unsubscribe Headers: How One-Click Unsubscribe Works

| 6 min read | Usama Ejaz
An ivory email envelope passes through one lime unsubscribe gate, where its delivery path ends.

To implement one-click unsubscribe, send both List-Unsubscribe and List-Unsubscribe-Post, put a recipient-and-list-specific opaque token in an HTTPS URL, cover both headers with DKIM, and accept the exact RFC 8058 POST without redirects or extra user interaction.

That is the short answer. The important part is that this is a small protocol, not just a link in an email footer.

I see the same two mistakes repeatedly: a sender adds only List-Unsubscribe and assumes one-click is done, or the unsubscribe URL changes state on GET. The first is incomplete. The second can unsubscribe people when a security scanner merely inspects the URL.

The two headers form one contract

RFC 2369 defines List-Unsubscribe as a list command. It can advertise more than one mechanism, commonly an HTTPS URL and a mailto: URL.

RFC 8058 adds the signal that the HTTPS URL accepts an automatic one-click request. A hypothetical message would contain:

List-Unsubscribe: <https://mail.example/unsubscribe/opaque-demo-token>,
 <mailto:unsubscribe@example?subject=unsubscribe>
List-Unsubscribe-Post: List-Unsubscribe=One-Click

The angle brackets matter. So does the HTTPS URL. For one-click support, RFC 8058 requires one List-Unsubscribe field containing an HTTPS URI and one List-Unsubscribe-Post field containing exactly List-Unsubscribe=One-Click.

The HTTPS URL must already identify the recipient and the list. The receiver does not add an email address or campaign ID to the POST body. That body carries only the fixed one-click signal.

Finally, a valid DKIM signature must cover both headers. In the DKIM-Signature field, both names need to appear in the h= list. Without that authenticated binding, a mailbox provider should not offer the one-click action.

What request reaches your server?

After the recipient consents in their mail client, the receiver sends an HTTPS POST to the advertised URL. With URL-encoded form data, the transaction looks like this:

POST /unsubscribe/opaque-demo-token HTTP/1.1
Host: mail.example
Content-Type: application/x-www-form-urlencoded
Content-Length: 26

List-Unsubscribe=One-Click

The endpoint should validate the token and the exact body, record the opt-out, and return a successful response directly. RFC 8058 says the sender must not redirect this POST because redirects have historically changed POST requests into GET requests.

It also says the request must not depend on cookies or other browsing context. This call comes from a receiving system, not from the subscriber's logged-in browser. Requiring a session, CAPTCHA, preference page, or confirmation click means it is no longer one-click.

Why must GET be harmless?

Email security systems often fetch URLs before a person clicks them. That behavior is the reason RFC 8058 separates an automatic POST from an ordinary URL visit.

A safe endpoint gives the two methods different meanings:

  • GET may show a confirmation or preferences page, but it does not change subscription state.
  • POST with the exact one-click body performs the scoped unsubscribe.

“Why not make GET unsubscribe too? It is simpler.” It is simpler until a link scanner visits every URL in a campaign and removes active subscribers. Safe HTTP semantics are doing real work here.

Make the POST idempotent and scoped

A mailbox provider can retry a request. Two identical POSTs should therefore create one durable suppression event, not two conflicting records or an error on the second call.

I would model the URL as a capability with three properties:

  1. The token is opaque and hard to guess. Do not put a raw email address in the path.
  2. The server maps it to one recipient and one mailing list or message category.
  3. The write is idempotent on that recipient-and-list pair.

RFC 8058 explicitly recommends an opaque or otherwise hard-to-forge component and server-side validation. The scope matters just as much. An opt-out from “product news” should not silently disable password resets, receipts, or security notices.

This compact Node.js fixture shows the state boundary. It hashes the token before lookup, keeps GET read-only, rejects the wrong body, and deduplicates the suppression event:

function handleUnsubscribe({ method, token, body }) {
  if (method === "GET") {
    return { status: 200, view: "confirmation", changed: false };
  }

  if (method !== "POST" || body !== "List-Unsubscribe=One-Click") {
    return { status: 400, changed: false };
  }

  const subject = tokens.get(sha256(token));
  if (!subject) return { status: 404, changed: false };

  const key = `${subject.recipientId}:${subject.listId}`;
  const changed = !suppressionEvents.has(key);
  if (changed) suppressionEvents.set(key, {
    ...subject,
    reason: "one_click_unsubscribe"
  });

  return { status: 204, changed };
}

I ran four checks against the complete fixture:

4/4 checks passed
GET changed state: no
duplicate POST events: 0

In production, use a database uniqueness constraint or atomic upsert for the same guarantee. An in-memory map only makes the state transition easy to inspect.

Headers are not the whole unsubscribe system

One-click is a transport for an opt-out request. It does not decide what “unsubscribe” means inside your product, and it does not replace the visible unsubscribe link in the message body.

Your endpoint should write the same durable event that a preference page writes. The send path should consult that state before every non-transactional message. My suppression-list model covers that downstream decision in detail, including scope and event history.

For current operational context, Gmail requires one-click unsubscribe for marketing and subscribed messages from senders that deliver more than 5,000 messages a day to Gmail accounts, and its sender guidelines show the same two-header and POST pattern. That provider rule is a reason to implement the standard, but the implementation should follow the RFC contract rather than a screenshot of one inbox UI.

The deployment test I would require

Before enabling these headers across a campaign stream, capture a delivered message and verify the raw source:

  1. There is exactly one field with each header name.
  2. The HTTPS URL contains a valid opaque token for the intended recipient and list.
  3. The DKIM signature validates and its h= tag covers both headers.
  4. A plain GET causes no state change.
  5. The exact POST succeeds without a redirect, cookie, or second step.
  6. Repeating the POST leaves one suppression event.
  7. The next marketing send excludes that scoped recipient.

That last check closes the loop. A perfect header with a disconnected suppression store still sends unwanted mail.

The clean mental model is simple: the headers authenticate and advertise the action, the POST carries the action, and your suppression state enforces it. Keep those three parts explicit and one-click unsubscribe becomes predictable instead of mysterious.

Continue reading