.. 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-check-whitelist =========================== *Mark mail from trusted senders as non-spam.* Role ==== ``pepsi-stage-check-whitelist`` consults a named whitelist of trusted senders and, when the message's ``From:`` header matches, records ``state.spam = false`` so a later :doc:`pepsi-stage-anti-spam` lets the message through without payment. Place it ahead of the anti-spam stage. It only annotates the state and advances — it never drops, bounces or pauses a message. Reference: :manpage:`pepsi-stage-check-whitelist(1)`. Features ======== * **Database-backed whitelists:** the ``pepsi.whitelist`` table groups rows under a ``whitelist_name``. ``WHITELIST_NAME`` is a **comma-separated list** of the groups to consult, and an entry may be a *template* containing ``{localpart}`` or ``{login}``, expanded per envelope recipient — so ``WHITELIST_NAME = correspondents, {localpart}/correspondents`` consults the operator's shared list *and* each recipient's own, the namespace :doc:`pepsi-whitelist` lets that user manage. Only a recipient at a **local domain** may name a whitelist (otherwise an attacker-chosen co-recipient would expand to any local user's list), and ``{login}`` additionally needs the address to resolve to a passwd account, so place the stage after :doc:`pepsi-stage-aliases` when using it. Every applicable name is checked in one query. * **POSIX ERE matching:** each ``whitelist_regex`` is an extended POSIX regular expression matched **case-insensitively** (PostgreSQL's ``~*`` operator, in a single round-trip) against the field the row's ``match_field`` names: * ``from`` (the default) — the **addr-spec parsed out of** the ``From:`` header, never the raw header value: otherwise a whitelisted address sitting in the display-name position would spoof a match. RFC 5322 comments and CFWS are removed, and a ``From:`` naming more than one mailbox matches **nothing** (the DKIM/DMARC verdict is computed for the first address, so matching a later one would answer two questions about two senders). * ``list-id`` — the RFC 2919 identifier of the message's ``List-Id:`` header. A mailing list's postings carry arbitrary ``From:`` addresses, so no ``from`` pattern can say "anything that came through this list". The expression itself is unanchored; anchor it with ``^``/``$`` to match a whole address (which is what :doc:`pepsi-stage-auto-whitelist` writes). * **Optional conditions per row:** * ``dkim_required`` → matches only if the ``From:`` domain is authenticated: ``state.auth.dkim = pass``, or an ARC pass that DMARC also confirms (``state.auth.arc = pass`` **and** ``state.auth.dmarc = pass``). ARC pass alone is only chain integrity (a one-hop chain can be self-minted over a forged ``From:``), so it does not satisfy the row by itself. The column defaults to true, as ``pepsi-whitelist add`` does. * ``signature_required`` → matches only if ``state.signature_verified`` is ``true``, which :doc:`pepsi-stage-decrypt` sets for a message whose end-to-end signature verified against a **trusted** key. A ``valid-untrusted`` verdict deliberately does not set it, so such a row is a genuine "this correspondent signs, verifiably" gate rather than a "someone signed this" one. Place a decrypt stage before this one, or the row never matches. * ``sealer_domain`` → matches only if the message arrived with an ARC chain that **validated** and the named ADMD is among its sealers (``state.auth.arc = pass`` and the domain in ``state.auth.arc_sealers``). This is what makes a ``list-id`` row safe: ``List-Id:`` is unauthenticated text anyone can copy, and a valid ARC chain conveys no trust by itself (RFC 8617 §8.4) — naming the sealer is what turns the pair into evidence. * **Non-destructive:** on a match it merges ``spam: false`` and advances to ``NEXT_STAGE``; on no match it advances unchanged. A message with no ``From:`` header, and one no configured whitelist name applies to, match nothing. Configuration ============= ``[stage-]``: ``PROGRAM = pepsi-stage-check-whitelist``, ``WHITELIST_NAME`` (required — the comma-separated list of ``whitelist_name`` groups, literal or templated), ``NEXT_STAGE`` *(required — the stage always hands the message on)*, and the shared locality options ``LOCAL_DOMAINS`` (defaulting to ``[pepsi-ingress] ACCEPTED_DOMAINS``), ``TARGETS`` and ``RECIPIENT_DELIMITER``, which are used **only** to expand the placeholders. See :manpage:`pepsi-stage-check-whitelist(1)`. State ===== * **Inputs:** ``state.auth.dkim`` / ``state.auth.arc`` / ``state.auth.dmarc`` (for ``dkim_required`` rows), ``state.auth.arc_sealers`` (for ``sealer_domain`` rows) and ``state.signature_verified`` (for ``signature_required`` rows; written by :doc:`pepsi-stage-decrypt`). The ``From:`` header comes from the ``from_header`` column; ``List-Id:`` is read from the header block, which is why this stage loads the headers (but never the body). * **Outputs:** on a match, ``spam: false`` merged into ``state``; otherwise ``state`` is unchanged. See also ======== :doc:`pepsi-stage-anti-spam`, :doc:`pepsi-stage-arc`, :doc:`../features`, :manpage:`pepsi-stage-check-whitelist(1)`.