85.1.21. pepsi-stage-list-deliver¶
finalise one member’s copy of a mailing-list post
- Manual section:
1
85.1.21.1.1. Name¶
pepsi-stage-list-deliver - the per-member delivery stage of the Pepsi mailing-list pipeline.
85.1.21.1.2. Synopsis¶
pepsi-stage-list-deliver [GLOBAL-OPTIONS] worker
85.1.21.1.3. Description¶
pepsi-stage-list-deliver is a stage program run by pepsi-dispatch(1). pepsi-stage-list-post(1) has already made one row per recipient; this is what makes each of them that member’s copy rather than another copy of the same message. One row in, one row out.
Three things happen:
``List-Unsubscribe`` is finalised with that member’s one-click
https:URI, alongside themailto:form, andList-Unsubscribe-Post: List-Unsubscribe=One-Clickis added (RFC 8058).The message is decorated — the list’s header and footer, in the member’s language — when the list’s
personalizeisindividualorfull.``To:`` is munged to name the member, for
fullonly. Upstream’spersonalize_tois a no-op forindividual, and so is this.
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.
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(1), 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.
85.1.21.1.4. 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 has 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;and 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.
85.1.21.1.5. Decoration¶
The header and footer are rendered from the [pepsi] TEMPLATE_DIR as
list-header.<lang>.body and list-footer.<lang>.body, with the member’s
language chosen first and en as the fallback. A list with no template files
is not decorated, which is the common case and costs nothing. A template that
exists but cannot be read or rendered is an error, and the copy is retried
rather than sent without the footer the owner configured.
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.
85.1.21.1.6. 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, 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.
85.1.21.1.7. Configuration¶
- NEXT_STAGE (required)
The signing tail. pepsi-setup refuses a relay here.
Everything else this stage needs is the member descriptor the fan-out wrote and
the list’s own attributes; [pepsi-list] BASE_URL and
UNSUBSCRIBE_SECRET are read from the site section.
85.1.21.1.8. State¶
Inputs: state.list — the list id, the member’s language, their
unsubscribe serial and the duplicate flag — and the row’s single rcpt_to,
which is the authority on the address.
Outputs: a rewritten header block and body. No state is changed.
Transitions: advances to NEXT_STAGE, the only transition.
85.1.21.1.9. Commands¶
- worker
Run as a persistent pepsi-dispatch(1) worker, reading message ids on standard input.
85.1.21.1.10. Global Options¶
The usual set — -c/–config, -L/–log, -v/–verbose, -h/–help and -V/–version — behaving as they do for every Pepsi program; see pepsi-config(1).
85.1.21.1.11. Errors and retries¶
A copy whose list was deleted after the fan-out is dropped, with a warning in the log: the owner deleted the list, a copy delivered now would carry an unsubscribe link to nothing, and there is nobody left to report to.
A copy that cannot be parsed for decoration is delivered undecorated, with a warning. The fan-out built those bytes from a message it had parsed, so this is not the sender’s doing and a retry cannot change it; the copy still carries the list’s own
List-*headers (themailto:unsubscribe included), and dropping it would lose the member’s mail.A row with no member descriptor is failed at once.
Any other error (the database, a template) is retried with back-off until MAX_LIFETIME (see pepsi-dispatch(1)). A copy that is finally given up on is deleted by pepsi-failure-bouncer(1), not bounced, and so never counts against the member.
85.1.21.1.12. Exit Status¶
- 0
The copy was finalised and advanced.
- 1
The worker could not run (its database could not be opened, or standard input/output failed). The reason is written to the log.
85.1.21.1.13. Examples¶
[stage-list-deliver]
PROGRAM = pepsi-stage-list-deliver
NEXT_STAGE = dkim-sign
85.1.21.1.14. See Also¶
pepsi-list(1), pepsi-stage-list-post(1), pepsi-stage-list(1), pepsi-stage-dkim-sign(1), pepsi-stage-arc(1), pepsi-dispatch(1), pepsi.conf(5), pepsi.state(7), pepsi-setup(1)
85.1.21.1.15. Bugs¶
Report bugs to the Pepsi issue tracker.