85.1.22. pepsi-stage-list-command

subscribe, unsubscribe and confirm by e-mail

Manual section:

1

85.1.22.1.1. Name

pepsi-stage-list-command - the mailing-list command stage of the Pepsi pipeline.

85.1.22.1.2. Synopsis

pepsi-stage-list-command [GLOBAL-OPTIONS] worker

85.1.22.1.3. Description

pepsi-stage-list-command is a stage program run by pepsi-dispatch(1). It 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.

The sentence that 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.

85.1.22.1.4. Addresses

Seven, and the first five are not parsed at all:

Address

What happens

<list>-join,

one synthetic join; the Subject and body are

<list>-subscribe

never read, and no results mail is sent

<list>-leave,

one synthetic leave, likewise

<list>-unsubscribe

<list>-confirm+<token>

one synthetic confirm, with the token taken from the address and never from the message

<list>-request

the Subject and the body are parsed

<list>-owner

not a command at all: forwarded to every owner and moderator

-owner mail never reaches the parser, which is upstream’s behaviour and matters: a message to the owners whose Subject happens to read unsubscribe must reach a human, not run a command.

85.1.22.1.5. Commands

confirm

Confirm a pending request using its token.

echo

Echo back the arguments, for testing.

end

Stop processing commands (alias: stop).

help

Show the command list, or help for one command.

join

Subscribe (alias: subscribe).

leave

Unsubscribe (alias: unsubscribe).

who

Show the roster, if you are allowed to see it.

Three 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.

85.1.22.1.6. Parsing

Upstream’s leniency rules, reproduced because twenty years of “send the word HELP to…” instructions exist:

  1. An address command wins outright (see the table above).

  2. The Subject: is the first command line, RFC 2047-decoded and then forced to ASCII.

  3. 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: executing commands out of HTML is a place where being helpful is a security decision.

  4. There is no quote, signature or -- handling whatsoever.

  5. Every token is lower-cased; blank lines are skipped silently.

  6. An unknown first word is retried after dropping exactly one word. That is the entire Re: mechanism, and it is deliberately not a general prefix stripper — Fwd: help me would then run help.

  7. A command that stops ends the run; the rest is reported as unprocessed.

  8. Before any of that, a message carrying Precedence: bulk, junk or list without X-Ack: yes is discarded silently.

Two of these look like bugs and are not. The retry in rule 6 drops one word and no more. And the cap in rule 3 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.

85.1.22.1.7. 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.

85.1.22.1.8. 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(1). Deleting a token discards its workflow state with it, which is what makes a replayed confirmation find nothing.

85.1.22.1.9. Configuration

RESPONSE_STAGE (required)

Where replies and confirmation notices are injected. The signing tail, not the list delivery stage: pepsi-stage-list-deliver(1) only accepts a member’s copy from the fan-out and fails anything else, so a notice sent there never goes out. pepsi-setup(1) refuses the wrong value.

NEXT_STAGE (required)

Where -owner mail goes once it has been readdressed to the list’s owners. pepsi-setup refuses a value that points back at the router, which would re-route the forward and loop.

MAX_COMMAND_LINES (default 10)

How many lines of the body the parser reads — upstream’s [mailman] email_commands_max_lines. Counts lines, not commands.

85.1.22.1.10. State

Inputs: state.list.id and state.list.role (written by pepsi-stage-list(1)), and state.list.token for a -confirm address.

Outputs: none. Everything this stage changes is a database row.

Transitions: finishes (every command path), or advances to NEXT_STAGE (the -owner forward).

85.1.22.1.11. Commands

worker

Run as a persistent pepsi-dispatch(1) worker, reading message ids on standard input.

85.1.22.1.12. 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.22.1.13. Errors and retries

A message that names a list which no longer exists is reported to the sender through the stage’s BOUNCE_STAGE (one DSN per recipient, saying the mailing list no longer exists), or failed when the stage has none; a null-sender message is dropped instead. A message the MIME parser refuses is failed at once. Any other error is a fault of the host and is retried with back-off until MAX_LIFETIME (see pepsi-dispatch(1)).

The grace-period claim for the results mail is recorded before list_command_commit commits the commands and the mail, so it is keyed on the message’s queue token: a message retried after that commit failed is allowed to send its results again rather than being told it was already answered today.

85.1.22.1.14. Exit Status

0

The message was handled.

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.22.1.15. Examples

[stage-list-command]
PROGRAM = pepsi-stage-list-command
RESPONSE_STAGE = dkim-sign
NEXT_STAGE = local
MAX_COMMAND_LINES = 10

85.1.22.1.16. See Also

pepsi-list(1), pepsi-stage-list(1), pepsi-stage-list-post(1), pepsi-dispatch(1), pepsi.conf(5), pepsi.state(7), pepsi-setup(1)

85.1.22.1.17. Bugs

Report bugs to the Pepsi issue tracker.