feat(PROJ-17): Admin Dashboard Systemauslastung immer anzeigen
- Systemauslastungs-Sektion wird immer gerendert (nicht nur bei Erfolg) - Fehlermeldung wenn /api/admin/system/stats nicht erreichbar ist - Feature-Status auf In Review gesetzt Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
# PROJ-5: E-Mail-Speicherung & Volltext-Indexierung
|
||||
|
||||
## Status: In Progress
|
||||
**Created:** 2026-03-12
|
||||
**Last Updated:** 2026-03-12
|
||||
|
||||
## Dependencies
|
||||
- None (Basis-Feature, wird von Import-Features genutzt)
|
||||
|
||||
## User Stories
|
||||
- Als System möchte ich E-Mails unveränderlich (immutable) speichern, damit die Archivintegrität gewährleistet ist.
|
||||
- Als System möchte ich E-Mail-Inhalte (Betreff, Absender, Empfänger, Body, Anhang-Namen) volltext-indexieren, damit schnelle Suche möglich ist.
|
||||
- Als Admin möchte ich den Speicherverbrauch einsehen können, damit ich die Kapazität planen kann.
|
||||
- Als System möchte ich Anhänge getrennt vom E-Mail-Body speichern, damit der Speicher effizient genutzt wird.
|
||||
|
||||
## Acceptance Criteria
|
||||
- [ ] Jede E-Mail wird mit ihrer originalen MIME-Struktur gespeichert (kein Datenverlust)
|
||||
- [ ] Metadaten in PostgreSQL: `message_id`, `from`, `to`, `cc`, `subject`, `date`, `size` (Bytes), Attachment-Infos (Dateiname, MIME-Type, Größe, Hash)
|
||||
- [ ] Kein E-Mail-Body in der DB – Body liegt ausschließlich in `/var/archivmail/store/` verschlüsselt auf Disk
|
||||
- [ ] Volltext-Index umfasst: Betreff, Absender, Empfänger, CC, BCC, Plain-Text-Body
|
||||
- [ ] Anhang-Dateinamen und MIME-Types werden indexiert (Inhalt von Anhängen optional)
|
||||
- [ ] Deduplizierung: Gleiche Message-ID wird nur einmal gespeichert
|
||||
- [ ] SHA-256-Hash des originalen RFC-2822-Inhalts für Integritätsprüfung gespeichert
|
||||
- [ ] Admin-Dashboard zeigt: Gesamtanzahl E-Mails, Speicherverbrauch (Store + Astore)
|
||||
- [ ] Mailkörper gespeichert unter `/var/archivmail/store/<server_id>/<customer_id>/<hash>/xxxxx.m` (AES-256-GCM verschlüsselt)
|
||||
- [ ] Anhänge gespeichert unter `/var/archivmail/astore/<hash>` (AES-256-GCM verschlüsselt)
|
||||
- [ ] Anhänge werden dedupliziert: gleicher Hash → eine Datei, mehrere Referenzen in der DB
|
||||
- [ ] Verschlüsselungsschlüssel wird beim Start aus `/etc/archivmail/keyfile` geladen (Pfad konfigurierbar)
|
||||
- [ ] Key-Datei: `chmod 400`, Owner `archivmail`-Systembenutzer
|
||||
- [ ] Server verweigert Start wenn Key-Datei fehlt, nicht lesbar oder Schlüssel ≠ 32 Byte nach Base64-Dekodierung
|
||||
- [ ] Xapian-Index enthält keinen vollständigen E-Mail-Body (nur Terme/Tokens)
|
||||
- [ ] PostgreSQL speichert ausschließlich Metadaten + Dateipfade – kein E-Mail-Body in der DB
|
||||
|
||||
## Edge Cases
|
||||
- E-Mail ohne Body (nur Anhang) → Body als leer speichern, Anhang indexieren
|
||||
- HTML-Body ohne Plain-Text-Alternative → HTML zu Plain-Text konvertieren für Index
|
||||
- E-Mail mit sehr vielen Empfängern (> 500) → TO/CC/BCC werden vollständig gespeichert
|
||||
- Sonderzeichen und Nicht-ASCII in Headern (RFC 2047 encoded) → dekodieren
|
||||
- Anhang-Deduplizierung: gleicher Inhalt in 1000 E-Mails → nur eine Datei in `astore/`, DB zählt Referenzen; Löschen einer E-Mail dekrementiert Referenzzähler, Datei erst bei 0 gelöscht
|
||||
- Speicherplatz voll → Import-Fehler mit klarer Meldung, keine partiellen Einträge
|
||||
- Verschlüsselungsschlüssel fehlt beim Start → Server startet nicht, klare Fehlermeldung
|
||||
- Schlüssel-Rotation: alte `.enc`-Dateien müssen mit neuem Schlüssel re-verschlüsselt werden (Admin-Tool, nicht automatisch)
|
||||
|
||||
## Technical Requirements
|
||||
- **Volltext-Index: Xapian** (via CGo-Bindings, z.B. `github.com/rcaught/go-xapian` oder direkte CGo-Integration)
|
||||
- Xapian-Datenbank liegt auf dem Dateisystem (kein externer Dienst nötig)
|
||||
- Felder als Xapian-Terms und -Values indexiert: Subject, From, To, CC, BCC, Body
|
||||
- Stemming für Deutsch und Englisch (Xapian Snowball Stemmer)
|
||||
- Anhang-Dateinamen als zusätzliche Terms indexiert
|
||||
- **Speicherung: Verschlüsselt im Dateisystem (AES-256-GCM)**
|
||||
- Mailkörper (ohne Anhänge) als `.m`-Datei:
|
||||
```
|
||||
/var/archivmail/store/<server_id>/<customer_id>/<hash>/xxxxx.m
|
||||
```
|
||||
- Anhänge dedupliziert in separatem Store (ein Anhang = eine Datei, unabhängig wie oft er vorkommt):
|
||||
```
|
||||
/var/archivmail/astore/<hash>
|
||||
```
|
||||
- Hash = SHA-256 des Inhalts → dient gleichzeitig als Pfad und Integritätsprüfung
|
||||
- Beide Stores AES-256-GCM verschlüsselt auf Disk
|
||||
- Verschlüsselungsschlüssel (32 Byte) aus dedizierter Key-Datei: `/etc/archivmail/keyfile`
|
||||
- Dateiformat: Base64-kodierter 32-Byte-Schlüssel, eine Zeile
|
||||
- Dateiberechtigungen: `chmod 400`, Owner: `archivmail` (Systembenutzer des Dienstes)
|
||||
- Pfad zur Key-Datei konfigurierbar in `config.yml` (`encryption.keyfile`)
|
||||
- Schlüssel wird beim Start einmalig in den Prozessspeicher geladen – danach keine Disk-Zugriffe mehr auf die Key-Datei
|
||||
- Server verweigert Start wenn Key-Datei fehlt, nicht lesbar oder Inhalt nicht exakt 32 Byte (nach Base64-Dekodierung)
|
||||
- PostgreSQL speichert folgende Metadaten (kein Mail-Body):
|
||||
- `message_id`, `from`, `to`, `cc`, `subject`, `date`, `size`
|
||||
- Attachment-Tabelle: `filename`, `mime_type`, `size`, `hash` (→ Pfad in `astore/`)
|
||||
- Pfadreferenz zur `.m`-Datei in `store/`
|
||||
- Xapian-Datenbank liegt unverschlüsselt auf Disk (enthält nur Text-Terme, keinen vollständigen Body)
|
||||
- Xapian-Schreibzugriffe serialisiert (WritableDatabase nicht thread-safe) – Background-Worker-Queue
|
||||
- Indexierung innerhalb 5 Sekunden nach E-Mail-Eingang
|
||||
- Retention-Policy: konfigurierbare automatische Löschung alter E-Mails (DSGVO) löscht sowohl DB-Eintrag als auch Xapian-Dokument
|
||||
|
||||
---
|
||||
## Tech Design (Solution Architect)
|
||||
|
||||
### Komponentenstruktur
|
||||
|
||||
```
|
||||
archivmail (Go-Binary)
|
||||
│
|
||||
├── Storage Coordinator ← Einziger Eintrittspunkt für alle Schreibvorgänge
|
||||
│ ├── MIME Parser ← Zerlegt eingehende E-Mail in Body + Anhänge
|
||||
│ ├── Mail Store ← Schreibt .m-Datei verschlüsselt auf Disk
|
||||
│ │ └── Encryption Layer ← AES-256-GCM (Schlüssel aus /etc/archivmail/keyfile)
|
||||
│ ├── Attachment Store ← Schreibt Anhänge in astore/, prüft Duplikate per Hash
|
||||
│ │ └── Encryption Layer ← gleiche AES-256-GCM Instanz
|
||||
│ └── Metadata Writer ← Schreibt Metadaten in PostgreSQL
|
||||
│
|
||||
├── Index Worker (Hintergrund) ← Serialisierte Warteschlange für Xapian-Schreibzugriffe
|
||||
│ ├── Text Extractor ← HTML → Plain-Text, RFC 2047 Header-Dekodierung
|
||||
│ └── Xapian WritableDatabase ← Ein Schreiber gleichzeitig (Queue verhindert Konflikte)
|
||||
│
|
||||
└── Xapian ReadonlyDatabase ← Beliebig viele parallele Lesezugriffe (Suche)
|
||||
```
|
||||
|
||||
### Datenfluss: E-Mail eingehend
|
||||
|
||||
```
|
||||
E-Mail (RFC 2822) – primär via SMTP-BCC
|
||||
│
|
||||
▼
|
||||
MIME Parser
|
||||
┌────┴──────────────────────────────────┐
|
||||
│ │
|
||||
▼ ▼
|
||||
Body (ohne Anhänge) Anhänge (0..n)
|
||||
│ │
|
||||
├─ SHA-256(Body) → Hash ├─ SHA-256(Anhang) → Hash
|
||||
├─ AES-256-GCM verschlüsseln ├─ Hash in astore/ vorhanden? → nur ref_count++
|
||||
└─ /var/archivmail/store/ ├─ AES-256-GCM verschlüsseln
|
||||
<server>/<customer>/<hash>/x.m └─ /var/archivmail/astore/<hash>
|
||||
+ ref_count++ in DB
|
||||
│ │
|
||||
└──────────────┬────────────────────────┘
|
||||
▼
|
||||
PostgreSQL (Metadaten)
|
||||
message_id, from, to, cc,
|
||||
subject, date, size,
|
||||
store_path, sha256,
|
||||
indexed_at = NULL
|
||||
│
|
||||
▼
|
||||
Index Worker Queue (Channel)
|
||||
│
|
||||
▼
|
||||
Text Extractor
|
||||
(HTML→Text, Encoding-Normalisierung)
|
||||
│
|
||||
▼
|
||||
Xapian WritableDatabase
|
||||
Subject, From, To, CC, Body als Terms
|
||||
indexed_at = NOW() in PostgreSQL
|
||||
```
|
||||
|
||||
### Datenmodell
|
||||
|
||||
**Tabelle `emails`** – eine Zeile pro archivierter E-Mail:
|
||||
|
||||
| Feld | Beschreibung |
|
||||
|------|-------------|
|
||||
| `message_id` | RFC-2822 Message-ID (Primärschlüssel, Duplikatschutz) |
|
||||
| `from` | Absender |
|
||||
| `to` | Empfänger |
|
||||
| `cc` | CC-Empfänger |
|
||||
| `subject` | Betreff |
|
||||
| `date` | Sendedatum (UTC) |
|
||||
| `size` | Größe des Originals in Bytes |
|
||||
| `store_path` | Pfad zur .m-Datei |
|
||||
| `sha256` | Hash des Originals (Integritätsprüfung) |
|
||||
| `indexed_at` | Zeitpunkt der Xapian-Indexierung (NULL = ausstehend) |
|
||||
|
||||
**Tabelle `attachments`** – ein Eintrag pro einzigartigem Anhang:
|
||||
|
||||
| Feld | Beschreibung |
|
||||
|------|-------------|
|
||||
| `hash` | SHA-256 des Inhalts (= Dateiname in astore/) |
|
||||
| `filename` | Originaldateiname |
|
||||
| `mime_type` | z.B. application/pdf |
|
||||
| `size` | Größe in Bytes |
|
||||
| `ref_count` | Anzahl E-Mails die diesen Anhang referenzieren |
|
||||
|
||||
**Tabelle `email_attachments`** – Verknüpfung E-Mail ↔ Anhang (n:m)
|
||||
|
||||
### Technische Entscheidungen
|
||||
|
||||
| Entscheidung | Begründung |
|
||||
|---|---|
|
||||
| Body und Anhänge getrennt | Anhang-Deduplizierung: gleicher PDF in 1000 Mails = eine Datei auf Disk |
|
||||
| SHA-256 als Dateipfad | Hash dient gleichzeitig als Pfad und Integritätsprüfung – kein separates Mapping |
|
||||
| AES-256-GCM | Authentifizierte Verschlüsselung erkennt Dateimanipulationen (Tamper Detection) |
|
||||
| Index Worker Queue | Xapian erlaubt nur einen Schreiber – Queue serialisiert ohne Datenverlust |
|
||||
| `indexed_at` NULL-Flag | Nach Absturz können nicht-indexierte Mails beim Neustart nachindexiert werden |
|
||||
| Metadaten in PostgreSQL, Body auf Disk | Filterabfragen (Datum, Absender) ohne Disk-Zugriff; Body nur bei Bedarf lesen |
|
||||
| Storage Coordinator als Single Entry Point | Alle Importwege (SMTP, IMAP, EML/MBOX) rufen dieselbe Schreiblogik auf |
|
||||
|
||||
### Go-Abhängigkeiten
|
||||
|
||||
| Paket | Zweck |
|
||||
|---|---|
|
||||
| Xapian CGo-Bindings | Volltext-Index |
|
||||
| `pgx` | PostgreSQL-Treiber |
|
||||
| `crypto/aes`, `crypto/cipher` | AES-256-GCM (Go Stdlib) |
|
||||
| `crypto/sha256` | Hashing (Go Stdlib) |
|
||||
| `mime`, `mime/multipart` | MIME-Parsing (Go Stdlib) |
|
||||
| `golang.org/x/net/html` | HTML → Plain-Text für Index |
|
||||
|
||||
## QA Test Results
|
||||
_To be added by /qa_
|
||||
|
||||
## Deployment
|
||||
_To be added by /deploy_
|
||||
Reference in New Issue
Block a user