74. pepsi-stage-list-command

Subscribe, unsubscribe and confirm by e-mail; forward `-owner` mail.

74.1. Role

pepsi-stage-list-command handles everything a mailing list does by mail that is not a post: the seven e-mail commands, both subscription workflows, and the -owner forward. Reference: pepsi-stage-list-command(1).

One sentence governs the whole stage:

The only path that adds an address without that address proving it wants to be there is an explicit pre_verified and pre_confirmed from an authenticated REST client or the CLI.

Nothing in this stage can set either flag. A message is not an authenticated client, so a join always either asks the named address to confirm or refuses.

This subsystem is a reimplementation of GNU Mailman 3; see Mailing lists, which names what was taken from upstream.

74.2. Addresses

Seven, and the first five are not parsed at all: -join/-subscribe become one synthetic join (the Subject and body are never read, and no results mail is sent), -leave/-unsubscribe one synthetic leave likewise, and -confirm+<token> one synthetic confirm whose token is taken from the address and never from the message. -request is the one whose Subject and body are parsed. -owner is not a command at all: it is forwarded to every owner and moderator, and never reaches the parser — which is upstream’s behaviour and matters, because a message to the owners whose Subject happens to read unsubscribe must reach a human rather than run a command.

74.3. Commands

confirm, echo, end (alias stop), help, join (alias subscribe), leave (alias unsubscribe) and who. Three of them carry a security decision rather than a formatting one.

  • ``confirm`` de-duplicates tokens already confirmed in the same message — a mail client that quotes the original produces the token twice — and always stops processing. A token that does not match gets the same answer as one that has expired, because distinguishing them is an oracle for which tokens exist.

  • ``leave`` takes no arguments at all. join address= costs the named address one confirmation message and subscribes nobody; an equivalent leave address= would let a forged From: unsubscribe a stranger outright. The sender’s own address must additionally be verified.

  • ``who`` is gated on member_roster_visibility, and an unauthorised request is refused rather than answered with an empty list — an empty answer is indistinguishable from a list with no members, which tells a harvester the address is worth trying again.

74.4. Parsing

Upstream’s leniency rules, reproduced because twenty years of “send the word HELP to…” instructions exist. An address command wins outright. The Subject: is the first command line, RFC 2047-decoded and then forced to ASCII. The body contributes only the first ``text/plain`` part, and only its first MAX_COMMAND_LINES lines — a message with no plain part contributes nothing, because executing commands out of HTML is a place where being helpful is a security decision. There is no quote, signature or -- handling whatsoever; every token is lower-cased; blank lines are skipped. An unknown first word is retried after dropping exactly one word, which is the entire Re: mechanism and deliberately not a general prefix stripper (Fwd: help me would otherwise run help). A command that stops ends the run and the rest is reported as unprocessed. Before any of that, a message carrying Precedence: bulk|junk|list without X-Ack: yes is discarded silently.

Two of these look like bugs and are not: the retry drops one word and no more, and the line cap counts lines, not commands — so eleven blank lines followed by help executes nothing and reports the help as ignored. Raising MAX_COMMAND_LINES is the fix for “my mail client put a header block in front of my command”, not quote detection.

74.5. The reply

A results mail with upstream’s shape: a preamble, the original message’s details, the results, the unprocessed and ignored lines, and “- Done.”

The original message is never attached. A command message’s From: is forgeable, so a results mail goes to an address that may not have sent anything; attaching the original would make this stage a backscatter amplifier.

74.6. Confirmation tokens

A token is 256 bits of randomness in base32. It is also the -confirm+<token> sub-address and the last path segment of the confirmation URL, so guessing one is completing somebody else’s subscription.

Its lifetime depends on who is being waited for: a token owned by a moderator lives 180 days, and everything else lives three. A moderator may be on holiday, and a subscription request that expired in three days would be silently lost rather than decided. Expired tokens are swept by pepsi-list tasks --once, which a systemd timer runs; see pepsi-list. Deleting a token discards its workflow state with it, which is what makes a replayed confirmation find nothing.

74.7. Configuration

[stage-<name>]: PROGRAM = pepsi-stage-list-command, RESPONSE_STAGE (required — the signing tail, not the list delivery stage, which accepts only a member’s copy from the fan-out and would swallow a notice), NEXT_STAGE (required — where -owner mail goes once readdressed; never the list router) and MAX_COMMAND_LINES (default 10). pepsi-setup checks both targets.

74.8. State

  • Inputs: state.list — the list id, the role the address named, and any -confirm token — written by pepsi-stage-list.

  • Outputs: injected reply and notice messages; the command message itself is consumed.

  • Transitions: finish (a command message is answered, not forwarded), or advance for the -owner forward.

74.9. See also

Mailing lists, pepsi-stage-list, pepsi-stage-list-post, pepsi-list, pepsi-stage-list-command(1).