list-unsubscribe
Parse List-Unsubscribe (RFC 2369) and List-Unsubscribe-Post (RFC 8058)
email headers into a typed action enum.
use ;
let header = "<mailto:u@example.com>, <https://example.com/unsub?u=abc>";
let post = Some;
match parse_with_post
Why this crate exists
In February 2024 Gmail and Yahoo introduced
bulk-sender deliverability requirements.
One of them is mandatory RFC 8058 one-click unsubscribe for senders above
5,000 messages/day. This promoted List-Unsubscribe-Post from "obscure RFC"
to "required for inbox placement", and elevated the audience for clients
that honour it.
List-Unsubscribe describes an action, not just a header value.
Callers still need to choose between mailto, web-link, and RFC 8058
one-click actions, while handling mailto: query parameters
consistently.
This crate keeps that policy surface small: parse the headers into a typed action enum, then leave execution to the caller.
What it does
- Parses RFC 2369 multi-method headers like
<mailto:list@x>, <https://x/u>. - Distinguishes RFC 8058 one-click (POST endpoint) from a plain web link
via the accompanying
List-Unsubscribe-Postheader. - Captures the
?subject=parameter frommailto:URIs. - Skips unparseable URIs silently and falls through to the next candidate.
What it does not do
- It does not POST to the one-click endpoint. The caller picks an
HTTP client (
reqwest,ureq, whatever) and executes the action. - It does not send the unsubscribe mail. The caller hands the
Mailtovariant to a mail composer. - It does not scrape unsubscribe links from the message body. That is a policy decision that belongs above the crate.
- It does not capture
?body=frommailto:URIs. See "Intentional divergences" below. - It does not verify the unsubscribe endpoint actually works. The contract is "parse the header, classify the method".
Spec anchors
- RFC 2369 — the
List-Unsubscribeheader. - RFC 8058 — one-click
unsubscribe with
List-Unsubscribe-Post. - RFC 6068 — the
mailto:URI scheme. - Google sender rules — the deliverability backstory.
Conformance
The full coverage matrix lives in
testdata/coverage.md. Each fixture is a
language-neutral JSON file under
testdata/conformance/ so a future
TypeScript or other-language port can load the same corpus.
Three tests enforce the integrity of the corpus:
- Every fixture file is referenced in
coverage.md. - Every contract-critical fixture exists on disk.
- The actual parser output matches
expectedfor every fixture.
Run them with:
Intentional divergences
These are decisions where this crate is narrower or more opinionated than the spec.
- Mailto preferred over http when both are present and no one-click Post header. Mailto unsubscribe does not require a browser session and tends to be faster for power users; clients that want the opposite preference can pattern-match on the returned enum.
?body=dropped frommailto:URIs. Including it would let clients silently send pre-canned text on the user's behalf, which is a UX and safety footgun.- Multiple URLs of the same scheme: first wins. RFC 2369 does not specify ordering; this gives callers a deterministic single choice.
Feature flags
serde— derivesSerialize+DeserializeforUnsubscribeMethod(internally tagged withkind), and pulls inurl/serde.mail-parser— addsparse_from_message(&mail_parser::Message<'_>)for callers that already use themail-parsercrate to parse RFC 5322 messages.
The default feature set is empty. The crate has one required dependency
(url) and no transitive runtime cost beyond that.
Companion direction
A future v2 could add executor features (reqwest-backed
oneclick.unsubscribe(), lettre-backed mailto.send()) but v1 is
deliberately parse-only. If you want them, open an issue.
Maintenance
- File bug reports at https://github.com/planetaryescape/list-unsubscribe/issues.
- Patches that change behaviour must add or update a fixture in
testdata/conformance/and a row intestdata/coverage.md.
License
MIT OR Apache-2.0. See LICENSE-MIT and LICENSE-APACHE.