.. 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-dot-forward ======================= *Process per-user ~/.forward files.* Role ==== ``pepsi-stage-dot-forward`` lets a local account owner redirect their own mail with a ``~/.forward`` file. For each envelope recipient that is *local* (the same ``LOCAL_DOMAINS`` / ``TARGETS`` / ``RECIPIENT_DELIMITER`` test as :doc:`pepsi-stage-relay-to-maildir`) it runs that user's ``~/.forward`` via the setuid-root :manpage:`pepsi-helper-dot-forward(1)` helper, which drops to the user first. Non-local recipients are left untouched. Normally placed just before local delivery. Reference: :manpage:`pepsi-stage-dot-forward(1)`. Features ======== Per-recipient outcomes ---------------------- * **No ~/.forward** — the recipient is a passthrough and advances to ``NEXT_STAGE`` (typically local delivery). * **Forwarded** — the recipient is replaced by the addresses the ``~/.forward`` named; those restart the pipeline at ``RESTART_STAGE`` (default ``init``) so they are re-authenticated and re-routed like fresh mail. An empty result (the message was consumed by a ``|pipe``/``/file`` directive) drops the recipient. * **Failed** — a failed ``~/.forward`` (pipe command error, file write error, or a disabled directive) routes the recipient to ``BOUNCE_STAGE``. * **Host problem** — the helper could not act at all (not installed setuid-root, a passwd lookup failure, a home directory or ``~/.forward`` that is not exclusively the user's). Nothing is bounced: the message stays queued and is retried once the host is fixed. A message with several recipients can mix these; the forwarded addresses, the per-recipient bounces and the kept recipients are reconciled in one database round-trip. ~/.forward directives --------------------- The helper acts on each non-empty, non-``#`` line as the user: a bare address becomes a forwarding address; ``|command`` pipes the message to a shell when ``ALLOW_PIPE`` is on; ``/absolute/path`` appends the message to a file when ``ALLOW_FILE`` is on. When both ``ALLOW_PIPE`` and ``ALLOW_FILE`` are off the message body is not even handed to the helper. An address written with sendmail's leading ``\`` means "deliver to this name and expand it no further", and is **not** restarted at ``RESTART_STAGE``. The canonical keep-a-local-copy line — ``\bob`` in ``~bob/.forward`` — simply keeps that envelope recipient on this row, so it advances to ``NEXT_STAGE`` and is delivered locally; any other ``\name`` becomes a sibling row at ``NEXT_STAGE``. That is also what stops a ``\`` entry re-entering this stage. Loop prevention --------------- Because forwarding restarts the pipeline, every forwarded row carries its forwarding chain in ``state["dot-forwarders"]`` (see :doc:`../state`): the chain of the row it came from plus the login whose ``~/.forward`` forwarded it. A recipient whose user is already in its row's chain is routed to ``BOUNCE_STAGE`` without re-running its ``~/.forward`` (a null-sender message is dropped instead), so a forwarding cycle terminates and the sender is told. The chain is per forwarding path: several recipients of one message that forward each get a sibling row carrying only their own chain. Privileges ========== The stage must be installed setgid ``pepsi-forward`` (mode ``2550``, owner ``pepsi:pepsi-forward`` — owner-execute, so no other local user reaches the setgid bit); that group membership is what lets the dispatcher's ``pepsi`` worker exec the ``4750 root:pepsi-forward`` helper, which alone runs the ``~/.forward`` as the target user and refuses to act for ``root``. Carrying that bit is why the stage is a **standalone** binary rather than one of the programs folded into the unified ``pepsi`` binary. Configuration ============= ``[stage-]``: ``PROGRAM = pepsi-stage-dot-forward``, ``NEXT_STAGE`` (where a passthrough recipient goes — normally local delivery), ``BOUNCE_STAGE`` (where a failed ``~/.forward`` recipient goes), ``RESTART_STAGE`` (default ``init``), ``ALLOW_PIPE`` and ``ALLOW_FILE`` (both default ``yes``), ``HELPER`` (default ``pepsi-helper-dot-forward``) and the shared locality options ``LOCAL_DOMAINS`` (defaulting to ``[pepsi-ingress] ACCEPTED_DOMAINS``), ``TARGETS`` and ``RECIPIENT_DELIMITER``. ``pepsi-setup`` checks that ``RESTART_STAGE`` names a real stage. See :manpage:`pepsi-stage-dot-forward(1)`. State ===== * **Inputs:** ``state["dot-forwarders"]`` (the loop guard) and ``state.dsn``. * **Outputs:** ``state["dot-forwarders"]`` extended with each login whose ``~/.forward`` ran, on every row the fan-out produces; ``state.dsn.rcpt`` is rebuilt in lockstep with each row's recipients, and a bounced recipient's row carries ``state.bounce``. See also ======== :doc:`pepsi-stage-relay-to-maildir`, :doc:`pepsi-stage-aliases`, :doc:`pepsi-stage-bounce`, :doc:`../features`, :manpage:`pepsi-stage-dot-forward(1)`, :manpage:`pepsi-helper-dot-forward(1)`.