CAN-SPAM §5, GDPR Art. 21, and Gmail/Yahoo's 2024 bulk sender requirements all mandate a working one-click unsubscribe path — RFC 8058 specifically requires both the List-Unsubscribe and List-Unsubscribe-Post headers plus a POST endpoint. Missing either header or burying the body link in illegible gray text is a direct regulatory violation that exposes the business to FTC enforcement action and causes Gmail to route bulk sends to spam, tanking deliverability for every user on the list. The business impact compounds: a single CAN-SPAM complaint can trigger an FTC investigation; a spam classification tanks open rates across the entire subscriber base.
Critical because missing unsubscribe headers violates CAN-SPAM §5, GDPR Art. 21, and 2024 Gmail/Yahoo bulk sender policy simultaneously, risking FTC enforcement and domain-level spam classification that destroys deliverability for all users.
Add List-Unsubscribe and List-Unsubscribe-Post headers to every commercial email send call, and implement both GET and POST endpoints at /api/unsubscribe.
// lib/email.ts — add unsubscribe headers to every marketing send
export async function sendMarketingEmail({ to, subject, html, unsubscribeToken }: {
to: string; subject: string; html: string; unsubscribeToken: string
}) {
const unsubUrl = `https://example.com/api/unsubscribe?token=${unsubscribeToken}`
return resend.emails.send({
from: 'Your Product <hello@example.com>',
to, subject, html,
headers: {
'List-Unsubscribe': `<${unsubUrl}>`,
'List-Unsubscribe-Post': 'List-Unsubscribe=One-Click',
},
})
}
The POST handler (RFC 8058 one-click) must suppress without requiring login. Store the unsubscribe token on the subscriber record, not the campaign.
ID: email-sms-compliance.unsubscribe.one-click-unsubscribe
Severity: critical
What to look for: Enumerate every relevant item. Find all locations where marketing or commercial emails are sent. For each send call, inspect whether a List-Unsubscribe header is included in the email headers. CAN-SPAM requires an unsubscribe mechanism in the body; Gmail and Yahoo's 2024 bulk sender requirements additionally require the RFC 2369 List-Unsubscribe header with an HTTPS unsubscribe URL. RFC 8058 one-click unsubscribe also requires a List-Unsubscribe-Post: List-Unsubscribe=One-Click header and a POST endpoint. Check the email body templates: does every commercial email contain a visible, working unsubscribe link — not buried in tiny gray text, not a link that goes to a page where users must confirm multiple steps? Check that the unsubscribe link is in the footer of every marketing email template. This check does not apply to purely transactional emails (password reset, purchase receipt, account security alerts) — only to commercial/marketing email.
Pass criteria: At least 1 of the following conditions is met. Every commercial email includes: (1) a List-Unsubscribe header with an HTTPS URL, (2) a List-Unsubscribe-Post header for RFC 8058 one-click compliance, and (3) a visible unsubscribe link in the email body. The unsubscribe link does not require the user to log in and processes the opt-out in a single click or a single confirmation step with no additional persuasion. Before evaluating, extract and quote the relevant configuration or code patterns found. Report the count of items checked even on pass.
Fail criteria: No List-Unsubscribe header in email-sending code. Unsubscribe link is absent from email templates. Unsubscribe requires login or presents a multi-step flow designed to discourage opt-out.
Do NOT pass when: The item exists only as a placeholder, stub, or TODO comment — partial implementation does not count as passing.
Skip (N/A) when: The application sends no commercial or marketing email of any kind — only purely transactional email (password reset, receipts, account notifications with no promotional content).
Cross-reference: For related security patterns, the Security Headers audit covers server-side hardening.
Detail on fail: Specify what is missing. Example: "Marketing emails sent via SendGrid with no List-Unsubscribe header set. No unsubscribe link found in email templates in src/emails/." or "List-Unsubscribe header present but points to a login-gated preferences page. RFC 8058 List-Unsubscribe-Post header absent.".
Remediation: Add List-Unsubscribe headers and a one-click unsubscribe endpoint. Example using the Resend SDK (pattern applies to SendGrid, Postmark, etc.):
// lib/email.ts — add unsubscribe headers to every commercial email
import { Resend } from 'resend'
const resend = new Resend(process.env.RESEND_API_KEY)
export async function sendMarketingEmail({
to,
subject,
html,
unsubscribeToken,
}: {
to: string
subject: string
html: string
unsubscribeToken: string
}) {
const unsubUrl = `https://example.com/api/unsubscribe?token=${unsubscribeToken}`
return resend.emails.send({
from: 'Your Product <hello@example.com>',
to,
subject,
html,
headers: {
// RFC 2369 — required by Gmail/Yahoo bulk sender policy
'List-Unsubscribe': `<${unsubUrl}>`,
// RFC 8058 — enables one-click unsubscribe in Gmail UI
'List-Unsubscribe-Post': 'List-Unsubscribe=One-Click',
},
})
}
// app/api/unsubscribe/route.ts — handle both GET (link click) and POST (one-click)
export async function GET(req: Request) {
const { searchParams } = new URL(req.url)
const token = searchParams.get('token')
if (!token) return new Response('Invalid token', { status: 400 })
const subscriber = await db.subscriber.findUnique({ where: { unsubToken: token } })
if (!subscriber) return new Response('Not found', { status: 404 })
await db.subscriber.update({
where: { id: subscriber.id },
data: { marketingOptOut: true, optOutAt: new Date() },
})
// Redirect to a confirmation page — no login required
return Response.redirect('https://example.com/unsubscribed')
}
// RFC 8058 one-click — Gmail sends a POST to this same URL
export async function POST(req: Request) {
const { searchParams } = new URL(req.url)
const token = searchParams.get('token')
if (!token) return new Response('Invalid token', { status: 400 })
await db.subscriber.updateMany({
where: { unsubToken: token },
data: { marketingOptOut: true, optOutAt: new Date() },
})
return new Response('OK', { status: 200 })
}
Also add an unsubscribe link to every email body template:
<!-- In every marketing email footer -->
<p style="color: #999; font-size: 12px;">
You're receiving this because you signed up for updates from Example.
<a href="{{unsubscribeUrl}}">Unsubscribe</a> at any time.
</p>