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

origin

Die SMTP-Ursprungsherkunft der zustellenden Verbindung — Client-IP, HELO/EHLO, die ESMTP/SMTPUTF8/BODY=-Deklarationen, TLS-Parameter, der Listener, Reverse-DNS/iprev und die authserv_id dieses Empfängers. Die ARC-Stage benötigt die authserv_id, um ihre Authentication-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_method und Verwandte), sowie from_bound: ob das From: genau ein Postfach ist, das durch einen konkreten, wildcard-freien USERNAME_MAP-Eintrag (oder dessen Standard login@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 oder from_bound wahr ist.

auth

Die eingehenden SPF/DKIM/DMARC-Urteile (auch im vorangestellten Authentication-Results-Header widergespiegelt). Das Mitglied arc beginnt als none und wird von der ARC-Stage überschrieben.

local_origin

Ein Boolean: true, wenn die zustellende Sitzung authentifiziert war (in MYNETWORKS, über SASL AUTH, mit einem passenden TLS-Client-Zertifikat oder — auf einem UNIX-Socket mit AUTH_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.

dsn

Die vom Absender angeforderten RFC-3461-Parameter: ret/envid auf Nachrichtenebene und ein Array von notify/orcpt pro Empfänger, parallel zur Empfängerliste. Jede Stage muss es bewahren und beachten. Nur vorhanden, wenn der Absender überhaupt etwas angefordert hat.

spam

Wird als false gesä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 ein RCPT an <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.arc mit dem Urteil der von ihr neu bewerteten Kette;

  • jede Stage, die eine Nachricht zu ihrem BOUNCE_STAGE routet (die Zustell- und Discard-Stages sowie Richtlinien-Stages wie die Milter-, Secretary- und Krypto-Stages), schreibt ein bounce-Objekt — sein kind (permanent/success/delay), das menschenlesbare diagnostic und failed_recipient, die kopierten notify/orcpt/envid und 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.original fest, 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 language fest, die pepsi-stage-block-language(1) dann bewertet;

  • pepsi-stage-check-whitelist und pepsi-stage-anti-spam arbeiten über die Kurzschluss-Flags spam/paid und die pay_deadline zusammen;

  • pepsi-stage-secretary hält unter secretary die Challenge fest, auf die eine zurückgehaltene Nachricht wartet (das Cookie, seine deadline, die whitelist), markiert sie als confirmed oder abandoned und setzt bei der Freigabe spam = 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-forwarders fest, um ~/.forward-Weiterleitungsschleifen zu durchbrechen;

  • pepsi-stage-milter hält unter milter fest, welche Stage den Filter ausgeführt hat, welches verdict er zurückgab und, falls etwas schiefging, den error;

  • pepsi-stage-vacation hält unter vacation fest, 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.out fest, was es pro Empfänger signiert und verschlüsselt hat, und setzt encrypted, wenn die Nachricht für einen Empfänger tatsächlich verschlüsselt wurde — das Flag, das pepsi-stage-auto-whitelist in seine signature_required-Spalte kopiert. crypto.out.key_attached besagt, dass der Schlüssel des Absenders als Datei mitgeschickt wurde, und crypto.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.in das Spiegelbild fest — ob die Nachricht verschlüsselt ankam, was aus dem Chiffrat wurde (decryption = for-client, mit for_client und client_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 (mit covers_plaintext) und die geordnete Schichtenliste — und setzt signature_verified für das Urteil valid und nur für dieses, das Flag, an das pepsi-stage-check-whitelist einen signature_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 sind crypto.in.encrypted und crypto.in.outer_gossip (ob der ankommende Header-Block bereits ein Autocrypt-Gossip:-Feld trug) nur hier beantwortbar. pepsi-stage-autocrypt-learn liest beide — ihre Namen liegen in pepsi_common::crypto_state, sodass keine der beiden Krypto-Stages das Vokabular besitzt;

  • pepsi-stage-reencrypt hält unter crypto.store fest, was es mit einer Nachricht getan hat, die Decrypt geöffnet hat: outcome reencrypted (mit protocol, MUA-Schlüssel-fingerprint, container und downgraded), plaintext oder bounced (mit einem reason). Es führt das ganze crypto-Objekt zusammen und trägt dabei crypto.in mit, und das Vorhandensein des Schlüssels verhindert, dass es einen Datensatz je zweimal versiegelt;

  • der Dispatcher schreibt dispatch_error, wenn er einen Datensatz auf failed/timeout zwingt, 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).