.. 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-secretary ===================== *Hold mail from unknown senders until they confirm by replying.* Role ==== ``pepsi-stage-secretary`` is confirm-to-send, after qmail's qsecretary. Mail from a sender no whitelist knows is held, and the sender is asked — in their language — to reply once. The reply whitelists them and releases everything held for them; later mail from them passes :doc:`pepsi-stage-check-whitelist` and is never held again. Without a reply by ``HOLD_TIME`` the held mail takes the timeout path (``BOUNCE_STAGE`` if one is wired, else it is deleted). Reference: :manpage:`pepsi-stage-secretary(1)`. It is weaker than pay-to-send (:doc:`pepsi-stage-anti-spam`): a spammer with a working mailbox can reply, and the declaration the sender agrees to (``PENALTY``) is a legal deterrent rather than an economic one. It is still worth having, because most bulk mail comes from addresses that cannot receive, and a real correspondent pays one reply, once. :doc:`../features` compares the two. What is challenged ================== A null-sender message, locally submitted mail and a whitelisted sender (``state.spam = false``) pass untouched; ``state.spam = true`` takes the timeout path. Everything else is challenged — unless it must not be, in which case it goes to ``UNCHALLENGEABLE_STAGE`` (unset: the timeout path): * RFC 3834 says do not answer it (lists, ``Precedence: bulk``, ``Auto-Submitted:``, service senders, delivery reports); * its ``From:`` is not exactly the envelope sender — the challenge goes to MAIL FROM and the whitelist matches ``From:``, so they must be one address; * ``REQUIRE_AUTHENTICATED`` (default on) and neither SPF nor DMARC passed — the backscatter gate; * the sender has had ``MAX_CHALLENGES_PER_SENDER`` (default 5) challenges in the last 24 hours, across every whitelist. One challenge covers every message from one sender to one whitelist: a second message joins the open challenge and inherits its deadline. A message whose recipients expand ``WHITELIST_NAME`` to different lists is split one row per list. The challenge and the reply =========================== The challenge is a null-sender ``text/plain`` message with ``Auto-Submitted: auto-replied``, injected at ``RESPONSE_STAGE``. Its ``From:`` and ``Reply-To:`` are the reply address ``-@`` (default ``secretary-…``), whose cookie is a random 128-bit identifier. It never quotes the held message — a challenge to a forged sender would otherwise deliver the spam for the spammer — beyond an 8-character hint of its subject (``SUBJECT_HINT_LENGTH``). The text comes from ``MESSAGE_`` in the stage's own section (a user's own, through the settings layer) or the ``secretary-challenge..body`` template, per language, with a built-in English fallback; the fields are ``SENDER_NAME``, ``RECIPIENT``, ``SUBJECT_HINT``, ``ORIGINAL_DATE``, ``DEADLINE``, ``REPLY_ADDRESS`` and ``PENALTY``. Language choice and the two-space rule are :doc:`pepsi-stage-vacation`'s. A reply to the address confirms when it comes from the challenged address (its envelope sender or ``From:``). A null-sender reply means the challenge bounced, and releases the held mail to the timeout path at once; a reply marked as automatic (an out-of-office) is ignored. A reply after the deadline has no effect: the challenge is gone. The reply itself is always consumed. Placement ========= After :doc:`pepsi-stage-detect-language` and :doc:`pepsi-stage-check-whitelist`, before :doc:`pepsi-stage-aliases`, the list router and local delivery. Run :doc:`pepsi-stage-auto-whitelist` into the same ``WHITELIST_NAME`` on the outbound path so the people your users write to are never challenged. ``pepsi-setup``'s wizard wires all of it when asked "Ask unknown senders to confirm by reply". Configuration ============= ``[stage-]``: ``PROGRAM = pepsi-stage-secretary``, ``NEXT_STAGE`` and ``RESPONSE_STAGE`` (mandatory), ``WHITELIST_NAME`` (mandatory, one name or a ``{login}``/``{localpart}`` template; the operator may name any whitelist, a shared one included, while a value an account owner sets by mail must stay in their ``/...`` namespace or be the operator's -- enforced by :doc:`pepsi-stage-edit-settings` when it stores the value), ``BOUNCE_STAGE``, ``UNCHALLENGEABLE_STAGE``, ``CONTROL_LOCAL_PART``, ``HOLD_TIME``, ``REQUIRE_AUTHENTICATED``, ``MAX_CHALLENGES_PER_SENDER``, ``DKIM_REQUIRED``, ``CONFIRM_NOTICE``, ``PENALTY``, ``SUBJECT_HINT_LENGTH``, ``TEMPLATE``, ``SUBJECT``, ``DEFAULT_LANGUAGE``, the ``MESSAGE_``/``SUBJECT_``/``CONFIRM_MESSAGE_`` overrides and the locality options. ``pepsi-setup`` rejects unresolvable targets and a ``DEFAULT_LANGUAGE`` with no text, and warns when no check-whitelist consults the whitelist, no auto-whitelist writes it, or ``CONTROL_LOCAL_PART`` is a login or an alias. See :manpage:`pepsi-stage-secretary(1)` and :doc:`../configuration`. State ===== * **Inputs:** ``state.spam``, ``state.local_origin``, ``state.auth`` (SPF, DMARC, DKIM, ARC), ``state.language``. * **Outputs:** ``state.secretary`` on a held row (``challenge``, ``deadline``, ``whitelist``; then ``confirmed`` or ``abandoned``), ``state.secretary.unchallengeable`` on mail routed to ``UNCHALLENGEABLE_STAGE``, and ``state.spam = false`` on released mail. The timeout path removes ``state.secretary``. ``state.dsn`` is preserved. * **Transitions:** advance to ``NEXT_STAGE``; pause while held; reroute to ``UNCHALLENGEABLE_STAGE`` or ``BOUNCE_STAGE``; delete a reply, or timed-out mail when there is no ``BOUNCE_STAGE``. Database ======== ``pepsi.secretary_challenge`` (one open challenge per whitelist and sender, deleted when it is answered, bounces or expires) and ``pepsi.secretary_sent`` (when each challenge was sent, without its cookie, for the daily cap). A confirmation writes an ordinary ``pepsi.whitelist`` row, escaped and anchored exactly as :doc:`pepsi-stage-auto-whitelist` writes one. See also ======== :doc:`pepsi-stage-check-whitelist`, :doc:`pepsi-stage-auto-whitelist`, :doc:`pepsi-stage-anti-spam`, :doc:`pepsi-whitelist`, :doc:`pepsi-settings`, :doc:`../features`, :manpage:`pepsi-stage-secretary(1)`.