.. This file is part of PEPSI. Copyright (C) 2026 GNUnet e.V. 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-relay-to-lmtp ========================= *Deliver locally over LMTP to an MDA (Dovecot) that runs Sieve.* Role ==== ``pepsi-stage-relay-to-lmtp`` is a local-delivery stage that hands each message to a Mail Delivery Agent — typically **Dovecot** — over **LMTP** (RFC 2033). The MDA files the message and, in doing so, runs the recipient's **Sieve** (RFC 5228) script, so server-side filtering is delegated to the MDA instead of being implemented in Pepsi. It is an alternative to ``pepsi-stage-relay-to-maildir``: LMTP gains Sieve and a clean per-recipient delivery status, and needs **no** setuid helper (the MDA performs the per-user delivery), at the cost of running a local IMAP/LMTP server. LMTP-native delivery, deliver-and-route ======================================= All envelope recipients are offered in one LMTP transaction; the MDA returns one reply per recipient after end-of-DATA (RFC 2033 §4.2). The stage **never builds a delivery-status notification itself** — it delivers, and otherwise *routes* each recipient onward by reason, recording why under ``state.bounce`` for a downstream **pepsi-stage-bounce** (or a relay) to act on: * ``2xx`` — delivered; a ``NOTIFY=SUCCESS`` notification (and ``DELAY`` warnings) is routed to the optional ``NOTIFY_STAGE``; * a permanent reject is routed by its RFC 3463 enhanced status — a bad mailbox (``x.1.x``, ``x.2.x`` other than ``x.2.2``, and a bare ``550`` with no enhanced status at all) to ``NEXT_STAGE``, over-quota (``x.2.2``) to ``QUOTA_LIMIT_STAGE`` (else ``NEXT_STAGE``), a Sieve / policy reject (``x.7.x``) to ``SIEVE_REJECT_STAGE`` (else ``NEXT_STAGE``), anything else to ``NEXT_STAGE``; * ``4xx`` — transient: retry that recipient (exponential backoff) until ``MAX_LIFETIME``, then route by the same classification. Delivered, routed and still-deferred recipients are reconciled in one fan-out: the delivered recipients leave the row, each routed recipient becomes a sibling ``pending`` row sliced to itself (so a relay target never re-delivers to the others) at its target stage, and the row is reduced to the deferred recipients for the next retry. A failure of the whole session (connect, ``STARTTLS``, authentication, data transfer) retries the whole message, or — when permanent or past ``MAX_LIFETIME`` — routes every recipient to ``NEXT_STAGE``. Because the MDA is the authority on which addresses are local, the stage does no locality check of its own — place it after alias expansion so only intended-local recipients reach it. ``NEXT_STAGE`` is **mandatory** (set it to a **pepsi-stage-bounce** to bounce, or to a relay stage to forward off-site); a message reaching the stage without one is marked ``failed`` before the MDA is contacted, so nothing is delivered. Transport ========= The target is either a UNIX-domain socket (``SOCKET``, the common Dovecot layout: no TLS, no credentials) or TCP (``HOST``/``PORT``, ``PORT`` default 24) with optional ``TLS = off|starttls|tls`` and ``AUTH = none|plain|login|external``. Authentication is refused over cleartext, so any ``AUTH`` other than ``none`` requires TLS. The shared LMTP client reuses the SMTP client's transport, TLS and SASL machinery (``pepsi_common::smtp::deliver_lmtp``). Configuration ============= ``[stage-]``: ``PROGRAM = pepsi-stage-relay-to-lmtp``, ``NEXT_STAGE`` *(mandatory)*, the optional reason targets ``QUOTA_LIMIT_STAGE`` / ``SIEVE_REJECT_STAGE`` / ``NOTIFY_STAGE``, the target (``SOCKET``, or ``HOST``/``PORT`` with ``TLS``, ``TLS_VERIFY``, ``TLS_CA``, ``TLS_CLIENT_CERT``, ``TLS_CLIENT_KEY``, ``AUTH`` and its ``USERNAME``/``PASSWORD``), ``SERVER_NAME`` (the ``LHLO`` name; defaults to ``[pepsi-ingress] HOSTNAME``), the timeouts (``CONNECT_TIMEOUT``/``COMMAND_TIMEOUT``/``DATA_TIMEOUT``) and the retry policy (``RETRY_INITIAL``/``RETRY_MAX_INTERVAL``/``RETRY_FACTOR``/``MAX_LIFETIME``/ ``DELAY_DSN_AFTER``). ``pepsi-setup`` requires ``NEXT_STAGE`` and checks that each of the three reason targets names a real stage. This stage has **no** ``BOUNCE_STAGE``: it never builds a DSN itself. State ===== * **Inputs:** ``state.dsn`` (per-recipient ``NOTIFY``/``ORCPT``) and the retry bookkeeping it wrote on an earlier pass. * **Outputs:** ``state.bounce`` on each routed sibling row (recording why the MDA refused, or that the delivery succeeded for a ``SUCCESS``/``DELAY`` notification), and ``attempts``/``last_error`` (plus ``delay_sent`` once a delay warning was routed) on the deferred row. ``state.dsn.rcpt`` is sliced in lockstep with every row's recipients. See also ======== :doc:`pepsi-stage-relay-to-maildir`, :doc:`pepsi-stage-aliases`, :doc:`pepsi-stage-bounce`, :doc:`pepsi-quota`, :doc:`../features`, :manpage:`pepsi-stage-relay-to-lmtp(1)`, :manpage:`pepsi.conf(5)`.