All posts

One-Click Unsubscribe (RFC 8058): How to Implement It Properly

··7 min read

One-click unsubscribe means adding two headers to every marketing email, List-Unsubscribe: <https://...> and List-Unsubscribe-Post: List-Unsubscribe=One-Click, signing both with DKIM, and running an HTTPS endpoint that unsubscribes the recipient when it receives a POST. Gmail and Yahoo require it from bulk senders, and both expect the request honoured within two days.

The specification, RFC 8058, is short, and most implementations follow the example and miss the details around it. We built a minimal endpoint and tested it against the request formats the RFC allows. The results, and the code, are below.

The headers

List-Unsubscribe: <https://example.com/u?l=newsletter&e=ana%40example.com&t=7ccb9814fd361a8d016d9d07670fe461>, <mailto:unsubscribe@example.com?subject=unsubscribe>
List-Unsubscribe-Post: List-Unsubscribe=One-Click

What RFC 8058 section 3.1 requires:

Rule RFC wording
One of each header "places one List-Unsubscribe header field and one List-Unsubscribe-Post header field in the message"
HTTPS URL present "The List-Unsubscribe header field MUST contain one HTTPS URI. It MAY contain other non-HTTP/S URIs such as MAILTO:"
Exact POST value "The List-Unsubscribe-Post header MUST contain the single key/value pair 'List-Unsubscribe=One-Click'"
URL identifies recipient and list "MUST contain enough information to identify the mail recipient and the list"
Hard-to-forge token "SHOULD include an opaque identifier or another hard-to-forge component"
DKIM covers both headers Both headers "MUST be covered by the signature and included in the 'h=' tag of a valid DKIM-Signature header field"

The DKIM rule is the one most often broken. If your platform adds the headers after signing, or signs with a header list that leaves them out, receivers "SHOULD NOT offer a one-click unsubscribe for that message." Check a sent message: the h= list in its DKIM-Signature should include List-Unsubscribe:List-Unsubscribe-Post.

What the receiver sends you

When a Gmail user clicks unsubscribe, Gmail makes an HTTPS POST to your URL. Google's sender guidelines show the request:

POST /unsubscribe/example HTTP/1.1
Host: solarmora.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 26

List-Unsubscribe=One-Click

But RFC 8058 section 3.2 says the POST "SHOULD be sent as 'multipart/form-data' ... or MAY be sent as 'application/x-www-form-urlencoded'." The multipart form is the preferred encoding in the standard, and its body looks nothing like the urlencoded one:

Content-Type: multipart/form-data; boundary=---FormBoundaryjWmhtjORrn

---FormBoundaryjWmhtjORrn
Content-Disposition: form-data; name="List-Unsubscribe"

One-Click
---FormBoundaryjWmhtjORrn--

An endpoint written by copying Google's example, checking that the body equals List-Unsubscribe=One-Click, will reject every receiver that follows the RFC's preference. Handle both.

A minimal endpoint, tested

This uses only the Python standard library. The token is an HMAC of the list and address, so nobody can unsubscribe other people by guessing URLs. The same URL serves a confirmation page on GET, which matters for a reason explained below.

import hmac, hashlib
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import urlparse, parse_qs

SECRET = b"replace-with-a-long-random-secret"

def token(list_id, email):
    msg = (list_id + ":" + email).encode()
    return hmac.new(SECRET, msg, hashlib.sha256).hexdigest()[:32]

def unsubscribe(list_id, email):
    print("unsubscribed", email, "from", list_id)  # write to your suppression list here

class Handler(BaseHTTPRequestHandler):
    def params(self):
        q = parse_qs(urlparse(self.path).query)
        return q.get("l", [""])[0], q.get("e", [""])[0], q.get("t", [""])[0]

    def do_POST(self):
        list_id, email, t = self.params()
        length = int(self.headers.get("Content-Length", 0))
        body = self.rfile.read(length)
        if not hmac.compare_digest(t, token(list_id, email)):
            self.send_response(403); self.end_headers(); return
        urlencoded = b"List-Unsubscribe=One-Click" in body
        multipart = b'name="List-Unsubscribe"' in body and b"One-Click" in body
        if urlencoded or multipart:
            unsubscribe(list_id, email)
        self.send_response(200); self.end_headers()
        self.wfile.write(b"You have been unsubscribed.")

    def do_GET(self):
        self.send_response(200)
        self.send_header("Content-Type", "text/html")
        self.end_headers()
        self.wfile.write(b"<form method='post'><button>Confirm unsubscribe</button></form>")

HTTPServer(("0.0.0.0", 8058), Handler).serve_forever()

In production this sits behind HTTPS, and the unsubscribe function writes to the same suppression list your sending platform reads. We ran it locally on 1 October 2026 and sent it four requests with curl:

Request Command (abbreviated) Response Unsubscribed?
urlencoded POST, valid token curl -X POST --data "List-Unsubscribe=One-Click" "$URL" 200 Yes
multipart POST, valid token curl -X POST -F "List-Unsubscribe=One-Click" "$URL" 200 Yes
POST with forged token ... t=deadbeef 403 No
Plain GET (what a link scanner does) curl "$URL" 200, confirmation page No

curl -F produces a genuine multipart/form-data body, so the second row is the encoding RFC 8058 prefers. If your own endpoint fails that row, that's the first fix.

Five mistakes that quietly break it

1. Unsubscribing on GET. Corporate mail gateways and security scanners fetch links in incoming mail to check them. If a GET to your unsubscribe URL unsubscribes the recipient, scanners will unsubscribe people who never clicked. RFC 8058 exists partly for this reason: the one-click action is a POST, and the RFC notes the POST target "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." Serve a confirmation page on GET, and act on POST.

2. Redirecting. "The mail sender MUST NOT return an HTTPS redirect, since redirected POST actions have historically not worked reliably." If your unsubscribe URL redirects from http to https, from a tracking domain to your app, or from one path to another, the POST may arrive as a GET or not at all. Put the final URL in the header.

3. Requiring a login, cookie or form. "The POST request MUST NOT include cookies, HTTP authorization, or any other context information." Everything the endpoint needs has to be in the URL. No "which lists would you like to leave?" step.

4. A firewall eating the POSTs. Google's Postmaster Tools documentation is blunt: unsubscribe requests count against your compliance "whether or not your servers receive it," and "if services like Cloudflare block these requests and keep them from your servers, you're still responsible." Bot protection that challenges unfamiliar POST requests will do exactly this. Allow the unsubscribe path through, and log every request that reaches it so you can compare against what you expected.

5. Unsubscribing from the wrong thing. Google's FAQ notes that one-click removes the recipient "only from the mailing list associated with the message." If your newsletter and your product updates come from separate lists, encode the list in the URL (the l= parameter above). Google's dashboard troubleshooting also says "Each list should have a unique List-ID or 'From:' address."

Forged requests, and why the token matters

RFC 8058's security section describes an attack worth designing against. Someone sends spam that carries List-Unsubscribe headers pointing at your list, so that when recipients report the spam, their mail clients unsubscribe them from your list as a side effect. Or they skip the email entirely and POST directly to your endpoint with a list of addresses.

Both fail if the URL carries a value only you could have produced. That's what the HMAC token in the code above does. An attacker can't compute it without your secret, so a forged request gets a 403, as the third row of our test shows. The RFC recommends exactly this: the server "SHOULD verify that the opaque or hard-to-forge component is valid."

Two practical notes. Keep the token stable for a given address and list, so a message sent months ago can still unsubscribe someone. And if you rotate the secret, keep accepting tokens made with the old one for as long as old messages might still be opened.

The RFC also points out that a successful unsubscribe confirms the address was valid. It notes there are simpler ways to learn that, so this isn't a reason to skip one-click, but it is a reason not to send any confirmation email after the POST. The recipient asked to stop hearing from you.

Gmail and Yahoo differences

Gmail Yahoo
Required for Marketing and subscribed mail from bulk senders Same
RFC 8058 POST Required "Highly recommended"
mailto only Doesn't meet the requirement "Acceptable"
Visible link in body as well Required Required; may point to a preference page
Processing deadline 48 hours 2 days

Build to Gmail's column and you satisfy both. The body link can go to a preference centre. Google's FAQ confirms body links "aren't required to be one-click" provided the headers are in place.

For the wider set of rules these sit within, see Gmail and Yahoo bulk sender requirements.

Checking your own mail

  1. Send a marketing message to a Gmail and a Yahoo test account.
  2. View the original message source and confirm both headers are present, the URL is HTTPS, and h= in the DKIM signature includes both header names.
  3. Copy the URL and run the two curl POST commands above against it. Both should unsubscribe the test address, and neither should redirect (curl -s -o /dev/null -w "%{http_code}" should print 200, not 301 or 302).
  4. Run a plain GET and confirm it does not unsubscribe.
  5. Check the address is suppressed in your sending platform within minutes, not days.

If unsubscribes do arrive and you honour them, fewer people reach for the spam button, which is what both providers are trying to achieve. For list hygiene beyond unsubscribes, start with our deliverability audit and a sunset policy for inactive subscribers.

Common questions

What headers are needed for one-click unsubscribe?

Two: a List-Unsubscribe header containing an HTTPS URL, and List-Unsubscribe-Post: List-Unsubscribe=One-Click. Both must be covered by a valid DKIM signature. A mailto URL can be included as well, but the HTTPS URL is the part that makes it one-click.

Is a mailto List-Unsubscribe header enough for Gmail?

No. Google's FAQ says mailto and URL links in the body don't meet its one-click requirement; it needs the RFC 8058 headers. Yahoo describes mailto as acceptable but highly recommends the RFC 8058 POST method, so implement RFC 8058 for both.

How quickly must one-click unsubscribes be processed?

Google recommends within 48 hours and lists slower processing in its enforcement table. Yahoo says an unsubscribe not honoured within 2 days does not meet its requirement. Processing them immediately is simplest.

Do transactional emails need one-click unsubscribe?

No. Both Google and Yahoo require it only for marketing and subscribed messages and give password resets and order confirmations as examples of mail that is excluded. Google notes recipients, not Google, decide what feels promotional, so mixed messages are risky.

Why isn't Gmail showing the unsubscribe link next to my sender name?

Google says the link at the top of a message appears only for messages that pass its automated eligibility checks, which include meeting the sender requirements and having correct headers. Yahoo similarly says it needs sufficient reputation and engagement. Correct headers are necessary but don't guarantee the button.

Verify unlimited addresses for $29.99/month

Real SMTP mailbox checks. No credits, no per-email fees.

Get Started