70.1.27. pepsi-stage-route

route each recipient to a next hop by their domain

Manual section:

1

70.1.27.1.1. Name

pepsi-stage-route - the recipient-domain routing stage of the Pepsi pipeline.

70.1.27.1.2. Synopsis

pepsi-stage-route [GLOBAL-OPTIONS] worker

70.1.27.1.3. Description

pepsi-stage-route is a stage program run by pepsi-dispatch(1) as a persistent worker reading message ids on standard input. It loads that pepsi.ingress row (refusing to act unless its status is running), reads its [stage-<stage>] section, and chooses a next stage for each envelope recipient from that recipient’s domain.

It exists for one deployment shape: a gateway sitting in front of another mail system, such as Microsoft Exchange or Exchange Online. Such a gateway has one routing decision to make on every inbound message — does this recipient belong to the system behind me, or to the internet? — and getting it wrong is not a delivery failure but a mail loop. The domains behind the gateway are exactly the domains whose public MX is the gateway, so handing one to a direct-to-MX relay posts the message straight back into our own ingress, which accepts it, routes it out again, and repeats until MAX_HOP_COUNT stops it.

The stage never rewrites the message, never delivers, never bounces and never drops one. The only column it moves is stage, so state — including state.dsn — is preserved exactly. Neither the headers nor the body is loaded.

Unlike the other metadata-only stages it does not default to FUSION = yes. A fused stage runs inside its predecessor’s worker process, and this stage’s recipient fan-out refuses to run while a predecessor’s content changes are still uncommitted; rather than make that interaction a run-time surprise, the stage is always dispatched normally. See pepsi-dispatch(1).

70.1.27.1.4. Routing

For each recipient, in this order:

  1. the explicit ROUTES rules, in configured order — the first whose domain pattern matches wins;

  2. the managed-domain set (MANAGED_DOMAINS, defaulting to [pepsi-ingress] ACCEPTED_DOMAINS) — a recipient at one of those domains goes to MANAGED_STAGE, when it is set;

  3. otherwise NEXT_STAGE.

Explicit rules are consulted first so a carve-out is expressible: one subdomain of a served domain can be sent somewhere other than the system behind the gateway without removing the parent from MANAGED_DOMAINS, which would also stop ingress accepting mail for it.

Domain patterns are compared case-insensitively and may contain *, which matches any run of characters. They are globs, not regular expressions, and are anchored at both ends: *.example.org matches mail.example.org but neither example.org itself nor mail.example.org.evil.test. A pattern that looks like a regular expression is rejected at configuration time rather than silently matching nothing.

MANAGED_DOMAINS defaults to what ingress accepts rather than making the operator restate it: a routing table that has drifted from ACCEPTED_DOMAINS is precisely a domain whose mail goes to the wrong side of the gateway.

70.1.27.1.5. Recipient fan-out

Recipients that route to the same stage travel together. When they diverge, the message is split: this row keeps the first group and every other group becomes a sibling pending row at its own target, all in one database call. Each sibling carries only its own recipients, and each row’s state.dsn.rcpt is sliced to match its rcpt_to, so a later bounce or delivery report still describes the right addresses.

Grouping is by first-seen order, so a message that is retried keeps the same recipients on the same row.

70.1.27.1.6. Loop prevention

pepsi-setup(1) refuses a configuration in which a message for a served domain could reach a direct-to-MX relay (pepsi-stage-relay-to-internet(1)). It walks forward from every stage a managed recipient can reach, following NEXT_STAGE and any further routing stages’ targets. BOUNCE_STAGE is deliberately not followed: a bounce is a new null-sender message about a delivery that already failed, not the message continuing down the pipeline, and a bounce legitimately reaches a relay.

Separately, pepsi-setup(1) refuses a [pepsi-stage-relay-to-smarthost-mta-*] section whose HOST is this host (its [pepsi-ingress] HOSTNAME, localhost, or a loopback address) and whose PORT is one an ingress listener is bound to. Both halves are required, because either alone is a legitimate configuration.

These are refusals rather than warnings. A loop only stops when the relay stages’ MAX_HOP_COUNT guard fires, tens of copies later, and the bounce that guard then produces is itself addressed back into the loop.

70.1.27.1.7. Configuration

Options live in the stage’s own [stage-<name>] section (PROGRAM = pepsi-stage-route): ROUTES, MANAGED_DOMAINS, MANAGED_STAGE and the required NEXT_STAGE. They are documented in pepsi.conf(5).

70.1.27.1.8. State

Inputs: the envelope recipients (rcpt_to). No member of state is read.

Outputs: none — state is never modified, only sliced per recipient group when a message is split. The state layout is described in pepsi.state(7).

Transitions: advances to the stage each recipient’s domain selects, and splits the message into sibling pending rows when recipients disagree. The stage never pauses, fails, reroutes or finishes.

70.1.27.1.9. Commands

worker

Run as a persistent pepsi-dispatch(1) worker, reading message ids on standard input.

70.1.27.1.10. Global Options

-c FILE, –config FILE

Read the configuration from FILE instead of searching the default locations.

-L LOGLEVEL, –log LOGLEVEL

Set the logging verbosity (default info).

-v, –verbose

Show log messages from all sources.

-h, –help; -V, –version

Print a usage summary / the version and exit.

70.1.27.1.11. Exit Status

0

The message was routed (advanced, and split first if its recipients diverged).

1

An error occurred (message not found or not running, misconfigured stage — e.g. a missing NEXT_STAGE — or a database error). The reason is written to the log.

70.1.27.1.12. Examples

Route a single message (it must be running):

echo 42 | pepsi-stage-route -c /etc/pepsi/pepsi.conf worker

The gateway topology in two options — mail for the domains this host serves goes to Exchange, everything else out to the internet:

[stage-route]
PROGRAM = pepsi-stage-route
MANAGED_STAGE = exchange
NEXT_STAGE = internet

The same, with one legacy subdomain carved out and a partner domain given its own next hop:

[stage-route]
PROGRAM = pepsi-stage-route
ROUTES = legacy.example.org=maildir partner.example=partner-relay
MANAGED_STAGE = exchange
NEXT_STAGE = internet

70.1.27.1.13. See Also

pepsi-config(1), pepsi-stage-if(1), pepsi-stage-relay-to-smarthost(1), pepsi-stage-relay-to-internet(1), pepsi-dispatch(1), pepsi.conf(5), pepsi.state(7), pepsi-setup(1)

70.1.27.1.14. Bugs

Report bugs to the Pepsi issue tracker.