.. This file is part of PEPSI. Copyright (C) 2026 GNUnet e.V. PEPSI is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version. ======================== pepsi-stage-list-command ======================== *Subscribe, unsubscribe and confirm by e-mail; forward `-owner` mail.* 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: :manpage:`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 :doc:`../mailing-lists`, which names what was taken from upstream. 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+`` 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. 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. 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. 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. Confirmation tokens =================== A token is 256 bits of randomness in base32. It is **also** the ``-confirm+`` 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 :doc:`pepsi-list`. Deleting a token discards its workflow state with it, which is what makes a replayed confirmation find nothing. Configuration ============= ``[stage-]``: ``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. State ===== * **Inputs:** ``state.list`` — the list id, the role the address named, and any ``-confirm`` token — written by :doc:`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. See also ======== :doc:`../mailing-lists`, :doc:`pepsi-stage-list`, :doc:`pepsi-stage-list-post`, :doc:`pepsi-list`, :manpage:`pepsi-stage-list-command(1)`.