18. Archive

Dieses Archiv ist eine Neuimplementierung von HyperKitty, dem Archivierer von GNU Mailman 3, dessen Datenmodell, Threading-Regeln, Message-ID-Hash und URL-Schema es folgt. HyperKitty ist urheberrechtlich geschützt durch die Free Software Foundation und ihre Beitragenden, und die Dokumentation von upstream beschreibt dieselben Begriffe; die Kompatibilität ist beabsichtigt und tragend, nicht beiläufig. Ein Server, der von Mailman migriert, behält seine Archivlinks, und das Archiv zu importieren kommt einer Tabellenkopie nahe.

Was sich unterscheidet, ist, wo das Archiv liegt: in PostgreSQL, neben der Warteschlange und der Mitgliederliste, statt in einem Suchindex plus einem Dokumentenspeicher. Genau das macht das möglich, womit dieses Kapitel beginnt.

Das Archiv ist die eine Hälfte eines Subsystems; die andere sind die Listen selbst. Mailinglisten ist das Kapitel für Listen, Mitglieder, Moderation und Sammelnachrichten, und Wie das Subsystem aufgebaut ist dort ist die Übersicht über das Ganze – woher das Archiv geschrieben wird (der to-archive-Handler von pepsi-stage-list-post) und was sonst daraus liest. Das Werkzeug des Betreibers ist pepsi-archive; die Browseroberfläche ist Die Verwaltungskonsole; ein bestehendes Archiv zu importieren ist Teil einer Migration, siehe daher Installation; die Normen sind in RFC-Index gesammelt (insbesondere Archived-At nach RFC 5064); und Unterstützte Funktionen fasst zusammen, was implementiert ist und was nicht. Alles, was die REST-API zum Archivieren sagt, steht in Die REST-API von GNU Mailman 3.

GNU Mailman selbst wird ausführlicher in Mailinglisten gewürdigt, wo das Projekt, die Free Software Foundation und die GNU General Public License genannt sind, unter der das übernommene Material bleibt.

18.1. Eine Nachricht löschen

Eine Nachricht aus einem öffentlichen Archiv zu löschen ist ein Befehl.

# pepsi-archive purge --list announce@lists.example.org \
    'CAFxyz...@mail.example.org'
purged VHBHHV4YTF64QIJBGZ6XWA57YD6VRIZA from announce@lists.example.org;
tombstoned for 90 days so a resend or a re-run import cannot bring it back

Die Nachricht, ihre Anhänge und ihre Bewertungen sind fort; der Thread, in dem sie stand, wird neu numeriert; die Zähler werden reparieret; und ein event_log-Datensatz hält fest, wer was wann gelöscht hat.

Die Message-ID wird nicht gelöscht. Ein Grabstein bewahrt sie [pepsi-list] TOMBSTONE_RETENTION Tage lang auf (Standard 90), und solange dieser Grabstein steht, weigert sich das Archiv, diese Nachricht erneut zu speichern. Es gibt genau zwei Wege, auf denen eine gelöschte Nachricht zurückkommt – jemand stellt sie erneut zu, oder ein Importer wird ein zweites Mal ausgeführt – und beide sind Versehen und keine Entscheidungen. purge --forget überspringt den Grabstein, wenn die Entscheidung bewusst ist.

Das ist eine Abweichung von „löschen heißt fort“, und sie steht hier und nicht in einer Fußnote: TOMBSTONE_RETENTION Tage nach einer Löschung hält das Archiv weiterhin den Identifikator dieser Nachricht, ihr Löschdatum und den Namen dessen, der sie gelöscht hat.

18.2. Was archiviert wird und was nicht

Die archive_policy einer Liste entscheidet, wer das Archiv lesen darf: public, private (nur Mitglieder) oder never (es wird überhaupt nichts gespeichert). Die ersten beiden werden beim Lesen durchgesetzt, und genau das erlaubt einem Verwalter, eine Liste ohne erneuten Import von privat auf öffentlich umzustellen – und macht purge zum Einzigen, was jemals Inhalt entfernt.

Ein Absender kann eine einzelne Nachricht mit einem X-No-Archive:-Header (gleich welchen Werts) oder X-Archive: no ausnehmen.

Das Archiv behält die echte Absenderadresse. Sie zu verschleiern ist eine Darstellungsentscheidung: ein anonymer Leser sieht eine verschleierte Form, ein angemeldetes Mitglied sieht die Adresse. Upstream veröffentlicht Adressen so, wie sie gesendet wurden; wir speichern sie vollständig und entscheiden beim Ausgeben, weil Importer, Exporter und die mbox-Rundreise alle den echten Wert brauchen – und weil eine Einstellung, die beim Hereinkommen Daten zerstört, nicht zurückgenommen werden kann.

18.3. Suchen

Zwei Mechanismen, absichtlich beide, weil keiner den anderen umfasst.

Die gewichtete Wortsuche stammformt: eine Suche nach upgrade findet upgraded. Sie benutzt das Sprachwörterbuch der Liste selbst, und wo PostgreSQL keines hat, sagt sie simple, was weiterhin trifft – nur eben ohne Stammformen. Ein Treffer im Betreff steht höher als einer im Nachrichtentext, und der Name des Absenders ist durchsuchbar.

Die Teilzeichenfolgen- und Fuzzy-Suche stammformt nicht, und genau dafür ist sie da: ein Hostname, ein Konfigurationsschlüssel, ein Funktionsname, eine Zeile aus einem Traceback, ein falsch geschriebener Name. Auf einer technischen Liste ist das ein großer Teil dessen, wonach Leute tatsächlich suchen, und ein Stammform-Wörterbuch wirft genau dieses Material weg.

Das Suchfeld entscheidet, welche von beiden benutzt wird. Ein Wort oder eine Wortgruppe in Anführungszeichen ist eine Wortsuche; ein Bruchstück mit Satzzeichen (lists.example.org, --no-certbot, self.assertEqual) oder ein ausdrückliches *Platzhalter* ist eine Teilzeichenfolgensuche; und das Ergebnis nennt, welche geantwortet hat.

18.3.1. Die Trigramm-Stufe

Die Teilzeichenfolgensuche kostet Indexplatz, wie viel einer Liste dafür indiziert wird, ist daher eine Wahl – off, short oder full:

Stufe

Was für die Teilzeichenfolgensuche indiziert wird

off

Nichts. Die Spalte ist NULL, und der GIN-Index von PostgreSQL indiziert keine NULL-Werte – eine Liste, die sich dagegen entscheidet, kostet daher genau nichts, was wie ein Versehen aussieht und der ganze Mechanismus ist.

short

Der Betreff, der Betreff des Threads, Name und Adresse des Absenders. Der Standard: genug für „diese Nachricht von Alice über Releases“.

full

Das Obige und zusätzlich der Nachrichtentext. Die teure Stufe und die einzige, die ein Bruchstück eines Tracebacks findet.

[pepsi-list] SEARCH_TRIGRAM ist eine serverweite Obergrenze und kein Standardwert; eine Liste wählt bis zu ihr mit pepsi-list list set-ext <list> search_trigram <tier>. Wirksam ist die niedrigere der beiden.

Sie zu ändern ist ein Neuaufbau des Index und keine Migration:

# pepsi-archive reindex --list announce@lists.example.org
announce.lists.example.org: reindexed 12043 message(s) at tier full with the
english dictionary

Die serverweite Obergrenze zu senken verkleinert den Index daher beim nächsten Neuaufbau, statt nur neue Datensätze zu verbieten.

Eine Liste auf ``short`` sagt das. Eine Suche nach einem Textbruchstück darin antwortet „diese Liste indiziert Nachrichtentexte nicht für die Teilzeichenfolgensuche“ und nicht einfach nichts: eine Suche, die stillschweigend weniger ansieht, als der Leser meint, ist schlimmer als eine, die es zugibt.

Eine serverweite Suche liest jede Partition (das Archiv ist nach Liste partitioniert), sie ist daher die langsamere, ausdrückliche Wahl; eine auf eine Liste begrenzte Suche beschneidet auf eine Partition und benutzt das Wörterbuch dieser Liste.

18.4. URLs, die eine Migration behält

Jede archivierte Nachricht hat einen Message-ID-Hash: Base32 des SHA-1 ihrer Message-ID. Er ist der von upstream, unverändert, weil jeder Archived-At:-Header, den je eine HyperKitty-Installation ausgegeben hat, auf eine daraus abgeleitete URL zeigt, und ebenso jeder Link in jeder Mail, die jemand behalten hat:

URL

Was sie benennt

/archives/list/<list@domain>/

das Archiv der Liste

/archives/list/<list@domain>/message/<HASH>/

eine Nachricht

/archives/list/<list@domain>/thread/<HASH>/

ein Thread, über seine Eröffnungsnachricht

Der Identifikator eines Threads ist der Hash seiner Eröffnungsnachricht, und deshalb muss das Threading dem von upstream entsprechen und nicht bloß sinnvoll sein: ein anders gruppierter Thread ist ein Thread mit einer anderen URL.

18.5. Threading

Die Elternnachricht einer Nachricht ist ihr In-Reply-To, mit Rückfall auf den letzten Eintrag von References – die Nachricht, auf die tatsächlich geantwortet wurde, und nicht die Wurzel des Threads. Ist diese Nachricht auf dieser Liste nicht archiviert, eröffnet die Antwort einen eigenen Thread. Das ist die Regel von upstream, und sie wird beibehalten, damit ein importiertes Archiv so threadet wie vorher; die Reparatur ist ein gesonderter Befehl und keine andere Regel:

# pepsi-archive rebuild-threads --list announce@lists.example.org
announce.lists.example.org: reattached 37 orphaned repl(ies), renumbered
1284 thread(s)

Eine Antwortschleife – zwei Nachrichten, die einander als Eltern beanspruchen, was in echter Post vorkommt – lässt eine Kante fallen, statt sich aufzuhängen.

18.6. Zähler und der eine Weg, sie zu beschädigen

Die Übersichtsseiten lesen Zähler**spalten** (Nachrichten je Thread, wann ein Thread zuletzt aktiv war), statt bei jedem Rendern eine Million Datensätze zu zählen. Sie werden vom Schreiber gepflegt, und daraus folgt eine Sache, die ein Betreiber wissen muss:

Wenn Sie außerhalb von ``pepsi-archive`` in die Archivtabellen schreiben, lügen die Zähler.

Sie sind reparierbar, und genau deshalb sind sie Spalten und kein Zwischenspeicher:

# pepsi-archive recount --list announce@lists.example.org

18.7. Aufbewahrung und Export

pepsi-archive expire --list <addr> --before <date> löscht in Stapeln alles, was älter als ein Datum ist – das Prädikat ist ein Datum und die Partitionierung erfolgt nach Liste, es kann daher nicht beschneiden, und eine einzige Anweisung über ein Jahr einer belebten Liste würde eine Transaktion minutenlang offen halten.

Dieselbe Löschung läuft auch zeitgesteuert. pepsi-list tasks --once, das die Unit pepsi-list-tasks.timer täglich ausführt, lässt die Nachrichten jeder Liste verfallen, die älter als deren Aufbewahrungsdauer sind: Das eigene archive_retention_days einer Liste (gesetzt mit pepsi-list list set-ext) hat Vorrang, 0 eingeschlossen; eine Liste ohne diesen Wert verwendet [pepsi-list] ARCHIVE_RETENTION, dessen eigener Standard 0 ist; und 0 behält alles. Ein Archiv behält also alles, sofern niemand anders entscheidet, Threads und Zähler werden wie nach einem manuellen expire repariert, und eine Liste kann auf einer Site, die Nachrichten verfallen lässt, alles behalten. Ein gespeicherter Wert, der keine Anzahl von Tagen ist, wird mit einer Warnung übersprungen statt erraten, denn eine falsche Vermutung löscht Mail.

pepsi-archive export --list <addr> schreibt eine mboxrd-mbox auf die Standardausgabe, also das Format, das jeder andere Archivierer liest. Es ist absichtlich mboxrd und nicht mboxo: eine Textzeile, die schon mit >From `` begann, erhält ein weiteres ``>, was den Export umkehrbar macht – und ein umkehrbarer Export ist der Unterschied zwischen einer Sicherung und einer Annäherung.

18.8. Anhänge

Ein Anhang sind vom Benutzer gelieferte Bytes, ausgeliefert von unserem eigenen Origin, und das ist das Gefährlichste in diesem Subsystem. Vier Regeln, angewandt vom Archiv und nicht von dem, was die Seite rendert:

  • immer Content-Disposition: attachment, niemals inline;

  • ein gespeicherter text/html-Teil wird nie als text/html ausgeliefert;

  • der gespeicherte Inhaltstyp ist nur ein Hinweis – die Antwort verwendet einen sicheren Typ aus einer kurzen Erlaubnisliste (Klartext, PNG, JPEG, GIF, WebP, PDF) und für alles andere application/octet-stream, sodass ein Typ, an den niemand gedacht hat, langweilig und nicht gefährlich ist;

  • die Antwort trägt eine Content-Security-Policy von default-src 'none'; sandbox (dazu base-uri, form-action und frame-ancestors, alle 'none') und X-Content-Type-Options: nosniff.

18.9. Darin blättern

Das Archiv wird auf einem Listener mit dem Flag LISTS = yes angeboten (siehe Drei Listener-Flags, und bei zwei davon lautet der Rat entgegengesetzt), unter den URL-Formen von HyperKitty:

URL

Seite

/archives/list/<list@domain>/

Übersicht: jeder Monat mit einer Anzahl und die zuletzt aktiven Threads

/archives/list/<list@domain>/<year>/<month>/

Die Threads eines Monats

/archives/list/<list@domain>/thread/<hash>/

Ein ganzer Thread, nach der gespeicherten Tiefe eingerückt

/archives/list/<list@domain>/message/<hash>/

Eine Nachricht

/archives/list/<list@domain>/message/<hash>/attachment/<n>/<name>

Ein Anhang

/archives/list/<list@domain>/search?q=…

Diese Liste durchsuchen

Die Formen sind absichtlich erhalten. <hash> ist derselbe Message-ID-Hash, sodass jeder Archived-At:-Header, den eine migrierte Installation je ausgegeben hat, weiterhin auflöst – einschließlich derer, die schon in den Postfächern von Abonnenten liegen und in den Archiven anderer Leute zitiert sind. Sie zu behalten kostet eine Routentabelle und ist mehr wert als jede Verbesserung an ihnen.

18.9.1. Wer was lesen darf

Ein anonymer Besucher sieht das Archiv einer Liste nur, wenn deren archive_policy public ist. Alles andere ist ein 404 und kein 403: „verboten“ zu sagen würde bestätigen, dass die Liste existiert, und eine nicht beworbene Liste ist genau deshalb über eine URL erreichbar, damit dieser Unterschied zählt.

Jede Blätterseite ist auf eine Liste begrenzt, diese Entscheidung fällt daher einmal, bevor irgendeine Abfrage läuft. Die Suche ist die Ausnahme: sie geht über Listen hinweg und gewichtet sie, und eine Gewichtung, die über Datensätze berechnet wird, die der Betrachter nicht sehen darf, verrät deren Existenz über die Reihenfolge der sichtbaren – dort ist die Sichtbarkeitsregel daher Teil des WHERE und kein nachträglicher Filter.

18.9.2. Anhänge

Ein Anhang wird immer als Download ausgeliefert, nie als der Typ, den die Nachricht für ihn behauptet hat, und unter einer Richtlinie, die ihn nichts tun lässt, falls ein Browser sich doch entschließt, ihn zu rendern. Das ist eine Hilfsfunktion mit vier Regeln darin, und die Webseiten verweisen darauf, statt selbst etwas auszuliefern; ein Test prüft, dass kein Code auf der öffentlichen Oberfläche einen Inhaltstyp aus gespeicherten Daten setzt.

18.9.3. Zwei dokumentierte Unterschiede zu upstream

Beide sind für einen Leser sichtbar, und keiner ist versehentlich:

  • Adressen werden für anonyme Betrachter verschleiert (alice@…). Das Archiv behält die echte Adresse; das ist eine Darstellungsentscheidung und keine Sicherheitsmaßnahme – siehe Die Verwaltungskonsole.

  • ``archive_rendering_mode = markdown`` wird gespeichert und nicht gerendert. Das Attribut existiert, weil der Attributsatz der Kompatibilitätsvertrag ist, und beide Werte werden als Text dargestellt. Vom Benutzer geliefertes Markdown in unseren eigenen Origin zu rendern ist genau das, was die Content-Security-Policy dieser Seiten unmöglich machen soll.

18.9.4. Leseraktionen, die ein Konto brauchen

Zwei Dinge auf einer Archivseite gehören einer Person und nicht der Seite, und beide erfordern ein Mitgliedskonto (siehe Zwei Kontosysteme, und keines gewährt dem anderen etwas):

POST /archives/list/<list@domain>/message/<hash>/vote

Eine Bewertung von 1, -1 oder 0 zum Zurücknehmen. Über (Liste, Nachricht, Mitglied) verschlüsselt, daher konstruktionsbedingt idempotent – die Schaltfläche zweimal zu drücken ist eine Bewertung – und die Antwort ist eine Umleitung zurück zur Nachricht, sodass auch ein Neuladen nicht erneut bewerten kann.

Die Bewertungen werden je Nachricht gespeichert und die Zähler liegen am Thread, was die Anordnung von HyperKitty ist: eine Threadliste zeigt einen Punktestand, und ihn bei jedem Seitenaufbau zu berechnen würde einen Verbund über die Bewertungen jeder Nachricht bedeuten. Die Zähler werden daher in derselben Anweisung wie die Bewertung aus den Bewertungsdatensätzen neu berechnet, statt erhöht zu werden – ein Leser, der eine Aufwärtsbewertung in eine Abwärtsbewertung ändert, verschiebt die Zahl um zwei, und eine Erhöhung, die von eins ausgeht, würde abdriften.

POST /archives/list/<list@domain>/thread/<hash>/favourite

Ein Umschalter, denn das Bedienelement ist eine Schaltfläche, und eine Schaltfläche hat keinen Zustand zu senden.

Beide tragen ein CSRF-Token, und ein anonymer Leser, der eines davon anfordert, wird zum Anmeldeformular geschickt: das Archiv selbst bleibt ohne Konto lesbar, und nur diese Schreibvorgänge brauchen eines.

Ein angemeldetes Mitglied sieht außerdem die vollständige Absenderadresse statt der verschleierten Form. Das ist keine Aufweichung der obigen Regel – das Verschleiern war nie eine Sicherheitsmaßnahme, und wer auf der Liste ist, hat die Adresse ohnehin oben in seiner eigenen Kopie. Was es verhindert, ist die Masseneinsammlung, die ein öffentliches Archiv zu einer Spamquelle macht, und ein Adresssammler registriert kein Konto und bestätigt keine Adresse, um eine Seite auf einmal zu lesen. Die Folge, die man kennen sollte, betrifft das Zwischenspeichern: eine Seite, deren Inhalt davon abhängt, wer sie liest, wird mit no-store ausgeliefert, ein von angemeldeten Mitgliedern gelesenes Archiv ist daher von einem vorgelagerten Cache nicht gemeinsam nutzbar.

Das Archiv hat keine Thread-Verschlagwortung und keine Kategorien. pepsi-archive import liest mbox-Dateien mit den Reparaturen im Leser und baut danach die Threads neu auf; siehe pepsi-archive und Installation. Die Kommandozeile von pepsi-archive ist die gesamte Betreiberoberfläche des Archivs.