.. 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-block-language ========================== *Route a message by the human language(s) detected in its body.* Role ==== ``pepsi-stage-block-language`` is a language-policy filter. It scores the language(s) recorded under ``state.language`` (written upstream by :doc:`pepsi-stage-detect-language`) against a configured whitelist and blacklist and routes the message: above the score ``THRESHOLD`` it advances to ``NEXT_STAGE``, otherwise ``ENFORCEMENT`` decides — ``hard`` (the default) reroutes it to ``BOUNCE_STAGE`` carrying a failure-DSN ``state``, ``soft`` delivers it anyway with a marked ``Subject:``. The body is never loaded (the header block is, because soft mode rewrites a header) and the stage never pauses. Reference: :manpage:`pepsi-stage-block-language(1)`. Scoring ======= ``state.language`` is an HTTP ``Accept-Language``-style string of ``code;q=quality`` items, each ``quality`` being the detection probability of that language. The stage collapses it to one number: .. code-block:: text score = Σ q over languages listed in WHITELIST − Σ q over languages listed in BLACKLIST Languages on neither list contribute nothing. With English whitelisted and French blacklisted, ``en;q=0.7, fr;q=0.3`` scores ``0.7 − 0.3 = 0.4``. A message whose score is **strictly greater** than ``THRESHOLD`` advances; otherwise it is blocked. * **Undetected mail.** When no language was detected (``state.language`` absent) the score is computed against the pseudo-distribution ``none;q=1``. The literal ``none`` may itself appear in ``WHITELIST``/``BLACKLIST``, so the operator decides the fate of undetected mail (blacklisting ``none`` scores it ``−1``, whitelisting ``+1``, listing it nowhere leaves it at ``0``). * **Whitelist bypass.** A message carrying an explicit ``state.spam = false`` verdict — set by an upstream :doc:`pepsi-stage-check-whitelist` match, or by ingress for the reserved ```` mailbox (RFC 5321 §4.5.1) — skips the language policy entirely and advances to ``NEXT_STAGE``. * **Never bounce a bounce.** A message that is already a bounce (the null envelope sender) advances to ``NEXT_STAGE`` regardless of its score (RFC 5321 §6.1). Hard and soft enforcement ========================= ``ENFORCEMENT`` decides what happens to a message that fails the policy, and both modes select exactly the same set of messages — which is the point: the flag *predicts* the bounce, so an operator can tune ``WHITELIST``/``BLACKLIST``/ ``THRESHOLD`` against real traffic before any mail is refused. * ``hard`` (the default) reroutes to ``BOUNCE_STAGE`` with a permanent failure-DSN ``state``, so :doc:`pepsi-stage-bounce` emits a bounce only if the sender's ``NOTIFY`` requests ``FAILURE`` (the default when absent). No header is touched. * ``soft`` advances the message to ``NEXT_STAGE`` instead, appending ``SUBJECT_FLAG_LABEL`` (default ``[!LANG]``) to its ``Subject:`` and adding a ``X-Pepsi-Detected-Languages`` header. Nothing is bounced, so no ``BOUNCE_STAGE`` is needed. Because ``ENFORCEMENT`` reaches the stage through the per-address settings layer, one account can be in training while the rest of the deployment enforces. Rewriting the ``Subject:`` breaks the originator's DKIM signature and our own ARC ``AMS``, which both over-sign it — free on a branch that ends in local delivery, not free on one that relays onward, so soft mode belongs on the former. Fusion ====== The stage defaults to ``FUSION = yes``: under the unified ``pepsi`` binary with ``[pepsi] ALLOW_FUSION`` on (the default), a predecessor may run it **in its own worker process** rather than dispatching it separately. See :doc:`pepsi-dispatch`. Configuration ============= ``[stage-]``: ``PROGRAM = pepsi-stage-block-language``, ``NEXT_STAGE`` *(required — every message the stage does not block advances there, including a whitelisted sender, a bounce, and every message merely flagged under ``ENFORCEMENT = soft``)*, ``BOUNCE_STAGE``, the ``WHITELIST``/``BLACKLIST`` language lists (at least one must be non-empty, or every message would fail), the numeric ``THRESHOLD`` (default ``0.0``), ``ENFORCEMENT`` (``hard`` — the default — or ``soft``) and ``SUBJECT_FLAG_LABEL`` (default ``[!LANG]``, used only in soft mode). ``pepsi-setup`` rejects an invalid language code or an overlap between the two lists, and warns about ``ENFORCEMENT = hard`` with no ``BOUNCE_STAGE`` (the first blocked message would fail) and about a ``BLACKLIST`` with an empty ``WHITELIST`` and a non-negative ``THRESHOLD``. See :manpage:`pepsi-stage-block-language(1)` and :doc:`../configuration`. State ===== * **Inputs:** ``state.language`` (treated as ``none;q=1`` when absent) and, for a blocked message, ``state.dsn`` and the envelope recipient. * **Outputs:** on the pass path ``state`` is untouched; on the hard block path the failure-DSN keys :doc:`pepsi-stage-bounce` consumes (``state.bounce``) are merged; on the soft path ``state`` is untouched and the header block is rewritten instead. * **Transitions:** score above ``THRESHOLD`` → advance to ``NEXT_STAGE``; score at or below → reroute to ``BOUNCE_STAGE`` (``hard``) or advance to ``NEXT_STAGE`` flagged (``soft``). See also ======== :doc:`pepsi-stage-detect-language`, :doc:`pepsi-stage-check-whitelist`, :doc:`pepsi-stage-bounce`, :doc:`../features`, :manpage:`pepsi-stage-block-language(1)`.