46. pepsi-stage-check-whitelist

Mark mail from trusted senders as non-spam.

46.1. 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 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: pepsi-stage-check-whitelist(1).

46.2. 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 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 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 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 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.

46.3. Configuration

[stage-<name>]: 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 pepsi-stage-check-whitelist(1).

46.4. 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 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.

46.5. See also

pepsi-stage-anti-spam, pepsi-stage-arc, Supported Features, pepsi-stage-check-whitelist(1).