.. This file is part of PEPSI. Copyright (C) 2026 Pepsi contributors 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-route ================= *Send each recipient to the next hop their domain selects.* Role ==== ``pepsi-stage-route`` makes the one routing decision a gateway in front of another mail system has on every inbound message: *does this recipient belong to the system behind me, or to the internet?* It picks a next stage per envelope recipient from that recipient's domain, splitting the message when recipients disagree. It only moves the ``stage`` column — it never rewrites, delivers, bounces or drops a message, and ``state`` (including ``state.dsn``) is preserved. Reference: :manpage:`pepsi-stage-route(1)`. Getting that decision 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. See :doc:`../exchange` for the deployment this stage exists for. Features ======== * **Managed domains by default.** ``MANAGED_DOMAINS`` defaults to ``[pepsi-ingress] ACCEPTED_DOMAINS``, so the set of domains treated as "behind the gateway" cannot drift from the set ingress actually accepts. With ``MANAGED_STAGE`` set, the whole gateway topology is two options. * **Explicit rules for carve-outs.** ``ROUTES`` takes whitespace- or comma-separated ``=`` pairs, consulted *before* the managed set, so one subdomain can go elsewhere without removing its parent from ``MANAGED_DOMAINS`` (which would also stop ingress accepting it). * **Glob patterns, not regexes.** ``*`` matches any run of characters and the match is anchored at both ends: ``*.example.org`` matches ``mail.example.org`` but not ``example.org`` nor ``mail.example.org.evil.test``. A pattern that looks like a regular expression is rejected at configuration time rather than silently matching nothing. * **Per-group recipient fan-out.** Divergent recipients become sibling ``pending`` rows in one database call, each carrying only its own recipients with ``state.dsn.rcpt`` sliced to match ``rcpt_to``. * **Deterministic grouping.** Groups are formed in first-seen order, so a retried message keeps the same recipients on the same row. * **Metadata only.** Neither the headers nor the body is loaded. .. note:: Unlike the other metadata-only stages, this one does **not** default to ``FUSION = yes``. It is a candidate on paper — it is folded into the unified binary and loads no message content — but a fused stage runs inside its predecessor's worker, and this stage's recipient fan-out refuses to run with a predecessor's uncommitted content changes pending. Rather than make that interaction a run-time surprise, the stage is simply dispatched normally. Loop prevention =============== ``pepsi-setup`` **refuses** — not warns about — two configurations: * one in which a message for a served domain could reach ``pepsi-stage-relay-to-internet``, found by walking forward from every stage a managed recipient can reach. ``BOUNCE_STAGE`` is not followed: a bounce is a new null-sender message about a delivery that already failed, and it legitimately reaches a relay. * a ``[pepsi-stage-relay-to-smarthost-mta-*]`` section whose ``HOST`` is this host **and** whose ``PORT`` is one an ingress listener is bound to. Both halves are required — a site may legitimately run an unrelated relay on the same host, or reach a different machine on port 25. They are refusals because 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. Configuration ============= .. list-table:: :header-rows: 1 :widths: 24 76 * - Option - Meaning * - ``ROUTES`` - Whitespace/comma separated ``=`` pairs, consulted first, in order. Optional. * - ``MANAGED_DOMAINS`` - The domains behind this gateway. Defaults to ``[pepsi-ingress] ACCEPTED_DOMAINS``; at least one is required. * - ``MANAGED_STAGE`` - Where a recipient at a managed domain goes. Optional; without it managed recipients take ``NEXT_STAGE``, which ``pepsi-setup`` then has to prove is not a loop. * - ``NEXT_STAGE`` - Where every other recipient goes. **Required** — the stage never delivers, bounces or drops, so every recipient must have somewhere to be. Placement ========= On the **inbound** path, after authentication and decryption and before the relay stages — typically ``arc`` → ``decrypt`` → ``aliases`` → ``route``, with ``MANAGED_STAGE`` naming the smarthost stage that relays to Exchange and ``NEXT_STAGE`` naming the direct-to-MX stage. See also ======== * :doc:`../exchange` — the deployment chapter. * :doc:`pepsi-stage-if` — the general-purpose branch, for conditions that are not about the recipient's domain. * :doc:`pepsi-stage-relay-to-smarthost`, :doc:`pepsi-stage-relay-to-internet`.