41. pepsi-stage-dot-forward

Process per-user ~/.forward files.

41.1. 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 pepsi-stage-relay-to-maildir) it runs that user’s ~/.forward via the setuid-root 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: pepsi-stage-dot-forward(1).

41.2. Features

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

41.2.2. ~/.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.

41.2.3. Loop prevention

Because forwarding restarts the pipeline, every forwarded row carries its forwarding chain in state["dot-forwarders"] (see The message 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.

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

41.4. Configuration

[stage-<name>]: 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 pepsi-stage-dot-forward(1).

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

41.6. See also

pepsi-stage-relay-to-maildir, pepsi-stage-aliases, pepsi-stage-bounce, Supported Features, pepsi-stage-dot-forward(1), pepsi-helper-dot-forward(1).