Guard List-Unsubscribe

RFC 2369 List-Unsubscribe + RFC 8058 one-click List-Unsubscribe-Post header validator. Gates the outbound submission path's marketing / transactional mail so messages carrying a List-Id (or any mailing-list shape) emit headers that Gmail / Yahoo / Outlook one-click unsubscribe machinery actually accepts.

## Why this primitive vs. inline header construction

Gmail's bulk-sender requirements (effective 2024-02) and Yahoo's matching policy refuse mail that doesn't carry the RFC 8058 pair correctly. Operators get senders rate-limited or buckets-dropped when the headers are malformed. Common pitfalls this primitive refuses:

- **No HTTPS URI** — Gmail+Yahoo require at least one https:// URI in the List-Unsubscribe header. mailto: alone is no longer sufficient post-2024. - **http:// instead of https://** — refused; one-click endpoint MUST be TLS. - **javascript: / data: / file: schemes** — always refused regardless of context. - **List-Unsubscribe-Post: List-Unsubscribe=One-Click** — MUST be EXACTLY this token. Operator-supplied variants (OneClick, one-click, lowercased = value) refused. - **HTTPS URI without paired List-Unsubscribe-Post** — the Post header opts the endpoint into one-click. Without it, Gmail's UI treats the HTTPS URI as a regular link (operator loses the inbox-list "Unsubscribe" button).

## Verdict shape

{
  action:        "accept" | "refuse",
  reason:        string,
  uris:          [{ scheme, raw, oneClickEligible }, ...],
  hasHttpsUri:   bool,
  hasMailtoUri:  bool,
  postHeaderOk:  bool,
  oneClickReady: bool,
}

Under strict (default for HIPAA / PCI / GDPR / SOC2 mailings that need bulk-sender compliance), oneClickReady: falseaction: "refuse". Under balanced, the primitive returns the verdict but always accepts — operator's outbound pipeline makes the policy decision downstream.

## CVE / threat model

- **Unsubscribe-link injection** — operator's template-rendered List-Unsubscribe could be tampered through prompt-injection into an AI-generated newsletter. CRLF refused (header injection); javascript: / data: / file: refused (XSS via mail-client rendering); URL length cap (default 2048). - **Open-redirect via List-Unsubscribe** — operator validates the HTTPS URI's target host with their own safeRedirect / safeUrl allowlist downstream; this guard checks the SHAPE, not the operator's target-host policy. - **Email client mishandling** (Outlook's history of fetching mailto: automatically) — the primitive doesn't render the header; consumers using it inside b.guardEmail.validateMessage get layered defense.

b.guardListUnsubscribe.validate(headers, opts?) #

stable0.9.39
{
  profile:   "strict" | "balanced" | "permissive",
  posture:   "hipaa" | "pci-dss" | "gdpr" | "soc2",
}

Validate the RFC 2369 / RFC 8058 header pair on an outbound marketing or transactional message. Returns the verdict shape; operator's submission listener consults verdict.action to accept / refuse the send.

var v = b.guardListUnsubscribe.validate({
  listUnsubscribe:      ", ",
  listUnsubscribePost:  "List-Unsubscribe=One-Click",
});
if (v.action === "refuse") throw new Error(v.reason);

b.guardListUnsubscribe.compliancePosture(name) #

stable0.9.39hipaapci-dssgdprsoc2

Return the effective profile NAME for a compliance posture, or null for a name this parser does not map. Unlike the content-guard variant this returns the resolved profile string (every line-protocol parser composes gateContract.ALL_STRICT_POSTURES, so "hipaa" / "pci-dss" / "gdpr" / "soc2" all resolve to "strict") and never throws — the parser shape carries no overlay-clone, no buildProfile, and no loadRulePack. Wired by gateContract.defineParser.

b.guardListUnsubscribe.compliancePosture("hipaa");                   // → "strict"
b.guardListUnsubscribe.compliancePosture("not-a-regime");            // → null

Last updated 2026-08-08T16:39:49.652Z by seeder.