Gmail and Yahoo made RFC 8058 one-click unsubscribe a hard requirement for bulk senders in 2024, and inbox providers keep tightening enforcement. Agents sending recurring notifications, digests, or automated updates to multiple recipients need to implement this correctly — or watch their deliverability decay.
This post covers the exact header format, the difference between mailto and HTTP unsubscribe mechanisms, how to process the POST callback an agent will receive, and how to build a suppression check into every send path.
What List-Unsubscribe headers actually do
The List-Unsubscribe header is a machine-readable signal in the email envelope that tells inbox providers where to send an unsubscribe request on behalf of the recipient — without the recipient clicking anything inside the message body. Gmail, Yahoo, and Apple Mail surface a one-click "Unsubscribe" link in their UI driven entirely by this header, not by any link in your HTML.
Two mechanisms exist:
- mailto: — the provider sends an email to the address you specify
- https: (RFC 8058) — the provider POSTs to a URL you control
RFC 8058 introduced the companion List-Unsubscribe-Post header to signal that your endpoint accepts the one-click POST. Without it, providers treat the https: URL as a regular link and won't fire it automatically.
A correct pair looks like this:
List-Unsubscribe: <https://your-agent.example.com/unsubscribe?token=abc123>, <mailto:unsub-abc123@in.yourdomain.com>
List-Unsubscribe-Post: List-Unsubscribe=One-Click
Both values on the same header. Modern clients prefer and use the HTTPS URL first; the mailto fallback handles older providers.
When an AI agent must include these headers
Not every send needs List-Unsubscribe. The rule is simple: any message a recipient could reasonably want to stop receiving on a recurring basis needs the header.
Required:
- Weekly or daily digest emails
- Automated status updates sent on a schedule
- Agent-generated reports delivered to multiple recipients
- Notification batches (e.g., a monitoring agent emailing on-call engineers)
Not required (but harmless to include):
- Transactional one-offs: OTPs, password resets, invoice receipts
- Direct replies to a specific inbound message
- Single-send confirmations triggered by user action
The practical test: if a human receiving this email might reasonably want fewer of them, include the header. Gmail's sender guidelines — updated in early 2024 — explicitly require it for any sender pushing more than 5,000 messages per day to Gmail addresses, and for anything that looks like a mailing list regardless of volume.
Constructing the headers in code
When you send via the REST API, you add the headers to your message. Note that the Mails.ai email API does not accept a generic headers object on the send payload — unsubscribe handling runs through your own endpoint and suppression logic, with the token embedded in the recipient-specific URL you generate.
Here is the raw REST shape:
curl -X POST https://api.mails.ai/v1/messages \
-H "Authorization: Bearer $MAILS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "hello",
"to": "user@example.com",
"subject": "Your weekly digest",
"body_text": "Here is your summary for this week...",
"body_html": "<p>Here is your summary...</p><p><a href=\"https://your-agent.example.com/unsubscribe?token=abc123\">Unsubscribe</a></p>"
}'
For agents implemented in TypeScript:
import { mails } from "@mailsai/sdk";
const hello = mails.agent("hello");
async function sendDigest(recipientEmail: string, token: string) {
const unsubUrl = `https://your-agent.example.com/unsubscribe?token=${token}`;
const unsubMailto = `mailto:unsub-${token}@in.yourdomain.com`;
await hello.send({
to: recipientEmail,
subject: "Your weekly digest",
body_text: "Here is your summary.\n\nTo unsubscribe: " + unsubUrl,
body_html: `<p>Here is your summary.</p>
<p><a href="${unsubUrl}">Unsubscribe</a></p>`,
// Store the unsubscribe metadata for your suppression system
metadata: { unsubscribe_token: token, recipient: recipientEmail },
});
}
The List-Unsubscribe header itself must be injected at the SMTP/API layer by your sending infrastructure before the message leaves your system. If your provider doesn't expose raw header injection, handle suppression entirely through your own endpoint and block future sends at the application layer — which is the right architecture regardless.
Generating safe unsubscribe tokens
Every unsubscribe URL must contain a recipient-specific, unforgeable token. A predictable token — a raw email address, a sequential ID — lets anyone unsubscribe anyone else.
Use an HMAC-based token tied to the recipient address and an agent-held secret:
import { createHmac } from "crypto";
function generateUnsubToken(email: string, secret: string): string {
return createHmac("sha256", secret)
.update(email.toLowerCase())
.digest("hex");
}
function verifyUnsubToken(
email: string,
token: string,
secret: string
): boolean {
const expected = generateUnsubToken(email, secret);
// Constant-time comparison
return (
expected.length === token.length &&
createHmac("sha256", secret).update(expected).digest("hex") ===
createHmac("sha256", secret).update(token).digest("hex")
);
}
Do not put expiry on unsubscribe tokens. A user who unsubscribes from a three-year-old digest email expects that unsubscribe to stick. Expiring tokens create support tickets and spam complaints.
The one-click POST endpoint
When Gmail fires a one-click unsubscribe, it sends an HTTP POST to your URL with the body:
List-Unsubscribe=One-Click
Content-Type is application/x-www-form-urlencoded. The request comes from Google's infrastructure, not the recipient's browser — no session, no cookie, no referrer. Your endpoint must:
- Parse the token from the URL query string
- Validate the HMAC
- Add the address to your suppression store
- Return HTTP 200 with any body (empty is fine)
import { createServer, IncomingMessage, ServerResponse } from "http";
import { parse } from "url";
const server = createServer(async (req: IncomingMessage, res: ServerResponse) => {
if (req.method === "POST" && req.url?.startsWith("/unsubscribe")) {
const { query } = parse(req.url, true);
const token = query.token as string;
// Read POST body to confirm it's a one-click request
const body = await readBody(req);
const isOneClick = body.includes("List-Unsubscribe=One-Click");
if (token && isOneClick) {
const email = await lookupEmailByToken(token); // your DB lookup
if (email && verifyUnsubToken(email, token, process.env.UNSUB_SECRET!)) {
await addToSuppression(email);
res.writeHead(200);
res.end();
return;
}
}
res.writeHead(400);
res.end();
}
});
Return 200 even if the token is already in your suppression list — idempotency matters here. Returning 4xx causes Gmail to retry, and repeated failures flag your domain.
Suppression list architecture for agents
A suppression list is the set of addresses your agent must never send to again, regardless of what the calling code requests. The suppression check must happen as close to the send call as possible — ideally as a wrapper around your SDK call, not as a pre-send query in your business logic that someone might accidentally skip.
import { mails } from "@mailsai/sdk";
import type { SendInput } from "@mailsai/sdk";
const hello = mails.agent("hello");
async function safeSend(input: SendInput): Promise<void> {
const recipients = [
...(Array.isArray(input.to) ? input.to : [input.to]),
...(input.cc ?? []),
...(input.bcc ?? []),
];
const suppressed = await checkSuppression(recipients); // your DB
if (suppressed.length > 0) {
console.warn(`Skipping send: ${suppressed.join(", ")} is suppressed`);
return;
}
await hello.send(input);
}
Store suppression records with:
- The normalized email address (lowercase, trimmed)
- The timestamp of the unsubscribe
- The source (
one_click,mailto,manual,bounce,complaint)
Never delete suppression records. Complaints filed with inbox providers persist indefinitely, and re-emailing a suppressed address is one of the fastest ways to cross the 0.3% complaint rate threshold that causes your sending domain to be paused.
For teams using Mails.ai's email infrastructure, the mails_check_suppression MCP tool (available since version 0.3.0 of @mailsai/mcp-server) can be wired into an agent's tool calls to verify suppression status before any send decision is made.
Handling mailto unsubscribes via inbound email
The mailto mechanism routes unsubscribe requests as actual emails. If your List-Unsubscribe header includes <mailto:unsub-abc123@in.yourdomain.com>, you need an inbound handler listening on that address.
With an inbound email parsing setup, you can route all emails matching the pattern unsub-*@in.yourdomain.com to a dedicated handler:
import { mails } from "@mailsai/sdk";
const hello = mails.agent("hello");
hello.onMessage(async (event) => {
// The injection_score check is good hygiene
if (event.quarantined) return;
const toAddress = event.data.to?.[0]?.address ?? "";
if (toAddress.startsWith("unsub-")) {
const token = toAddress.split("unsub-")[1].split("@")[0];
const email = await lookupEmailByToken(token);
if (email) {
await addToSuppression(email);
console.log(`Unsubscribed via mailto: ${email}`);
}
}
});
Note that event.quarantined is a flag, not a gate — Mails.ai delivers the event to your handler even when injection_score is high; your code decides whether to act on it. For unsubscribe handlers, skipping quarantined messages is correct: an attacker could craft a spoofed unsubscribe to remove a legitimate recipient.
Flow: one-click unsubscribe end to end
sequenceDiagram
participant Agent
participant Gmail
participant UnsubEndpoint
participant SuppressionDB
Agent->>Gmail: Message with List-Unsubscribe header
Gmail->>Gmail: User clicks Unsubscribe in UI
Gmail->>UnsubEndpoint: POST /unsubscribe?token=abc123 body=List-Unsubscribe=One-Click
UnsubEndpoint->>SuppressionDB: Verify token lookup email
SuppressionDB-->>UnsubEndpoint: email found
UnsubEndpoint->>SuppressionDB: INSERT suppression record
UnsubEndpoint-->>Gmail: HTTP 200
Agent->>SuppressionDB: Check before next send
SuppressionDB-->>Agent: Address suppressed skip send
Common implementation mistakes
Token in the URL only, not verified against the recipient. If you store {token: abc123, email: user@example.com} and look up the email by token without verifying the HMAC, an attacker with any valid token structure can probe your endpoint. Always verify the HMAC.
Unsubscribe endpoint behind authentication. Gmail's one-click POST comes from Google's servers, not the user's browser. Any authentication middleware — session checks, CSRF tokens, API keys — on the endpoint will cause it to fail silently.
Suppression check in application logic but not at the send wrapper. If the check lives in your "send digest" job but not in the function that actually calls hello.send(), any future code path that sends from elsewhere bypasses suppression entirely. Wrap the SDK call.
Using the same token for mailto and HTTP. Both mechanisms are valid, but if you use the same token in both, make sure your token lookup handles both delivery paths. The token embedded in the mailto address and the URL query param should resolve to the same record.
Not including a plain-text unsubscribe link in the body. Even with one-click headers, include a visible unsubscribe link in the HTML and a URL in the plain-text body. Some clients don't surface the header-driven UI, and a recipient who can't find a way to unsubscribe files a spam complaint instead.
Frequently Asked Questions
Does every automated email need a List-Unsubscribe header?
No. Transactional one-offs — OTPs, password resets, invoice receipts, replies to a specific user message — do not need it. The header is for recurring or subscription-style sends where the recipient might want to stop receiving that category of message. When in doubt, include it anyway; it has no negative effect on transactional mail.
What happens if I don't implement one-click unsubscribe?
Gmail's bulk sender requirements, which took effect in February 2024, require one-click unsubscribe for any sender pushing more than 5,000 messages per day to Gmail addresses. Missing headers increase the likelihood of Promotions or Spam folder classification. More concretely, recipients who can't find an unsubscribe path click "Mark as Spam" — and a complaint rate above 0.3% triggers sending pauses with most infrastructure providers.
Can an AI agent process unsubscribes without human review?
Yes, and that's the correct architecture. The unsubscribe signal from Gmail is machine-generated (a POST to your endpoint), not a human-composed email. The agent's handler should process it atomically: verify the token, write to suppression, return 200. No human review queue, no grace period. Delaying suppression is a compliance risk, not a feature.
How do I handle a user who unsubscribes and then re-subscribes?
Store the full suppression history rather than deleting records. On re-subscribe (explicit opt-in from the user), insert a new record with resubscribed_at and a source of explicit_consent. Your suppression check queries the most recent record per address: if it's a resubscribe, allow sends; if it's an unsubscribe, block. This audit trail matters if you ever need to demonstrate compliance.
Do List-Unsubscribe headers affect deliverability positively?
Directly, yes. Gmail uses the presence of List-Unsubscribe and List-Unsubscribe-Post as a positive signal when classifying messages. More importantly, they reduce spam complaints — which are the metric that actually damages sender reputation. A recipient who can unsubscribe in one click does not need to mark your message as spam. The indirect deliverability benefit from lower complaint rates outweighs any direct classification signal.
What's the difference between RFC 2369 and RFC 8058?
RFC 2369 (1998) defined the List-Unsubscribe header and the mailto mechanism. RFC 8058 (2017) added the HTTPS one-click mechanism and the companion List-Unsubscribe-Post: List-Unsubscribe=One-Click header that signals to inbox providers that your endpoint accepts automated POSTs. Gmail and Yahoo's 2024 requirements specifically reference RFC 8058 — the HTTPS mechanism with the Post header is what they're looking for, not just the older mailto form.
Related guides: Handling Bounces and Complaints for Agent Email, SPF DKIM DMARC for Automated Email Senders, Email Deliverability for Automated and AI Senders and AI for Email: Integrating Agents Into Transactional Flows.
Explore the product: MCP email server, Inbound email parsing, Sender reputation, All features and Pricing.
