85.1.35. pepsi-stage-route

route each recipient to a next hop by their domain

Manual section:

1

85.1.35.1.1. Name

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

85.1.35.1.2. Synopsis

pepsi-stage-route [GLOBAL-OPTIONS] worker

85.1.35.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.workqueue 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).

85.1.35.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. The resulting set must not be empty: pepsi-setup(1) refuses a route stage with no managed domains, since it could neither tell the two sides apart nor check for the loop described below.

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

85.1.35.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 every option that sends the message itself on: NEXT_STAGE, any further routing stages’ targets, pepsi-stage-if(1)’s TRUE_STAGE and FALSE_STAGE, pepsi-stage-milter(1)’s ACCEPT_STAGE and QUARANTINE_STAGE, pepsi-stage-decrypt(1)’s QUARANTINE_STAGE, pepsi-stage-encrypt(1)’s SECURE_LINK_STAGE, pepsi-stage-secretary(1)’s UNCHALLENGEABLE_STAGE and pepsi-stage-anti-spam(1)’s BOUNCE_TARGET_STAGE.

Three kinds of target are deliberately not followed. BOUNCE_STAGE and the options that route a failed recipient towards a report (pepsi-stage-milter(1)’s REJECT_STAGE, the local-delivery stages’ QUOTA_LIMIT_STAGE, SIEVE_REJECT_STAGE, NOTIFY_STAGE and UNKNOWN_MAILBOX_STAGE): what travels there becomes a null-sender DSN about a delivery that already failed, not the message continuing down the pipeline, and a DSN legitimately reaches a relay. RESPONSE_STAGE and its kin, which inject a new message to somebody else. And the targets that re-address the message to new recipients the pipeline routes afresh — pepsi-stage-dot-forward(1)’s RESTART_STAGE and the mailing-list stages’ POST_STAGE, COMMAND_STAGE and DELIVERY_STAGE.

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.

85.1.35.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, plus RECIPIENT_CHECK (none, probe or map) with PROBE_HOST, PROBE_PORT, PROBE_TLS, PROBE_TIMEOUT and RECIPIENT_MAP. The last group is not read by the stage: it tells pepsi-ingress(1) how to ask the backend whether an address exists, so that an unknown one is refused at RCPT time rather than bounced by the backend later. All are documented in pepsi.conf(5).

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

85.1.35.1.9. Commands

worker

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

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

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

78

The worker refused to start: its section does not parse. The dispatcher requeues what it handed over and retries the stage later. A database error while a message is being split is retried like any other fault of this host (see pepsi-dispatch(1)); a retry finds the row already reduced to the recipients that stayed and routes them on their own.

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

85.1.35.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)

85.1.35.1.14. Bugs

Report bugs to the Pepsi issue tracker.