.. 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-anti-spam ===================== *Gate a message behind a GNU Taler payment (pay-to-send).* Role ==== ``pepsi-stage-anti-spam`` requires a GNU Taler payment for an otherwise-unclassified message before it may continue down the pipeline. On first sight it creates a merchant order and pauses the message; once the message is woken again -- by the ``pepsi-resume`` merchant webhook, which ``pepsi-setup`` provisions on the Taler backend to call ``POST /resume`` on :doc:`pepsi-httpd`, or by the deadline lapsing -- it forwards, rejects, or waits. Reference: :manpage:`pepsi-stage-anti-spam(1)`. Decision ======== Driven by the message's ``state`` JSON: * ``spam: true`` → **drop** (delete the row). * ``spam: false`` **or** ``paid: true`` → **forward** to ``NEXT_STAGE`` (an explicit classification overrides the payment flow). * ``paid: false`` present → ask the merchant whether the order is paid: * paid → **forward** to ``NEXT_STAGE``; * unpaid past the stored deadline → **reject**: reroute to ``BOUNCE_STAGE`` (a :doc:`pepsi-stage-bounce` rejection DSN, or a :doc:`pepsi-stage-discard` silent drop), or delete when no ``BOUNCE_STAGE`` is wired; * unpaid but still before the deadline → with ``DELAY_DSN_AFTER`` set, this is the delay checkpoint: **e-mail the sender a one-shot delayed DSN** (RFC 3461, only if they asked for ``NOTIFY=DELAY``) without releasing the message, then re-pause until ``PAYMENT_DEADLINE``; otherwise re-pause until the deadline with a warning (should not happen). The row stays ``paused``, never ``pending``, which would busy-loop against the early wake-up. * no ``paid`` field → **create a v1 order** priced by ``ORDER_CHOICES`` (``order_id`` is the message token), **pause** until ``PAYMENT_DEADLINE`` (or until the ``DELAY_DSN_AFTER`` checkpoint when that falls earlier), recording ``paid: false`` and ``pay_deadline`` in ``state``, and **e-mail the sender a payment request** (see below). The deadline is kept in ``state.pay_deadline`` because the dispatcher clears the row's ``timeout`` when it requeues a paused message. A message with the **null envelope sender** (a bounce or auto-reply) is never gated: the stage instead tries to verify it as a reply to mail this deployment genuinely sent, using the ``Pepsi-Origin:`` proof-of-origin header (see :doc:`../smtp-extensions`). A bounce whose embedded header verifies is forwarded to ``NEXT_STAGE``; an unverifiable bounce — a forged or expired header, or *any* bounce when proof-of-origin is unconfigured — is routed to ``BOUNCE_TARGET_STAGE`` (e.g. a quarantine) when one is configured, and forwarded to ``NEXT_STAGE`` unchanged when it is not — never silently dropped. Payment-request reply ===================== After that first pause, the stage originates an auto-reply to the sender (when ``BLOCK_RESPONSE_STAGE`` is set) — unless RFC 3834 says the message must not be answered automatically (``Auto-Submitted`` other than ``no``, ``Precedence: bulk|list|junk``, ``X-Auto-Response-Suppress``, any ``List-*`` field, a service sender such as ``mailer-daemon`` or a VERP list bounce address, a ``multipart/report``), the same shared rules the vacation responder applies. The message is held for payment either way; only the prompt is withheld, so the gate does not turn list traffic and other responders into backscatter. The same holds within ``BLOCK_RESPONSE_SUPPRESS`` (default ``1 h``) of a previous request to the same sender for the same protected mailbox: a flood bearing one forged sender draws one request per window, not one per message. The window is claimed and recorded in one statement (``payment_request_should_send`` over ``pepsi.payment_request_reply``, the ``vacation_should_reply`` pattern), so concurrent workers cannot both send. It explains that the recipient requires unknown senders to pay first, and carries the ``taler://pay//`` URI as a link and as a QR code (an inline ``image/png`` referenced by ``cid:``, so it renders in the widest range of clients while the message stays self-contained), the acceptable payment options (the ``ORDER_CHOICES``, *omitting* token-family choices), and a pointer to ``https://wallet.taler.net/`` for senders without a wallet. When the original ``From:`` carries a display name, the sender is addressed by it. The body is rendered from the ``payment-request..body`` :doc:`Mustache template ` under ``[pepsi] TEMPLATE_DIR``, one chunk per detected language that has a template, falling back to ``PAYMENT_MESSAGE_DEFAULT_LANGUAGE`` (``en`` by default). The reply uses the **null sender** ``<>`` and enters at ``BLOCK_RESPONSE_STAGE`` (normally a signing/relay chain), so it is signed and delivered like any other mail. The reply is rendered *before* the pause, so a missing default-language template fails the stage rather than pausing a message whose sender could never be told; queueing the rendered reply afterwards is best-effort — a failure is logged but never undoes the pause. The reply also carries two machine-readable headers so a sending Pepsi can settle the demand automatically (see :doc:`pepsi-stage-auto-pay`): a ``Taler:`` header with the ``taler://pay/…`` URI, and the original message's ``Pepsi-Origin:`` header copied **verbatim** (when present), letting the originating site verify the demand is for its own mail and meter its per-message spend. Configuration ============= ``[stage-]``: ``PROGRAM = pepsi-stage-anti-spam``, ``NEXT_STAGE`` *(required — a paid message, one classified as not spam and a verified bounce all go there)*, ``BOUNCE_STAGE``, ``ORDER_CHOICES`` (the verbatim Taler v1 order ``choices`` JSON array, required), ``PAYMENT_DEADLINE`` (``h``/``m``/``s`` units; default 2 days), ``DELAY_DSN_AFTER`` (``h``/``m``/``s``; optional, send a delayed DSN mid-window — see above), ``SUMMARY`` (default ``E-mail delivery``) and ``FULFILLMENT_MESSAGE``. For the payment-request reply: ``BLOCK_RESPONSE_STAGE`` (where to inject it; unset disables the reply), ``BLOCK_RESPONSE_FROM`` (its ``From:``; default the original recipient), ``BLOCK_RESPONSE_SUBJECT``, ``BLOCK_RESPONSE_SUPPRESS`` (``h``/``m``/``s``; default ``1 h``, ``0 s`` answers every message) and ``PAYMENT_MESSAGE_DEFAULT_LANGUAGE`` (default ``en``, and its ``payment-request..body`` template must exist — ``pepsi-setup`` enforces that). ``BOUNCE_TARGET_STAGE`` takes an unverifiable bounce; when it is unset such a bounce is forwarded to ``NEXT_STAGE`` unchanged. Requires the shared ``[pepsi-payments]`` merchant backend and, for the resume webhook, ``[pepsi-httpd] RESUME_AUTHORIZATION_TOKEN``. See :manpage:`pepsi-stage-anti-spam(1)`. State ===== * **Inputs:** ``state.spam`` / ``state.paid`` (booleans), ``state.pay_deadline`` (epoch seconds), ``state.dsn`` (recipient ``notify``/``orcpt``). * **Outputs:** on the first encounter, ``{paid: false, pay_deadline}`` merged into ``state``, the row paused, and a payment-request reply injected at ``BLOCK_RESPONSE_STAGE``; on rejection, a ``state.bounce`` object (``permanent``) for the bounce stage. See also ======== :doc:`pepsi-stage-bounce`, :doc:`pepsi-stage-discard`, :doc:`pepsi-httpd`, :doc:`pepsi-setup`, :doc:`../smtp-extensions`, :manpage:`pepsi-stage-anti-spam(1)`.