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) :

origin

La 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_id de ce récepteur. L’étape ARC a besoin de l”authserv_id pour reproduire l’identité de son Authentication-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_method et consorts) ainsi que from_bound : si le From: est exactement une boîte aux lettres liée au compte par une entrée USERNAME_MAP concrète, sans joker (ou par sa valeur par défaut login@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 que from_bound est vrai.

auth

Les verdicts SPF/DKIM/DMARC entrants (également reflétés dans l’en-tête Authentication-Results ajouté en tête). Le membre arc commence à none et est écrasé par l’étape ARC.

local_origin

Un booléen : true lorsque la session de remise a été authentifiée (dans MYNETWORKS, via SASL AUTH, avec un certificat client TLS correspondant, ou — sur une socket UNIX avec AUTH_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é.

dsn

Les paramètres RFC 3461 demandés par l’expéditeur : les ret/envid au niveau du message et un tableau notify/orcpt par 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.

spam

Semé à 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 un RCPT <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.arc avec 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 objet bounce — son kind (permanent/success/delay), les diagnostic et failed_recipient lisibles par un humain, les notify/orcpt/envid copié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.original l’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 language dé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/paid et le pay_deadline ;

  • pepsi-stage-secretary note sous secretary le défi qu’attend un message retenu (le cookie, son deadline, la whitelist), le marque confirmed ou abandoned et, à la libération, positionne spam = false ; le chemin d’expiration retire de nouveau la clé ;

  • pepsi-stage-dot-forward note dans dot-forwarders la chaîne des logins redirecteurs d’une ligne redirigée, pour briser les boucles de redirection ~/.forward ;

  • pepsi-stage-milter note sous milter quelle étape a exécuté le filtre, le verdict qu’il a renvoyé et, lorsque cela s’est mal passé, l”error ;

  • pepsi-stage-vacation note sous vacation qu’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.out ce qu’il a signé et chiffré, par destinataire, et positionne encrypted lorsque le message d’un destinataire a réellement été chiffré — l’indicateur que pepsi-stage-auto-whitelist recopie dans sa colonne signature_required. crypto.out.key_attached indique que la clé de l’expéditeur est partie sous forme de fichier, et crypto.out.client_protected que 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.in l’image miroir — si le message est arrivé chiffré, ce qu’est devenu le texte chiffré (decryption = for-client, avec for_client et client_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 (avec covers_plaintext) et la liste ordonnée des couches — et positionne signature_verified pour le verdict valid et pour celui-là seul, l’indicateur qui conditionne une ligne signature_required chez 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 que crypto.in.encrypted et crypto.in.outer_gossip (si le bloc d’en-têtes arrivant portait déjà un champ Autocrypt-Gossip:) ne peuvent recevoir de réponse qu’ici. pepsi-stage-autocrypt-learn lit les deux — leurs noms résident dans pepsi_common::crypto_state, de sorte qu’aucune des deux étapes de cryptographie ne possède le vocabulaire ;

  • pepsi-stage-reencrypt enregistre sous crypto.store ce qu’elle a fait d’un message que le déchiffrement a ouvert : outcome vaut reencrypted (avec protocol, la fingerprint de la clé MUA, container et downgraded), plaintext ou bounced (avec une reason). Elle fusionne l’objet crypto entier, en emportant crypto.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_error lorsqu’il force une ligne à failed/timeout parce 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).