85.3.1. pepsi.state

per-message state object carried through the Pepsi pipeline

Section du manuel:

7

85.3.1.1.1. Nom

pepsi.state - l’objet JSON state attaché à chaque message Pepsi.

85.3.1.1.2. Description

Chaque message en transit est une ligne de la table pepsi.workqueue. Outre l’enveloppe et le corps du message, chaque ligne porte un objet JSON de forme libre — la colonne state (JSONB PostgreSQL) — qui voyage avec le message depuis l’ingress jusqu’à son étape terminale. Le state est le seul canal par lequel les étapes du pipeline communiquent : une étape enregistre un verdict ou un élément de provenance, une étape ultérieure le lit.

Cette page documente la disposition de cet objet : les clés que les étapes standard écrivent, quand elles sont écrites, et quelles étapes les consomment. C’est une référence de format de données, indépendante de toute configuration ; le graphe du pipeline et les options qui pilotent chaque étape sont décrits dans pepsi.conf(5).

85.3.1.1.3. Sémantique de fusion

state est construit de façon incrémentale. Chaque helper terminal d’étape qui modifie une ligne fusionne ses nouvelles clés dans l’objet existant (les fonctions SQL terminales évaluent state || $new), plutôt que de le remplacer. Par conséquent, une clé écrite tôt — la provenance origin semée par l’ingress, les paramètres dsn — survit pendant toute la durée de vie du message, à moins qu’une étape ne réécrive délibérément cette clé précise.

La fusion est superficielle : state || {"auth": {"arc": "pass"}} remplacerait tout l’objet auth, et pas seulement son membre arc. Les étapes qui mettent à jour un membre d’un objet imbriqué lisent donc l’objet courant, l’amendent et le réécrivent en entier (c’est ce que font set_auth_verdict/set_auth_fields pour auth.arc).

La seule réécriture structurelle est dsn.rcpt, qui est un tableau parallèle à rcpt_to : chaque helper qui réduit une ligne à un sous-ensemble de ses destinataires, ou éclate un groupe sur une ligne sœur, redécoupe ce tableau de concert, et l’étape d’alias le reconstruit lorsqu’elle écrit une liste de destinataires entièrement nouvelle. Une étape ne doit jamais toucher à rcpt_to sans passer par ces helpers.

Il existe exactement une exception : pepsi-stage-bounce(1) remet state à null lorsqu’il réécrit un message en notification d’état de remise, car le rebond est un tout nouveau message à expéditeur nul qui n’hérite d’aucune provenance de l’original.

85.3.1.1.4. Clés de premier niveau

Les clés ci-dessous sont écrites par le pipeline standard. Une étape qui ne reconnaît pas une clé la laisse simplement intacte (la règle de fusion la préserve), de sorte que l’ensemble est ouvert ; des étapes personnalisées peuvent ajouter les leurs.

85.3.1.1.4.1. origin

(objet) Les métadonnées d’origine SMTP de la connexion qui a remis le message, semées par pepsi-ingress(1). Elles sont lues par pepsi-stage-arc(1) (qui a besoin de l”authserv_id et du fragment ar_results rendu pour reproduire l’identité Authentication-Results de l’ARC sans réexécuter SPF/DKIM/DMARC) et par les étapes de relais, dont la décision de rétrogradation 8BITMIME/SMTPUTF8 par saut consulte les déclarations BODY=/smtputf8 originales

{
  "origin": {
    "remote_ip":    "203.0.113.7",      // connecting client IP (null for local injection)
    "helo":         "mail.example.com", // HELO/EHLO name announced by the client
    "esmtp":        true,               // client used EHLO (ESMTP) rather than HELO
    "smtputf8":     false,              // SMTPUTF8 requested on MAIL FROM
    "body_8bit":    false,              // BODY=8BITMIME declared on MAIL FROM (RFC 6152)
    "declared_size": 12345,             // SIZE= declared on MAIL FROM (null if absent)
    "tls":          { "version": "TLSv1.3",
                      "cipher":  "TLS13_AES_128_GCM_SHA256",
                      "sni":     "mx.example.net" },  // null on a cleartext session
    "listener":     { "local_addr": "0.0.0.0:25", "mode": "starttls" },
    "reverse_dns":  "host.example.com", // PTR name of remote_ip (null if none)
    "iprev":        "pass",             // RFC 8601 iprev / FCrDNS verdict
    "auth_method":  "sasl",             // how the session authenticated (null if not)
    "auth_mechanism": "PLAIN",          // SASL mechanism (sasl sessions only)
    "auth_identity":  "alice",          // authentication id / peer login
    "auth_authzid":   null,             // authorization id, when distinct
    "from_bound":   true,               // From: bound to the account (see below)
    "authserv_id":  "mail.example.org", // this receiver's id (for pepsi-stage-arc)
    "ar_results":   ";\r\n\tdkim=pass header.d=example.com;\r\n\tdmarc=pass ..."
                                        // rendered Authentication-Results body, reused by
                                        // pepsi-stage-arc for the AAR (no SPF/DKIM/DMARC re-run)
  }
}

Les quatre membres auth_* décrivent comment la session s’est identifiée, par opposition à la clé voisine auth ci-dessous, qui porte sur ce que le message prétend. Ils sont notés parce que local_origin est un unique booléen et confond deux affirmations très différentes : « un compte que nous avons authentifié a soumis ceci » et « ceci est arrivé d’une adresse que nous avons choisi de croire ». Une étape de politique — ou un milter, à qui ils sont remis sous forme des macros RFC 4954 {auth_type} / {auth_authen} / {auth_author} / {auth_ssf} — ne peut pas retrouver cette distinction à partir du booléen.

auth_method

Comment la session s’est authentifiée : sasl (un AUTH RFC 4954 a réussi), peercred (un pair de socket UNIX dont l’uid s’est résolu en un login autorisé), client-cert (un certificat client TLS a correspondu à un épinglage) ou mynetworks (l’adresse du pair figure dans MYNETWORKS). null lorsque la session ne s’est pas authentifiée. Un AUTH SASL ultérieur écrase une méthode antérieure établie à la connexion : c’est la prétention la plus forte, et la seule qui porte un nom d’utilisateur.

auth_mechanism

Le mécanisme SASL qui a réussi (PLAIN, LOGIN), en majuscules. Positionné uniquement pour auth_method = sasl. Les méthodes non SASL le laissent à null plutôt que de placer un jeton qui n’est pas un mécanisme dans un champ qu’un filtre compare peut-être à une liste de mécanismes réels.

auth_identity

L’identité d’authentification : l”authcid SASL, ou le login auquel l’uid d’un pair UNIX s’est résolu. null pour mynetworks et client-cert, qui authentifient une adresse ou une clé et ne nomment personne. peercred produit donc une identité sans mécanisme — la description honnête d’une session que le noyau, et non SASL, a authentifiée.

auth_authzid

L’identité d’autorisation, notée seulement lorsque le client a demandé à agir en tant que quelqu’un d’autre que celui en tant que qui il s’est authentifié (le champ authzid de SASL PLAIN). null lorsque les deux coïncident, ce qui est le cas normal : la RFC 4954 permet une authzid vide et les clients y répètent couramment l’authcid.

from_bound

true lorsque le champ From: nomme exactement une boîte aux lettres et que cette boîte a correspondu à une entrée USERNAME_MAP concrète (sans joker) du compte authentifié, ou à son login@HOSTNAME par défaut ; false sinon, y compris pour une session sans aucune autorisation de compte. Être autorisé à envoyer en tant qu’une adresse suffit pour envoyer ; être lié à elle est ce qui permet à une soumission de parler en son nom. pepsi-stage-encrypt(1) n’enregistre une clé tirée du message, ou n’exécute une commande de clé par e-mail, que lorsque auth_method vaut mynetworks ou client-cert, ou vaut sasl/peercred avec from_bound = true : un compte autorisé pour *@example.org peut envoyer au nom de ses collègues, mais ne doit pas pouvoir implanter une clé pour eux.

85.3.1.1.4.2. auth

(objet) Les verdicts d’authentification entrante que l’ingress a calculés, également reflétés dans l’en-tête Authentication-Results ajouté en tête. Le verdict arc est semé comme none et écrasé par pepsi-stage-arc(1) une fois qu’il réévalue la chaîne ARC entrante (ses voisins spf/dkim/dmarc sont laissés intacts). Lu par pepsi-stage-check-whitelist(1) (dont la barrière dkim_required accepte une ligne sur auth.dkim ou auth.arc)

{
  "auth": {
    "spf":   "pass",   // SPF verdict (pass/fail/softfail/neutral/temperror/permerror/none)
    "dkim":  "pass",   // DKIM verdict (strongest of the message signatures)
    "dmarc": "pass",   // DMARC verdict (pass/fail/temperror/permerror/none)
    "arc":   "none",   // inbound ARC chain verdict (filled in by pepsi-stage-arc)
    "arc_sealers": []  // d= of each inbound ARC-Seal (filled in by pepsi-stage-arc)
  }
}

85.3.1.1.4.3. local_origin

(booléen) Semé par l’ingress : true lorsque la session de remise a été authentifiée (le client était dans MYNETWORKS, a mené à bien un AUTH SASL, a présenté un certificat client TLS correspondant, ou — sur une socket UNIX avec AUTH_PEERCRED, qui y est la valeur par défaut — était un pair dont l’uid s’est résolu en un login autorisé, le chemin que prend tout message soumis localement ; voir les sections [pepsi-ingress-listener-*] dans pepsi.conf(5)), false sinon. Il marque le courrier provenant d’un expéditeur de confiance et est préservé à travers le pipeline pour que les étapes en aval le testent (la couche de paramètres par adresse s’indexe sur l”expéditeur pour les messages local_origin, et pepsi-stage-edit-settings(1) ne traite un message comme un message de contrôle que lorsqu’il est local_origin).

85.3.1.1.4.4. dsn

(objet) Les paramètres de notification d’état de remise RFC 3461 que l’expéditeur a demandés, semés par l’ingress : les ret/envid au niveau du message et un tableau par destinataire parallèle à rcpt_to de notify/orcpt. Chaque étape doit le préserver et l’honorer — les étapes de relais propagent les paramètres au saut suivant seulement lorsqu’il annonce DSN, et pepsi-stage-bounce(1) consulte le notify par destinataire pour décider d’émettre ou non un rapport

{
  "dsn": {
    "ret":   "hdrs",                     // RET= (full|hdrs), if given
    "envid": "QQ314159",                 // ENVID= xtext, if given
    "rcpt":  [ { "notify": ["success", "failure"],   // NOTIFY= keywords
                 "orcpt":  "rfc822;user@example.com" } ]  // ORCPT= xtext
  }
}

85.3.1.1.4.5. bounce

(objet) Écrit par toute étape qui achemine un message vers son BOUNCE_STAGE — les étapes de remise (pepsi-stage-relay-to-internet(1), pepsi-stage-relay-to-smarthost(1), pepsi-stage-relay-to-maildir(1), pepsi-stage-relay-to-lmtp(1)) et pepsi-stage-discard(1), mais tout autant pepsi-stage-milter(1), pepsi-stage-block-language(1), pepsi-stage-anti-spam(1), pepsi-stage-encrypt(1), pepsi-stage-decrypt(1), pepsi-stage-secretary(1) et pepsi-stage-dot-forward(1). Toutes le construisent avec les mêmes helpers partagés, de sorte que la forme ci-dessous est la même quelle que soit l’étape qui l’a écrit. Il est consommé par pepsi-stage-bounce(1) pour construire le rapport. Son kind est permanent (un rebond d’échec, Action: failed), success (un rapport positif, Action: delivered) ou delay (Action: delayed) ; diagnostic et failed_recipient sont la raison lisible par un humain et le destinataire concerné, et les notify/orcpt/envid facultatifs sont copiés depuis l’entrée state.dsn du destinataire afin que l’étape de rebond puisse honorer NOTIFY et renvoyer en écho les ENVID/ORCPT originaux. Les étapes de relais notent aussi un détail structuré du saut suivant (remote_mta, smtp_code, enhanced_status, phase, reply_text) afin qu’un modèle de rebond puisse énoncer précisément pourquoi le MTA suivant a refusé le message, et enhanced_status alimente le champ Status: du DSN. Le ret au niveau du message voyage ici lui aussi, pour la même raison que envid : l’étape de rebond efface state en réécrivant le message, de sorte que tout ce dont elle a besoin passé ce point doit se trouver à l’intérieur de cet objet

{ "bounce": { "kind": "permanent",
              "diagnostic": "550 5.1.1 user unknown",
              "failed_recipient": "bob@example.net",
              "remote_mta": "mx.example.net",
              "smtp_code": 550,
              "enhanced_status": "5.1.1",
              "phase": "rcpt",
              "reply_text": "user unknown",
              "notify": ["failure"],
              "orcpt": "rfc822;bob@example.net",
              "envid": "QQ314159",
              "ret": "hdrs" } }

85.3.1.1.4.6. attempts / last_error / delay_sent

Données de travail que les étapes de remise notent lorsqu’elles mettent un message en pause pour réessai : attempts (nombre) est le nombre de tentatives de remise jusqu’ici, ce qui est ce qui pilote le recul entre réessais, et delay_sent (booléen, présent seulement lorsqu’il est vrai) marque que le DSN NOTIFY=DELAY à usage unique a déjà été émis, de sorte qu’il n’est jamais envoyé deux fois.

last_error (chaîne) est le texte d’erreur le plus récent. Il n’est pas confiné au chemin de réessai : les étapes de remise, de milter, de cryptographie, de signature DKIM et de ~/.forward l’écrivent aussi lorsqu’elles font échouer un message de manière terminale, de sorte que sur une ligne failed c’est la première chose à lire (pepsi-queue(1) l’affiche comme partie du state de la ligne, et pepsi-status(1) le liste sous chaque message bloqué).

85.3.1.1.4.7. temporary_failures / temporary_stage

(nombre / chaîne) Écrits, avec last_error, par l’infrastructure des étapes lorsque l’erreur d’une étape n’a pas été marquée permanente – imputable à l’hôte plutôt qu’au message (voir pepsi-dispatch(1)) : le nombre de fois que le message a été mis en pause pour cette raison jusqu’ici à temporary_stage, qui détermine l’espacement des réessais, d’une minute doublant jusqu’à une heure. Un compte enregistré à une autre étape n’est pas poursuivi. Supprimés lorsque le worker abandonne.

85.3.1.1.4.8. failed_stage / failure_class / failed_at

La trace d’un échec survenu sur cet hôte plutôt qu’au prochain saut. failed_stage (chaîne) est l’étape où il s’est produit. failure_class (chaîne) dit comment il s’est terminé : permanent (l’étape a marqué l’erreur comme un défaut du message), retries-exhausted (réessayé jusqu’à MAX_LIFETIME), crashed ou timed-out (le message a fait planter ou bloqué son worker trois fois). failed_at (chaîne, RFC 3339) est apposé par la base de données chaque fois que la ligne devient failed ou timeout ; le MIN_AGE de pepsi-failure-bouncer(1) se compte à partir de lui. pepsi-stage-bounce(1) construit un DSN à partir de failed_stage et failure_class lorsqu’aucun relais n’a signalé l’échec (bounce ci-dessus), avec un texte générique ; last_error n’est jamais copié dans un DSN.

85.3.1.1.4.9. arc_temperrors

(nombre) Écrit par pepsi-stage-arc(1) chaque fois que la validation d’une chaîne ARC entrante a rencontré une erreur DNS temporaire : le message est mis en pause et réessayé, et après la troisième tentative de ce genre la chaîne est scellée cv=fail comme le serait une chaîne en échec.

85.3.1.1.4.10. strikes

(nombre) Écrit par pepsi-dispatch(1) chaque fois que le message a fait planter son worker ou l’a retenu au-delà de MAX_RUNTIME ; la troisième pénalité met le message en échec.

85.3.1.1.4.11. retry_since

(chaîne, RFC 3339) Écrit par pepsi-stage-bounce(1) sur le DSN qu’elle construit, qui remplace le message dans sa ligne : l’instant à partir duquel le MAX_LIFETIME des étapes est compté au lieu de l’heure d’arrivée de la ligne, de sorte qu’un DSN ne naisse pas déjà expiré.

85.3.1.1.4.12. srs

(objet) Écrit par pepsi-stage-srs(1) lorsqu’il réécrit l’expéditeur d’enveloppe, notant l’adresse qu’il a remplacée

{ "srs": { "original": "alice@example.org" } }

Elle existe pour un DSN que ce déploiement émet lui-même. SRS s’exécute nécessairement avant l’étape de remise — réécrire l’enveloppe pour le passage de relais est toute sa raison d’être — de sorte qu’au moment où une étape de remise appelle complete_success, ou où un relais achemine un échec vers son BOUNCE_STAGE, le mail_from de la ligne est l’un de nos propres alias SRS. pepsi-stage-bounce(1) adresserait alors le DSN à cet alias, l’envoyant vers le saut suivant puis de nouveau vers l’intérieur par notre propre MX pour être décodé — en survivant à tout le pipeline entrant en tant que message à expéditeur nul — pour n’aboutir qu’à une adresse qui figurait dans la ligne depuis le début. Là où cet aller-retour ne fonctionne pas, le DSN est perdu sans que rien ne le signale.

L’étape de rebond préfère donc cette valeur à mail_from. Elle est notée plutôt que récupérée en inversant l’alias parce que Srs::reverse a besoin du secret HMAC de SRS, et qu’une étape dont le travail est de composer un rebond n’a aucune raison de détenir du matériel de clé : l’expéditeur est une métadonnée de message ordinaire, ce à quoi state sert.

Écrite uniquement lorsqu’elle est absente, de sorte qu’un message réécrit deux fois conserve l’adresse qui nomme un correspondant réel plutôt que l’alias intermédiaire. Un rebond (expéditeur nul) n’est jamais réécrit et n’en note donc jamais ; une valeur vide se lit comme absente, et l’étape de rebond se rabat sur l’expéditeur d’enveloppe.

85.3.1.1.4.13. pay_deadline

(nombre) Écrit par pepsi-stage-anti-spam(1) : des secondes Unix depuis l’époque, l’échéance avant laquelle le bon de commande du péage à l’envoi doit être réglé. Elle est conservée dans state (plutôt que dans la colonne timeout de la ligne) car le dispatcher annule timeout chaque fois qu’il remet en file un message en pause. Une valeur écrite comme chaîne JSON est lue comme « aucune échéance », de sorte que tout ce qui la pose à la main doit écrire un nombre. L’étape court-circuite sa barrière de paiement lorsque state.paid est déjà positionné.

85.3.1.1.4.14. spam / paid

(booléen) Indicateurs de court-circuit anti-abus.

spam est écrit par pepsi-stage-check-whitelist(1), qui le met à false sur une correspondance de liste blanche (et le laisse sinon non positionné), par pepsi-ingress(1), qui le met à false pour un message dont l”unique destinataire est <Postmaster> (joignabilité RFC 5321 §4.5.1), et par pepsi-stage-secretary(1), qui le met à false sur un message qu’il libère parce que son expéditeur a confirmé. Une valeur false fait transmettre le message par pepsi-stage-block-language(1), pepsi-stage-secretary(1) et pepsi-stage-anti-spam(1) sans appliquer leur barrière. Une valeur true fait abandonner le message à pepsi-stage-anti-spam(1) au lieu de le soumettre à la barrière, l’envoie sur le chemin d’expiration de pepsi-stage-secretary(1), supprime l’apprentissage de clés dans pepsi-stage-autocrypt-learn(1) à moins que le LEARN_FROM_SPAM de cette étape ne soit activé, et bloque les répondeurs automatiques de la RFC 3834 qui utilisent les règles de suppression partagées (pepsi-stage-vacation(1), le défi du secrétaire et les réponses automatiques des listes de diffusion). Seul un booléen explicite compte : une clé absente ne déclenche aucun des deux comportements.

Notez qu”aucune étape livrée n’écrit jamais spam = true : les écrivains ci-dessus n’écrivent jamais que false. La valeur true est le point d’accroche qu’utilisent un pipeline piloté par milter, un opérateur ou une étape propre au site — le même arrangement que paid ci-dessous.

paid est écrit par pepsi-stage-anti-spam(1) elle-même, et uniquement comme false : elle stocke paid = false à côté de pay_deadline à chaque mise en pause, ce qui marque le message comme en attente de paiement. Lorsque le marchand signale le bon de commande réglé, l’étape avance simplement le message ; aucune étape de l’arbre n’écrit donc jamais paid = true — cette valeur est un point de redéfinition pour un opérateur ou une étape propre au site, et la barrière de paiement est sautée lorsqu’elle est positionnée (comme elle l’est lorsque spam vaut false).

85.3.1.1.4.15. dot-forwarders

(tableau de chaînes) Les noms de login en minuscules dont les fichiers ~/.forward ont redirigé cette ligne, c’est-à-dire la chaîne de redirection qui y a mené, maintenue par pepsi-stage-dot-forward(1) comme protection contre les boucles de redirection. Une ligne redirigée reçoit la chaîne de la ligne dont elle est issue plus l’unique login qui l’a redirigée, de sorte que la liste décrit un seul chemin de redirection, et non chaque ~/.forward que le message a traversé. Comme un message redirigé redémarre le pipeline, un destinataire dont l’utilisateur est déjà listé ici fait l’objet d’un rebond au lieu d’être repassé par son ~/.forward, de sorte qu’un cycle (~bob → alice, ~alice → bob) se termine.

85.3.1.1.4.16. language

(chaîne) Écrit par pepsi-stage-detect-language(1) comme une chaîne de style Accept-Language décrivant la ou les langues détectées du corps, avec des valeurs q par ordre de préférence.

pepsi-stage-block-language(1) la lit pour noter le message par rapport à ses listes d’autorisation/de refus, mais c’est tout autant la clé de localisation : chaque étape qui compose de la prose destinée à un humain y choisit la langue de son modèle ou de son message, retombant sur son propre DEFAULT_LANGUAGE lorsqu’elle est absente ou ne nomme rien de pris en charge — pepsi-stage-vacation(1) (l’avis d’absence), pepsi-stage-anti-spam(1) (la demande de paiement), pepsi-stage-secretary(1) (le défi), pepsi-stage-encrypt(1), pepsi-stage-auto-pay(1) et pepsi-stage-secure-link(1) (leurs messages de réponse) et pepsi-stage-edit-settings(1) (sa réponse de confirmation ou d’erreur). C’est pourquoi un déploiement qui ne fait aucun blocage de langue peut tout de même vouloir l’étape de détection dans le pipeline.

85.3.1.1.4.17. vacation

(objet) Écrit par pepsi-stage-vacation(1) lorsqu’il a répondu à un message pour le compte du destinataire, notant ce qu’il a fait

{ "vacation": { "notified":  true,
                "recipient": "alice@example.org",   // who is away
                "range":     "2026-08-01:2026-08-14",
                "language":  "de" } }               // the notice's language

Rien ne le lit : il existe pour que la décision soit visible dans pepsi-queue(1), pour que pepsi-stage-if(1) puisse brancher dessus, et pour qu’une question de support sur une réponse d’absence surprenante trouve réponse dans la ligne plutôt que dans le journal. Absent sur tout message auquel il n’a pas été répondu.

85.3.1.1.4.18. secretary

(objet) Écrit par pepsi-stage-secretary(1). Sur un message qu’il retient

{ "secretary": { "challenge": "00112233445566778899aabbccddeeff",  // the cookie
                 "deadline":  1769812345,          // Unix seconds, when it expires
                 "whitelist": "correspondents" } } // where a confirmation writes

Une confirmation ajoute "confirmed": true en libérant la ligne (l’étape la transmet alors avec spam = false) ; un défi revenu en rebond ajoute "abandoned": true, l’envoyant plus tôt sur le chemin d’expiration. Un message que l’étape ne voulait pas soumettre à un défi et qu’elle a acheminé vers son UNCHALLENGEABLE_STAGE porte à la place { "secretary": { "unchallengeable": "<reason>" } }. Le chemin d’expiration supprime entièrement la clé : rien d’un défi ne lui survit. Comme pay_deadline, l’échéance est conservée dans state parce que le dispatcher efface le timeout de la ligne lorsqu’il remet en file un message en pause.

85.3.1.1.4.19. keydisc

(objet) Ce qu’attend un message garé : l’adresse du correspondant dont la clé n’a pas pu être trouvée dans le cache, et le moment où l’attente a commencé

{ "keydisc": { "address": "bob@example.com",   // the address a key is wanted for
               "since":   1769812345 } }       // Unix seconds, when the park happened

Il est écrit par le terminal de mise en attente de la découverte de clés, qui en un aller-retour valide tout ce que l’étape avait déjà réécrit, fusionne cette clé, met la ligne en paused avec un délai de réessai, et met une demande en file dans pepsi.key_request. Un service pepsi-keydisc(1) qui règle cette demande apparie les attendants sur keydisc.address — via un index partiel portant exactement sur cette expression — et les rebascule en pending, de sorte qu”une étape ne doit ni renommer ni écrire cette clé à la main. Le délai de réessai est le filet de sécurité d’un déploiement qui n’exploite aucun service de découverte : il est toujours fixé après l’échéance de découverte, de sorte que la libération se produit normalement d’abord.

La libération ne déplace que le status de la ligne et efface son timeout, de sorte que la clé y survit : un message repris porte encore la trace de l’attente qui vient de s’achever, et une étape qui gare de nouveau l’écrase simplement.

85.3.1.1.4.21. tlsrpt

(booléen) Un indicateur de protection contre les boucles posé sur un message qui est lui-même un rapport SMTP TLS Reporting (pepsi-tlsrpt(1)). Les étapes de relais ne notent pas de ligne pepsi.tls_session pour un tel message, de sorte qu’un rapport sur une remise de rapport échouée ne peut récurser.

85.3.1.1.4.22. auth.arc

Écrasé par pepsi-stage-arc(1) avec le verdict de la chaîne ARC entrante qu’il a réévaluée, remplaçant le none que l’ingress a semé sous auth (voir la clé auth ci-dessus).

85.3.1.1.4.23. auth.arc_sealers

(tableau de chaînes) Écrit par pepsi-stage-arc(1) à côté de auth.arc : le d= de chaque ARC-Seal que le message portait à son arrivée, en minuscules, point racine final retiré, dédupliqué et ordonné par instance de sorte que l’ADMD la plus proche de l’auteur vienne en premier. Notre propre ARC_DOMAIN n’y figure jamais — la liste est lue avant que le propre ensemble de cette étape ne soit rendu

{ "auth": { "arc": "pass", "arc_sealers": ["lists.example.org", "mx.forwarder.example"] } }

Il existe parce qu”auth.arc seul ne peut pas soutenir une décision de politique : la RFC 8617 §8.4 énonce qu’une chaîne valide ne transmet aucune fiabilité, seulement que les ADMD nommées ont manipulé le message, et laisse le reste au destinataire. Nommer un intermédiaire acceptable est cette politique locale, et voici ce à quoi une telle règle est comparée — les lignes sealer_domain de pepsi-stage-check-whitelist(1).

Le tableau est écrit que la chaîne ait validé ou non, car il est la trace de ce que le message a prétendu ; un sceau s’analyse sans se vérifier. Tout lecteur doit donc exiger auth.arc = pass avant de faire confiance à une entrée, ce que fait check-whitelist en écartant sinon toute la liste.

85.3.1.1.4.24. crypto

(objet) Ce qu’ont fait les étapes de cryptographie de bout en bout. La moitié sortante, crypto.out, est écrite par pepsi-stage-encrypt(1)

{ "crypto": { "out": {
    "signed":            true,
    "protocol":          "openpgp",
    "signer":            "alice@example.org",
    "signer_fingerprint": "ABCD…",
    "recipients": {
      "bob@example.net": { "outcome":           "encrypted",
                           "protocol":          "openpgp",
                           "fingerprint":       "1234…",
                           "key_source":        "wkd",
                           "trust":             "wkd-advanced",
                           "content_algorithm": "seipdv2-aead",
                           "downgraded":        false } },
    "key_attached":      true } } }

signer est l’auteur du From:, jamais l’expéditeur d’enveloppe : la signature porte sur l’auteur que voit le destinataire. L”outcome de chaque destinataire vaut encrypted, signed-only, cleartext, secure-link, bounced ou oversize, et route nomme le chemin ON_NO_KEY/ON_OVERSIZE emprunté lorsque ce n’était pas le chemin ordinaire. reason porte une courte formule pour le journal de l’opérateur et pour le rebond.

content_algorithm et downgraded sont notés afin qu’un opérateur puisse répondre à « à qui parlons-nous encore en SEIPDv1 ? » sans lire de journaux. downgraded n’apparaît qu’à côté d’un content_algorithm : un false sur un destinataire qui n’a pas été chiffré du tout se lirait comme « nous avons chiffré, sans rétrograder », ce qui est une autre affirmation.

key_source et trust valent tous deux own lorsque le destinataire est une adresse pour laquelle ce déploiement détient sa propre identité. Ce n’est pas un rang sur l’échelle de découverte : notre propre clé préempte entièrement le cache pepsi.peer_key pour une telle adresse, de sorte qu’aucun plancher MIN_TRUST ne s’applique et qu’aucune clé découverte n’a été consultée. Voir pepsi-stage-encrypt(1).

Comme l’étape scinde les destinataires divergents à raison d’un par ligne, l’objet recipients d’une ligne contient normalement exactement les destinataires du rcpt_to de cette ligne.

key_attached (booléen, présent seulement lorsqu’il est vrai) note que la clé publique de l’expéditeur est partie sous forme de fichier application/pgp-keys (ATTACH_KEYS_AS_FILES). client_protected (booléen, présent seulement lorsqu’il est vrai) note que le propre client de messagerie de l’utilisateur avait déjà chiffré la soumission, de sorte que l’étape n’y a pas touché : ni chiffrement, ni signature, ni en-tête Autocrypt:, ni fichier de clé de la part de Pepsi.

La moitié entrante, crypto.in, est écrite par pepsi-stage-decrypt(1)

{ "crypto": { "in": {
    "encrypted":      true,
    "outer_gossip":   true,
    "decrypted":      true,
    "decryption":     "decrypted",
    "protocol":       "openpgp",
    "decrypted_with": { "identity":    7,
                        "address":     "bob@example.org",
                        "fingerprint": "ABCD…" },
    "signature":      { "status":      "valid",
                        "signer":      "alice@example.net",
                        "fingerprint": "1234…",
                        "key_source":  "wkd",
                        "trust":       "wkd-advanced",
                        "algorithm":   "ed25519",
                        "signed_at":   1785000000,
                        "covers_plaintext": true },
    "layers": [ { "kind": "encrypted", "protocol": "openpgp",
                  "encoding": "pgp-mime", "verdict": "good",
                  "container": "seipdv1-mdc", "downgraded": true },
                { "kind": "signed", "protocol": "openpgp",
                  "encoding": "pgp-mime", "verdict": "good" } ] } } }

decryption vaut not-encrypted, decrypted, failed ou for-client ; sur failed, une clé voisine failure porte la raison lisible par une machine (no-decryption-key, decryption-failed, too-large, …). route nomme le chemin ON_DECRYPT_FAILURE/ON_BAD_SIGNATURE emprunté lorsque ce n’était pas le chemin ordinaire.

for-client signifie que le message est chiffré pour la propre clé MUA du destinataire (une identité avec custody = client, dont la moitié privée ne vit que dans son client de messagerie) et qu’il a été transmis sans être ouvert, même lorsque l’une de nos propres clés figurait aussi parmi les destinataires. Ce n’est pas un échec : ON_DECRYPT_FAILURE n’est pas consulté. La clé voisine for_client vaut alors true et client_fingerprint nomme la clé. Rien n’a été vérifié, donc signature_verified n’est pas positionné.

signature.covers_plaintext est présent dès qu’il existe un verdict de signature : true lorsque le verdict a été accordé par une signature sur le texte en clair, false lorsqu’il provient d’une couche enveloppant du texte chiffré. pepsi-stage-autocrypt-learn(1) ne note qu’un correspondant détient l’une de nos clés (pepsi.peer_has_own_key) que sur une signature valid ou valid-untrusted avec covers_plaintext = true dont le signataire est l’adresse From:, sur un message que nous avons déchiffré.

outer_gossip (booléen, présent uniquement lorsqu’il est vrai) note que le message tel qu’il est arrivé portait déjà un champ Autocrypt-Gossip: sur son bloc d’en-têtes extérieur — quelque chose que n’importe quel saut du chemin a pu écrire, et que le réassemblage PGP en ligne peut faire traverser le déchiffrement. Il est écrit ici parce que le texte clair est validé par-dessus les octets arrivants, de sorte que rien en aval ne pourrait répondre à la question plus tard. Avec encrypted, c’est ce que pepsi-stage-autocrypt-learn(1) consulte avant d’apprendre la moindre clé issue du gossip : un champ extérieur non authentifié met son veto à l’ensemble. Si LEARN_GOSSIP est activé et que rien n’est appris d’un message chiffré, c’est la clé à regarder.

signature.status vaut l’un de :

none

Aucune signature.

valid

Cryptographiquement saine et la clé était ancrée — une chaîne X.509 vers une ancre ca_trust, ou une clé OpenPGP issue d’une source de découverte classée.

valid-untrusted

Saine, mais la clé n’a pu être rattachée à rien : une clé vue pour la première fois, une clé apprise du message lui-même, ou un certificat d’une AC pour laquelle ce déploiement ne détient aucune ancre. Jamais rapportée comme ``valid`` ; faire cette distinction est la raison d’être de l’étape.

invalid

La signature ne vérifie pas, ou le certificat du signataire est expiré, révoqué ou lié à une autre adresse. signature.failure dit lequel.

unverifiable

Aucune clé n’était disponible pour le signataire prétendu.

signature.key_source et signature.trust portent ici le vocabulaire de provenance propre à la couche cryptographique (anchor, dane, wkd, autocrypt, pinned, attached, unknown), plus grossier que les noms d’échelons de la moitié sortante. Une signature vérifiée contre notre propre identité pour l’expéditeur prétendu rapporte pinned — la valeur la plus forte de ce vocabulaire, et la bonne, puisque la clé est consignée pour cette adresse. C’est la même préemption que la moitié sortante appelle own : pour une adresse dont ce déploiement détient une clé, aucune ligne peer_key n’est offerte au vérificateur. Voir pepsi-stage-decrypt(1).

Un message portant plusieurs couches de signature prend le pire de leurs verdicts — parmi les couches qui couvrent le texte clair. Une couche de signature marquée covers_ciphertext (voir ci-dessous) n’est consultée que s’il n’y en a pas d’autre, et ne peut même alors jamais donner valid.

layers est ordonné le plus extérieur d’abord, et cet ordre compte : c’est à partir de lui que sont rendues les marques de sujet, de sorte que encrypted(signed(body)) et signed(encrypted(body)) soient distinguables. Il survit donc à une mise en pause, raison pour laquelle l’étape le note plutôt que de le recalculer. container et downgraded n’apparaissent que sur les couches de texte chiffré, afin qu’un opérateur voie quelle part du courrier entrant est encore pré-AEAD.

"covers_ciphertext": true apparaît sur une couche de signature à l’intérieur de laquelle une couche de texte chiffré est imbriquée — la signature extérieure de signed(encrypted(body)). Elle n’est présente que lorsqu’elle est vraie. Une telle signature a été calculée sur le texte chiffré : elle dit donc que le signataire a transmis le bloc et rien de ce qui en est sorti ; quiconque obtient un message chiffré peut l’envelopper dans sa propre signature et le réexpédier. La couche conserve son propre verdict honnête, mais elle ne peut pas élever signature.status à valid et ne peut donc pas positionner signature_verified. Dans signed(encrypted(signed(body))), seule la couche extérieure est marquée, et c’est la couche intérieure — celle qui parle pour le contenu — qui décide du verdict.

La moitié stockage, crypto.store, est écrite par pepsi-stage-reencrypt(1) sur une ligne dont le crypto.in indique que le message a été déchiffré

"crypto": {
  "in": { … },
  "store": { "outcome": "reencrypted", "protocol": "openpgp",
             "fingerprint": "…", "container": "seipdv2-aead",
             "downgraded": false }
}

outcome vaut reencrypted (scellé pour la propre clé MUA du destinataire, désignée par fingerprint), plaintext (classé tel quel : aucune clé MUA utilisable sous ON_NO_CLIENT_KEY = plaintext, ou destinataire non local — reason précise lequel) ou bounced (refusé sous ON_NO_CLIENT_KEY = bounce ; reason). L’étape fusionne l’objet crypto entier, de sorte que crypto.in survit, et c’est la présence de store qui l’empêche de sceller une ligne deux fois.

85.3.1.1.4.25. encrypted

(booléen) Mis à true par pepsi-stage-encrypt(1) lorsque le message de cette ligne a réellement été chiffré vers une clé de destinataire, et par pepsi-stage-decrypt(1) lorsque le message est arrivé chiffré. pepsi-stage-auto-whitelist(1) le recopie dans la colonne signature_required de la nouvelle ligne de liste blanche, de sorte qu’un correspondant atteint sous chiffrement soit soumis plus tard à la même exigence. Absent (plutôt que false) lorsque rien n’a été chiffré.

85.3.1.1.4.26. signature_verified

(booléen) Mis à true par pepsi-stage-decrypt(1) pour le verdict de signature valid, et pour celui-là seul. La barrière signature_required de pepsi-stage-check-whitelist(1) le teste : laisser valid-untrusted le positionner ouvrirait donc une barrière que l’opérateur destinait à une clé de confiance — et, pour la même raison, une signature qui ne couvre que du texte chiffré (covers_ciphertext, ci-dessus) ne le positionne jamais non plus. Absent (plutôt que false) sinon.

85.3.1.1.4.27. milter

(objet) Ce qu’a fait pepsi-stage-milter(1) — le verdict du filtre, la version de protocole négociée, la durée de la conversation, et chaque modification appliquée, étiquetée

{ "milter": { "stage":             "spam-filter",
              "verdict":           "reject",
              "version":           6,
              "elapsed_ms":        42,
              "actions":           ["addheader:X-Spam-Status", "replbody"],
              "reply_code":        "550 5.7.1 blocked",
              "quarantine_reason": "virus found",
              "rejected_recipients": 1 } }

verdict vaut continue, accept, reject, tempfail, discard ou — lorsque le filtre n’a pas pu être contacté du tout — failed, auquel cas une clé voisine error porte le diagnostic et l’acheminement a suivi l”ON_FAILURE de l’étape. reply_code, quarantine_reason et rejected_recipients ne sont présents que lorsqu’ils s’appliquent.

Rien ne le lit. Il existe pour que pepsi-queue(1) puisse montrer pourquoi un message a été étiqueté, pour que pepsi-stage-if(1) puisse brancher dessus, et pour qu’une question de support sur un en-tête surprenant trouve réponse dans la ligne plutôt que dans le journal — la même justification que pour vacation ci-dessus. Un message rejeté porte en outre l’objet bounce habituel, avec le code SMTP propre du filtre, le code d’état étendu RFC 3463 et le texte répartis dans leurs propres champs.

85.3.1.1.4.28. crypto_policy_refused

(booléen, présent seulement lorsqu’il est vrai) Écrit à côté de last_error par pepsi-stage-encrypt(1), pepsi-stage-decrypt(1) et pepsi-stage-secure-link(1) lorsqu’ils font échouer un message parce que la politique configurée l’a refusé — ENCRYPT = required sans clé de destinataire utilisable et sans route ON_NO_KEY, un message arrivé chiffré sans route ON_DECRYPT_FAILURE, et ainsi de suite. Il distingue un refus délibéré d’une erreur de transport ou de programmation, qui autrement se ressemblent à l’identique sur une ligne failed : réessayer le premier ne servira jamais à rien.

85.3.1.1.4.29. milter_error

(chaîne) Écrit par pepsi-stage-milter(1) lorsqu’un filtre a rejeté un message mais que ni REJECT_STAGE ni BOUNCE_STAGE n’était configuré, de sorte que l’étape n’avait nulle part où l’acheminer. À distinguer de milter.error à l’intérieur de l’objet milter ci-dessus, qui est le diagnostic d’un filtre auquel on n’a pas pu parler du tout.

85.3.1.1.4.30. list

(objet) Le descripteur de liste de diffusion. pepsi-stage-list(1) l’écrit sur chaque ligne qu’il achemine vers un rôle de liste (l”id de la liste, le role, l”address à laquelle le message est arrivé, et une adresse token ou verp lorsque la sous-adresse en portait une) ; pepsi-stage-list-post(1) écrit un descripteur par membre (id, member, lang, serial, duplicate) sur chaque ligne de remise qu’il déploie, plus pending_rcpt sur une publication partiellement déployée et rejected sur une publication refusée ; pepsi-list(1) positionne moderator_approved (et digest sur une livraison de résumé). Lu par pepsi-stage-list-post(1), pepsi-stage-list-command(1), pepsi-stage-list-deliver(1) et pepsi-stage-list-bounce(1) ; voir ces pages pour les champs qu’utilise chacun.

85.3.1.1.4.31. dispatch_error

(chaîne) Écrit par pepsi-dispatch(1) lorsqu’il force une ligne à un état terminal qu’elle n’a pas atteint d’elle-même — le diagnostic pour un programme d’étape qui a planté (failed), pour un programme qui a dépassé MAX_RUNTIME (timeout), ou pour une ligne restée à une étape que la configuration ne définit pas. pepsi-status(1) le liste sous chaque message bloqué.

85.3.1.1.5. Voir aussi

pepsi.conf(5), pepsi-ingress(1), pepsi-dispatch(1), pepsi-stage-arc(1), pepsi-stage-bounce(1), pepsi-stage-check-whitelist(1), pepsi-stage-anti-spam(1), pepsi-stage-detect-language(1), pepsi-stage-block-language(1), pepsi-stage-vacation(1), pepsi-stage-secretary(1), pepsi-stage-milter(1), pepsi-stage-encrypt(1), pepsi-stage-decrypt(1), pepsi-stage-auto-whitelist(1), pepsi-stage-list(1), pepsi-tlsrpt(1), pepsi-keydisc(1), pepsi-keys(1)