73. pepsi-stage-list-deliver¶
Make one member’s copy of a list post that member’s own copy.
73.1. Role¶
pepsi-stage-list-post has already made one row per recipient;
pepsi-stage-list-deliver is what makes each of them that member’s copy
rather than another copy of the same message. One row in, one row out. It
finalises List-Unsubscribe with that member’s one-click https: URI
alongside the mailto: form and adds
List-Unsubscribe-Post: List-Unsubscribe=One-Click (RFC 8058); it decorates
the message with the list’s header and footer in the member’s language when the
list’s personalize is individual or full; and it munges To: to
name the member, for full only — upstream’s personalize_to is a no-op
for individual, and so is this. Reference:
pepsi-stage-list-deliver(1).
This subsystem is a reimplementation of GNU Mailman 3; see Mailing lists, which names what was taken from upstream.
Warning
This stage must run before signing. Every header it touches belongs in
the DKIM h= list, and List-Unsubscribe is exactly the header a mailbox
provider inspects when it decides whether to show an unsubscribe button. A
pipeline that signed first would produce mail that is signed and then
modified, which fails DKIM everywhere. So NEXT_STAGE points at
pepsi-stage-dkim-sign, not at a relay, and pepsi-setup refuses the
reverse. Upstream says the same thing in the comment that removes
arc-sign from their own pipeline.
73.2. One-click unsubscribe¶
The token in the https: URI is a keyed hash over the list, the member’s
address and a serial the member can rotate. It is not stored: a
List-Unsubscribe URI is emitted once per member per post, so a table of
them would grow with deliveries rather than with members. Three things follow.
The token never travels between stages — this stage holds the secret and computes it per copy.
Revoking every URI ever emitted for one subscription is a bump of that member’s
unsubscribe_serial: a counter update rather than a table sweep.There is nothing to expire, because a subscription that no longer exists fails the membership lookup whatever the token says.
The secret is [pepsi-list] UNSUBSCRIBE_SECRET. With no secret, or no
BASE_URL to build a URI from, the mailto: form stands alone, as
upstream’s does: an unverifiable URI is worse than none. Rotating the secret
invalidates every URI already sitting in every subscriber’s mailbox.
73.3. Decoration¶
The header and footer are rendered from [pepsi] TEMPLATE_DIR as
list-header.<lang>.body and list-footer.<lang>.body, the member’s
language first and en as the fallback. A list with no template files is not
decorated, which is the common case and costs nothing.
The footer is appended to the first ``text/plain`` part and to nothing else.
Appending to a multipart/signed part breaks the signature and appending to a
PDF corrupts it, so a message with no plain part is delivered undecorated rather
than mangled.
A copy that avoid-duplicates flagged — the member was already named in the
message’s own To or Cc and kept their list copy anyway — is stamped
X-Pepsi-List-Copy: yes, so the reader can see why they have two.
73.4. Two invariants¶
Sibling rows start here, never at the pipeline head. Re-entering at init
would re-run ARC verification and re-enter the router, which explodes
combinatorially on an umbrella list.
A member’s personal settings do not reconfigure list delivery. Pepsi layers
per-address pepsi.settings overrides automatically (see
Configuration), and a sibling row’s single recipient is a member,
whose vacation or language override has no business changing this stage’s
targets — a NEXT_STAGE taken from it could route the copy past signing. So
NEXT_STAGE, the [pepsi-list] options and the [pepsi]
TEMPLATE_DIR are read from the site-wide configuration, which no domain:
or address: configuration override and no pepsi.settings row reaches,
and the member’s language comes from the descriptor the fan-out wrote. Nothing
is taken from the overlay.
73.5. Configuration¶
[stage-<name>]: PROGRAM = pepsi-stage-list-deliver and NEXT_STAGE
(required — the signing tail; pepsi-setup refuses a relay). Everything else
it needs is the member descriptor the fan-out wrote and the list’s own
attributes, plus [pepsi-list] BASE_URL and UNSUBSCRIBE_SECRET from
the site section.
73.6. State¶
Inputs:
state.list— the list id, the member’s language, their unsubscribe serial and the duplicate flag — and the row’s singlercpt_to, which is the authority on the address.Outputs: a rewritten header block and body. No
stateis changed.Transitions: advance to
NEXT_STAGE, the only transition.
73.7. See also¶
Mailing lists, pepsi-stage-list-post, pepsi-stage-dkim-sign, pepsi-list, pepsi-stage-list-deliver(1).