.. 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. ======================== SMTP Protocol Extensions ======================== Beyond the standardised ESMTP service extensions catalogued in :doc:`features` and :doc:`rfc-index`, Pepsi defines two **message header fields** of its own that extend the e-mail protocol with Pepsi-specific semantics. Neither is an IANA- or RFC-registered field; both are private extensions that only a Pepsi deployment (and, for the payment header, a GNU-Taler-aware wallet) interprets, and both are designed to pass transparently through intermediate MTAs that do not understand them. This chapter is the normative reference for those two fields: * the ``Pepsi-Origin:`` proof-of-origin header, stamped on outbound mail; and * the ``Taler:`` payment header, which advertises a pay-to-send payment request to a wallet. Both are **optional fields** in the sense of RFC 5322 §3.6.8: a field name of printable US-ASCII followed by an unstructured value, which every conforming MTA relays untouched and every conforming reader ignores if it does not recognise it. That is what makes a private extension safe to deploy. Neither is registered with IANA under RFC 3864, and neither carries the ``X-`` prefix — deliberately. RFC 6648 deprecates ``X-`` precisely because a field that succeeds has to be renamed, breaking every implementation that adopted it early. For ``Pepsi-Origin:`` there is a second, sharper reason: :doc:`programs/pepsi-stage-decrypt` strips the whole ``X-Pepsi-*`` namespace off inbound mail so a remote sender cannot forge a verdict, and a proof-of-origin header inside that namespace would be stripped along with it — exactly on the returning bounce where it is needed. .. _smtp-ext-pepsi-origin: The ``Pepsi-Origin:`` proof-of-origin header ============================================ When Pepsi relays an outbound message off-site it stamps it with a ``Pepsi-Origin:`` header that cryptographically proves *this* deployment originated the message and records *which* of its authorised senders did. The goal is to let a returning bounce be trusted: a DSN that comes back days later embeds (a copy of) the original message — and therefore the ``Pepsi-Origin:`` header — inside its report body, so :doc:`programs/pepsi-stage-anti-spam` can recover that header and confirm the bounce is a reply to mail Pepsi really sent, rather than backscatter or a forged bounce aimed at the pay-to-send gate. Field syntax ------------ The header value is a list of ``key=value`` fields separated by ``;``: .. code-block:: text Pepsi-Origin: v=1; host=mail.example.org; sender=alice@example.org; nonce=; ts=; mac= ========== ================================================================ Field Meaning ========== ================================================================ ``v`` Format version; a verifier accepts only ``1``. ``host`` The originating deployment's hostname (``[pepsi-ingress] HOSTNAME``), in the clear. ``sender`` The authorised envelope-sender account the message originated from. ``nonce`` A random 128-bit nonce, base64url-encoded (RFC 4648, no padding). ``ts`` The Unix timestamp (seconds) at which the header was minted. ``mac`` ``HMAC-SHA256`` over the preceding fields, base64url-encoded. ========== ================================================================ The MAC covers the version, hostname, sender account, nonce and timestamp, each field length-prefixed with its big-endian ``u32`` length and concatenated, so no field boundary can be shifted. The hostname is **both** carried in the clear and folded into the MAC: the clear copy lets a verifier reject a bounce minted for a *different* Pepsi deployment cheaply, before any database lookup, while binding it into the MAC means a captured header cannot be replayed against a sibling deployment that happens to share a secret. Because only Pepsi ever generates or verifies these headers, the wire format does not need to interoperate with anything. Minting and verification ------------------------ * **Stamping.** Both relay stages — :doc:`programs/pepsi-stage-relay-to-internet` and :doc:`programs/pepsi-stage-relay-to-smarthost` — call the shared ``pepsi-common::origin`` engine to prepend the header on whichever outbound path a message takes, and record the freshly minted nonce in the ``pepsi.origin_nonce`` table with a two-week expiry. Stamping is best-effort: if the feature is off the message is unchanged, and a failure to record the nonce is logged but never blocks delivery. * **Verification.** :doc:`programs/pepsi-stage-anti-spam`, on a returning bounce, scans the *whole* message (the header sits inside the DSN body, not the bounce's own header block), unfolds any continuation lines, and accepts the first header whose version and hostname match and whose MAC verifies under the secret. Only if the cheap cryptographic check passes is the (slower) ``pepsi.origin_nonce`` lookup consulted; a bounce verifies only if its nonce is still tracked. A bounce that does not verify — a missing/forged/expired header, or *any* bounce when proof-of-origin is not configured — is routed to the stage's ``BOUNCE_TARGET_STAGE`` (e.g. a quarantine or discard stage) rather than relayed back to a sender; a genuine bounce is forwarded normally. A bounce is never silently dropped. Configuration ------------- Keying lives in the shared ``[pepsi-origin]`` section (``SECRET`` / ``SECRET_FILE``, plus the required ``[pepsi-ingress] HOSTNAME`` that is bound into every MAC). The feature is **on by default**: when no secret is configured, ``pepsi-setup`` provisions a random one on first run. The secret must stay stable over time (a bounce returned days later must still verify) and be identical across all instances of a deployment. Removing the section disables the feature — outbound mail is then unstamped and every bounce is unverifiable. See :doc:`programs/pepsi-stage-anti-spam` and ``pepsi.conf(5)``. .. _smtp-ext-taler: The ``Taler:`` payment header ============================= When :doc:`programs/pepsi-stage-anti-spam` holds a message for payment it originates a payment-request auto-reply to the sender (``Auto-Submitted: auto-replied`` per RFC 3834, null envelope sender). That reply carries the GNU Taler payment URI .. code-block:: text Taler: taler://pay/// as a header field, **and** in its body as a clickable link and an inline ``cid:`` PNG QR code (RFC 2392 for the ``cid:`` reference), together with a pointer to ``https://wallet.taler.net/`` for senders without a wallet. The header is added alongside the body copy, never instead of it, so a human reader and a non-Taler client are wholly unaffected. The value is exactly the URI the stage mints, with ``order_id == token``, so a wallet resolves the order against the merchant backend just as it would from the body link. ``taler://`` is not an IANA-registered URI scheme (RFC 7595 sets the procedure); it is GNU Taler's own, and the wire form is documented by that project rather than here. Why a header and not only the body ---------------------------------- The header is what makes the demand **machine-detectable**, and Pepsi is itself the machine that reads it. :doc:`programs/pepsi-stage-auto-pay` — the sending-side counterpart, run on the inbound path — recognises a returning payment demand by this field: it unfolds continuation lines, accepts **several URIs in one field and several ``Taler:`` fields** (a message to a list can draw more than one demand), and ignores ``taler://`` URIs of any other kind so only genuine pay demands are ever settled. Detection alone is not authorisation. The stage pays only for mail this deployment provably originated, which it establishes with the ``Pepsi-Origin:`` header described above — recovered from the returned message and checked against ``pepsi.origin_nonce``. The two extensions in this chapter are therefore a pair: one proves origin, the other states the price, and the budget is booked against the origin nonce so a single original message cannot be charged twice. The reply also copies the original message's ``Pepsi-Origin:`` header verbatim, which is what lets the correlation survive the round trip. Interaction with DKIM and ARC ----------------------------- The auto-reply is injected at ``BLOCK_RESPONSE_STAGE``, so it is DKIM-signed on the way out like any other message. Whether the ``Taler:`` field is *covered* by that signature follows the stage's ordinary ``h=`` configuration (:doc:`programs/pepsi-stage-dkim-sign`); it is not special-cased. Because DKIM header selection is bottom-up (RFC 6376 §5.4.2), adding the field does not disturb any signature already present. See also ======== :doc:`programs/pepsi-stage-anti-spam`, :doc:`programs/pepsi-stage-auto-pay`, :doc:`programs/pepsi-stage-relay-to-internet`, :doc:`programs/pepsi-stage-relay-to-smarthost`, :doc:`features`, :doc:`rfc-index`.