Decknative
Writing8 min

RFC 8058 one-click unsubscribe, done right

RFC 8058 one-click unsubscribe requires two DKIM-signed headers — List-Unsubscribe carrying an HTTPS URI, and List-Unsubscribe-Post carrying the literal value List-Unsubscribe=One-Click — plus an endpoint where GET renders a confirmation page and changes nothing, while POST performs the removal. Acting on GET is the trap. RFC 9110 defines GET as safe, so security gateways and prefetchers fetch URLs out of a message before the human sees it, and they will unsubscribe recipients who never clicked.

Two headers and one routing decision. That is the whole of RFC 8058. The headers are easy and almost everyone gets them right. The routing decision is where lists die: if a GET on your unsubscribe URL performs the unsubscribe, you have built a machine that unsubscribes people who never touched the link.

What does RFC 8058 actually require?

RFC 8058, "Signaling One-Click Functionality for List Email Headers", is a Standards Track document from January 2017. It layers a signal on top of the older List-Unsubscribe header defined in RFC 2369, which since 1998 has carried "the command (preferably using mail) to directly unsubscribe the user".

Section 3.1 puts four obligations on the sender:

  • The List-Unsubscribe header MUST contain one HTTPS URI. It MAY also contain other non-HTTP/S URIs such as a mailto:.
  • The List-Unsubscribe-Post header MUST contain the single key/value pair "List-Unsubscribe=One-Click". No other value is legal — the ABNF in Section 5 defines postarg as exactly that literal.
  • The message MUST carry a valid DKIM signature covering at least the List-Unsubscribe and List-Unsubscribe-Post headers.
  • The URI MUST carry enough information to identify the recipient and the list on its own, because "there is no provision for extra POST arguments". One-click cannot ask the user anything.
The headers as they appear in the message. The h= tag is the part people forget.
List-Unsubscribe: <mailto:unsub@example.com>,
    <https://example.com/u/9f3a1c2e4b7d>
List-Unsubscribe-Post: List-Unsubscribe=One-Click
DKIM-Signature: v=1; a=rsa-sha256; d=example.com; s=sel1;
    h=from:to:subject:date:message-id:list-unsubscribe:
      list-unsubscribe-post;
    bh=...; b=...

Section 3.2 then describes what a receiver does with it. The mail receiver — in practice the recipient’s mailbox provider, sometimes their client — POSTs to the HTTPS URI with the key/value pair as the request body, encoded as multipart/form-data (SHOULD) or application/x-www-form-urlencoded (MAY). Crucially: "The target of the POST action is the same as the one in the GET action for a manual unsubscription, so this is intended to allow the same server code to handle both." One URL. Two methods. Two very different behaviours.

Why must GET not perform the unsubscribe?

Because HTTP says so, and because the mail ecosystem takes HTTP at its word. RFC 9110 Section 9.2.1 defines a safe method as one whose "defined semantics are essentially read-only; i.e., the client does not request, and does not expect, any state change on the origin server". GET, HEAD, OPTIONS and TRACE are the safe methods.

The same section explains why the distinction exists at all: "to allow automated retrieval processes (spiders) and cache performance optimization (pre-fetching) to work without fear of causing harm." Then it states the rule that this entire article is about, and it is a MUST:

If the purpose of such a resource is to perform an unsafe action, then the resource owner MUST disable or disallow that action when it is accessed using a safe request method. Failure to do so will result in unfortunate side effects when automated processes perform a GET on every URI reference for the sake of link maintenance, pre-fetching, building a search index, etc.

RFC 9110, Section 9.2.1

Note where RFC 9110 puts the responsibility. The definition of safety "does not prevent an implementation from including behavior that is potentially harmful, that is not entirely read-only, or that causes side effects while invoking a safe method." What is important, the section says, "is that the client did not request that additional behavior and cannot be held accountable for it." A scanner that GETs your unsubscribe URL is behaving correctly. The bug is yours.

This is not a theoretical concern that the RFC authors invented. It is the stated motivation for RFC 8058 existing. From its own abstract: "The need for this arises out of the actuality that mail software sometimes fetches URLs in mail header fields, and thereby accidentally triggers unsubscriptions in the case of the List-Unsubscribe header field."

And the fetching is documented behaviour on the receiving side too, not folklore. Microsoft’s own Safe Links documentation states that "As long as Safe Links protection is turned on, URLs are scanned prior to message delivery, regardless of whether the URLs are rewritten or not", that "URLs that don’t have a valid reputation are detonated asynchronously in the background", and that one policy option holds messages entirely until scanning finishes. Scanning happens before the recipient has seen the message, let alone clicked anything.

What does the failure actually look like?

Worth recognising before it happens to you, because the signature is distinctive and follows deductively from the two RFCs above. A campaign goes out. Within minutes, unsubscribe records start arriving — long before most recipients have opened anything.

Three things in the data give it away:

  • The timestamps cluster tightly around delivery time instead of spreading across the working day the way human behaviour does.
  • They arrive in blocks grouped by recipient mail provider, because each provider’s gateway processes its queue at its own pace.
  • None of them have a matching click on the visible footer link, and many are for addresses that never registered an open at all.

Nobody unsubscribed. The endpoint treats the token in the path as the instruction rather than as an identifier, so a plain GET is enough to suppress the address — and every security gateway, link checker and preview fetcher on the receiving side dutifully performs that GET. How often this happens across the industry is not something anyone publishes a measurement of, so treat the frequency as unknown. The mechanism is not in doubt: RFC 8058 names it in its own abstract as the reason the standard exists.

What is the correct endpoint shape?

One route, one opaque token, behaviour keyed on the method. RFC 8058 Section 3.1 says the URI "SHOULD include an opaque identifier or another hard-to-forge component", and the server "SHOULD verify" it — a signed token or a random 128-bit value, not a base64 of the email address.

One URL, four request types, four correct behaviours.
RequestWho sends itWhat the endpoint must doResponse
GET /u/<token>Link scanners, prefetchers, and humans clicking the footer linkNothing. Validate the token, render a page with a POST form.200 with a form
HEAD /u/<token>Gateways checking that the link resolvesNothing.200, empty body
POST /u/<token> with body List-Unsubscribe=One-ClickThe recipient’s mailbox provider or client, on their one-click actionSuppress the address immediately. No confirmation step.200, and no redirect
POST /u/<token> from the confirmation formA human who pressed the buttonSuppress the address.200 with a confirmation page

Two more constraints come straight out of Section 3.1, and a third follows from them. They are the next three bugs after the GET one:

  1. No redirect. "The mail sender MUST NOT return an HTTPS redirect, since redirected POST actions have historically not worked reliably, and many browsers have turned redirected HTTP POSTs into GETs." The post-redirect-get pattern your framework does by default is wrong here.
  2. No cookies, no auth. "The POST request MUST NOT include cookies, HTTP authorization, or any other context information." That means the request arrives with no session and no CSRF token, so any cookie-bound CSRF middleware will reject it. Exempt the route explicitly, and lean on the opaque token for authorisation instead.
  3. No login wall on the GET. A confirmation page that asks the recipient to sign in is not a one-click unsubscribe.

How do you check your own endpoint in a minute?

GET the URL, then ask your database whether anything changed. If it did, you have the bug. Below is that check run against a deliberately correct implementation, where GET renders a form, POST mutates state, and /state exposes the suppression list.

Real transcript. The GET leaves the suppression list empty; only the POST changes it.
$ curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8931/u/9f3a1c2e
200

$ curl -s http://127.0.0.1:8931/state
[]

$ curl -s -X POST -d 'List-Unsubscribe=One-Click' http://127.0.0.1:8931/u/9f3a1c2e
unsubscribed one_click=True

$ curl -s http://127.0.0.1:8931/state
['9f3a1c2e']

The other half of the check: the POST your code receives should match the one RFC 8058 Section 8.1 specifies. The body is 26 bytes.

curl -v against the same endpoint, abridged to the request and status lines — nothing shown is altered. Compare with RFC 8058 Section 8.1.
$ printf 'List-Unsubscribe=One-Click' | wc -c
26

$ curl -sv -X POST -d 'List-Unsubscribe=One-Click' http://127.0.0.1:8931/u/7b21
> POST /u/7b21 HTTP/1.1
> Host: 127.0.0.1:8931
> Content-Length: 26
> Content-Type: application/x-www-form-urlencoded
>
< HTTP/1.1 200 OK
unsubscribed one_click=True

Handle multipart/form-data as well as urlencoded — the RFC says receivers SHOULD use the former and MAY use the latter, so you will see both. Read the field too: a POST without List-Unsubscribe=One-Click came from your own confirmation form, not a mail client, and is worth logging separately.

Why does one-click need a DKIM signature?

Because otherwise anyone could forge the headers. RFC 8058 Section 4 is blunt: senders MUST apply at least one valid DKIM signature, the List-Unsubscribe and List-Unsubscribe-Post headers MUST be covered by that signature and included in the "h=" tag, and if the signature is missing the receiver "SHOULD NOT offer a one-click unsubscribe for that message".

This is the quiet failure: headers present, endpoint correct, and the one-click button simply never appears. The usual cause is ordering — something adds the List-Unsubscribe headers after signing, or the signer’s h= list was configured once and never updated. Dump a delivered message and check both header names appear in h=. If they do not, no amount of endpoint work will help.

Section 6 explains the other half: the header value is restricted to one known key/value pair "to prevent an attacker from creating malicious messages where the POST operation could simulate a user filling in an arbitrary form on a victim website."

What do Gmail and Outlook require, and where do they differ?

They differ more than most summaries admit. Gmail names one-click unsubscribe as a hard requirement above a volume threshold. Microsoft’s published requirements for high-volume senders to its consumer domains are authentication-only; unsubscribe appears under recommendations. Read both pages rather than trusting any table, including this one.

From each provider’s own current sender guidance. Microsoft’s consumer-domain post covers outlook.com, hotmail.com and live.com.
GmailOutlook.com (consumer domains)
Volume triggerMore than 5,000 messages per dayMore than 5,000 emails per day
One-click unsubscribeRequired: "your marketing and subscribed messages must support one-click unsubscribe"Not in the requirements list. "Functional Unsubscribe Links" appear under Additional Email Hygiene Recommendations
Visible unsubscribe link in the bodyRequired, alongside one-clickRecommended: "an easy, clearly visible way for recipients to opt out of further messages"
SPF and DKIMRequiredRequired — both must pass
DMARCRequired; enforcement policy "can be set to none", with the From: domain aligned to SPF or DKIMRequired, at least p=none, aligned with SPF or DKIM (preferably both)
Spam rateRequired below 0.30% in Postmaster Tools; separately recommends staying below 0.10%Not published as a numeric threshold
EnforcementFrom February 1, 2024Announced April 2, 2025, enforcement from May 5, 2025. An April 29, 2025 update to that post says non-compliant mail is rejected with "550; 5.7.515 Access denied"; an adjacent paragraph still describes routing to Junk

Gmail’s help page documents the same POST body the RFC does, down to Content-Length: 26, and links out to RFC 2369 and RFC 8058 by name. It also notes that other unsubscribe options "can also be used but they should not replace one-click unsubscribe". A mailto: on its own does not satisfy it, though you may list one in addition — RFC 8058 explicitly allows the header to carry non-HTTP/S URIs alongside the required HTTPS one.

How quickly must an unsubscribe be honoured?

In the United States, the FTC’s CAN-SPAM compliance guide says the opt-out mechanism "must be able to process opt-out requests for at least 30 days after you send your message", and that "You must honor a recipient’s opt-out request within 10 business days." It also caps what you may demand: you cannot charge a fee, require personally identifying information beyond an email address, or "make the recipient take any step other than sending a reply email or visiting a single page on an Internet website" as a condition of honouring the request.

That last clause is what makes the correct design legal as well as correct: a confirmation page with a button on it is a single page; a multi-step preference centre behind a login is not. The guide also states that each separate violating email is subject to penalties of up to $53,088.

Ten business days is the legal floor, not the engineering target. RFC 8058 makes the practical case in its own introduction: "if an unsubscription process is too difficult, the recipient’s alternative is to report mail from the sender as junk until the mail no longer appears in the recipient’s inbox." Suppress synchronously, in the same request, and let the rest of the pipeline read from that table.

Does one-to-one cold outbound need any of this?

On volume, usually not: a sender working through their own Microsoft 365 or Google Workspace mailbox is typically nowhere near 5,000 messages a day, so neither bulk-sender requirement is triggered. Do it anyway. CAN-SPAM has no volume floor and "makes no exception for business-to-business email". And a recipient who finds a working unsubscribe has one less reason to reach for the spam button — which matters because spam rate is the one deliverability metric Gmail publishes a hard threshold for, at below 0.30% in Postmaster Tools.

Decknative puts the RFC 8058 header pair, a visible footer link and a physical address on every message it sends through a customer’s mailbox for that reason: the requirement does not formally apply, and the cost of getting it right is one route handler.

The implementation checklist

  1. Generate an opaque, hard-to-forge token per recipient per list, verified server-side. Never encode the raw address.
  2. Emit List-Unsubscribe with one HTTPS URI, optionally plus a mailto:.
  3. Emit List-Unsubscribe-Post: List-Unsubscribe=One-Click, exactly that value.
  4. Sign with DKIM and confirm both header names appear in the h= tag of a delivered message.
  5. GET on the URL renders a confirmation form and mutates nothing. HEAD likewise.
  6. POST suppresses immediately, accepts both body encodings, and returns 200 — never a redirect.
  7. Exempt the route from cookie-based CSRF and from any auth middleware.
  8. Keep a visible unsubscribe link in the message body pointing at the same URL.
  9. Re-run the curl check against production after any deploy touching the route.
  10. Alert on unsubscribe rate per campaign. A spike in the first minutes of a send is this bug, not an unpopular email.

Common questions

Does one-click unsubscribe still need a confirmation page?
The one-click POST must act immediately, with no confirmation step — that is the whole point of RFC 8058, which exists so a receiver can process an unsubscribe in the background and know it completed. The confirmation page still exists, but it is what a GET renders, for humans arriving from the footer link and for scanners fetching the URL.
Can GET and POST share one URL?
Yes, and they should. RFC 8058 Section 3.2 says the target of the POST action is the same as the one in the GET action for a manual unsubscription, "so this is intended to allow the same server code to handle both". One route, one token, behaviour keyed on the method.
Why does the one-click POST arrive with no session or CSRF token?
Because RFC 8058 Section 3.1 forbids it: "The POST request MUST NOT include cookies, HTTP authorization, or any other context information." The reason given is that the unsubscribe is logically unrelated to any previous web activity. Exempt the route from cookie-bound CSRF checks and authorise the request with the opaque token in the URL instead.
Can I redirect the browser after processing the POST?
No. RFC 8058 states the mail sender MUST NOT return an HTTPS redirect, because redirected POST actions have historically not worked reliably and many browsers turn a redirected POST into a GET. Return 200 with a body. This means disabling the post-redirect-get behaviour many web frameworks apply by default.
My headers look right but Gmail shows no unsubscribe button. Why?
Check DKIM coverage first. RFC 8058 Section 4 requires a valid DKIM signature whose h= tag includes both List-Unsubscribe and List-Unsubscribe-Post, and says a receiver SHOULD NOT offer one-click without it. The usual cause is a pipeline that injects the headers after signing, or an h= list that was never updated.
Is a mailto: List-Unsubscribe enough for Gmail?
Not on its own above the threshold. Gmail requires marketing and subscribed messages from senders above 5,000 messages per day to support one-click unsubscribe, which means the HTTPS URI plus the List-Unsubscribe-Post header. RFC 8058 permits a mailto: to be listed in addition to the required HTTPS URI, and Gmail notes other options may be offered but should not replace one-click.
How fast do I have to honour an unsubscribe?
The FTC’s CAN-SPAM compliance guide says within 10 business days, and that the mechanism must keep working for at least 30 days after the message was sent. Google’s current sender guidelines page does not publish a processing deadline of its own. This is a summary of published guidance, not legal advice.

Sources

Read next