23. L’état du message¶
Chaque message en vol est une ligne de la table pepsi.workqueue. Outre l’enveloppe et le corps, 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. La référence exhaustive des clés est la page de manuel pepsi.state(7) ; ce chapitre en est le compagnon narratif.
23.1. L’état est le seul canal¶
Les étapes ne se parlent pas directement. Le pipeline est une machine à états sur une table unique (voir Architecture) : une étape charge une ligne, effectue son travail et écrit exactement un résultat terminal. L’objet state est le seul endroit où une étape peut laisser de l’information qu’une étape ultérieure pourra lire. L’ingress note ce qu’il a appris de la connexion et de l’intention de l’expéditeur ; l’étape ARC note le verdict de la chaîne qu’elle a réévaluée ; une étape de relais note pourquoi une remise a échoué afin que l’étape de rebond puisse l’expliquer. Rien de tout cela ne réside dans le message lui-même — cela réside dans state.
Comme state est du simple JSON sans schéma fixe, l’ensemble des clés est ouvert. Une étape personnalisée (voir Étendre le pipeline) peut inventer ses propres clés ; le seul contrat est la règle de fusion ci-dessous.
23.2. Sémantique de fusion¶
state est construit de façon incrémentale plutôt qu’écrasé. Chaque helper terminal d’étape qui modifie une ligne fusionne ses nouvelles clés dans l’objet existant — une fusion d’objets superficielle, clé par clé, avec exactement la sémantique state || $new de PostgreSQL, repliée dans le worker pour la validation d’une ligne unique et évaluée en SQL par les fonctions d’éclatement (workqueue_split, workqueue_pause, workqueue_finish_with_clones) — de sorte qu’une clé écrite tôt survit, à moins qu’une étape ne remplace délibérément cette clé précise. C’est pourquoi la provenance origin semée par l’ingress reste lisible par une étape de relais à l’extrémité d’un long pipeline, et pourquoi les paramètres dsn de la RFC 3461 parviennent intacts à l’étape de rebond.
Il existe exactement une exception : pepsi-stage-bounce 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.
23.3. Ce que sème l’ingress¶
Lorsque pepsi-ingress accepte un message, il sème trois familles de clés, plus deux conditionnelles (dsn et spam) :
originLa provenance d’origine SMTP de la connexion qui a remis le message — IP du client, HELO/EHLO, les déclarations ESMTP/SMTPUTF8/
BODY=, les paramètres TLS, le listener, le DNS inverse/iprev et l”authserv_idde ce récepteur. L’étape ARC a besoin de l”authserv_idpour reproduire l’identité de sonAuthentication-Results, et les étapes de relais consultent les déclarations pour décider s’il faut rétrograder le contenu 8 bits/UTF-8 pour un saut suivant donné. Elle note aussi comment une soumission s’est authentifiée (auth_methodet consorts) ainsi quefrom_bound: si leFrom:est exactement une boîte aux lettres liée au compte par une entréeUSERNAME_MAPconcrète, sans joker (ou par sa valeur par défautlogin@HOSTNAME). L’étape de chiffrement ne laisse une soumission enregistrer une clé ou exécuter une commande de clé par e-mail que lorsqu’elle provient d’un relais de confiance ou quefrom_boundest vrai.authLes verdicts SPF/DKIM/DMARC entrants (également reflétés dans l’en-tête
Authentication-Resultsajouté en tête). Le membrearccommence ànoneet est écrasé par l’étape ARC.local_originUn booléen :
truelorsque la session de remise a été authentifiée (dansMYNETWORKS, via SASLAUTH, avec un certificat client TLS correspondant, ou — sur une socket UNIX avecAUTH_PEERCRED, la valeur par défaut à cet endroit — en tant que pair dont l’uid s’est résolu en un login autorisé, ce par quoi arrive tout message soumis localement). Il distingue le courrier sortant d’un expéditeur de confiance du courrier entrant venu d’Internet, et la couche de paramètres par adresse ainsi que le canal de contrôle edit-settings s’en servent comme clé.dsnLes paramètres RFC 3461 demandés par l’expéditeur : les
ret/envidau niveau du message et un tableaunotify/orcptpar destinataire, parallèle à la liste des destinataires. Chaque étape doit le préserver et l’honorer. Présent uniquement lorsque l’expéditeur a demandé quelque chose.spamSemé à
false, et dans un seul cas : un message adressé uniquement à la boîte aux lettres réservée<Postmaster>, que la RFC 5321 §4.5.1 exige de garder remettable. Les barrières de langue, de confirmation à l’envoi et de péage à l’envoi font suivre tout message déjà marqué comme non indésirable, de sorte que c’est ce qui garde cette boîte aux lettres joignable. La restriction au cas du destinataire unique est délibérée : autrement, un spammeur pourrait mettre des co-destinataires en liste blanche en ajoutant unRCPT<Postmaster>à côté d’une véritable victime.
23.4. Ce qu’écrivent les étapes¶
À mesure que le message avance, les étapes y fusionnent leurs propres clés :
l’étape ARC écrase
auth.arcavec le verdict de la chaîne qu’elle a réévaluée ;toute étape qui achemine un message vers son
BOUNCE_STAGE(les étapes de remise et de rejet, et des étapes de politique comme le milter, le secrétaire et les étapes de cryptographie) écrit un objetbounce— sonkind(permanent/success/delay), lesdiagnosticetfailed_recipientlisibles par un humain, lesnotify/orcpt/envidcopiés et le détail structuré du saut suivant (remote_mta/smtp_code/enhanced_status/phase/reply_text) — que pepsi-stage-bounce transforme en DSN ;les étapes de relais conservent des données de travail de réessai (
attempts,last_error,delay_sent) ;pepsi-stage-srs note sous
srs.originall’expéditeur d’enveloppe qu’il a remplacé, de sorte qu’un DSN émis par ce déploiement parvienne au véritable expéditeur plutôt qu’à son propre alias SRS ;pepsi-stage-list et les étapes de liste qui le suivent portent le descripteur de la liste de diffusion sous
list;pepsi-stage-detect-language note la
languagedétectée que pepsi-stage-block-language(1) évalue ensuite ;pepsi-stage-check-whitelist et pepsi-stage-anti-spam coopèrent via les indicateurs de court-circuit
spam/paidet lepay_deadline;pepsi-stage-secretary note sous
secretaryle défi qu’attend un message retenu (le cookie, sondeadline, lawhitelist), le marqueconfirmedouabandonedet, à la libération, positionnespam = false; le chemin d’expiration retire de nouveau la clé ;pepsi-stage-dot-forward note dans
dot-forwardersla chaîne des logins redirecteurs d’une ligne redirigée, pour briser les boucles de redirection~/.forward;pepsi-stage-milter note sous
milterquelle étape a exécuté le filtre, leverdictqu’il a renvoyé et, lorsque cela s’est mal passé, l”error;pepsi-stage-vacation note sous
vacationqu’il a répondu à un message pour le compte du destinataire, et dans quelle langue — lu par personne, afin qu’une réponse d’absence surprenante soit explicable à partir de la ligne ;pepsi-stage-encrypt note sous
crypto.outce qu’il a signé et chiffré, par destinataire, et positionneencryptedlorsque le message d’un destinataire a réellement été chiffré — l’indicateur que pepsi-stage-auto-whitelist recopie dans sa colonnesignature_required.crypto.out.key_attachedindique que la clé de l’expéditeur est partie sous forme de fichier, etcrypto.out.client_protectedque le propre client de messagerie de l’utilisateur avait déjà chiffré le message, de sorte que l’étape n’y a pas touché ;pepsi-stage-decrypt note sous
crypto.inl’image miroir — si le message est arrivé chiffré, ce qu’est devenu le texte chiffré (decryption = for-client, avecfor_clientetclient_fingerprint, lorsqu’il était chiffré pour la clé du MUA propre au destinataire et a été transmis sans être ouvert), laquelle de nos identités l’a ouvert, le verdict de signature (aveccovers_plaintext) et la liste ordonnée des couches — et positionnesignature_verifiedpour le verdictvalidet pour celui-là seul, l’indicateur qui conditionne une lignesignature_requiredchez pepsi-stage-check-whitelist. Deux de ces membres existent parce que rien en aval ne pourrait les retrouver : le texte en clair est validé par-dessus les octets arrivants, de sorte quecrypto.in.encryptedetcrypto.in.outer_gossip(si le bloc d’en-têtes arrivant portait déjà un champAutocrypt-Gossip:) ne peuvent recevoir de réponse qu’ici. pepsi-stage-autocrypt-learn lit les deux — leurs noms résident danspepsi_common::crypto_state, de sorte qu’aucune des deux étapes de cryptographie ne possède le vocabulaire ;pepsi-stage-reencrypt enregistre sous
crypto.storece qu’elle a fait d’un message que le déchiffrement a ouvert :outcomevautreencrypted(avecprotocol, lafingerprintde la clé MUA,containeretdowngraded),plaintextoubounced(avec unereason). Elle fusionne l’objetcryptoentier, en emportantcrypto.in, et c’est la présence de cette clé qui l’empêche de jamais sceller une ligne deux fois ;le dispatcher écrit
dispatch_errorlorsqu’il force une ligne àfailed/timeoutparce qu’une étape a planté ou s’est exécutée trop longtemps.
Les deux étapes cryptographiques vivent sous la même clé crypto avec les mêmes noms de champs, et toutes deux font une fusion superficielle, de sorte qu’une ligne porte crypto.out ou crypto.in (plus crypto.store, que l’étape de rechiffrement écrit à côté) et non les deux — ce qui est correct, puisqu’un message est sur le chemin sortant ou sur le chemin entrant. La liste complète, avec les formes JSON exactes et l’étape productrice/consommatrice de chaque clé, figure dans pepsi.state(7).