23. Der Nachrichten-State¶
Jede in Bearbeitung befindliche Nachricht ist ein Datensatz der Tabelle pepsi.workqueue. Neben dem Umschlag und dem Nachrichtentext trägt jeder Datensatz ein frei strukturiertes JSON-Objekt — die Spalte state (PostgreSQL-JSONB) —, das mit der Nachricht vom Ingress bis zu ihrer terminalen Stage reist. Die vollständige Schlüsselreferenz ist die Handbuchseite pepsi.state(7); dieses Kapitel ist der erzählende Begleiter.
23.1. Der State ist der einzige Kanal¶
Stages reden nicht direkt miteinander. Die Pipeline ist ein Einzeltabellen-Zustandsautomat (siehe Architektur): Eine Stage lädt einen Datensatz, erledigt ihre Arbeit und schreibt genau ein terminales Ergebnis. Das state-Objekt ist der einzige Ort, an dem eine Stage Informationen für eine spätere Stage hinterlassen kann. Ingress hält fest, was es über die Verbindung und die Absicht des Absenders erfahren hat; die ARC-Stage hält das Urteil der von ihr neu bewerteten Kette fest; eine Relay-Stage hält fest, warum eine Zustellung fehlschlug, damit die Bounce-Stage es erklären kann. Nichts davon liegt in der Nachricht selbst — es liegt in state.
Weil state einfaches JSON ohne festes Schema ist, ist die Menge der Schlüssel offen. Eine eigene Stage (siehe Die Pipeline erweitern) darf ihre eigenen Schlüssel erfinden; der einzige Vertrag ist die Zusammenführungsregel unten.
23.2. Zusammenführungssemantik¶
state wird inkrementell aufgebaut statt überschrieben. Jeder terminale Stage-Helper, der einen Datensatz verändert, führt seine neuen Schlüssel in das bestehende Objekt zusammen — eine flache, schlüsselweise Objektzusammenführung mit exakt der Semantik von PostgreSQLs state || $new, im Worker für das Commit des einzelnen Datensatzes eingefaltet und von den Fan-out-Funktionen (workqueue_split, workqueue_pause, workqueue_finish_with_clones) in SQL ausgewertet — sodass ein früh geschriebener Schlüssel erhalten bleibt, sofern nicht eine Stage genau diesen Schlüssel absichtlich ersetzt. Deshalb sind die von Ingress gesäten origin-Herkunftsdaten noch von einer Relay-Stage am fernen Ende einer langen Pipeline lesbar, und deshalb erreichen die RFC-3461-dsn-Parameter die Bounce-Stage unversehrt.
Es gibt genau eine Ausnahme: pepsi-stage-bounce setzt state auf null, wenn es eine Nachricht in eine Zustellungsstatusbenachrichtigung umschreibt, weil der Bounce eine brandneue Null-Absender-Nachricht ist, die keine der Herkunftsdaten des Originals erbt.
23.3. Was Ingress sät¶
Wenn pepsi-ingress eine Nachricht annimmt, sät es drei Familien von Schlüsseln, dazu zwei bedingte (dsn und spam):
originDie SMTP-Ursprungsherkunft der zustellenden Verbindung — Client-IP, HELO/EHLO, die ESMTP/SMTPUTF8/
BODY=-Deklarationen, TLS-Parameter, der Listener, Reverse-DNS/iprev und dieauthserv_iddieses Empfängers. Die ARC-Stage benötigt dieauthserv_id, um ihreAuthentication-Results-Identität zu reproduzieren, und die Relay-Stages ziehen die Deklarationen heran, wenn sie entscheiden, ob sie 8-Bit-/UTF-8-Inhalte für einen bestimmten nächsten Hop herabstufen. Außerdem hält sie fest, wie sich eine Einlieferung authentifiziert hat (auth_methodund Verwandte), sowiefrom_bound: ob dasFrom:genau ein Postfach ist, das durch einen konkreten, wildcard-freienUSERNAME_MAP-Eintrag (oder dessen Standardlogin@HOSTNAME) an das Konto gebunden ist. Die Encrypt-Stage lässt eine Einlieferung nur dann einen Schlüssel registrieren oder einen E-Mail-Schlüsselbefehl ausführen, wenn sie von einem vertrauenswürdigen Relay kam oderfrom_boundwahr ist.authDie eingehenden SPF/DKIM/DMARC-Urteile (auch im vorangestellten
Authentication-Results-Header widergespiegelt). Das Mitgliedarcbeginnt alsnoneund wird von der ARC-Stage überschrieben.local_originEin Boolean:
true, wenn die zustellende Sitzung authentifiziert war (inMYNETWORKS, über SASLAUTH, mit einem passenden TLS-Client-Zertifikat oder — auf einem UNIX-Socket mitAUTH_PEERCRED, dem dortigen Standardwert — als Peer, dessen uid auf ein erlaubtes Login auflöste, so wie jede lokal eingelieferte Nachricht ankommt). Er unterscheidet ausgehende Mail von einem vertrauenswürdigen Absender von eingehender Mail aus dem Internet, und die Einstellungsschicht pro Adresse sowie der Edit-Settings-Steuerkanal stützen sich darauf.dsnDie vom Absender angeforderten RFC-3461-Parameter:
ret/envidauf Nachrichtenebene und ein Array vonnotify/orcptpro Empfänger, parallel zur Empfängerliste. Jede Stage muss es bewahren und beachten. Nur vorhanden, wenn der Absender überhaupt etwas angefordert hat.spamWird als
falsegesät, und nur in einem Fall: bei einer Nachricht, die ausschließlich an das reservierte Postfach<Postmaster>gerichtet ist, das laut RFC 5321 §4.5.1 zustellbar bleiben muss. Die Sprach-, die Confirm-to-Send- und die Pay-to-Send-Schranke leiten jede bereits als Nicht-Spam markierte Nachricht weiter, und genau das hält dieses Postfach erreichbar. Die Beschränkung auf den Fall des alleinigen Empfängers ist Absicht: andernfalls könnte ein Spammer Mitempfänger auf die Whitelist setzen, indem er neben einem echten Opfer einRCPTan<Postmaster>hinzufügt.
23.4. Was die Stages schreiben¶
Während die Nachricht weiterschaltet, mischen die Stages ihre eigenen Schlüssel hinein:
die ARC-Stage überschreibt
auth.arcmit dem Urteil der von ihr neu bewerteten Kette;jede Stage, die eine Nachricht zu ihrem
BOUNCE_STAGEroutet (die Zustell- und Discard-Stages sowie Richtlinien-Stages wie die Milter-, Secretary- und Krypto-Stages), schreibt einbounce-Objekt — seinkind(permanent/success/delay), das menschenlesbarediagnosticundfailed_recipient, die kopiertennotify/orcpt/envidund strukturierte Detailangaben zum nächsten Hop (remote_mta/smtp_code/enhanced_status/phase/reply_text) —, das pepsi-stage-bounce in die DSN verwandelt;die Relay-Stages halten Notizdaten für Wiederholungen (
attempts,last_error,delay_sent);pepsi-stage-srs hält den ersetzten Umschlag-Absender unter
srs.originalfest, sodass eine DSN, die diese Installation erzeugt, an den echten Absender geht statt an dessen eigenen SRS-Alias;pepsi-stage-list und die nachfolgenden Listen-Stages tragen den Mailinglisten-Deskriptor unter
list;pepsi-stage-detect-language hält die erkannte
languagefest, die pepsi-stage-block-language(1) dann bewertet;pepsi-stage-check-whitelist und pepsi-stage-anti-spam arbeiten über die Kurzschluss-Flags
spam/paidund diepay_deadlinezusammen;pepsi-stage-secretary hält unter
secretarydie Challenge fest, auf die eine zurückgehaltene Nachricht wartet (das Cookie, seinedeadline, diewhitelist), markiert sie alsconfirmedoderabandonedund setzt bei der Freigabespam = false; der Timeout-Pfad entfernt den Schlüssel wieder;pepsi-stage-dot-forward hält die Kette der weiterleitenden Logins einer weitergeleiteten Zeile in
dot-forwardersfest, um~/.forward-Weiterleitungsschleifen zu durchbrechen;pepsi-stage-milter hält unter
milterfest, welche Stage den Filter ausgeführt hat, welchesverdicter zurückgab und, falls etwas schiefging, denerror;pepsi-stage-vacation hält unter
vacationfest, dass es eine Nachricht stellvertretend für den Empfänger beantwortet hat, und in welcher Sprache — von niemandem gelesen, damit eine überraschende Abwesenheitsantwort aus dem Datensatz heraus erklärbar ist;pepsi-stage-encrypt hält unter
crypto.outfest, was es pro Empfänger signiert und verschlüsselt hat, und setztencrypted, wenn die Nachricht für einen Empfänger tatsächlich verschlüsselt wurde — das Flag, das pepsi-stage-auto-whitelist in seinesignature_required-Spalte kopiert.crypto.out.key_attachedbesagt, dass der Schlüssel des Absenders als Datei mitgeschickt wurde, undcrypto.out.client_protected, dass der eigene Mail-Client des Benutzers die Nachricht bereits verschlüsselt hatte, sodass die Stage sie unverändert ließ;pepsi-stage-decrypt hält unter
crypto.indas Spiegelbild fest — ob die Nachricht verschlüsselt ankam, was aus dem Chiffrat wurde (decryption = for-client, mitfor_clientundclient_fingerprint, wenn es an den eigenen MUA-Schlüssel des Empfängers verschlüsselt war und ungeöffnet weitergereicht wurde), welche unserer Identitäten sie geöffnet hat, das Signatururteil (mitcovers_plaintext) und die geordnete Schichtenliste — und setztsignature_verifiedfür das Urteilvalidund nur für dieses, das Flag, an das pepsi-stage-check-whitelist einensignature_required-Datensatz knüpft. Zwei dieser Mitglieder existieren, weil nichts weiter unten sie wiederherstellen könnte: Der Klartext wird über die ankommenden Bytes hinweg festgeschrieben, daher sindcrypto.in.encryptedundcrypto.in.outer_gossip(ob der ankommende Header-Block bereits einAutocrypt-Gossip:-Feld trug) nur hier beantwortbar. pepsi-stage-autocrypt-learn liest beide — ihre Namen liegen inpepsi_common::crypto_state, sodass keine der beiden Krypto-Stages das Vokabular besitzt;pepsi-stage-reencrypt hält unter
crypto.storefest, was es mit einer Nachricht getan hat, die Decrypt geöffnet hat:outcomereencrypted(mitprotocol, MUA-Schlüssel-fingerprint,containerunddowngraded),plaintextoderbounced(mit einemreason). Es führt das ganzecrypto-Objekt zusammen und trägt dabeicrypto.inmit, und das Vorhandensein des Schlüssels verhindert, dass es einen Datensatz je zweimal versiegelt;der Dispatcher schreibt
dispatch_error, wenn er einen Datensatz auffailed/timeoutzwingt, weil eine Stage abgestürzt ist oder zu lange lief.
Beide Krypto-Stages leben unter demselben crypto-Schlüssel mit denselben Feldnamen, und beide sind ein flacher Merge, sodass ein Datensatz crypto.out oder crypto.in trägt (dazu crypto.store, das die Neuverschlüsselungs-Stage daneben schreibt) und nicht beides — was richtig ist, denn eine Nachricht ist auf dem ausgehenden oder auf dem eingehenden Pfad. Die vollständige Liste, mit den genauen JSON-Formen und der erzeugenden/verbrauchenden Stage für jeden Schlüssel, steht in pepsi.state(7).