85.3.1. pepsi.state

per-message state object carried through the Pepsi pipeline

Handbuchabschnitt:

7

85.3.1.1.1. Name

pepsi.state - das JSON-state-Objekt, das an jede Pepsi-Nachricht angehängt ist.

85.3.1.1.2. Beschreibung

Jede in Bearbeitung befindliche Nachricht ist ein Datensatz der pepsi.workqueue-Tabelle. 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. Das state ist der einzige Kanal, über den die Stages der Pipeline kommunizieren: Eine Stage hält ein Urteil oder ein Stück Herkunft fest, eine spätere Stage liest es.

Diese Seite dokumentiert das Layout dieses Objekts: die Schlüssel, die die Standard-Stages schreiben, wann sie geschrieben werden und welche Stages sie verbrauchen. Sie ist eine Datenformat-Referenz, unabhängig von jeder Konfiguration; der Pipeline-Graph und die Optionen, die jede Stage antreiben, werden in pepsi.conf(5) beschrieben.

85.3.1.1.3. Zusammenführungssemantik

state wird inkrementell aufgebaut. Jeder terminale Stage-Helper, der einen Datensatz verändert, führt seine neuen Schlüssel in das bestehende Objekt zusammen (die terminalen SQL-Funktionen werten state || $new aus), statt es zu ersetzen. Folglich überdauert ein früh geschriebener Schlüssel — die von Ingress gesäte origin-Herkunft, die dsn-Parameter — die gesamte Lebensdauer der Nachricht, sofern nicht eine Stage genau diesen Schlüssel absichtlich überschreibt.

Die Zusammenführung ist flach: state || {"auth": {"arc": "pass"}} würde das gesamte auth-Objekt ersetzen, nicht nur dessen arc-Mitglied. Stages, die ein Mitglied eines verschachtelten Objekts aktualisieren, lesen daher das aktuelle Objekt, ergänzen es und schreiben es als Ganzes zurück (genau das tun set_auth_verdict/set_auth_fields für auth.arc).

Die eine strukturelle Umschreibung ist dsn.rcpt, ein Array parallel zu rcpt_to: Jeder Helper, der einen Datensatz auf eine Teilmenge seiner Empfänger reduziert oder eine Gruppe auf einen Geschwisterdatensatz auffächert, schneidet dieses Array im Gleichschritt neu zu, und die Alias-Stage baut es neu auf, wenn sie eine gänzlich neue Empfängerliste schreibt. Eine Stage darf rcpt_to niemals ohne diese Helper anfassen.

Es gibt genau eine Ausnahme: pepsi-stage-bounce(1) 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.

85.3.1.1.4. Schlüssel der obersten Ebene

Die folgenden Schlüssel werden von der Standard-Pipeline geschrieben. Eine Stage, die einen Schlüssel nicht erkennt, lässt ihn einfach unberührt (die Zusammenführungsregel bewahrt ihn), sodass die Menge offen ist; eigene Stages dürfen ihre eigenen hinzufügen.

85.3.1.1.4.1. origin

(Objekt) Die SMTP-Ursprungs-Metadaten der Verbindung, die die Nachricht zugestellt hat, von pepsi-ingress(1) gesät. Es wird von pepsi-stage-arc(1) gelesen (das die authserv_id und das gerenderte ar_results-Fragment benötigt, um die ARC-Authentication-Results-Identität ohne erneutes Ausführen von SPF/DKIM/DMARC zu reproduzieren) und von den Relay-Stages, deren Herabstufungsentscheidung zu 8BITMIME/SMTPUTF8 je Hop die ursprünglichen BODY=/smtputf8-Deklarationen heranzieht:

{
  "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)
  }
}

Die vier auth_*-Mitglieder beschreiben, wie sich die Sitzung ausgewiesen hat, im Unterschied zum benachbarten auth-Schlüssel unten, bei dem es darum geht, was die Nachricht behauptet. Sie werden festgehalten, weil local_origin ein einzelner Boolescher ist und zwei sehr verschiedene Aussagen zusammenfallen lässt: „ein von uns authentifiziertes Konto hat dies eingeliefert“ und „dies kam von einer Adresse, der wir zu vertrauen beschlossen haben“. Eine Richtlinien-Stage — oder ein Milter, dem sie als die RFC-4954-Makros {auth_type} / {auth_authen} / {auth_author} / {auth_ssf} übergeben werden — kann diese Unterscheidung aus dem Booleschen nicht wiedergewinnen.

auth_method

Wie sich die Sitzung authentifiziert hat: sasl (ein AUTH nach RFC 4954 war erfolgreich), peercred (ein UNIX-Socket-Peer, dessen uid auf ein erlaubtes Login auflöste), client-cert (ein TLS-Client-Zertifikat passte auf eine Pinnung) oder mynetworks (die Peer-Adresse liegt in MYNETWORKS). null, wenn sich die Sitzung nicht authentifiziert hat. Ein späteres SASL-AUTH überschreibt eine frühere Methode aus der Verbindungszeit: Es ist die stärkere Behauptung und die einzige, die einen Benutzernamen trägt.

auth_mechanism

Der erfolgreiche SASL-Mechanismus (PLAIN, LOGIN), großgeschrieben. Nur bei auth_method = sasl gesetzt. Die Nicht-SASL-Methoden lassen ihn null, statt ein Nicht-Mechanismus-Token in ein Feld zu setzen, das ein Filter womöglich gegen eine Liste echter Mechanismen vergleicht.

auth_identity

Die Authentifizierungsidentität: die SASL-authcid oder das Login, auf das die uid eines UNIX-Peers auflöste. null bei mynetworks und client-cert, die eine Adresse oder einen Schlüssel authentifizieren und niemanden benennen. peercred liefert daher eine Identität ohne Mechanismus — die ehrliche Beschreibung einer Sitzung, die der Kernel und nicht SASL authentifiziert hat.

auth_authzid

Die Autorisierungsidentität, nur festgehalten, wenn der Client darum bat, als jemand anderes zu handeln als der, als der er sich authentifiziert hat (das authzid-Feld von SASL PLAIN). null, wenn beide gleich sind, was der Normalfall ist: RFC 4954 erlaubt eine leere authzid, und Clients wiederholen dort routinemäßig die authcid.

from_bound

true, wenn das From:-Feld genau ein Postfach benennt und dieses Postfach auf einen konkreten (platzhalterfreien) USERNAME_MAP-Eintrag des authentifizierten Kontos oder auf dessen Standard login@HOSTNAME passte; andernfalls false, auch für eine Sitzung ohne Kontofreigabe. Die Erlaubnis, als eine Adresse zu senden, genügt zum Senden; an sie gebunden zu sein ist das, was einer Einlieferung erlaubt, für sie zu sprechen. pepsi-stage-encrypt(1) registriert einen Schlüssel aus der Nachricht oder führt einen E-Mail-Schlüsselbefehl nur aus, wenn auth_method mynetworks oder client-cert ist oder sasl/peercred mit from_bound = true: Ein Konto, dem *@example.org gewährt ist, darf als seine Kollegen senden, darf aber keinen Schlüssel für sie unterschieben können.

85.3.1.1.4.2. auth

(Objekt) Die eingehenden Authentifizierungsurteile, die Ingress berechnet hat, auch im vorangestellten Authentication-Results-Header widergespiegelt. Das arc-Urteil wird als none gesät und von pepsi-stage-arc(1) überschrieben, sobald es die eingehende ARC-Kette neu bewertet (seine spf/dkim/dmarc-Geschwister bleiben unberührt). Gelesen von pepsi-stage-check-whitelist(1) (dessen dkim_required-Schranke einen Datensatz auf auth.dkim oder auth.arc akzeptiert):

{
  "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

(boolescher Wert) Von Ingress gesät: true, wenn die zustellende Sitzung authentifiziert war (der Client war in MYNETWORKS, hat ein SASL-AUTH abgeschlossen, ein passendes TLS-Client-Zertifikat vorgelegt oder war — auf einem UNIX-Socket mit AUTH_PEERCRED, dem dortigen Standardwert — ein Peer, dessen uid auf ein erlaubtes Login auflöste, der Weg, den jede lokal eingelieferte Nachricht nimmt; siehe die [pepsi-ingress-listener-*]-Abschnitte in pepsi.conf(5)), sonst false. Es markiert Mail, die von einem vertrauenswürdigen Absender stammt, und bleibt durch die Pipeline erhalten, damit nachgelagerte Stages es testen können (die Einstellungsschicht je Adresse stützt sich für local_origin-Nachrichten auf den Absender, und pepsi-stage-edit-settings(1) behandelt eine Nachricht nur dann als Steuernachricht, wenn sie local_origin ist).

85.3.1.1.4.4. dsn

(Objekt) Die vom Absender angeforderten RFC-3461-Zustellungsstatusbenachrichtigungs-Parameter, von Ingress gesät: das ret/envid auf Nachrichtenebene und ein zu rcpt_to paralleles Array von notify/orcpt pro Empfänger. Jede Stage muss es bewahren und beachten — die Relay-Stages propagieren die Parameter nur dann an den nächsten Hop, wenn er DSN ankündigt, und pepsi-stage-bounce(1) zieht das empfängerweise notify heran, um zu entscheiden, ob überhaupt ein Bericht ausgegeben wird:

{
  "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

(Objekt) Von jeder Stage geschrieben, die eine Nachricht zu ihrem BOUNCE_STAGE routet — den Zustell-Stages (pepsi-stage-relay-to-internet(1), pepsi-stage-relay-to-smarthost(1), pepsi-stage-relay-to-maildir(1), pepsi-stage-relay-to-lmtp(1)) und pepsi-stage-discard(1), aber ebenso 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) und pepsi-stage-dot-forward(1). Sie alle bauen es mit denselben gemeinsamen Helpern, sodass die Form unten dieselbe ist, welche Stage es auch geschrieben hat. Es wird von pepsi-stage-bounce(1) verbraucht, um den Bericht zu bauen. Sein kind ist permanent (ein Failure-Bounce, Action: failed), success (ein positiver Bericht, Action: delivered) oder delay (Action: delayed); diagnostic und failed_recipient sind der menschenlesbare Grund und der betroffene Empfänger, und die optionalen notify/orcpt/envid werden aus dem state.dsn-Eintrag des Empfängers kopiert, sodass die Bounce-Stage NOTIFY beachten und das ursprüngliche ENVID/ORCPT übernehmen kann. Die Relay-Stages halten außerdem strukturierte Detailangaben zum nächsten Hop fest (remote_mta, smtp_code, enhanced_status, phase, reply_text), sodass eine Bounce-Vorlage genau angeben kann, warum der nächste MTA die Nachricht abgelehnt hat, und enhanced_status speist das DSN-Status:-Feld. Das ret auf Nachrichtenebene reist aus demselben Grund wie envid hier mit: Die Bounce-Stage löscht state, während sie die Nachricht umschreibt, sodass alles, was sie danach noch braucht, in diesem Objekt liegen muss:

{ "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

Zwischendaten, die die Zustell-Stages festhalten, wenn sie eine Nachricht mit pause für eine Wiederholung pausieren: attempts (Zahl) ist die Anzahl der bisherigen Zustellversuche, was den Wiederholungs-Backoff steuert, und delay_sent (boolescher Wert, nur vorhanden, wenn wahr) markiert, dass die einmalige NOTIFY=DELAY-DSN bereits ausgegeben wurde, sodass sie nie zweimal gesendet wird.

last_error (Zeichenkette) ist der jüngste Fehlertext. Er ist nicht auf den Wiederholungspfad beschränkt: Die Zustell-, Milter-, Krypto-, DKIM-Signier- und ~/.forward-Stages schreiben ihn ebenfalls, wenn sie eine Nachricht terminal fehlschlagen lassen, sodass er bei einem failed-Datensatz das Erste ist, was man liest (pepsi-queue(1) zeigt ihn als Teil des state des Datensatzes, und pepsi-status(1) führt ihn unter jeder hängengebliebenen Nachricht auf).

85.3.1.1.4.7. temporary_failures / temporary_stage

(Zahl / Zeichenkette) Wird zusammen mit last_error vom Stage-Gerüst geschrieben, wenn der Fehler einer Stage nicht als permanent markiert war – die Schuld des Hosts und nicht der Nachricht (siehe pepsi-dispatch(1)): wie oft die Nachricht bisher deswegen bei temporary_stage pausiert wurde, was den Backoff von einer Minute, sich verdoppelnd, bis zu einer Stunde steuert. Ein bei einer anderen Stage festgehaltener Zähler wird nicht fortgesetzt. Entfernt, wenn der Worker aufgibt.

85.3.1.1.4.8. failed_stage / failure_class / failed_at

Die Aufzeichnung eines Fehlschlags, der auf diesem Host geschah und nicht beim nächsten Hop. failed_stage (Zeichenkette) ist die Stage, bei der er geschah. failure_class (Zeichenkette) sagt, wie er endete: permanent (die Stage markierte den Fehler als Mangel der Nachricht), retries-exhausted (bis MAX_LIFETIME wiederholt), crashed oder timed-out (die Nachricht hat ihren Worker dreimal abstürzen oder hängen lassen). failed_at (Zeichenkette, RFC 3339) wird von der Datenbank gestempelt, sobald der Datensatz failed oder timeout wird; MIN_AGE von pepsi-failure-bouncer(1) zählt ab diesem Zeitpunkt. pepsi-stage-bounce(1) baut aus failed_stage und failure_class eine DSN mit einem generischen Text, wenn kein Relay den Fehlschlag gemeldet hat (bounce oben); last_error wird nie in eine DSN kopiert.

85.3.1.1.4.9. arc_temperrors

(Zahl) Von pepsi-stage-arc(1) jedes Mal geschrieben, wenn die Validierung einer eingehenden ARC-Kette auf einen vorübergehenden DNS-Fehler traf: Die Nachricht wird pausiert und wiederholt, und nach dem dritten solchen Versuch wird die Kette mit cv=fail versiegelt, wie es bei einer fehlgeschlagenen geschähe.

85.3.1.1.4.10. strikes

(Zahl) Von pepsi-dispatch(1) jedes Mal geschrieben, wenn die Nachricht ihren Worker zum Absturz brachte oder ihn über MAX_RUNTIME hinaus festhielt; der dritte Strike lässt die Nachricht fehlschlagen.

85.3.1.1.4.11. retry_since

(Zeichenkette, RFC 3339) Von pepsi-stage-bounce(1) auf der DSN geschrieben, die es baut und die die Nachricht in ihrem Datensatz ersetzt: der Zeitpunkt, ab dem das MAX_LIFETIME der Stages statt ab der Ankunftszeit des Datensatzes gezählt wird, sodass eine DSN nicht schon abgelaufen zur Welt kommt.

85.3.1.1.4.12. srs

(Objekt) Von pepsi-stage-srs(1) geschrieben, wenn es den Umschlagabsender umschreibt, und hält die ersetzte Adresse fest:

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

Es existiert für eine DSN, die diese Installation selbst erzeugt. SRS läuft notwendigerweise vor der Zustell-Stage — den Umschlag für die Übergabe umzuschreiben ist sein ganzer Zweck —, sodass zu dem Zeitpunkt, zu dem eine Zustell-Stage complete_success aufruft oder ein Relay einen Fehlschlag zu seiner BOUNCE_STAGE routet, das mail_from des Datensatzes einer unserer eigenen SRS-Aliase ist. pepsi-stage-bounce(1) würde die DSN dann an diesen Alias adressieren, sie hinaus zum nächsten Hop und über unseren eigenen MX zum Dekodieren wieder herein schicken — wobei sie als Null-Absender-Nachricht die gesamte eingehende Pipeline überstehen müsste — nur um bei einer Adresse anzukommen, die die ganze Zeit im Datensatz stand. Wo diese Rundreise nicht funktioniert, geht die DSN verloren, ohne dass irgendetwas es meldet.

Die Bounce-Stage zieht diesen Wert daher mail_from vor. Er wird festgehalten, statt durch Umkehrung des Alias wiedergewonnen zu werden, weil Srs::reverse das SRS-HMAC-Geheimnis benötigt und eine Stage, deren Aufgabe das Verfassen eines Bounce ist, keinen Grund hat, Schlüsselmaterial zu halten: Der Absender ist gewöhnliche Nachrichtenmetadaten, und dafür ist state da.

Wird nur geschrieben, wenn abwesend, sodass eine zweimal umgeschriebene Nachricht die Adresse behält, die einen echten Korrespondenten benennt, und nicht den Zwischenalias. Ein Bounce (Null-Absender) wird nie umgeschrieben und hält daher nie einen fest; ein leerer Wert liest sich als abwesend, und die Bounce-Stage fällt auf den Umschlagabsender zurück.

85.3.1.1.4.13. pay_deadline

(Zahl) Von pepsi-stage-anti-spam(1) geschrieben: Unix-Epoch-Sekunden, die Frist, bis zu der die Pay-to-Send-Bestellung beglichen sein muss. Sie wird in state gehalten (statt in der timeout-Spalte des Datensatzes), weil der Dispatcher timeout nullt, wann immer er eine pausierte Nachricht erneut einreiht. Ein als JSON-Zeichenkette geschriebener Wert wird als „keine Frist“ gelesen; wer dies von Hand setzt, muss also eine Zahl schreiben. Die Stage schließt ihre Zahlungsschranke kurz, wenn state.paid bereits gesetzt ist.

85.3.1.1.4.14. spam / paid

(boolescher Wert) Kurzschluss-Flags gegen Missbrauch.

spam wird von pepsi-stage-check-whitelist(1) geschrieben, das es bei einem Treffer in der weißen Liste auf false setzt (und es andernfalls ungesetzt lässt), von pepsi-ingress(1), das es für eine Nachricht auf false setzt, deren einziger Empfänger <Postmaster> ist (Erreichbarkeit nach RFC 5321 §4.5.1), und von pepsi-stage-secretary(1), das es bei einer Nachricht auf false setzt, die es freigibt, weil ihr Absender bestätigt hat. Ein Wert false lässt pepsi-stage-block-language(1), pepsi-stage-secretary(1) und pepsi-stage-anti-spam(1) die Nachricht weiterleiten, ohne ihre Schranke anzuwenden. Ein Wert true lässt pepsi-stage-anti-spam(1) die Nachricht verwerfen, statt sie an einer Schranke zu halten, schickt sie auf den Timeout-Pfad von pepsi-stage-secretary(1), unterdrückt das Lernen von Schlüsseln in pepsi-stage-autocrypt-learn(1), sofern nicht das LEARN_FROM_SPAM jener Stage an ist, und blockiert die RFC-3834-Auto-Responder, die die gemeinsamen Unterdrückungsregeln verwenden (pepsi-stage-vacation(1), die Challenge des Sekretärs und die automatischen Antworten der Mailinglisten). Nur ein expliziter boolescher Wert zählt: Ein fehlender Schlüssel löst keines der beiden Verhalten aus.

Beachten Sie: Keine mitgelieferte Stage schreibt jemals spam = true: Die obigen Schreiber schreiben stets nur false. Der Wert true ist der Haken, den eine milter-getriebene Pipeline, ein Operator oder eine standortspezifische Stage verwendet — dieselbe Anordnung wie bei paid weiter unten.

paid wird von pepsi-stage-anti-spam(1) selbst geschrieben, und immer nur als false: Die Stage speichert bei jeder Pause paid = false neben pay_deadline, und das ist es, was die Nachricht als auf Zahlung wartend markiert. Meldet der Händler die Bestellung als beglichen, schaltet die Stage die Nachricht einfach weiter, sodass keine Stage im Baum je paid = true schreibt — dieser Wert ist ein Überschreibungshaken für einen Operator oder eine standortspezifische Stage, und die Zahlungsschranke wird übersprungen, wenn er gesetzt ist (so wie sie es wird, wenn spam false ist).

85.3.1.1.4.15. dot-forwarders

(Array von Zeichenketten) Die kleingeschriebenen Login-Namen, deren ~/.forward-Dateien diesen Datensatz weitergeleitet haben, d. h. die Weiterleitungskette, die zu ihm geführt hat, von pepsi-stage-dot-forward(1) als Weiterleitungsschleifen-Schutz gepflegt. Ein weitergeleiteter Datensatz erhält die Kette des Datensatzes, aus dem er stammt, plus das eine Login, das ihn weitergeleitet hat, sodass die Liste einen Weiterleitungspfad beschreibt, nicht jedes ~/.forward, das die Nachricht durchlaufen hat. Weil eine weitergeleitete Nachricht die Pipeline neu startet, wird ein Empfänger, dessen Benutzer hier bereits aufgeführt ist, gebounct, statt erneut durch sein ~/.forward geführt zu werden, sodass ein Zyklus (~bob → alice, ~alice → bob) terminiert.

85.3.1.1.4.16. language

(Zeichenkette) Von pepsi-stage-detect-language(1) als Accept-Language-artige Zeichenkette geschrieben, die die erkannte(n) Sprache(n) des Nachrichtentexts beschreibt, mit q-Werten in Präferenzreihenfolge.

pepsi-stage-block-language(1) liest sie, um die Nachricht gegen seine Allow-/Deny-Listen zu bewerten, doch sie ist ebenso der Lokalisierungs-Schlüssel: Jede Stage, die Prosa für einen Menschen verfasst, wählt daraus die Sprache ihrer Vorlage oder Nachricht und fällt auf ihr eigenes DEFAULT_LANGUAGE zurück, wenn sie fehlt oder nichts Unterstütztes benennt — pepsi-stage-vacation(1) (die Abwesenheitsnotiz), pepsi-stage-anti-spam(1) (die Zahlungsaufforderung), pepsi-stage-secretary(1) (die Challenge), pepsi-stage-encrypt(1), pepsi-stage-auto-pay(1) und pepsi-stage-secure-link(1) (ihre Antwortnachrichten) und pepsi-stage-edit-settings(1) (seine Bestätigungs- oder Fehlerantwort). Deshalb kann eine Installation, die gar keine Sprach-Blockierung betreibt, die Erkennungs-Stage dennoch in der Pipeline wollen.

85.3.1.1.4.17. vacation

(Objekt) Von pepsi-stage-vacation(1) geschrieben, wenn es eine Nachricht stellvertretend für den Empfänger beantwortet hat, und hält fest, was es getan hat:

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

Nichts liest es: Es existiert, damit die Entscheidung in pepsi-queue(1) sichtbar ist, damit pepsi-stage-if(1) darauf verzweigen kann und damit eine Supportfrage zu einer überraschenden Abwesenheitsantwort aus dem Datensatz heraus statt aus dem Journal beantwortbar ist. Abwesend bei jeder Nachricht, die nicht beantwortet wurde.

85.3.1.1.4.18. secretary

(Objekt) Von pepsi-stage-secretary(1) geschrieben. Bei einer Nachricht, die es zurückhält:

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

Eine Bestätigung fügt "confirmed": true hinzu, während sie den Datensatz freigibt (die Stage leitet ihn dann mit spam = false weiter); eine gebouncte Challenge fügt "abandoned": true hinzu und schickt ihn vorzeitig auf den Timeout-Pfad. Eine Nachricht, für die die Stage keine Challenge stellen wollte und die sie zu ihrem UNCHALLENGEABLE_STAGE geroutet hat, trägt stattdessen { "secretary": { "unchallengeable": "<reason>" } }. Der Timeout-Pfad entfernt den Schlüssel vollständig: Nichts von einer Challenge überdauert ihn. Wie pay_deadline wird die Frist in state gehalten, weil der Dispatcher das timeout des Datensatzes löscht, wenn er eine pausierte Nachricht wieder einreiht.

85.3.1.1.4.19. keydisc

(Objekt) Worauf eine geparkte Nachricht wartet: die Korrespondentenadresse, deren Schlüssel im Cache nicht gefunden werden konnte, und wann das Warten begann:

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

Es wird vom Park-Terminal der Schlüsselentdeckung geschrieben, das in einem Round-Trip alles committet, was die Stage bereits umgeschrieben hatte, diesen Schlüssel zusammenführt, den Datensatz mit einem Wiederholungs-Timeout auf paused setzt und eine Anfrage in pepsi.key_request einreiht. Ein pepsi-keydisc(1)-Dienst, der diese Anfrage erledigt, findet die Wartenden über keydisc.address — durch einen partiellen Index genau auf diesem Ausdruck — und schaltet sie zurück auf pending, sodass eine Stage den Schlüssel weder umbenennen noch von Hand schreiben darf. Das Wiederholungs-Timeout ist das Sicherheitsnetz für eine Installation, die überhaupt keinen Entdeckungsdienst betreibt: Es ist stets später gesetzt als die Entdeckungsfrist, sodass die Freigabe normalerweise zuerst geschieht.

Die Freigabe bewegt nur den status des Datensatzes und löscht sein timeout, sodass der Schlüssel sie überlebt: Eine fortgesetzte Nachricht trägt weiterhin die Aufzeichnung des soeben beendeten Wartens, und eine Stage, die erneut parkt, überschreibt sie schlicht.

85.3.1.1.4.21. tlsrpt

(boolescher Wert) Ein Schleifenschutz-Flag, das auf einer Nachricht gesetzt wird, die selbst ein SMTP-TLS-Reporting-Bericht ist (pepsi-tlsrpt(1)). Die Relay-Stages überspringen das Festhalten eines pepsi.tls_session-Datensatzes für eine solche Nachricht, sodass ein Bericht über eine fehlgeschlagene Berichtszustellung nicht rekursieren kann.

85.3.1.1.4.22. auth.arc

Von pepsi-stage-arc(1) mit dem Urteil der von ihm neu bewerteten eingehenden ARC-Kette überschrieben, das das none ersetzt, das Ingress unter auth gesät hat (siehe den auth-Schlüssel oben).

85.3.1.1.4.23. auth.arc_sealers

(Array von Zeichenketten) Von pepsi-stage-arc(1) neben auth.arc geschrieben: das d= jedes ARC-Seal, das die Nachricht bei ihrer Ankunft trug, kleingeschrieben, um einen abschließenden Wurzelpunkt gekürzt, dedupliziert und nach Instanz geordnet, sodass die dem Urheber nächste ADMD zuerst kommt. Unser eigenes ARC_DOMAIN ist nie enthalten — die Liste wird gelesen, bevor das eigene Set dieser Stage gerendert wird:

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

Es existiert, weil auth.arc allein keine Richtlinienentscheidung tragen kann: RFC 8617 §8.4 stellt fest, dass eine gültige Kette keine Vertrauenswürdigkeit vermittelt, sondern nur, dass die benannten ADMDs die Nachricht bearbeitet haben, und überlässt den Rest dem Empfänger. Einen akzeptablen Vermittler zu benennen ist diese lokale Richtlinie, und dies ist das, wogegen eine solche Regel abgeglichen wird — die sealer_domain-Datensätze von pepsi-stage-check-whitelist(1).

Das Array wird geschrieben, ob die Kette validiert hat oder nicht, denn es ist die Aufzeichnung dessen, was die Nachricht behauptet hat; ein Siegel parst, ohne zu verifizieren. Jeder Leser muss daher auth.arc = pass verlangen, bevor er einem Eintrag traut, was check-whitelist tut, indem es andernfalls die ganze Liste verwirft.

85.3.1.1.4.24. crypto

(Objekt) Was die Ende-zu-Ende-Krypto-Stages getan haben. Die ausgehende Hälfte, crypto.out, wird von pepsi-stage-encrypt(1) geschrieben:

{ "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 ist der From:-Autor, nie der Umschlagabsender: Die Signatur handelt von dem Autor, den der Empfänger sieht. Das outcome jedes Empfängers ist eines von encrypted, signed-only, cleartext, secure-link, bounced oder oversize, und route benennt den ON_NO_KEY/ON_OVERSIZE-Pfad, wenn es nicht der gewöhnliche war. reason trägt eine kurze Wendung für das Journal des Operators und für den Bounce.

content_algorithm und downgraded werden festgehalten, damit ein Operator „mit wem reden wir noch in SEIPDv1?“ beantworten kann, ohne Journale zu lesen. downgraded erscheint nur neben einem content_algorithm: ein false bei einem Empfänger, an den überhaupt nicht verschlüsselt wurde, läse sich als „wir haben verschlüsselt, ohne herabzustufen“, was eine andere Behauptung ist.

key_source und trust lauten beide own, wenn der Empfänger eine Adresse ist, für die diese Installation eine eigene Identität hält. Das ist kein Rang auf der Entdeckungsleiter: Unser eigener Schlüssel verdrängt den pepsi.peer_key-Cache für eine solche Adresse vollständig, sodass keine MIN_TRUST-Untergrenze gilt und kein entdeckter Schlüssel herangezogen wurde. Siehe pepsi-stage-encrypt(1).

Da die Stage auseinandergehende Empfänger auf je einen Datensatz aufteilt, enthält das recipients-Objekt eines Datensatzes normalerweise genau die Empfänger im rcpt_to dieses Datensatzes.

key_attached (boolesch, nur vorhanden, wenn wahr) hält fest, dass der öffentliche Schlüssel des Absenders als application/pgp-keys-Datei mitging (ATTACH_KEYS_AS_FILES). client_protected (boolesch, nur vorhanden, wenn wahr) hält fest, dass der eigene Mail-Client des Benutzers die Einlieferung bereits verschlüsselt hatte, sodass die Stage sie unangetastet ließ: keine Verschlüsselung, keine Signatur, kein Autocrypt:-Header und keine Schlüsseldatei von Pepsi.

Die eingehende Hälfte, crypto.in, wird von pepsi-stage-decrypt(1) geschrieben:

{ "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 ist not-encrypted, decrypted, failed oder for-client; bei failed trägt ein benachbarter failure-Schlüssel den maschinenlesbaren Grund (no-decryption-key, decryption-failed, too-large, …). route benennt den ON_DECRYPT_FAILURE/ON_BAD_SIGNATURE-Pfad, wenn es nicht der gewöhnliche war.

for-client bedeutet, dass die Nachricht an den eigenen MUA-Schlüssel des Empfängers verschlüsselt ist (eine Identität mit custody = client, deren privater Teil nur in seinem Mail-Client liegt) und ungeöffnet weitergereicht wurde, auch wenn einer unserer eigenen Schlüssel ebenfalls Empfänger war. Es ist kein Fehlschlag: ON_DECRYPT_FAILURE wird nicht herangezogen. Das benachbarte for_client ist dann true, und client_fingerprint benennt den Schlüssel. Es wurde nichts verifiziert, daher wird signature_verified nicht gesetzt.

signature.covers_plaintext ist vorhanden, wann immer es ein Signatururteil gibt: true, wenn das Urteil von einer Signatur über den Klartext gewährt wurde, false, wenn es von einer um Chiffretext gelegten Schicht stammt. pepsi-stage-autocrypt-learn(1) hält fest, dass ein Korrespondent einen unserer Schlüssel besitzt (pepsi.peer_has_own_key), nur bei einer valid- oder valid-untrusted-Signatur mit covers_plaintext = true, deren Unterzeichner die From:-Adresse ist, auf einer Nachricht, die wir entschlüsselt haben.

outer_gossip (boolescher Wert, nur vorhanden, wenn wahr) hält fest, dass die Nachricht so, wie sie ankam, bereits ein Autocrypt-Gossip:-Feld auf ihrem äußeren Header-Block trug — etwas, das jeder Hop auf dem Weg geschrieben haben könnte und das der Wiederzusammenbau von Inline-PGP durch die Entschlüsselung tragen kann. Es wird hier geschrieben, weil der Klartext über die eintreffenden Bytes committet wird, sodass nachgelagert niemand die Frage später beantworten könnte. Zusammen mit encrypted ist es das, was pepsi-stage-autocrypt-learn(1) heranzieht, bevor es einen gegossipten Schlüssel lernt: Ein nicht authentifiziertes äußeres Feld legt sein Veto gegen alle ein. Wenn LEARN_GOSSIP an ist und aus einer verschlüsselten Nachricht nichts gelernt wird, ist dies der Schlüssel, den man ansehen sollte.

signature.status ist eines von:

none

Keine Signatur.

valid

Kryptographisch einwandfrei und der Schlüssel war verankert — eine X.509-Kette zu einem ca_trust-Anker oder ein OpenPGP-Schlüssel aus einer eingestuften Entdeckungsquelle.

valid-untrusted

Einwandfrei, aber der Schlüssel ließ sich an nichts binden: ein erstmals gesehener Schlüssel, einer aus der Nachricht selbst gelernter oder ein Zertifikat einer CA, für die diese Installation keinen Anker hält. Nie als ``valid`` gemeldet; diese Unterscheidung zu treffen ist der Zweck, für den die Stage existiert.

invalid

Die Signatur verifiziert nicht, oder das Zertifikat des Signierenden ist abgelaufen, widerrufen oder an eine andere Adresse gebunden. signature.failure sagt, welches davon.

unverifiable

Für den behaupteten Signierenden war kein Schlüssel verfügbar.

signature.key_source und signature.trust tragen hier das eigene Herkunftsvokabular der Kryptoschicht (anchor, dane, wkd, autocrypt, pinned, attached, unknown), das gröber ist als die Leiternamen der ausgehenden Hälfte. Eine Signatur, die gegen unsere eigene Identität für den behaupteten Absender geprüft wurde, meldet pinned — den stärksten Wert, den dieses Vokabular hat, und den richtigen, denn der Schlüssel ist für diese Adresse verzeichnet. Es ist dieselbe Verdrängung, die die ausgehende Hälfte own nennt: Für eine Adresse, für die diese Installation einen Schlüssel hält, wird dem Verifizierer überhaupt kein peer_key-Datensatz angeboten. Siehe pepsi-stage-decrypt(1).

Eine Nachricht mit mehreren Signaturschichten nimmt das schlechteste ihrer Urteile — unter den Schichten, die den Klartext abdecken. Eine als covers_ciphertext markierte Signaturschicht (siehe unten) wird nur herangezogen, wenn es keine andere gibt, und kann selbst dann nie valid ergeben.

layers ist von außen nach innen geordnet, und die Reihenfolge ist wichtig: Aus ihr werden die Betreffmarkierungen gerendert, sodass encrypted(signed(body)) und signed(encrypted(body)) unterscheidbar sind. Sie überlebt daher eine Pause, weshalb die Stage sie festhält, statt sie neu zu berechnen. container und downgraded erscheinen nur an Chiffratschichten, sodass ein Operator sehen kann, wie viel eingehende Mail noch vor-AEAD ist.

"covers_ciphertext": true erscheint an einer Signaturschicht, in die eine Chiffratschicht eingeschachtelt ist — die äußere Signatur von signed(encrypted(body)). Es ist nur vorhanden, wenn es zutrifft. Eine solche Signatur wurde über das Chiffrat berechnet, sagt also, dass der Signierende den Blob übertragen hat, und nichts darüber, was daraus hervorging; jeder, der eine verschlüsselte Nachricht erlangt, kann sie in seine eigene Signatur wickeln und weiterleiten. Die Schicht behält ihr eigenes ehrliches Urteil, kann aber signature.status nicht auf valid heben und daher signature_verified nicht setzen. In signed(encrypted(signed(body))) ist nur die äußere Schicht markiert, und die innere — diejenige, die für den Inhalt spricht — entscheidet das Urteil.

Die Speicherhälfte, crypto.store, wird von pepsi-stage-reencrypt(1) auf einem Datensatz geschrieben, dessen crypto.in besagt, dass die Nachricht entschlüsselt wurde:

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

outcome ist reencrypted (versiegelt für den eigenen MUA-Schlüssel des Empfängers, benannt durch fingerprint), plaintext (so abgelegt, wie sie ist: kein brauchbarer MUA-Schlüssel unter ON_NO_CLIENT_KEY = plaintext oder kein lokaler Empfänger — reason sagt, welches von beiden) oder bounced (abgelehnt unter ON_NO_CLIENT_KEY = bounce; reason). Die Stage führt das ganze crypto-Objekt zusammen, sodass crypto.in erhalten bleibt, und das Vorhandensein von store verhindert, dass sie einen Datensatz zweimal versiegelt.

85.3.1.1.4.25. encrypted

(boolescher Wert) Von pepsi-stage-encrypt(1) auf true gesetzt, wenn die Nachricht auf diesem Datensatz tatsächlich an einen Empfängerschlüssel verschlüsselt wurde, und von pepsi-stage-decrypt(1), wenn die Nachricht verschlüsselt ankam. pepsi-stage-auto-whitelist(1) kopiert ihn in die signature_required-Spalte des neuen Datensatzes in der weißen Liste, sodass ein Korrespondent, der unter Verschlüsselung erreicht wurde, später am selben Maßstab gemessen wird. Abwesend (statt false), wenn nichts verschlüsselt wurde.

85.3.1.1.4.26. signature_verified

(boolescher Wert) Von pepsi-stage-decrypt(1) auf true gesetzt für das Signatururteil valid, und nur für dieses. Die signature_required-Schranke von pepsi-stage-check-whitelist(1) prüft ihn, sodass valid-untrusted ihn setzen zu lassen eine Schranke öffnen würde, die der Operator für einen vertrauenswürdigen Schlüssel gedacht hatte — und aus demselben Grund setzt ihn auch eine Signatur nie, die nur Chiffrat abdeckt (covers_ciphertext, oben). Andernfalls abwesend (statt false).

85.3.1.1.4.27. milter

(Objekt) Was pepsi-stage-milter(1) getan hat — das Urteil des Filters, die ausgehandelte Protokollversion, wie lange das Gespräch dauerte, und jede angewandte Änderung, beschriftet:

{ "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 ist continue, accept, reject, tempfail, discard oder — wenn der Filter überhaupt nicht ansprechbar war — failed, in welchem Fall ein benachbartes error die Diagnose trägt und das Routing dem ON_FAILURE der Stage folgte. reply_code, quarantine_reason und rejected_recipients sind nur vorhanden, wenn sie zutreffen.

Nichts liest es. Es existiert, damit pepsi-queue(1) zeigen kann, warum eine Nachricht markiert wurde, damit pepsi-stage-if(1) darauf verzweigen kann und damit eine Supportfrage zu einem überraschenden Header aus dem Datensatz heraus statt aus dem Journal beantwortbar ist — dieselbe Begründung wie bei vacation oben. Eine abgelehnte Nachricht trägt zusätzlich das übliche bounce-Objekt, mit dem eigenen SMTP-Code des Filters, dem erweiterten Status nach RFC 3463 und dem Text, aufgeteilt auf eigene Felder.

85.3.1.1.4.28. crypto_policy_refused

(boolescher Wert, nur vorhanden, wenn wahr) Von pepsi-stage-encrypt(1), pepsi-stage-decrypt(1) und pepsi-stage-secure-link(1) neben last_error geschrieben, wenn sie eine Nachricht fehlschlagen lassen, weil die konfigurierte Richtlinie sie abgelehnt hat — ENCRYPT = required ohne brauchbaren Empfängerschlüssel und ohne ON_NO_KEY-Route, eine verschlüsselt angekommene Nachricht ohne ON_DECRYPT_FAILURE-Route und so fort. Es unterscheidet eine bewusste Ablehnung von einem Transport- oder Programmierfehler, die auf einem failed-Datensatz sonst gleich aussehen: Die erste erneut zu versuchen wird nie helfen.

85.3.1.1.4.29. milter_error

(Zeichenkette) Von pepsi-stage-milter(1) geschrieben, wenn ein Filter eine Nachricht abgelehnt hat, aber weder REJECT_STAGE noch BOUNCE_STAGE konfiguriert war, sodass die Stage sie nirgendwohin routen konnte. Zu unterscheiden von milter.error im milter-Objekt weiter oben, das die Diagnose für einen Filter ist, mit dem überhaupt nicht gesprochen werden konnte.

85.3.1.1.4.30. list

(Objekt) Der Mailinglisten-Deskriptor. pepsi-stage-list(1) schreibt ihn auf jeden Datensatz, den es zu einer Listenrolle routet (die id der Liste, die role, die address, an der die Nachricht ankam, und eine token- oder verp-Adresse, wenn die Unteradresse eine trug); pepsi-stage-list-post(1) schreibt einen Deskriptor je Mitglied (id, member, lang, serial, duplicate) auf jeden Zustelldatensatz, den es auffächert, dazu pending_rcpt bei einem teilweise aufgefächerten Beitrag und rejected bei einem abgelehnten; pepsi-list(1) setzt moderator_approved (und digest bei einer Digest-Ausgabe). Gelesen von pepsi-stage-list-post(1), pepsi-stage-list-command(1), pepsi-stage-list-deliver(1) und pepsi-stage-list-bounce(1); siehe deren Seiten für die Felder, die jede davon verwendet.

85.3.1.1.4.31. dispatch_error

(Zeichenkette) Von pepsi-dispatch(1) geschrieben, wenn er einen Datensatz in einen terminalen Zustand zwingt, den er nicht selbst erreicht hat — die Diagnose für ein Stage-Programm, das abgestürzt ist (failed), eines, das MAX_RUNTIME überschritten hat (timeout), oder einen Datensatz, der bei einer Stage sitzt, die die Konfiguration nicht definiert. pepsi-status(1) führt sie unter jeder hängengebliebenen Nachricht auf.

85.3.1.1.5. Siehe auch

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)