85.1.13. pepsi-stage-dot-forward¶
process per-user ~/.forward files
- Manual section:
1
85.1.13.1.1. Name¶
pepsi-stage-dot-forward - the ~/.forward processing stage of the Pepsi pipeline.
85.1.13.1.2. Synopsis¶
pepsi-stage-dot-forward [GLOBAL-OPTIONS] worker
85.1.13.1.3. Description¶
pepsi-stage-dot-forward is a stage program run by pepsi-dispatch(1).
For each envelope recipient that resolves to a local account it runs that
user’s ~/.forward file — the classic sendmail/Postfix mechanism by which an
account owner redirects their own mail. Locality is decided exactly as in
pepsi-stage-relay-to-maildir(1): the recipient’s domain must be in
LOCAL_DOMAINS, its mailbox name (local-part, sub-address after
RECIPIENT_DELIMITER removed, lower-cased) must resolve to a passwd entry, and
that uid must be permitted by TARGETS. A recipient that is not local is left
untouched and advances to NEXT_STAGE. A passwd lookup that fails (an LDAP
or sssd backend that is down) is not taken to mean “not local”: the message
is retried, rather than delivered past the user’s ~/.forward.
The ~/.forward itself is read and executed by the privileged helper
pepsi-helper-dot-forward(1), which drops to the target user first. The
helper’s exit status tells the stage what became of that recipient:
no ~/.forward — the recipient is a passthrough: it stays on the message, which advances to NEXT_STAGE (typically local delivery, then a smarthost). A recipient kept by its own
\nameentry advances to NEXT_STAGE too, but, whenever the message is split, on a sibling row of its own, so that no retry of this stage runs its~/.forwarda second time.forwarded — the recipient is replaced by the addresses the
~/.forwardnamed. Those restart the pipeline at RESTART_STAGE (defaultinit) on a newpendingsibling row, so the rewritten recipients are re-authenticated and re-routed like fresh mail. An empty address list (the message was consumed entirely by|pipe//filedirectives) simply drops the recipient.failed — the helper refused or failed for that recipient (exit
2): a pipe command exited non-zero, a file could not be written, or a disabled directive was requested. The recipient is routed to BOUNCE_STAGE so a failure DSN is generated (honouring the sender’sNOTIFY). A message that is itself a bounce (null sender) is never re-bounced.host problem — the helper reported an operational failure (exit
3: it is not installed setuid-root, the passwd lookup failed, the account is refused, or the home directory or~/.forwardis not exclusively the user’s — not owned by them, or group-/world-writable; the helper checks the home directory before looking for a~/.forward, so this also refuses accounts that have none), a|commandasked to be tried again (it exited75,EX_TEMPFAIL, or ran out of time), or the helper could not be run at all, was killed by a signal, or exited with a status it does not document. None of these is a verdict on the recipient, so nothing is bounced: the message is retried (one minute, doubling to an hour, until MAX_LIFETIME; see pepsi-dispatch(1)).The recipients processed earlier in the same pass have been dealt with — their pipes have run, their files have been written — so before the retry they are committed with their outcome (forwarded, kept, bounced) and the message is reduced to the recipients the helper did not reach. A retry therefore never runs a recipient’s
~/.forwardtwice. The one exception is the recipient whose own~/.forwarddeferred part-way through: directives above the one that deferred have acted, and act again.
A single message may have a mix of these outcomes across its recipients; the forwarded addresses, the per-recipient bounces and the kept (passthrough) recipients are reconciled in one split of the message, followed by its terminal (in the same statement when no recipient stays on the message). Every sibling row’s token is derived from the recipient or forwarding login it carries, so a retried pass can never collide with one an earlier pass created.
85.1.13.1.4. Forwarding loops¶
Because a forwarded message restarts the whole pipeline, two users forwarding to
each other (~bob → alice, ~alice → bob) would loop. To prevent
this every forwarded row carries its forwarding chain in
state["dot-forwarders"] (see pepsi.state(7)): the chain of the row it
was forwarded from plus the login whose ~/.forward forwarded it. A recipient
whose user is already in its row’s chain is not delivered and the helper is
not called (a warning is logged), so the chain terminates. The recipient is
routed to BOUNCE_STAGE like any other ~/.forward failure, so the sender is
told (RFC 5321 §6.1 requires a notification once the message has been accepted);
a message that is itself a bounce, or a stage with no BOUNCE_STAGE, drops it
instead.
The chain belongs to one forwarding path, not to the message: when several
recipients of one message forward, each one’s addresses go out on a sibling row
of their own carrying only that recipient’s chain, so one recipient’s
~/.forward never makes a later hop through another’s look like a loop. An
address that several of them forward to is delivered once, on the row of the
first recipient that named it.
85.1.13.1.5. Arguments and sub-commands¶
- worker
Run as a persistent pepsi-dispatch(1) worker, reading message ids on standard input and writing one status line each. This is how the dispatcher runs the stage in production.
85.1.13.1.6. Configuration¶
Read from the message’s [stage-<stage>] section:
- PROGRAM
Must be
pepsi-stage-dot-forward.- RESTART_STAGE
The stage a successfully forwarded message restarts at. Defaults to
init(re-running the full inbound pipeline, including authentication). Set it to a later stage (e.g.srs) to skip re-authenticating an internally forwarded message. Must name an existing stage.- ALLOW_PIPE
Whether
|commanddirectives in a~/.forwardare honoured (the message is piped to the command, run as the user). Defaults toyes.- ALLOW_FILE
Whether
/pathdirectives (append the message to a file) are honoured. Defaults toyes. When both ALLOW_PIPE and ALLOW_FILE arenothe stage does not pipe the message body to the helper at all (the helper only needs to read forwarding addresses).- TARGETS, LOCAL_DOMAINS, RECIPIENT_DELIMITER
The local-account selection, identical in meaning to pepsi-stage-relay-to-maildir(1). TARGETS defaults to the regular (non-system) uid range from
/etc/login.defs, and pepsi-helper-dot-forward(1) refuses any uid belowUID_MINwhatever it says (pepsi-setup(1) warns about a token that reaches below it); LOCAL_DOMAINS defaults to[pepsi-ingress] ACCEPTED_DOMAINS; RECIPIENT_DELIMITER defaults to[pepsi] RECIPIENT_DELIMITERand, failing that, to+(nonedisables sub-address stripping).- HELPER
The privileged
~/.forwardhelper to run. Defaults topepsi-helper-dot-forward(resolved on$PATH); set an absolute path to override.- NEXT_STAGE
Where passthrough recipients (no
~/.forward) advance. Required, since most recipients have no~/.forward. The stage is normally placed just before local delivery, so NEXT_STAGE is the local-delivery stage.- BOUNCE_STAGE
The DSN-generation stage a recipient is routed to when its
~/.forwardexecution fails.
85.1.13.1.7. A ~/.forward file¶
The helper interprets each non-empty, non-comment (#) line of the user’s
~/.forward as one of:
an e-mail address — collected and used as a new recipient;
\name— sendmail’s “deliver to name and expand it no further” marker. The address is not restarted at RESTART_STAGE: if it names the recipient’s own account (the canonical\bobin~bob/.forward, which is how a user keeps a local copy while also forwarding) that recipient simply stays on the message and advances to NEXT_STAGE; any other\name— a bare name is qualified with the recipient’s domain — becomes a sibling row at NEXT_STAGE. Because such a recipient never re-enters this stage, the marker cannot create an unbounded loop. Note that it also bypasses that user’s own~/.forward, which is what “expand no further” means;|command— when ALLOW_PIPE is on, the message is piped to/bin/sh -c commandrun as the user;/absolute/path— when ALLOW_FILE is on, the message is appended to that file (mbox style) as the user.
Forwarding addresses should be fully qualified (user@domain); a bare local
name is returned verbatim and may not re-resolve as local.
A |command is given 300 seconds to finish, after which its whole process
group is killed and the recipient is retried like a command that exited 75
(a slow delivery agent says nothing about the recipient; bouncing it discarded
mail that a minute later would have been taken). The stage in turn abandons and
kills a helper that has not finished after 330 seconds, which it reports as a
retryable error rather than a bounce. Both limits are compile-time constants, not
configuration: they exist so that one account’s |sleep infinity — or a file
directive naming a FIFO — cannot consume a dispatcher worker permanently, and
PARALLELISM of them cannot stop the stage. A file directive must name a
regular file; a FIFO, device or symlink is refused.
85.1.13.1.8. Installation and privileges¶
To reach the setuid-root helper, pepsi-stage-dot-forward must itself be
installed set-group-id to the pepsi-forward group (mode 2550, owner
pepsi:pepsi-forward — owner-execute and not world-execute, because the setgid
bit is the gate on the setuid-root helper; the exec right cannot come from the
group bits either, pepsi being deliberately not a member of the group, so it
comes from the owner). pepsi-dispatch(1) runs the stage as the
unprivileged pepsi service user; the setgid bit gives the worker an effective
gid of pepsi-forward, which is exactly what lets it execute the
group-restricted pepsi-helper-dot-forward(1) — and nothing else on the host
gains that ability. make install sets this up (its install-forward-stage
step), provided it is run as root and the pepsi-forward group exists;
otherwise it prints the exact commands to run by hand.
The setgid bit being that gate, the program states the same rule itself: unless
the real user is root or the pepsi service account it exits with an error
before any configuration is read, and — like the setuid crypto stages — it strips
the environment variables that steer configuration loading (HOME,
XDG_CONFIG_HOME, PG*, TALER_*, PEPSI_*) and pins PATH to a
safe default.
A file mode is a deployment fact and this is a program fact; neither is asked to
stand on its own.
85.1.13.1.9. See Also¶
pepsi-helper-dot-forward(1), pepsi-stage-relay-to-maildir(1), pepsi-stage-aliases(1), pepsi-stage-bounce(1), pepsi-dispatch(1), pepsi.conf(5), pepsi.state(7)
85.1.13.1.10. Bugs¶
Report bugs to the Pepsi issue tracker.