Parse List-Unsubscribe (RFC 2369) and List-Unsubscribe-Post (RFC 8058)
email headers into a typed action enum.
[dependencies]
list-unsubscribe = "0.1"use list_unsubscribe::{parse_with_post, UnsubscribeMethod};
let header = "<mailto:u@example.com>, <https://example.com/unsub?u=abc>";
let post = Some("List-Unsubscribe=One-Click");
match parse_with_post(header, post) {
UnsubscribeMethod::OneClick { url } => {
// POST to `url` with body `List-Unsubscribe=One-Click`
}
UnsubscribeMethod::Mailto { address, subject } => {
// Open mail composer to `address` with `subject`
}
UnsubscribeMethod::HttpLink { url } => {
// Open `url` in a browser
}
UnsubscribeMethod::None => {
// No header offered, or every candidate was unparseable
}
}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.
- 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.
- It does not POST to the one-click endpoint. The caller picks an
HTTP client such as
reqwestorureqand 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".
- 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.
The full coverage matrix lives in
testdata/coverage.md. Each fixture is a
language-neutral JSON file under
testdata/conformance/ so another
implementation 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:
cargo test --all-featuresThese 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 prepare message text on the user's behalf. This crate returns only the address and optional subject.- Multiple URLs of the same scheme: first wins. RFC 2369 does not specify ordering; this gives callers a deterministic single choice.
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).
Execution helpers are intentionally outside this crate's current
surface. Callers choose the HTTP client for one-click unsubscribe and
the mail composer or SMTP client for mailto: actions.
Request executor support with a concrete client or runtime use case.
- 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.
MIT OR Apache-2.0. See LICENSE-MIT and LICENSE-APACHE.