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:
+19
-1
@@ -12,7 +12,25 @@
|
||||
|
||||
| ID | Feature | Status | Spec | Created |
|
||||
|----|---------|--------|------|---------|
|
||||
| PROJ-1 | Nutzer-Authentifizierung & Rollen (User/Admin) | In Progress | [PROJ-1](PROJ-1-authentifizierung-und-rollen.md) | 2026-03-12 |
|
||||
| PROJ-2 | E-Mail-Import: EML/MBOX Upload | In Progress | [PROJ-2](PROJ-2-import-eml-mbox.md) | 2026-03-12 |
|
||||
| PROJ-3 | E-Mail-Import: IMAP-Verbindung | In Progress | [PROJ-3](PROJ-3-import-imap.md) | 2026-03-12 |
|
||||
| PROJ-4 | E-Mail-Import: SMTP-Eingang via BCC (primär) | In Progress | [PROJ-4](PROJ-4-import-smtp.md) | 2026-03-12 |
|
||||
| PROJ-5 | E-Mail-Speicherung & Volltext-Indexierung | In Progress | [PROJ-5](PROJ-5-speicherung-und-indexierung.md) | 2026-03-12 |
|
||||
| PROJ-6 | Volltext-Suche & Filterung | In Progress | [PROJ-6](PROJ-6-volltext-suche.md) | 2026-03-12 |
|
||||
| PROJ-7 | E-Mail-Ansicht (Lesen & Anhänge) | In Progress | [PROJ-7](PROJ-7-email-ansicht.md) | 2026-03-12 |
|
||||
| PROJ-8 | Automatischer IMAP-Sync (Cron-Job) | In Progress | [PROJ-8](PROJ-8-imap-auto-sync.md) | 2026-03-12 |
|
||||
| PROJ-9 | Ordner- & Label-Verwaltung | In Progress | [PROJ-9](PROJ-9-ordner-und-labels.md) | 2026-03-12 |
|
||||
| PROJ-10 | Admin-Bereich: Nutzer- & Postfachverwaltung | In Progress | [PROJ-10](PROJ-10-admin-bereich.md) | 2026-03-12 |
|
||||
| PROJ-11 | Audit-Log & Compliance-Berichte | In Progress | [PROJ-11](PROJ-11-audit-log.md) | 2026-03-12 |
|
||||
| PROJ-12 | E-Mail-Export (EML/PDF) | In Progress | [PROJ-12](PROJ-12-export.md) | 2026-03-12 |
|
||||
| PROJ-13 | REST API für externe CRM-Anbindung | In Progress | [PROJ-13](PROJ-13-rest-api-crm.md) | 2026-03-13 |
|
||||
| PROJ-14 | E-Mail-Import: POP3-Verbindung | In Progress | [PROJ-14](PROJ-14-import-pop3.md) | 2026-03-13 |
|
||||
| PROJ-15 | CLI Import & Export (archivmail-User) | In Progress | [PROJ-15](PROJ-15-cli-import-export.md) | 2026-03-13 |
|
||||
| PROJ-16 | LDAP / Active Directory Anbindung | In Progress | [PROJ-16](PROJ-16-ldap-active-directory.md) | 2026-03-13 |
|
||||
|
||||
| PROJ-17 | Admin Dashboard – Systemauslastung & Archiv-Übersicht | In Review | [PROJ-17](PROJ-17-system-dashboard.md) | 2026-03-14 |
|
||||
|
||||
<!-- Add features above this line -->
|
||||
|
||||
## Next Available ID: PROJ-1
|
||||
## Next Available ID: PROJ-18
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
# PROJ-1: Nutzer-Authentifizierung & Rollen
|
||||
|
||||
## Status: In Progress
|
||||
**Created:** 2026-03-12
|
||||
**Last Updated:** 2026-03-12
|
||||
|
||||
## Dependencies
|
||||
- PROJ-16 (LDAP / Active Directory Anbindung) — optionale Erweiterung des Login-Flows
|
||||
|
||||
## Rollen-Übersicht
|
||||
|
||||
| Rolle | Zugriff |
|
||||
|-------|---------|
|
||||
| `user` | Suche und Lesen eigener archivierter E-Mails |
|
||||
| `auditor` | Alle E-Mails lesen und suchen (postfachübergreifend) + Audit-Log einsehen und exportieren – keine Konfiguration |
|
||||
| `admin` | Konfiguration, Nutzerverwaltung, Import-Quellen, Systemeinstellungen – kein Zugriff auf E-Mails, kein Audit-Log |
|
||||
|
||||
## User Stories
|
||||
- Als Admin möchte ich mich mit Benutzername/Passwort einloggen, damit nur autorisierte Personen Zugriff haben.
|
||||
- Als Admin möchte ich neue Nutzer anlegen und ihnen eine Rolle zuweisen (`user`, `auditor`, `admin`).
|
||||
- Als Auditor möchte ich den Audit-Log einsehen und als CSV exportieren, ohne Zugriff auf E-Mails oder Konfiguration zu haben.
|
||||
- Als Nutzer möchte ich mich abmelden können, damit meine Session sicher beendet wird.
|
||||
- Als Admin möchte ich Passwörter zurücksetzen können, damit gesperrte Nutzer wieder Zugang erhalten.
|
||||
- Als System möchte ich Sessions nach Inaktivität automatisch beenden, damit unbefugter Zugriff verhindert wird.
|
||||
|
||||
## Acceptance Criteria
|
||||
- [ ] Login-Seite mit E-Mail/Benutzername und Passwort-Formular
|
||||
- [ ] Fehlermeldung bei falschen Zugangsdaten (kein Hinweis ob E-Mail oder Passwort falsch)
|
||||
- [ ] Session-Token wird sicher gespeichert (httpOnly Cookie oder JWT)
|
||||
- [ ] Sessions laufen nach konfigurierbarer Inaktivität ab (Standard: 8 Stunden)
|
||||
- [ ] Drei Rollen: `user`, `auditor`, `admin` – strikt getrennte Zugriffsrechte
|
||||
- [ ] `auditor` hat Zugriff auf alle E-Mails (postfachübergreifend, auch fremde Postfächer) + Audit-Log – keine Konfiguration
|
||||
- [ ] `admin` hat ausschließlich Zugriff auf Konfiguration und Nutzerverwaltung – kein Zugriff auf E-Mails und kein Zugriff auf Audit-Log
|
||||
- [ ] Admin kann Nutzer anlegen, deaktivieren und löschen
|
||||
- [ ] Admin kann Passwörter zurücksetzen (temporäres Passwort)
|
||||
- [ ] Alle API-Endpunkte prüfen Authentifizierung und Rolle
|
||||
- [ ] Logout löscht die Session serverseitig
|
||||
|
||||
## Edge Cases
|
||||
- Login mit deaktiviertem Account → klare Fehlermeldung, kein Zugang
|
||||
- Mehrfaches Fehllogin → Rate-Limiting oder Account-Sperre nach X Versuchen
|
||||
- Session-Token abgelaufen → automatische Weiterleitung zur Login-Seite
|
||||
- Erster Start: Zwei feste Default-User werden beim ersten Start automatisch angelegt:
|
||||
- `admin@archivmail` / `archivmailrockz` (Rolle: `admin`)
|
||||
- `auditor@archivmail` / `archivmailrockz` (Rolle: `auditor`)
|
||||
- Passwörter sollten nach dem ersten Login geändert werden (Hinweis in der UI)
|
||||
- Admin löscht sich selbst → verhindern wenn letzter Admin
|
||||
|
||||
## Technical Requirements
|
||||
- Passwörter mit bcrypt gehasht (min. Cost 12)
|
||||
- Alle Routen außer `/login` erfordern gültige Session
|
||||
- Admin-Routen (`/admin/*`) nur für `admin`-Rolle
|
||||
- Audit-Routen (`/audit/*`) und E-Mail-Suche/Ansicht nur für `auditor`- und `user`-Rolle
|
||||
- `admin` erhält bei E-Mail-Endpunkten HTTP 403 – keine Ausnahmen
|
||||
- Keine Rolle vereint `admin` + `auditor` – strikte Funktionstrennung
|
||||
- Audit-Log-Eintrag bei Login, Logout, fehlgeschlagenem Login
|
||||
|
||||
---
|
||||
<!-- Sections below are added by subsequent skills -->
|
||||
|
||||
## Tech Design (Solution Architect)
|
||||
|
||||
### Systemübersicht: Two-Tier Architektur
|
||||
|
||||
```
|
||||
Browser (Next.js App) Go REST API Backend
|
||||
│ │
|
||||
│ POST /api/auth/login │
|
||||
│ {email, password} │
|
||||
│ ─────────────────────────────────► │
|
||||
│ 1. Lokaler Account? → bcrypt verify
|
||||
│ 2. Nicht gefunden + LDAP aktiv?
|
||||
│ → LDAP-Bind (Service Account)
|
||||
│ → User-DN suchen
|
||||
│ → User-Bind mit Passwort
|
||||
│ → AD-Gruppen → Rolle bestimmen
|
||||
│ → UpsertLDAPUser in PostgreSQL
|
||||
│ 3. Session-Token erstellen
|
||||
│ Session in PostgreSQL speichern
|
||||
│ ◄─────────────────────────────────
|
||||
│ Set-Cookie: session=TOKEN │
|
||||
│ (httpOnly, Secure, SameSite) │
|
||||
│ │
|
||||
│ GET /api/search?q=... │
|
||||
│ Cookie: session=TOKEN │
|
||||
│ ─────────────────────────────────► │
|
||||
│ Session-Middleware: Token prüfen
|
||||
│ Role-Middleware: Route erlaubt?
|
||||
│ ◄─────────────────────────────────
|
||||
│ JSON-Antwort │
|
||||
```
|
||||
|
||||
> **LDAP ist vollständig optional.** Wenn `ldap.enabled: false` (Standard), verhält sich das System exakt wie ohne LDAP. Lokale Accounts funktionieren immer — auch wenn LDAP aktiviert ist (Fallback bei LDAP-Fehler).
|
||||
|
||||
### Komponentenstruktur
|
||||
|
||||
**Next.js Frontend:**
|
||||
```
|
||||
src/app/
|
||||
├── /login ← Login-Seite (öffentlich)
|
||||
├── /search ← Suche + E-Mail-Ansicht (user + auditor)
|
||||
├── /audit ← Audit-Log (nur auditor)
|
||||
└── /admin ← Admin-Bereich (nur admin)
|
||||
|
||||
src/components/
|
||||
├── LoginForm ← E-Mail + Passwort, Fehlermeldungen
|
||||
├── RoleGuard ← Schützt Routen clientseitig, redirect auf /login
|
||||
└── PasswordChangePrompt ← Hinweis bei Default-Passwort
|
||||
```
|
||||
|
||||
**Go Backend:**
|
||||
```
|
||||
HTTP-Server
|
||||
├── POST /api/auth/login ← Session ausstellen
|
||||
├── POST /api/auth/logout ← Session löschen
|
||||
├── Session Middleware ← prüft Token bei allen /api/* Routen
|
||||
├── Role Middleware
|
||||
│ ├── /api/admin/* → nur `admin`
|
||||
│ ├── /api/audit/* → nur `auditor`
|
||||
│ └── /api/* → `user` + `auditor` (admin → 403)
|
||||
├── Password Manager ← bcrypt Hash + Verify
|
||||
├── User Store ← PostgreSQL users-Tabelle
|
||||
└── Bootstrap ← Default-User beim ersten Start
|
||||
```
|
||||
|
||||
### Datenmodell
|
||||
|
||||
**Tabelle `users`:**
|
||||
|
||||
| Feld | Beschreibung |
|
||||
|------|-------------|
|
||||
| `id` | Interne ID |
|
||||
| `email` | Login-E-Mail (eindeutig) |
|
||||
| `password_hash` | bcrypt-Hash (Cost 12) — NULL bei LDAP-Usern |
|
||||
| `role` | `user` / `auditor` / `admin` |
|
||||
| `source` | `local` oder `ldap` — Herkunft des Accounts |
|
||||
| `active` | Deaktivierte Nutzer können sich nicht einloggen |
|
||||
| `created_at` | Erstellungszeitpunkt |
|
||||
| `last_login_at` | Letzter erfolgreicher Login |
|
||||
|
||||
**Tabelle `sessions`:**
|
||||
|
||||
| Feld | Beschreibung |
|
||||
|------|-------------|
|
||||
| `token` | Zufälliger 32-Byte-Token |
|
||||
| `user_id` | Referenz auf `users` |
|
||||
| `expires_at` | Ablaufzeitpunkt (rollierend, +8h bei Aktivität) |
|
||||
| `last_active_at` | Wird bei jeder Anfrage aktualisiert |
|
||||
|
||||
### Technische Entscheidungen
|
||||
|
||||
| Entscheidung | Begründung |
|
||||
|---|---|
|
||||
| **Next.js Frontend + Go REST API** | Klare Trennung: Next.js rendert die UI, Go verwaltet Daten und Sicherheit. Kein Java. |
|
||||
| **Session-Cookie (httpOnly)** | Next.js sendet Cookie automatisch mit – kein manuelles Token-Handling im Frontend-Code nötig |
|
||||
| **Server-side Sessions (nicht JWT)** | Logout und Admin-Deaktivierung wirken sofort – JWT wäre bis Ablauf weiterhin gültig |
|
||||
| **Role-Check im Go-Backend** | Sicherheits-kritische Prüfung liegt im Backend, nicht im Next.js-Client (der wäre manipulierbar) |
|
||||
| **RoleGuard in Next.js zusätzlich** | Verhindert kurzes Aufblitzen falscher Seiten – rein UX, kein Sicherheits-Feature |
|
||||
| **bcrypt Cost 12** | Ausreichend langsam gegen Brute-Force |
|
||||
| **LDAP als optionaler Fallback** | Login versucht erst lokalen Account, dann LDAP – Reihenfolge garantiert, dass lokale Admins immer funktionieren |
|
||||
| **LDAP-User in PostgreSQL gespiegelt** | Nach erstem Login landet LDAP-User in `users`-Tabelle (`source: ldap`) – einheitliche Session-Verwaltung, kein Sonder-Code |
|
||||
| **AD-Gruppen → Rollen-Mapping** | Rolle wird bei jedem Login aus AD-Gruppenmitgliedschaft neu bestimmt – Rollen-Änderung in AD wirkt beim nächsten Login |
|
||||
|
||||
### Abhängigkeiten
|
||||
|
||||
**Go Backend:**
|
||||
|
||||
| Paket | Zweck |
|
||||
|---|---|
|
||||
| `golang.org/x/crypto/bcrypt` | Passwort-Hashing |
|
||||
| `crypto/rand` | Sichere Token-Generierung (Stdlib) |
|
||||
| `github.com/go-ldap/ldap/v3` | LDAP/AD-Authentifizierung (PROJ-16) |
|
||||
|
||||
**Next.js Frontend:**
|
||||
|
||||
| Paket | Zweck |
|
||||
|---|---|
|
||||
| `react-hook-form` + `zod` | Login-Formular-Validierung (bereits im Template) |
|
||||
| `shadcn/ui` | UI-Komponenten (bereits installiert) |
|
||||
|
||||
## QA Test Results
|
||||
_To be added by /qa_
|
||||
|
||||
## Deployment
|
||||
_To be added by /deploy_
|
||||
@@ -0,0 +1,161 @@
|
||||
# PROJ-10: Admin-Bereich: Nutzer- & Postfachverwaltung
|
||||
|
||||
## Status: In Progress
|
||||
**Created:** 2026-03-12
|
||||
**Last Updated:** 2026-03-12
|
||||
|
||||
## Dependencies
|
||||
- Requires: PROJ-1 (Authentifizierung & Rollen) – nur Admins haben Zugang
|
||||
|
||||
## User Stories
|
||||
- Als Admin möchte ich alle Nutzer auflisten, bearbeiten, deaktivieren und löschen.
|
||||
- Als Admin möchte ich Postfächer (IMAP-Verbindungen) verwalten und Nutzern zuweisen.
|
||||
- Als Admin möchte ich Systemstatistiken sehen (Gesamtanzahl E-Mails, Speicher, aktive Nutzer).
|
||||
- Als Admin möchte ich Import-Konfigurationen (IMAP, SMTP) verwalten.
|
||||
- Als Admin möchte ich globale Systemeinstellungen konfigurieren (Sync-Intervall, max. Upload-Größe, Retention-Policy).
|
||||
|
||||
## Acceptance Criteria
|
||||
- [ ] Admin-Dashboard mit Übersicht: Nutzeranzahl, E-Mail-Anzahl, Speicherverbrauch
|
||||
- [ ] Nutzerliste: Anzeige aller Nutzer mit Rolle, Status, letztem Login
|
||||
- [ ] Nutzer anlegen / bearbeiten / deaktivieren / löschen (mit Bestätigungsdialog)
|
||||
- [ ] Postfach-Verwaltung: IMAP-Verbindungen anlegen, bearbeiten, testen, löschen
|
||||
- [ ] Postfach-Zuweisung: Nutzer einem oder mehreren Postfächern zuordnen
|
||||
- [ ] System-Einstellungen: Sync-Intervall, SMTP-Port, max. Upload-Größe, Retention-Tage
|
||||
- [ ] Alle Admin-Aktionen werden im Audit-Log erfasst
|
||||
|
||||
## Edge Cases
|
||||
- Admin löscht Nutzer mit archivierten E-Mails → E-Mails bleiben im Archiv, Nutzer wird anonymisiert (DSGVO)
|
||||
- Letzten Admin löschen/deaktivieren → verhindern mit Fehlermeldung
|
||||
- Postfach löschen mit laufendem Sync → Sync abbrechen, dann löschen
|
||||
|
||||
## Technical Requirements
|
||||
- Admin-Bereich unter eigenem Route-Prefix (/admin/*)
|
||||
- Alle Admin-API-Endpunkte prüfen Admin-Rolle
|
||||
- Änderungen an Systemeinstellungen erfordern Server-Neustart nur wenn unvermeidlich
|
||||
|
||||
---
|
||||
## Tech Design (Solution Architect)
|
||||
|
||||
### Komponentenstruktur
|
||||
|
||||
**Next.js Frontend (/admin/*):**
|
||||
```
|
||||
/admin
|
||||
├── Dashboard ← Einstiegsseite
|
||||
│ ├── StatCard: Gesamtanzahl Mails
|
||||
│ ├── StatCard: Speicherverbrauch (store + astore)
|
||||
│ ├── StatCard: Aktive Nutzer
|
||||
│ ├── StatCard: Letzter SMTP-Eingang
|
||||
│ └── ImportQueue-Status (laufende Imports)
|
||||
│
|
||||
├── /admin/users ← Nutzerverwaltung
|
||||
│ ├── NutzerTabelle (Name, Rolle, Status, letzter Login)
|
||||
│ ├── [Nutzer anlegen] Button → NutzerFormular
|
||||
│ └── NutzerRow-Aktionen
|
||||
│ ├── Bearbeiten (Rolle, Passwort zurücksetzen)
|
||||
│ ├── Deaktivieren / Aktivieren
|
||||
│ └── Löschen (Bestätigungsdialog + DSGVO-Hinweis)
|
||||
│
|
||||
├── /admin/imap ← IMAP-Verbindungen (PROJ-3 + PROJ-8)
|
||||
│ ├── IMAP-Verbindungsliste
|
||||
│ └── Postfach-Zuweisung (Nutzer ↔ IMAP-Account)
|
||||
│
|
||||
├── /admin/pop3 ← POP3-Verbindungen (PROJ-14)
|
||||
│
|
||||
├── /admin/smtp ← SMTP-Daemon-Status (PROJ-4)
|
||||
│ ├── Status (läuft / gestoppt), Port, TLS
|
||||
│ ├── Anzahl empfangener Mails (heute / gesamt)
|
||||
│ └── IP-Allowlist verwalten
|
||||
│
|
||||
├── /admin/upload ← EML/MBOX Upload (PROJ-2)
|
||||
│
|
||||
├── /admin/apikeys ← API-Keys (PROJ-13)
|
||||
│
|
||||
├── /admin/labels ← Globale Labels + Auto-Regeln (PROJ-9)
|
||||
│
|
||||
└── /admin/settings ← Systemeinstellungen
|
||||
├── max. Upload-Größe (MB)
|
||||
├── Retention-Tage (0 = unbegrenzt)
|
||||
├── Session-Timeout (Stunden)
|
||||
└── SMTP-Port (Hinweis: Neustart erforderlich)
|
||||
```
|
||||
|
||||
**Go Backend (/api/admin/*):**
|
||||
```
|
||||
Admin-Router (alle Routen prüfen admin-Rolle)
|
||||
│
|
||||
├── GET /api/admin/stats ← Dashboard-Zahlen
|
||||
│ (Mail-Count, Speicher, aktive User, letzter SMTP-Eingang)
|
||||
│
|
||||
├── Nutzer-Verwaltung
|
||||
│ ├── GET /api/admin/users
|
||||
│ ├── POST /api/admin/users ← anlegen
|
||||
│ ├── PATCH /api/admin/users/{id} ← bearbeiten
|
||||
│ ├── DELETE /api/admin/users/{id} ← löschen (DSGVO-Anonymisierung)
|
||||
│ └── POST /api/admin/users/{id}/reset-password
|
||||
│
|
||||
├── Postfach-Zuweisung
|
||||
│ ├── GET /api/admin/users/{id}/mailboxes
|
||||
│ ├── POST /api/admin/users/{id}/mailboxes/{account_id}
|
||||
│ └── DELETE /api/admin/users/{id}/mailboxes/{account_id}
|
||||
│
|
||||
└── Systemeinstellungen
|
||||
├── GET /api/admin/settings
|
||||
└── PATCH /api/admin/settings
|
||||
```
|
||||
|
||||
### DSGVO-Löschfluss (Nutzer löschen)
|
||||
|
||||
```
|
||||
Admin klickt "Nutzer löschen"
|
||||
│
|
||||
▼
|
||||
Bestätigungsdialog:
|
||||
"E-Mails bleiben im Archiv.
|
||||
Nutzerdaten werden anonymisiert."
|
||||
│
|
||||
▼
|
||||
DELETE /api/admin/users/{id}
|
||||
│
|
||||
├── Ist letzter Admin? → 409 Conflict (verhindern)
|
||||
│
|
||||
├── E-Mails des Nutzers → bleiben im Archiv (immutable)
|
||||
│
|
||||
├── Audit-Log-Einträge → user_id → "anonymized"
|
||||
│ IP-Adressen → gelöscht
|
||||
├── Sessions → alle gelöscht
|
||||
├── Labels des Nutzers → gelöscht
|
||||
└── User-Eintrag → gelöscht
|
||||
```
|
||||
|
||||
### Datenmodell
|
||||
|
||||
**Tabelle `settings`** – Key-Value-Store für Systemeinstellungen:
|
||||
|
||||
| Key | Standardwert | Beschreibung |
|
||||
|-----|-------------|-------------|
|
||||
| `max_upload_mb` | `500` | Max. Upload-Größe in MB |
|
||||
| `retention_days` | `0` | 0 = unbegrenzt |
|
||||
| `session_timeout_hours` | `8` | Session-Inaktivitäts-Timeout |
|
||||
| `smtp_port` | `2525` | SMTP-Daemon-Port (Neustart nötig) |
|
||||
|
||||
### Technische Entscheidungen
|
||||
|
||||
| Entscheidung | Begründung |
|
||||
|---|---|
|
||||
| **Admin-Bereich als eigene Next.js-Route** | Klare Trennung von User-Frontend – RoleGuard blockiert Nicht-Admins sofort |
|
||||
| **Dashboard-Stats vom Backend** | Mail-Count, Speicher aus DB/Dateisystem – kein Client-seitiges Berechnen |
|
||||
| **Settings als DB-Key-Value** | Einstellungen zur Laufzeit änderbar ohne Dateisystem-Zugriff oder Neustart (außer SMTP-Port) |
|
||||
| **DSGVO-Anonymisierung statt Hard-Delete** | Archiv-Integrität bleibt erhalten – E-Mails im Archiv haben keinen Personenbezug mehr nach Anonymisierung |
|
||||
| **Letzter-Admin-Schutz** | Verhindert Aussperrung – Backend prüft vor jedem Delete/Deaktivieren |
|
||||
|
||||
### Abhängigkeiten
|
||||
|
||||
**Next.js:** shadcn/ui Table, Dialog, Form (bereits installiert).
|
||||
**Go Backend:** Nur pgx + Stdlib (bereits vorhanden).
|
||||
|
||||
## QA Test Results
|
||||
_To be added by /qa_
|
||||
|
||||
## Deployment
|
||||
_To be added by /deploy_
|
||||
@@ -0,0 +1,145 @@
|
||||
# PROJ-11: Audit-Log & Compliance-Berichte
|
||||
|
||||
## Status: In Progress
|
||||
**Created:** 2026-03-12
|
||||
**Last Updated:** 2026-03-13
|
||||
|
||||
## Dependencies
|
||||
- Requires: PROJ-1 (Authentifizierung) – Audit-Einträge sind an Nutzer geknüpft
|
||||
|
||||
## User Stories
|
||||
- Als Admin möchte ich sicherheitsrelevante Ereignisse im Audit-Log einsehen, damit ich Zugriffe und Änderungen nachvollziehen kann.
|
||||
- Als Admin möchte ich den Audit-Log nach Datum, Nutzer und Ereignistyp filtern.
|
||||
- Als Admin möchte ich den Audit-Log als CSV exportieren, damit ich ihn für externe Prüfungen verwenden kann.
|
||||
|
||||
## Erfasste Ereignisse
|
||||
**Ja – wird geloggt:**
|
||||
- Login (Erfolg und Fehlschlag) inkl. IP-Adresse
|
||||
- Logout
|
||||
- Suchanfragen (Suchbegriff, Anzahl Treffer, Nutzer)
|
||||
- Import gestartet / abgeschlossen / fehlgeschlagen (Quelle, Anzahl E-Mails)
|
||||
- Export gestartet / abgeschlossen (Format, Anzahl E-Mails)
|
||||
|
||||
**Nein – wird nicht geloggt:**
|
||||
- Lesezugriff auf einzelne E-Mails (kein per-Mail-Leselogging)
|
||||
|
||||
## Acceptance Criteria
|
||||
- [ ] Jeder Log-Eintrag enthält: Zeitstempel (UTC), Nutzer-ID, Ereignistyp, Details (z.B. Suchbegriff, Import-Quelle), IP-Adresse
|
||||
- [ ] Audit-Log-Ansicht **nur für Auditoren** unter `/audit/` – kein Zugriff für `admin` oder `user`
|
||||
- [ ] Filterung nach: Datum (von–bis), Nutzer, Ereignistyp
|
||||
- [ ] Paginierung (50 Einträge pro Seite)
|
||||
- [ ] Export als CSV (gefilterte oder vollständige Ansicht, Streaming-Download)
|
||||
- [ ] Einträge sind unveränderlich: kein UPDATE/DELETE durch Admin oder Anwendung möglich
|
||||
- [ ] Retention konfigurierbar in `config.yml` (`audit.retention_days`), kein Standardwert erzwungen
|
||||
- [ ] Doppelte Speicherung: PostgreSQL (für GUI-Abfragen) + Append-only Logdatei auf Disk (als unveränderliches Backup)
|
||||
- [ ] Logdatei-Format: JSON Lines (ein Eintrag pro Zeile)
|
||||
|
||||
## Edge Cases
|
||||
- Audit-Log über Jahre sehr groß → paginierte DB-Abfragen mit Index auf `(timestamp, event_type, user_id)`, kein Full-Table-Scan
|
||||
- Nutzer DSGVO-gelöscht → Audit-Einträge behalten, `user_id` durch `"anonymized"` ersetzen, IP-Adresse löschen
|
||||
- Logdatei nicht beschreibbar beim Start → Warnung loggen, Dienst läuft weiter (DB-Log bleibt aktiv)
|
||||
- Gleichzeitige Schreibzugriffe auf Logdatei → Append mit file lock
|
||||
|
||||
## Technical Requirements
|
||||
- PostgreSQL: separate Tabelle `audit_log`, kein DELETE/UPDATE per DB-Constraint (Row-Level Security oder Trigger)
|
||||
- Logdatei: `/var/log/archivmail/audit.log` (Pfad konfigurierbar), append-only, JSON Lines
|
||||
- Log-Rotation über `logrotate` (extern konfiguriert), Datei wird nie vom Dienst selbst rotiert oder gelöscht
|
||||
- Zeitstempel immer UTC (RFC 3339)
|
||||
|
||||
---
|
||||
## Tech Design (Solution Architect)
|
||||
|
||||
### Komponentenstruktur
|
||||
|
||||
**Next.js Frontend (`/audit/*`):**
|
||||
```
|
||||
/audit ← nur für Auditoren (RoleGuard)
|
||||
└── AuditTable
|
||||
├── Spalten: Zeitstempel, Nutzer, Ereignis, Details, IP
|
||||
├── Filter-Leiste
|
||||
│ ├── Datepicker: von – bis
|
||||
│ ├── Dropdown: Nutzer auswählen
|
||||
│ └── Dropdown: Ereignistyp
|
||||
├── Paginierung (50 pro Seite)
|
||||
└── [CSV exportieren] Button → Streaming-Download
|
||||
```
|
||||
|
||||
**Go Backend:**
|
||||
```
|
||||
Audit-Service (intern, kein eigener HTTP-Handler)
|
||||
├── WriteEvent(event AuditEvent)
|
||||
│ ├── → INSERT INTO audit_log (kein UPDATE/DELETE je möglich)
|
||||
│ └── → Append zu /var/log/archivmail/audit.log (JSON Line)
|
||||
│ └── File-Lock beim Schreiben (sync.Mutex)
|
||||
│
|
||||
└── Audit-API (nur für Auditoren)
|
||||
├── GET /api/audit/events ← Paginiert, gefiltert
|
||||
└── GET /api/audit/export ← Streaming-CSV-Download
|
||||
```
|
||||
|
||||
### Datenmodell
|
||||
|
||||
**Tabelle `audit_log`** (append-only via DB-Trigger):
|
||||
|
||||
| Feld | Beschreibung |
|
||||
|------|-------------|
|
||||
| `id` | Sequentielle ID |
|
||||
| `timestamp` | UTC, RFC 3339 |
|
||||
| `user_id` | Nutzer-ID (NULL nach DSGVO-Löschung) |
|
||||
| `user_email` | E-Mail zum Zeitpunkt des Events (für Lesbarkeit nach Anonymisierung) |
|
||||
| `event_type` | `login_ok`, `login_fail`, `logout`, `search`, `import_start`, `import_done`, `import_fail`, `export_start`, `export_done` |
|
||||
| `details` | JSON: Suchbegriff / Import-Quelle / Anzahl / etc. |
|
||||
| `ip_address` | IPv4/IPv6 (NULL nach DSGVO-Löschung) |
|
||||
|
||||
**DB-Constraint:** PostgreSQL-Trigger verhindert `UPDATE` und `DELETE` auf der gesamten Tabelle → physische Unveränderlichkeit.
|
||||
|
||||
**Logdatei-Format** (`/var/log/archivmail/audit.log`, JSON Lines):
|
||||
```
|
||||
{"ts":"2024-03-01T10:00:00Z","user":"alice@firma.de","event":"search","details":{"q":"Rechnung","hits":42},"ip":"192.168.1.1"}
|
||||
```
|
||||
|
||||
### Schreibfluss
|
||||
|
||||
```
|
||||
Beliebige Aktion (Login, Suche, Import...)
|
||||
│
|
||||
▼
|
||||
audit.WriteEvent() aufgerufen
|
||||
│
|
||||
├── PostgreSQL INSERT (non-blocking, Goroutine)
|
||||
│
|
||||
└── File-Append mit sync.Mutex
|
||||
(Datei nicht beschreibbar? → Warnung auf stderr, Dienst läuft weiter)
|
||||
```
|
||||
|
||||
### DSGVO-Löschfluss (Nutzer anonymisieren)
|
||||
|
||||
```
|
||||
DELETE /api/admin/users/{id}
|
||||
│
|
||||
├── audit_log: user_id → NULL, ip_address → NULL
|
||||
│ user_email → "anonymized"
|
||||
└── Logdatei: bleibt unverändert (tamper-evident)
|
||||
```
|
||||
|
||||
### Technische Entscheidungen
|
||||
|
||||
| Entscheidung | Begründung |
|
||||
|---|---|
|
||||
| **Audit-Ansicht nur für Auditoren** | Strict role separation — Admin hat keine Einsicht in Zugriffsprotokolle |
|
||||
| **DB-Trigger für Unveränderlichkeit** | Applikationscode kann versehentlich löschen — Trigger ist eine härtere Garantie |
|
||||
| **Doppelte Speicherung** | DB für GUI-Abfragen; Logdatei als tamper-evident Backup für externe Prüfungen |
|
||||
| **logrotate extern** | Dienst rotiert nie selbst — Logdatei bleibt unter Systemadmin-Kontrolle |
|
||||
| **DSGVO: IP-Adresse löschen, Event behalten** | Personenbezug entfernen, Compliance-Nachweis bleibt erhalten |
|
||||
| **Composite Index `(timestamp, event_type, user_id)`** | Schnelle gefilterte Abfragen auch bei sehr großem Log über Jahre |
|
||||
|
||||
### Abhängigkeiten
|
||||
|
||||
**Go Backend:** Nur Stdlib + pgx (bereits vorhanden).
|
||||
**Next.js:** shadcn/ui Table, Select, DatePicker (bereits installiert).
|
||||
|
||||
## QA Test Results
|
||||
_To be added by /qa_
|
||||
|
||||
## Deployment
|
||||
_To be added by /deploy_
|
||||
@@ -0,0 +1,122 @@
|
||||
# PROJ-12: E-Mail-Export (EML / PDF)
|
||||
|
||||
## Status: In Progress
|
||||
**Created:** 2026-03-12
|
||||
**Last Updated:** 2026-03-13
|
||||
|
||||
## Dependencies
|
||||
- Requires: PROJ-1 (Authentifizierung)
|
||||
- Requires: PROJ-6 (Volltext-Suche) – Export aus Suchergebnissen
|
||||
- Requires: PROJ-7 (E-Mail-Ansicht)
|
||||
- Requires: PROJ-11 (Audit-Log) – Export-Aktionen werden geloggt
|
||||
|
||||
## User Stories
|
||||
- Als Nutzer möchte ich eine einzelne E-Mail als EML-Datei exportieren, damit ich sie in einem E-Mail-Client öffnen kann.
|
||||
- Als Nutzer möchte ich eine E-Mail als PDF drucken/exportieren, damit ich sie für Behörden oder Verträge verwenden kann.
|
||||
- Als Admin möchte ich mehrere E-Mails als ZIP-Archiv exportieren, damit ich bei einer Anfrage mehrere Mails auf einmal liefern kann.
|
||||
- Als System möchte ich jeden Export im Audit-Log erfassen.
|
||||
|
||||
## Acceptance Criteria
|
||||
- [ ] Einzelexport EML: Original-MIME-Inhalt wird unverändert heruntergeladen
|
||||
- [ ] Einzelexport PDF: E-Mail-Header + Body als gut lesbares PDF gerendert, Anhänge als separate Dateien erwähnt
|
||||
- [ ] Massenexport: Auswahl mehrerer E-Mails (Checkbox in Suchergebnissen), ZIP-Download
|
||||
- [ ] ZIP enthält: EML-Dateien + optionale Anhänge + Manifest (CSV mit Metadaten)
|
||||
- [ ] Massenexport-Limit konfigurierbar (Standard: max. 500 E-Mails pro Export)
|
||||
- [ ] Jeder Export wird im Audit-Log erfasst (Nutzer, Anzahl E-Mails, Format)
|
||||
- [ ] Zugriffsschutz: Nutzer kann nur eigene E-Mails exportieren
|
||||
|
||||
## Edge Cases
|
||||
- Export von 500 E-Mails mit großen Anhängen → Streaming-ZIP, kein Speicher-Overflow
|
||||
- PDF-Rendering von komplexem HTML → graceful Fallback auf Plain-Text-PDF
|
||||
- Nutzer wählt E-Mails aus, auf die er keinen Zugriff hat → diese werden aus Export-Liste entfernt
|
||||
|
||||
## Technical Requirements
|
||||
- ZIP-Erstellung als Stream (nicht komplett in Memory)
|
||||
- PDF-Generierung serverseitig (z.B. wkhtmltopdf oder Go-PDF-Bibliothek)
|
||||
|
||||
---
|
||||
## Tech Design (Solution Architect)
|
||||
|
||||
### Komponentenstruktur
|
||||
|
||||
**Next.js Frontend:**
|
||||
```
|
||||
Suchergebnisse (PROJ-6, Erweiterung)
|
||||
├── Checkbox pro Treffer (Multi-Select)
|
||||
├── [Exportieren] Button (aktiv wenn ≥1 ausgewählt)
|
||||
└── Export-Dialog
|
||||
├── Format: EML | PDF
|
||||
├── Anhänge einschließen: ja / nein
|
||||
└── [Download starten] → POST /api/export/zip (Streaming)
|
||||
|
||||
E-Mail-Ansicht (PROJ-7, Erweiterung)
|
||||
├── [Als EML herunterladen] Button → GET /api/export/eml/{id}
|
||||
└── [Als PDF exportieren] Button → GET /api/export/pdf/{id}
|
||||
```
|
||||
|
||||
**Go Backend:**
|
||||
```
|
||||
Export-Handler
|
||||
├── GET /api/export/eml/{id} ← Einzelexport EML
|
||||
│ └── Original-.m-Datei lesen, AES-256-GCM entschlüsseln, direkt streamen
|
||||
│
|
||||
├── GET /api/export/pdf/{id} ← Einzelexport PDF
|
||||
│ ├── .m-Datei lesen + entschlüsseln
|
||||
│ ├── Header + Body extrahieren
|
||||
│ └── → PDF-Bibliothek → PDF streamen
|
||||
│ Fallback: Plain-Text-PDF wenn HTML zu komplex
|
||||
│
|
||||
└── POST /api/export/zip ← Massenexport
|
||||
├── Body: { ids: [...], format: "eml"|"pdf", attachments: bool }
|
||||
├── Zugriffscheck: Nutzer darf nur eigene Mails exportieren
|
||||
├── Max-Limit prüfen (konfigurierbar, Standard: 500)
|
||||
├── Streaming-ZIP (archive/zip Writer → ResponseWriter direkt)
|
||||
│ ├── Pro Mail: <message_id>.eml oder <message_id>.pdf
|
||||
│ ├── Anhänge: attachments/<hash>/<filename> (wenn aktiviert)
|
||||
│ └── manifest.csv (Message-ID, From, To, Subject, Date, Dateiname)
|
||||
└── → Audit-Log: export_start + export_done (Anzahl, Format)
|
||||
```
|
||||
|
||||
### Export-Fluss (Massenexport)
|
||||
|
||||
```
|
||||
POST /api/export/zip
|
||||
│
|
||||
├── Zugriffsfilter: IDs auf user-eigene Mails beschränken
|
||||
│
|
||||
├── Audit-Log: export_start
|
||||
│
|
||||
├── ZIP-Stream öffnen (Content-Type: application/zip)
|
||||
│
|
||||
└── Für jede Mail:
|
||||
.m-Datei lesen → AES-256-GCM entschlüsseln
|
||||
→ Zu ZIP hinzufügen (kein vollständiges RAM-Buffering)
|
||||
↓
|
||||
manifest.csv Zeile anhängen
|
||||
↓
|
||||
ZIP schließen → Audit-Log: export_done
|
||||
```
|
||||
|
||||
### Technische Entscheidungen
|
||||
|
||||
| Entscheidung | Begründung |
|
||||
|---|---|
|
||||
| **Streaming-ZIP** | 500 Mails mit Anhängen können mehrere GB sein — kein RAM-Overhead |
|
||||
| **Serverseitiges PDF** | Browser-Print ist nicht reproduzierbar; serverseitiges PDF ist auditierbar und einheitlich |
|
||||
| **Plain-Text-Fallback für PDF** | Komplexes HTML kann PDF-Renderer zum Absturz bringen — graceful degradation |
|
||||
| **Zugriffscheck im Export-Handler** | Serverseitige Filterung verhindert Datenlecks durch manipulierte IDs |
|
||||
| **manifest.csv im ZIP** | Nachvollziehbarkeit bei Behördenanfragen ohne jede EML einzeln öffnen zu müssen |
|
||||
| **Audit-Log für jeden Export** | Compliance-Anforderung — wer hat wann was exportiert |
|
||||
|
||||
### Abhängigkeiten
|
||||
|
||||
| Paket | Zweck |
|
||||
|---|---|
|
||||
| `archive/zip` (Stdlib) | Streaming-ZIP-Erstellung ohne externe Abhängigkeit |
|
||||
| `github.com/SebastiaanKlippert/go-wkhtmltopdf` | PDF-Generierung aus HTML (serverseitig) |
|
||||
|
||||
## QA Test Results
|
||||
_To be added by /qa_
|
||||
|
||||
## Deployment
|
||||
_To be added by /deploy_
|
||||
@@ -0,0 +1,178 @@
|
||||
# PROJ-13: REST API für externe CRM-Anbindung
|
||||
|
||||
## Status: In Progress
|
||||
**Created:** 2026-03-13
|
||||
**Last Updated:** 2026-03-13
|
||||
|
||||
## Dependencies
|
||||
- Requires: PROJ-1 (Authentifizierung) – API-Keys sind an Nutzer/Rollen gebunden
|
||||
- Requires: PROJ-5 (Speicherung & Indexierung) – Daten kommen aus dem Archiv
|
||||
- Requires: PROJ-6 (Volltext-Suche) – Suche über API nutzbar
|
||||
|
||||
## Hinweis
|
||||
Externe Systeme (CRM, ERP, Helpdesk etc.) sollen über eine dokumentierte REST API auf das Archiv zugreifen können – **ausschließlich lesend**. Schreiboperationen (Importieren, Löschen, Labeln etc.) sind über die API nicht möglich und werden nicht implementiert. Authentifizierung über API-Keys, nicht über Session-Cookies.
|
||||
|
||||
## User Stories
|
||||
- Als CRM-Administrator möchte ich einen API-Key generieren, damit mein CRM-System auf das Archiv zugreifen kann.
|
||||
- Als CRM-System möchte ich E-Mails eines bestimmten Kontakts (E-Mail-Adresse) abrufen, damit ich die Kommunikationshistorie im CRM anzeigen kann.
|
||||
- Als CRM-System möchte ich E-Mails nach Datum, Absender oder Betreff durchsuchen, damit ich gezielt relevante Mails finden kann.
|
||||
- Als Admin möchte ich API-Keys verwalten (anlegen, deaktivieren, löschen), damit ich den Zugriff kontrollieren kann.
|
||||
- Als Admin möchte ich sehen, welcher API-Key wann welche Anfragen gestellt hat.
|
||||
|
||||
## Acceptance Criteria
|
||||
- [ ] API-Key-Verwaltung im Admin-Bereich: anlegen, benennen, deaktivieren, löschen
|
||||
- [ ] API-Keys haben eine konfigurierbare Rolle (`user` oder `auditor`) – bestimmt Zugriffsumfang (Leserechte)
|
||||
- [ ] Nur `GET`-Methoden erlaubt – `POST`, `PUT`, `PATCH`, `DELETE` geben generisch 405 Method Not Allowed zurück
|
||||
- [ ] Authentifizierung via HTTP-Header: `Authorization: Bearer <api-key>`
|
||||
- [ ] Endpunkt: `GET /api/v1/mails?from=&to=&subject=&date_from=&date_to=` – Suche/Filterung
|
||||
- [ ] Endpunkt: `GET /api/v1/mails/{message_id}` – einzelne E-Mail abrufen (Metadaten)
|
||||
- [ ] Endpunkt: `GET /api/v1/mails/{message_id}/raw` – Original-EML herunterladen
|
||||
- [ ] Endpunkt: `GET /api/v1/mails?contact=email@firma.de` – alle Mails eines Kontakts (From oder To)
|
||||
- [ ] Antwortformat: JSON für Metadaten, `application/octet-stream` für Raw-EML
|
||||
- [ ] Paginierung: `?page=1&limit=25` (max. 100 pro Anfrage)
|
||||
- [ ] API-Zugriffe werden im Audit-Log erfasst (API-Key-Name, Endpunkt, Zeitstempel)
|
||||
- [ ] OpenAPI/Swagger-Dokumentation unter `/api/v1/docs`
|
||||
|
||||
## Edge Cases
|
||||
- Ungültiger oder deaktivierter API-Key → 401 Unauthorized
|
||||
- API-Key mit `user`-Rolle fragt Mails ab, auf die er keinen Zugriff hat → 403
|
||||
- Rate-Limiting: zu viele Anfragen pro API-Key → 429 Too Many Requests
|
||||
- Sehr große Ergebnismengen (>10.000 Treffer) → Paginierung erzwingen, kein Full-Dump
|
||||
- CRM fragt nicht existierende Message-ID ab → 404
|
||||
|
||||
## Technical Requirements
|
||||
- **Reine Lese-API** – ausschließlich `GET`-Endpunkte, keine Schreiboperationen
|
||||
- Eigener Route-Prefix `/api/v1/` für externe API (getrennt von interner `/api/`)
|
||||
- API-Keys: zufällig generiert (32 Byte, Base64), bcrypt-gehasht in der DB (nie im Klartext)
|
||||
- Rate-Limiting pro API-Key konfigurierbar (Standard: 60 Anfragen/Minute)
|
||||
- OpenAPI 3.0 Spec wird aus Code generiert oder manuell gepflegt
|
||||
- Versionierung: `/api/v1/` – spätere Versionen brechen bestehende Clients nicht
|
||||
|
||||
---
|
||||
## Tech Design (Solution Architect)
|
||||
|
||||
### Systemübersicht
|
||||
|
||||
```
|
||||
CRM / ERP / Helpdesk
|
||||
│
|
||||
│ GET /api/v1/mails?contact=kunde@example.com
|
||||
│ Authorization: Bearer <api-key>
|
||||
▼
|
||||
Go Backend – Externer API-Router (/api/v1/*)
|
||||
│
|
||||
├── API-Key Middleware ← statt Session-Cookie
|
||||
├── Rate Limiter
|
||||
├── Shared Search Service ←──── dieselbe Logik wie interne Suche (PROJ-6)
|
||||
└── Shared Mail Service ←──── dieselbe Logik wie Mail-Abruf (PROJ-7)
|
||||
```
|
||||
|
||||
### Komponentenstruktur
|
||||
|
||||
**Go Backend:**
|
||||
```
|
||||
/api/v1/* (externer Prefix, getrennt von internem /api/*)
|
||||
│
|
||||
├── API-Key Middleware ← ersetzt Session-Middleware
|
||||
│ ├── Bearer-Token aus Header lesen
|
||||
│ ├── SHA-256-Hash → DB-Lookup ← schneller Lookup ohne bcrypt-Overhead
|
||||
│ ├── Key deaktiviert? → 401
|
||||
│ ├── Rolle laden (user/auditor)
|
||||
│ └── Rate-Limit-Konfiguration laden
|
||||
│
|
||||
├── Rate Limiter ← Token-Bucket pro API-Key
|
||||
│ └── Limit überschritten → 429 + Retry-After Header
|
||||
│
|
||||
├── Method Guard ← alles außer GET → 405
|
||||
│
|
||||
├── Shared Search Service ← identische Logik wie /api/search (PROJ-6)
|
||||
│ ├── Xapian QueryParser
|
||||
│ ├── Rollen-Filter (user/auditor)
|
||||
│ └── PostgreSQL Metadaten-Lookup
|
||||
│
|
||||
├── Shared Mail Service ← identische Logik wie /api/mails (PROJ-7)
|
||||
│ ├── .m-Datei lesen + entschlüsseln
|
||||
│ ├── MIME-Parser
|
||||
│ └── Anhang-Streaming
|
||||
│
|
||||
├── Audit Logger ← API-Key-Name + Endpunkt + Zeitstempel
|
||||
│
|
||||
└── API-Key-Verwaltung (Admin)
|
||||
├── POST /api/admin/apikeys ← Key generieren (einmalige Anzeige)
|
||||
├── GET /api/admin/apikeys ← Liste (Name + Rolle + letzter Zugriff)
|
||||
└── DELETE /api/admin/apikeys/{id}
|
||||
```
|
||||
|
||||
### API-Key Authentifizierungsfluss
|
||||
|
||||
```
|
||||
CRM-System
|
||||
│
|
||||
│ Authorization: Bearer am_a1b2c3d4e5f6...
|
||||
▼
|
||||
API-Key Middleware
|
||||
├─ Präfix "am_" prüfen
|
||||
├─ SHA-256(token) → DB-Lookup (indiziert)
|
||||
├─ Key gefunden + aktiv? Nein → 401
|
||||
└─ Ja → Rolle + Rate-Limit laden
|
||||
│
|
||||
▼
|
||||
Rate Limiter (Token-Bucket)
|
||||
├─ Limit erreicht? → 429 + Retry-After: 30
|
||||
└─ OK → weiter
|
||||
│
|
||||
▼
|
||||
Method Guard
|
||||
├─ POST/PUT/DELETE? → 405
|
||||
└─ GET → weiter
|
||||
│
|
||||
▼
|
||||
Shared Service Layer
|
||||
│
|
||||
▼
|
||||
Audit Logger → API-Key-Name + Endpunkt + Zeitstempel
|
||||
```
|
||||
|
||||
### Datenmodell
|
||||
|
||||
**Tabelle `api_keys`:**
|
||||
|
||||
| Feld | Beschreibung |
|
||||
|------|-------------|
|
||||
| `id` | Interne ID |
|
||||
| `name` | Bezeichnung (z.B. "CRM Salesforce") |
|
||||
| `token_hash` | SHA-256 des Tokens (für schnellen Lookup, indiziert) |
|
||||
| `role` | `user` oder `auditor` |
|
||||
| `active` | `true` / `false` |
|
||||
| `rate_limit` | Anfragen pro Minute (Standard: 60) |
|
||||
| `created_at` | Erstellungszeitpunkt |
|
||||
| `last_used_at` | Letzter erfolgreicher Zugriff |
|
||||
|
||||
**Key-Format:**
|
||||
```
|
||||
Generiert: am_<32-Byte-random-Base64>
|
||||
Gespeichert: SHA-256(token) in DB
|
||||
Angezeigt: einmalig im Admin-UI – danach nicht mehr abrufbar
|
||||
```
|
||||
|
||||
### Technische Entscheidungen
|
||||
|
||||
| Entscheidung | Begründung |
|
||||
|---|---|
|
||||
| **Shared Service Layer** | Suche und Mail-Abruf teilen dieselbe Go-Logik mit der internen API – kein doppelter Code |
|
||||
| **SHA-256 statt bcrypt** | API-Keys sind kryptografisch zufällig (32 Byte) – SHA-256 reicht, bcrypt wäre bei jeder Anfrage zu langsam |
|
||||
| **`am_`-Präfix** | Erkennungsmerkmal für archivmail-Keys – einfach filterbar in Logs |
|
||||
| **Token einmalig anzeigen** | Nur Hash gespeichert – kein späteres Auslesen möglich (wie GitHub PAT) |
|
||||
| **Token-Bucket Rate Limiter** | Gleichmäßige Anfragen erlaubt, kurze Bursts toleriert |
|
||||
| **`/api/v1/` Prefix** | Klare Versionierung – zukünftige `/api/v2/` bricht bestehende Clients nicht |
|
||||
| **Audit-Log bei API-Zugriffen** | Externe Zugriffe werden geloggt (anders als interne Lesezugriffe) |
|
||||
|
||||
### Abhängigkeiten
|
||||
|
||||
Kein zusätzliches Paket – Rate-Limiter und SHA-256 aus der Go-Stdlib (`crypto/sha256`, `sync`).
|
||||
|
||||
## QA Test Results
|
||||
_To be added by /qa_
|
||||
|
||||
## Deployment
|
||||
_To be added by /deploy_
|
||||
@@ -0,0 +1,147 @@
|
||||
# PROJ-14: E-Mail-Import: POP3-Verbindung
|
||||
|
||||
## Status: In Progress
|
||||
**Created:** 2026-03-13
|
||||
**Last Updated:** 2026-03-13
|
||||
|
||||
## Dependencies
|
||||
- Requires: PROJ-1 (Authentifizierung) – nur Admins verwalten POP3-Verbindungen
|
||||
- Requires: PROJ-5 (Speicherung & Indexierung) – importierte E-Mails werden gespeichert
|
||||
|
||||
## Hinweis
|
||||
POP3 kennt keine Ordnerstruktur – es gibt nur eine Inbox. Alle Mails werden importiert. Da POP3 keine UID-basierte Synchronisation unterstützt, ist nur ein einmaliger Initial-Import sinnvoll (kein regelmäßiger Sync wie bei IMAP).
|
||||
|
||||
## User Stories
|
||||
- Als Admin möchte ich einen POP3-Server konfigurieren (Host, Port, Zugangsdaten), damit ich E-Mails von dort importieren kann.
|
||||
- Als System möchte ich alle vorhandenen E-Mails vom POP3-Server herunterladen und archivieren.
|
||||
- Als Admin möchte ich den Verbindungsstatus und Importfortschritt sehen.
|
||||
- Als System möchte ich Duplikate (gleiche Message-ID) überspringen.
|
||||
|
||||
## Acceptance Criteria
|
||||
- [ ] Konfigurationsformular: Host, Port, Verbindungsmodus (SSL/TLS, STARTTLS, None), Benutzername, Passwort
|
||||
- [ ] **Verbindungsmodi:**
|
||||
- `SSL/TLS` – direkte TLS-Verbindung (Port 995)
|
||||
- `STARTTLS` – startet unverschlüsselt, wird auf TLS hochgestuft (Port 110)
|
||||
- `None` – unverschlüsselt, nur für lokale/Testumgebungen
|
||||
- [ ] Verbindungstest vor dem Speichern (Timeout: 10 Sekunden)
|
||||
- [ ] Passwörter AES-256-GCM verschlüsselt in der DB gespeichert
|
||||
- [ ] Import: alle Mails vom Server herunterladen
|
||||
- [ ] Duplikate (Message-ID) werden übersprungen
|
||||
- [ ] Fortschrittsanzeige während Import (X von Y Mails)
|
||||
- [ ] Abschlussbericht: importiert / übersprungen / Fehler
|
||||
- [ ] Mails bleiben nach dem Import auf dem POP3-Server (kein DELE-Befehl)
|
||||
|
||||
## Edge Cases
|
||||
- POP3-Server nicht erreichbar → Fehlermeldung mit Retry-Option
|
||||
- Falsche Zugangsdaten → klare Fehlermeldung
|
||||
- Mail ohne Message-ID → synthetische ID generieren (SHA-256 des Inhalts)
|
||||
- Verbindungsabbruch während Import → bei Neustart von vorne (POP3 hat keine UIDs zum Weiterführen)
|
||||
- Sehr großes Postfach (10.000+ Mails) → sequenzielles Herunterladen, kein Speicher-Overflow
|
||||
|
||||
## Technical Requirements
|
||||
- **Verbindungsmodi:** SSL/TLS (Port 995), STARTTLS (Port 110), None
|
||||
- POP3 unterstützt keine Ordner – es gibt nur die Inbox, keine Ordner-Erkennung nötig
|
||||
- Kein regelmäßiger Sync – nur manueller Import (POP3 bietet keine zuverlässige Duplikatserkennung über Sessions hinaus)
|
||||
- Zugangsdaten AES-256-GCM verschlüsselt in der DB
|
||||
|
||||
---
|
||||
## Tech Design (Solution Architect)
|
||||
|
||||
### Komponentenstruktur
|
||||
|
||||
**Next.js Frontend (Admin-Bereich):**
|
||||
```
|
||||
/admin/pop3
|
||||
├── POP3-Verbindungsliste
|
||||
│ └── VerbindungsCard
|
||||
│ ├── Name, Host, Status
|
||||
│ ├── Letzter Import + Anzahl
|
||||
│ └── Aktionen: Bearbeiten / Löschen / Import starten
|
||||
├── Verbindung-Formular
|
||||
│ ├── Host, Port, Verbindungsmodus (SSL/TLS | STARTTLS | None)
|
||||
│ ├── Benutzername, Passwort
|
||||
│ └── [Verbindung testen] Button
|
||||
└── Import-Fortschrittsanzeige
|
||||
├── Fortschrittsbalken
|
||||
└── Abschlussbericht
|
||||
```
|
||||
|
||||
**Go Backend:**
|
||||
```
|
||||
POP3-Dienst
|
||||
├── POST /api/admin/pop3 ← Verbindung anlegen
|
||||
├── POST /api/admin/pop3/test ← Verbindung testen
|
||||
├── GET /api/admin/pop3 ← auflisten
|
||||
├── DELETE /api/admin/pop3/{id} ← löschen
|
||||
│
|
||||
├── POP3-Client
|
||||
│ ├── SSL/TLS + STARTTLS Handler
|
||||
│ ├── USER/PASS Login
|
||||
│ ├── STAT → Anzahl Mails + Gesamtgröße
|
||||
│ ├── LIST → Message-Nummern
|
||||
│ └── RETR → Mail herunterladen (kein DELE)
|
||||
│
|
||||
└── Import-Worker (Hintergrund-Goroutine)
|
||||
├── Sequenziell: RETR 1, RETR 2, ...
|
||||
├── Duplikat-Check (Message-ID)
|
||||
├── → Storage Coordinator (PROJ-5)
|
||||
└── Fortschritt in DB
|
||||
```
|
||||
|
||||
### Verbindungsmodus-Übersicht
|
||||
|
||||
| Modus | Port | Ablauf |
|
||||
|-------|------|--------|
|
||||
| `SSL/TLS` | 995 | TLS direkt beim Verbindungsaufbau |
|
||||
| `STARTTLS` | 110 | Verbindung startet plain → STLS-Befehl → TLS |
|
||||
| `None` | 110 | Unverschlüsselt (nur Testumgebung) |
|
||||
|
||||
### Importfluss
|
||||
|
||||
```
|
||||
Admin klickt "Import starten"
|
||||
│
|
||||
▼
|
||||
POP3-Client verbindet (SSL/TLS oder STARTTLS)
|
||||
│
|
||||
▼
|
||||
STAT → Gesamtanzahl Mails (z.B. 3.842)
|
||||
│
|
||||
▼
|
||||
LIST → Message-Nummern [1, 2, 3, ..., 3842]
|
||||
│
|
||||
▼
|
||||
Für jede Message-Nummer:
|
||||
RETR <n> → rohe Mail (RFC 2822)
|
||||
Message-ID Duplikat? → überspringen
|
||||
→ Storage Coordinator (PROJ-5)
|
||||
Fortschritt: n / 3842
|
||||
│
|
||||
▼
|
||||
Kein DELE → Mails bleiben auf dem Server
|
||||
│
|
||||
▼
|
||||
QUIT → Verbindung trennen
|
||||
Abschlussbericht speichern
|
||||
```
|
||||
|
||||
### Technische Entscheidungen
|
||||
|
||||
| Entscheidung | Begründung |
|
||||
|---|---|
|
||||
| **Kein DELE** | Archiv löscht nichts vom Quellserver – nur lesen und archivieren |
|
||||
| **Kein regelmäßiger Sync** | POP3 hat keine UIDs – es gibt keine zuverlässige Möglichkeit festzustellen welche Mails bereits importiert wurden |
|
||||
| **Synthetische Message-ID bei Fehlen** | POP3-Mails ohne Message-ID bekommen SHA-256(Inhalt) als ID – Duplikatserkennung bleibt konsistent |
|
||||
| **Gleiche Codebasis wie IMAP-Worker** | Import-Worker-Struktur identisch – nur POP3-Client statt IMAP-Client |
|
||||
|
||||
### Abhängigkeiten
|
||||
|
||||
| Paket | Zweck |
|
||||
|---|---|
|
||||
| `github.com/emersion/go-message` | POP3-Client mit TLS/STARTTLS |
|
||||
|
||||
## QA Test Results
|
||||
_To be added by /qa_
|
||||
|
||||
## Deployment
|
||||
_To be added by /deploy_
|
||||
@@ -0,0 +1,195 @@
|
||||
# PROJ-15: CLI Import & Export
|
||||
|
||||
## Status: In Progress
|
||||
**Created:** 2026-03-13
|
||||
**Last Updated:** 2026-03-13
|
||||
|
||||
## Dependencies
|
||||
- Requires: PROJ-5 (Speicherung & Indexierung) – Import nutzt Storage Coordinator
|
||||
- Requires: PROJ-1 (Authentifizierung) – CLI läuft als Systembenutzer `archivmail`, kein Web-Login
|
||||
|
||||
## Hinweis
|
||||
Die CLI läuft direkt auf dem Server als Systembenutzer `archivmail` – kein Web-Login, kein API-Key. Zugriff über den gleichen Storage Coordinator wie der Daemon. Gedacht für automatisierte Skripte, Cron-Jobs und administrative Bulk-Operationen.
|
||||
|
||||
## User Stories
|
||||
- Als Systemadministrator möchte ich EML/MBOX-Dateien per CLI importieren, damit ich Bulk-Importe skriptbasiert automatisieren kann.
|
||||
- Als Systemadministrator möchte ich E-Mails per CLI exportieren (EML/MBOX), damit ich Sicherungen oder Migrationen durchführen kann.
|
||||
- Als Systemadministrator möchte ich Import/Export mit Pfadangabe starten, damit ich Quell- und Zielverzeichnisse flexibel festlegen kann.
|
||||
- Als System möchte ich Import-Fortschritt und Ergebnis auf stdout ausgeben, damit Skripte den Status auswerten können.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
### Import
|
||||
- [ ] `archivmail import --file /pfad/zu/datei.eml` – einzelne EML importieren
|
||||
- [ ] `archivmail import --file /pfad/zu/archiv.mbox` – MBOX importieren
|
||||
- [ ] `archivmail import --dir /pfad/zum/verzeichnis/` – alle EML-Dateien in einem Verzeichnis importieren (rekursiv optional: `--recursive`)
|
||||
- [ ] Fortschrittsausgabe auf stdout (eine Zeile pro Mail oder Fortschrittsbalken)
|
||||
- [ ] Exit-Code 0 bei Erfolg, 1 bei Fehler
|
||||
- [ ] Duplikate werden übersprungen (gleiche Message-ID), kein Fehler
|
||||
- [ ] `--dry-run` Flag: zeigt was importiert würde ohne tatsächlich zu speichern
|
||||
|
||||
### Export
|
||||
- [ ] `archivmail export --out /pfad/ziel/` – alle Mails als EML-Dateien exportieren
|
||||
- [ ] `archivmail export --out /pfad/archiv.mbox` – alle Mails als MBOX exportieren
|
||||
- [ ] `archivmail export --from alice@firma.de --out /pfad/` – Filter nach Absender
|
||||
- [ ] `archivmail export --date-from 2024-01-01 --date-to 2024-12-31 --out /pfad/` – Filter nach Datum
|
||||
- [ ] `archivmail export --query "Rechnung" --out /pfad/` – Filter per Volltext-Suche (Xapian)
|
||||
- [ ] Exportierte Mails werden entschlüsselt (Klartext EML auf Disk)
|
||||
- [ ] `--format eml` (Standard) oder `--format mbox`
|
||||
|
||||
### Allgemein
|
||||
- [ ] CLI läuft als Systembenutzer `archivmail` – liest Key aus `/etc/archivmail/keyfile`
|
||||
- [ ] Fehler werden auf stderr ausgegeben
|
||||
- [ ] `archivmail help` zeigt Übersicht aller Befehle
|
||||
- [ ] `archivmail version` zeigt Version
|
||||
|
||||
## Edge Cases
|
||||
- Verzeichnis beim Import enthält keine EML-Dateien → Hinweis + Exit-Code 0
|
||||
- Zieldatei beim Export bereits vorhanden → Fehler mit `--force` Flag zum Überschreiben
|
||||
- Kein Lese-/Schreibrecht auf Pfad → klare Fehlermeldung auf stderr
|
||||
- Import unterbrochen (Ctrl+C) → partiell importierte Mails werden gespeichert, kein Rollback (Archiv ist append-only)
|
||||
- Export bei leerem Archiv → leeres Verzeichnis / leere MBOX, Exit-Code 0
|
||||
|
||||
## Technical Requirements
|
||||
- CLI ist Teil desselben Go-Binaries (`archivmail`) – Subcommands via `archivmail <command>`
|
||||
- Zugriff auf Storage Coordinator direkt (kein HTTP-Umweg über den laufenden Daemon)
|
||||
- Key-Datei muss lesbar sein (`/etc/archivmail/keyfile`, `chmod 400`, Owner `archivmail`)
|
||||
- Kann parallel zum laufenden Daemon betrieben werden (Xapian WritableDatabase: Lock beachten)
|
||||
- Strukturierte Ausgabe optional: `--json` Flag für maschinenlesbare Ausgabe
|
||||
|
||||
---
|
||||
## Tech Design (Solution Architect)
|
||||
|
||||
### CLI-Struktur
|
||||
|
||||
```
|
||||
archivmail <command> [flags]
|
||||
|
||||
Commands:
|
||||
import E-Mails importieren (EML, MBOX, Verzeichnis)
|
||||
export E-Mails exportieren (EML, MBOX)
|
||||
version Version anzeigen
|
||||
help Hilfe anzeigen
|
||||
|
||||
archivmail import
|
||||
--file /pfad/datei.eml oder .mbox
|
||||
--dir /pfad/verzeichnis/
|
||||
--recursive Unterverzeichnisse einschließen (mit --dir)
|
||||
--dry-run Simulation ohne Speichern
|
||||
--json Maschinenlesbare Ausgabe (JSON)
|
||||
|
||||
archivmail export
|
||||
--out /pfad/ziel/ oder /pfad/archiv.mbox (Pflicht)
|
||||
--format eml (Standard) | mbox
|
||||
--from Absender-Filter
|
||||
--to Empfänger-Filter
|
||||
--date-from Datum von (ISO 8601: 2024-01-01)
|
||||
--date-to Datum bis (ISO 8601: 2024-12-31)
|
||||
--query Volltext-Suche (Xapian QueryParser)
|
||||
--force Zieldatei überschreiben
|
||||
--json Maschinenlesbare Ausgabe (JSON)
|
||||
```
|
||||
|
||||
### Komponentenstruktur
|
||||
|
||||
```
|
||||
archivmail (Go-Binary)
|
||||
│
|
||||
├── main.go ← Subcommand-Router (import / export / ...)
|
||||
│
|
||||
├── cmd/import.go
|
||||
│ ├── Flag-Parsing
|
||||
│ ├── Dateityp-Erkennung (.eml / .mbox / Verzeichnis)
|
||||
│ ├── EML-Parser
|
||||
│ ├── MBOX-Parser (zeilenweise)
|
||||
│ └── → Storage Coordinator (PROJ-5, direkt, kein HTTP)
|
||||
│
|
||||
└── cmd/export.go
|
||||
├── Flag-Parsing
|
||||
├── Filter-Builder (from, to, date, query)
|
||||
├── → Xapian ReadonlyDatabase (Suche/Filter)
|
||||
├── → PostgreSQL Metadaten-Lookup
|
||||
├── → .m-Datei lesen + AES-256-GCM entschlüsseln
|
||||
└── Schreiben als EML-Dateien oder MBOX
|
||||
```
|
||||
|
||||
### Import-Fluss
|
||||
|
||||
```
|
||||
$ archivmail import --dir /backup/mails/ --recursive
|
||||
|
||||
Key laden aus /etc/archivmail/keyfile
|
||||
Verzeichnis scannen → 3.842 .eml-Dateien gefunden
|
||||
[████████░░] 2.150 / 3.842 (übersprungen: 12 Duplikate)
|
||||
|
||||
Fertig:
|
||||
Importiert: 2.130
|
||||
Übersprungen: 12 (Duplikate)
|
||||
Fehler: 0
|
||||
```
|
||||
|
||||
### Export-Fluss
|
||||
|
||||
```
|
||||
$ archivmail export --from alice@firma.de \
|
||||
--date-from 2024-01-01 \
|
||||
--out /backup/export/
|
||||
|
||||
Key laden aus /etc/archivmail/keyfile
|
||||
Xapian: 847 Mails gefunden (Filter: from=alice, date>=2024-01-01)
|
||||
Exportiere nach /backup/export/
|
||||
[████████████] 847 / 847
|
||||
|
||||
Fertig:
|
||||
Exportiert: 847 EML-Dateien
|
||||
Ziel: /backup/export/
|
||||
```
|
||||
|
||||
### JSON-Ausgabe (--json Flag)
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "done",
|
||||
"imported": 2130,
|
||||
"skipped": 12,
|
||||
"errors": 0,
|
||||
"duration_sec": 42
|
||||
}
|
||||
```
|
||||
|
||||
### Xapian-Lock beim parallelen Betrieb
|
||||
|
||||
```
|
||||
Daemon läuft (WritableDatabase hält Lock für Index-Worker)
|
||||
│
|
||||
CLI export → ReadonlyDatabase → kein Lock-Konflikt ✓
|
||||
CLI import → Storage Coordinator → WritableDatabase
|
||||
│
|
||||
└── Lock bereits gehalten?
|
||||
→ Warten (max. 30 Sek.) → dann Fehlermeldung:
|
||||
"Index locked by running daemon. Stop daemon or retry."
|
||||
```
|
||||
|
||||
### Technische Entscheidungen
|
||||
|
||||
| Entscheidung | Begründung |
|
||||
|---|---|
|
||||
| **Gleiche Binary, Subcommands** | Kein separates CLI-Tool – `archivmail import` und `archivmail serve` teilen Code und Storage Coordinator |
|
||||
| **Direkter Speicherzugriff, kein HTTP** | CLI läuft als `archivmail`-User mit Dateisystem-Zugriff – kein laufender Daemon nötig für Import/Export |
|
||||
| **`--dry-run`** | Sicher testen ohne Daten zu verändern – wichtig für große Bulk-Imports |
|
||||
| **`--json` Flag** | Maschinenlesbar für Cron-Jobs, Monitoring-Skripte, Ansible-Playbooks |
|
||||
| **Exit-Codes** | 0 = Erfolg, 1 = Fehler – Standard für Shell-Skripting |
|
||||
| **Xapian ReadonlyDatabase für Export** | Export kann parallel zum Daemon laufen ohne Lock-Konflikte |
|
||||
|
||||
### Abhängigkeiten
|
||||
|
||||
| Paket | Zweck |
|
||||
|---|---|
|
||||
| `github.com/spf13/cobra` | Subcommand-CLI-Framework |
|
||||
| Xapian CGo-Bindings | Volltext-Filter beim Export (bereits PROJ-5) |
|
||||
|
||||
## QA Test Results
|
||||
_To be added by /qa_
|
||||
|
||||
## Deployment
|
||||
_To be added by /deploy_
|
||||
@@ -0,0 +1,109 @@
|
||||
---
|
||||
id: PROJ-16
|
||||
title: LDAP / Active Directory Anbindung
|
||||
status: In Progress
|
||||
priority: P1
|
||||
created: 2026-03-13
|
||||
---
|
||||
|
||||
# PROJ-16 — LDAP / Active Directory Anbindung
|
||||
|
||||
## Ziel
|
||||
Authentifizierung gegen einen LDAP-Server (OpenLDAP, Microsoft Active Directory, Samba AD).
|
||||
Lokale Accounts bleiben weiterhin nutzbar. LDAP-User werden beim ersten Login automatisch
|
||||
in der Datenbank angelegt (`source: ldap`) und bei jedem Login synchronisiert.
|
||||
|
||||
## User Stories
|
||||
|
||||
- **Als Admin** möchte ich LDAP in `config.yml` konfigurieren, damit Mitarbeiter ihre
|
||||
bestehenden Windows/AD-Zugangsdaten nutzen können.
|
||||
- **Als Endnutzer** möchte ich mich mit meinem AD-Passwort anmelden, ohne einen separaten
|
||||
archivmail-Account zu benötigen.
|
||||
- **Als Admin** möchte ich LDAP-User einer Rolle (user/auditor/admin) zuweisen können,
|
||||
entweder per fester Zuordnung oder über AD-Gruppen.
|
||||
- **Als Admin** möchte ich LDAP deaktivieren können, ohne den restlichen Betrieb zu stören.
|
||||
|
||||
## Akzeptanzkriterien
|
||||
|
||||
- [ ] Login mit LDAP-Credentials funktioniert wenn `ldap.enabled: true`
|
||||
- [ ] Lokale Accounts funktionieren weiterhin (Fallback wenn LDAP fehlschlägt oder deaktiviert)
|
||||
- [ ] LDAP-User werden beim Login automatisch via `UpsertLDAPUser` angelegt/aktualisiert
|
||||
- [ ] Rollen-Mapping via AD-Gruppen konfigurierbar (optional, Fallback: default_role)
|
||||
- [ ] STARTTLS und LDAPS (Port 636) werden unterstützt
|
||||
- [ ] Bind-User (Service Account) für AD-Suche konfigurierbar
|
||||
- [ ] Fehlermeldung bei falschem Passwort ist identisch zu lokalem Login (kein Info-Leak)
|
||||
- [ ] LDAP-Fehler landen im Audit-Log
|
||||
- [ ] Konfigurierbar per `config.yml` Abschnitt `ldap:`
|
||||
|
||||
## Konfigurationsformat (`config.yml`)
|
||||
|
||||
```yaml
|
||||
ldap:
|
||||
enabled: true
|
||||
url: "ldap://192.168.1.10:389" # oder ldaps://...
|
||||
bind_dn: "CN=archivmail-svc,OU=ServiceAccounts,DC=corp,DC=local"
|
||||
bind_password: "geheim"
|
||||
base_dn: "OU=Users,DC=corp,DC=local"
|
||||
user_filter: "(sAMAccountName=%s)" # %s wird durch eingegebenen Username ersetzt
|
||||
tls: false # STARTTLS
|
||||
tls_skip_verify: false
|
||||
default_role: "user" # Rolle für neue LDAP-User
|
||||
group_mappings: # optional: AD-Gruppe → archivmail-Rolle
|
||||
- group_dn: "CN=archivmail-admins,OU=Groups,DC=corp,DC=local"
|
||||
role: "admin"
|
||||
- group_dn: "CN=archivmail-auditors,OU=Groups,DC=corp,DC=local"
|
||||
role: "auditor"
|
||||
```
|
||||
|
||||
## Technische Umsetzung
|
||||
|
||||
### Neues Paket: `internal/ldapauth`
|
||||
|
||||
```
|
||||
internal/ldapauth/
|
||||
ldap.go — Client, Bind, Search, Authenticate
|
||||
ldap_test.go — Tests mit Mock-LDAP
|
||||
```
|
||||
|
||||
**Abhängigkeit:** `github.com/go-ldap/ldap/v3`
|
||||
|
||||
### Ablauf Login mit LDAP
|
||||
|
||||
1. `auth.Manager.Login(username, password)` prüft zuerst lokale DB
|
||||
2. Wenn lokaler User nicht gefunden UND LDAP aktiviert → LDAP-Auth versuchen
|
||||
3. LDAP-Bind mit Service Account → User-DN per `user_filter` suchen
|
||||
4. User-Bind mit gefundener DN + eingegebenem Passwort
|
||||
5. Optional: Gruppen-Mitgliedschaft abfragen → Rolle bestimmen
|
||||
6. `userstore.UpsertLDAPUser(username, email, role)` aufrufen
|
||||
7. JWT-Token wie bei lokalem Login ausstellen
|
||||
|
||||
### Felder aus LDAP lesen
|
||||
|
||||
| LDAP-Attribut | archivmail-Feld |
|
||||
|--------------|-----------------|
|
||||
| `sAMAccountName` / `uid` | username |
|
||||
| `mail` | email |
|
||||
| `memberOf` | → Gruppen-Mapping → role |
|
||||
| `displayName` | (für spätere Anzeige) |
|
||||
|
||||
### API-Endpunkt: `GET /api/admin/ldap/test` (admin only)
|
||||
|
||||
Testet die LDAP-Verbindung und gibt Status zurück:
|
||||
```json
|
||||
{"ok": true, "message": "LDAP-Verbindung erfolgreich", "users_found": 42}
|
||||
```
|
||||
|
||||
## Nicht in diesem Feature
|
||||
|
||||
- Automatische User-Synchronisation (Bulk-Import aller AD-User) — separates Feature
|
||||
- LDAP-Gruppen als Postfach-Zuweisungen
|
||||
- Kerberos / SAML / OAuth2 (separate Features)
|
||||
|
||||
## Dateien
|
||||
|
||||
- `internal/ldapauth/ldap.go` (neu)
|
||||
- `internal/auth/auth.go` (erweitert: LDAP-Fallback)
|
||||
- `config/config.go` (erweitert: `LDAPConfig`)
|
||||
- `cmd/archivmail/main.go` (erweitert: LDAP-Client initialisieren)
|
||||
- `internal/api/server.go` (erweitert: `/api/admin/ldap/test`)
|
||||
- `install.sh` (erweitert: LDAP-Kommentar in config.yml)
|
||||
@@ -0,0 +1,86 @@
|
||||
# PROJ-17: Admin Dashboard – Systemauslastung & Archiv-Übersicht
|
||||
|
||||
## Status: In Review
|
||||
**Created:** 2026-03-14
|
||||
**Last Updated:** 2026-03-14
|
||||
|
||||
## Dependencies
|
||||
- Requires: PROJ-1 (Authentifizierung) – nur Admins sehen das Dashboard
|
||||
- Requires: PROJ-5 (Speicherung) – erste/letzte Mail aus dem Archiv
|
||||
|
||||
## User Stories
|
||||
- Als Admin möchte ich die aktuelle CPU-Auslastung sehen, damit ich Engpässe erkennen kann.
|
||||
- Als Admin möchte ich die RAM-Auslastung (gesamt / verwendet / frei) sehen.
|
||||
- Als Admin möchte ich alle eingebundenen Festplatten/Partitionen mit Füllstand sehen (Balken).
|
||||
- Als Admin möchte ich die älteste und neueste archivierte Mail sehen (Datum, Von, Betreff), damit ich den Archivierungszeitraum auf einen Blick erkenne.
|
||||
|
||||
## Acceptance Criteria
|
||||
- [x] CPU: Load Average (1min / 5min / 15min) aus `/proc/loadavg`
|
||||
- [x] RAM: MemTotal, MemUsed, MemAvailable aus `/proc/meminfo`; Prozentbalken
|
||||
- [x] Disk: alle physischen Partitionen (keine tmpfs/proc/sysfs/devtmpfs/overlay) via `syscall.Statfs`; je Partition: Mountpoint, Gesamt, Belegt, Frei, Prozent
|
||||
- [x] Erste Mail im Archiv: Datum, Von, Betreff (älteste Datei im Store)
|
||||
- [x] Letzte Mail im Archiv: Datum, Von, Betreff (neueste Datei im Store)
|
||||
- [x] Endpoint: `GET /api/admin/system/stats` (Admin-only)
|
||||
- [x] Storage-Erweiterung: `store.FirstAndLastMail()` liefert Metadaten der ältesten und neuesten Mail
|
||||
|
||||
## API Response Schema
|
||||
|
||||
```json
|
||||
{
|
||||
"cpu": {
|
||||
"load1": 0.42,
|
||||
"load5": 0.38,
|
||||
"load15": 0.31,
|
||||
"num_cpu": 4
|
||||
},
|
||||
"ram": {
|
||||
"total_bytes": 8388608000,
|
||||
"used_bytes": 3221225472,
|
||||
"free_bytes": 5167382528,
|
||||
"used_pct": 38.4
|
||||
},
|
||||
"disks": [
|
||||
{
|
||||
"mount": "/",
|
||||
"total_bytes": 53687091200,
|
||||
"used_bytes": 12884901888,
|
||||
"free_bytes": 40802189312,
|
||||
"used_pct": 24.0,
|
||||
"fstype": "ext4"
|
||||
}
|
||||
],
|
||||
"archive": {
|
||||
"first_mail": { "id": "abc123", "date": "2024-01-15T08:00:00Z", "from": "...", "subject": "..." },
|
||||
"last_mail": { "id": "def456", "date": "2026-03-14T10:08:00Z", "from": "...", "subject": "..." }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Technical Design
|
||||
|
||||
### Backend (`internal/api/server.go`)
|
||||
- Neuer Handler `handleSystemStats`
|
||||
- CPU: `/proc/loadavg` parsen → load1, load5, load15 + `runtime.NumCPU()`
|
||||
- RAM: `/proc/meminfo` parsen → MemTotal, MemFree, MemAvailable, Buffers, Cached
|
||||
- `used = total - available`
|
||||
- Disks: `/proc/mounts` lesen, für jeden Eintrag `syscall.Statfs()` aufrufen
|
||||
- Ausschließen: fstype in {tmpfs, proc, sysfs, devtmpfs, cgroup, cgroup2, overlay, squashfs, debugfs, tracefs, securityfs, pstore, efivarfs, bpf, hugetlbfs, mqueue, ramfs}
|
||||
- Erste/letzte Mail: `store.FirstAndLastMail()` → walk store dir, min/max ModTime
|
||||
|
||||
### Storage (`internal/storage/storage.go`)
|
||||
- Neue Methode `FirstAndLastMail() (*MailRef, *MailRef, error)`
|
||||
- `MailRef{ID, ModTime}` → ID wird an `handleSystemStats` übergeben, der dann via `mailparser.Parse()` From+Subject+Date extrahiert
|
||||
|
||||
### Frontend (`src/app/admin/page.tsx`)
|
||||
- Neue Kacheln im Dashboard-Tab:
|
||||
- **CPU-Auslastung**: Load Average mit `num_cpu` Kontext
|
||||
- **Arbeitsspeicher**: Fortschrittsbalken (used/total), Zahlen darunter
|
||||
- **Festplatten**: eine Karte pro Partition mit Balken + Zahlen
|
||||
- **Archivzeitraum**: erste und letzte Mail als kompakte Zeilen (Datum · Von · Betreff)
|
||||
|
||||
## Implementation Notes
|
||||
- **Backend:** `handleSystemStats` in `internal/api/server.go` — CPU via `/proc/loadavg`, RAM via `/proc/meminfo`, alle Disks via `/proc/mounts` + `syscall.Statfs`, Archiv-Zeitspanne via `store.FirstAndLastMail()`
|
||||
- **Storage:** `FirstAndLastMail()` + `MailRef` in `internal/storage/storage.go` — walkt Store-Verzeichnis, liefert älteste/neueste Mail per ModTime
|
||||
- **Route:** `GET /api/admin/system/stats` (Admin-only, Token-Auth)
|
||||
- **Frontend:** Dashboard-Tab in `src/app/admin/page.tsx` mit CPU, RAM, Disk-Partitionen und Archivzeitraum; Auto-Refresh alle 30 Sekunden
|
||||
- **Bereit für Test auf** `root@192.168.1.131`
|
||||
@@ -0,0 +1,146 @@
|
||||
# PROJ-2: E-Mail-Import: EML/MBOX Upload
|
||||
|
||||
## Status: In Progress
|
||||
**Created:** 2026-03-12
|
||||
**Last Updated:** 2026-03-12
|
||||
|
||||
## Dependencies
|
||||
- Requires: PROJ-1 (Authentifizierung) – nur eingeloggte Admins dürfen importieren
|
||||
- Requires: PROJ-5 (Speicherung & Indexierung) – importierte E-Mails werden gespeichert
|
||||
|
||||
## User Stories
|
||||
- Als Admin möchte ich EML-Dateien per Drag-and-Drop hochladen, damit ich einzelne E-Mails archivieren kann.
|
||||
- Als Admin möchte ich MBOX-Dateien importieren, damit ich ganze Postfach-Exporte auf einmal archivieren kann.
|
||||
- Als Admin möchte ich den Fortschritt eines laufenden Imports sehen, damit ich weiß wie weit der Import ist.
|
||||
- Als Admin möchte ich nach dem Import eine Zusammenfassung sehen (importiert, übersprungen, Fehler), damit ich Probleme nachvollziehen kann.
|
||||
- Als System möchte ich Duplikate erkennen und überspringen, damit E-Mails nicht doppelt archiviert werden.
|
||||
|
||||
## Acceptance Criteria
|
||||
- [ ] Upload-Interface akzeptiert .eml und .mbox Dateien (auch mehrere gleichzeitig)
|
||||
- [ ] Maximale Dateigröße konfigurierbar (Standard: 500 MB pro Upload)
|
||||
- [ ] EML-Parser liest Envelope-Header (From, To, CC, BCC, Date, Subject, Message-ID)
|
||||
- [ ] MBOX-Parser iteriert über alle enthaltenen E-Mails in der Datei
|
||||
- [ ] Anhänge werden extrahiert und getrennt gespeichert
|
||||
- [ ] Fortschrittsanzeige während des Imports (Anzahl verarbeitet / gesamt)
|
||||
- [ ] Duplikate (gleiche Message-ID) werden erkannt und übersprungen
|
||||
- [ ] Import-Zusammenfassung: Anzahl importiert, übersprungen, fehlerhaft
|
||||
- [ ] Fehlerhafte E-Mails (korrupte Dateien) werden geloggt und übersprungen, brechen Import nicht ab
|
||||
|
||||
## Edge Cases
|
||||
- MBOX-Datei mit 100.000+ E-Mails → chunked processing, kein Timeout
|
||||
- EML-Datei ohne Message-ID → synthetische ID generieren (Hash des Inhalts)
|
||||
- E-Mail mit verschachtelten MIME-Teilen (multipart/mixed, multipart/alternative)
|
||||
- Encoding-Probleme (ISO-8859-1, Windows-1252) → automatische Konvertierung zu UTF-8
|
||||
- Upload wird unterbrochen → partiell importierte Daten bereinigen
|
||||
|
||||
## Technical Requirements
|
||||
- Streaming-Upload für große Dateien (kein komplettes In-Memory-Laden)
|
||||
- MBOX-Parsing als Background-Job mit Statusrückmeldung via WebSocket oder Polling
|
||||
- Maximale Anhang-Größe pro E-Mail konfigurierbar
|
||||
|
||||
---
|
||||
## Tech Design (Solution Architect)
|
||||
|
||||
### Komponentenstruktur
|
||||
|
||||
**Next.js Frontend (Admin-Bereich):**
|
||||
```
|
||||
/admin/upload
|
||||
├── DropZone ← Drag-and-Drop + Datei-Dialog
|
||||
│ ├── akzeptiert: .eml, .mbox
|
||||
│ └── Mehrfachauswahl möglich
|
||||
├── Upload-Queue ← Liste der hochgeladenen Dateien
|
||||
│ └── FileItem (pro Datei)
|
||||
│ ├── Dateiname + Größe
|
||||
│ ├── Typ-Badge (EML / MBOX)
|
||||
│ └── Status (wartend / läuft / fertig / Fehler)
|
||||
├── Fortschrittsanzeige (pro Datei)
|
||||
│ ├── Fortschrittsbalken (X von Y Mails verarbeitet)
|
||||
│ └── Aktueller Status
|
||||
└── Abschlussbericht
|
||||
├── Importiert: X
|
||||
├── Übersprungen (Duplikate): Y
|
||||
└── Fehler: Z (mit Liste der fehlerhaften Mails)
|
||||
```
|
||||
|
||||
**Go Backend:**
|
||||
```
|
||||
POST /api/admin/upload
|
||||
├── Session Middleware (Admin)
|
||||
├── Multipart-Stream-Handler ← Datei wird nicht komplett in RAM geladen
|
||||
├── Dateityp-Erkennung (.eml / .mbox)
|
||||
└── Import-Worker starten (Hintergrund-Goroutine)
|
||||
|
||||
Import-Worker
|
||||
├── EML-Modus
|
||||
│ └── Einzelne Mail direkt parsen → Storage Coordinator
|
||||
│
|
||||
├── MBOX-Modus
|
||||
│ ├── MBOX-Parser (iteriert über "From "-Trennzeilen)
|
||||
│ ├── Für jede Mail:
|
||||
│ │ ├── Duplikat-Check (Message-ID)
|
||||
│ │ └── → Storage Coordinator (PROJ-5)
|
||||
│ └── Fortschritt in DB schreiben
|
||||
│
|
||||
└── Encoding-Normalisierer
|
||||
└── ISO-8859-1 / Windows-1252 → UTF-8
|
||||
|
||||
GET /api/admin/upload/{job_id}/progress ← Polling alle 2 Sek.
|
||||
```
|
||||
|
||||
### Upload- und Importfluss
|
||||
|
||||
```
|
||||
Admin zieht Datei in DropZone
|
||||
│
|
||||
│ POST /api/admin/upload (multipart/form-data, streaming)
|
||||
▼
|
||||
Go Backend empfängt Stream
|
||||
│
|
||||
├── .eml? → direkt parsen → Storage Coordinator → fertig
|
||||
│
|
||||
└── .mbox? → Import-Worker (Hintergrund)
|
||||
│
|
||||
▼
|
||||
MBOX-Parser liest zeilenweise
|
||||
Trenner: Zeilen die mit "From " beginnen
|
||||
│
|
||||
└── Pro Mail:
|
||||
Encoding-Erkennung + UTF-8-Normalisierung
|
||||
Message-ID vorhanden?
|
||||
Nein → SHA-256(Inhalt) als ID
|
||||
Duplikat? → überspringen
|
||||
→ Storage Coordinator (PROJ-5)
|
||||
Fortschritt in DB aktualisieren
|
||||
│
|
||||
▼
|
||||
Abschlussbericht in DB speichern
|
||||
|
||||
Next.js pollt GET /progress alle 2 Sek.
|
||||
→ Fortschrittsbalken aktualisieren
|
||||
→ Bei status:"done" → Abschlussbericht anzeigen
|
||||
```
|
||||
|
||||
### Technische Entscheidungen
|
||||
|
||||
| Entscheidung | Begründung |
|
||||
|---|---|
|
||||
| **Streaming-Upload** | 500 MB MBOX nie komplett in RAM – Go liest den HTTP-Body als Stream direkt in den Parser |
|
||||
| **MBOX zeilenweises Parsen** | MBOX-Format trennt Mails durch `From `–Zeilen – kein vollständiges Einlesen der Datei nötig |
|
||||
| **Background-Worker + Polling** | MBOX mit 100k+ Mails dauert Minuten – HTTP-Request darf nicht so lange offen bleiben |
|
||||
| **Encoding-Normalisierung** | E-Mail-Exporte aus Outlook/Thunderbird kommen oft als ISO-8859-1 – Index und DB erwarten UTF-8 |
|
||||
| **Fehler überspringen, nicht abbrechen** | Eine korrupte Mail soll nicht den gesamten Import einer 50k-MBOX-Datei stoppen |
|
||||
| **Synthetische Message-ID** | EML-Dateien ohne Message-ID (selten aber möglich) bekommen SHA-256(Inhalt) – Duplikatschutz bleibt konsistent |
|
||||
|
||||
### Abhängigkeiten
|
||||
|
||||
| Paket | Zweck |
|
||||
|---|---|
|
||||
| `golang.org/x/text/encoding` | ISO-8859-1 / Windows-1252 → UTF-8 Konvertierung |
|
||||
| `mime`, `mime/multipart` | EML + MBOX MIME-Parsing (Stdlib) |
|
||||
|
||||
## QA Test Results
|
||||
_To be added by /qa_
|
||||
|
||||
## Deployment
|
||||
_To be added by /deploy_
|
||||
@@ -0,0 +1,205 @@
|
||||
# PROJ-3: E-Mail-Import: IMAP-Verbindung
|
||||
|
||||
## Status: In Review
|
||||
**Created:** 2026-03-12
|
||||
**Last Updated:** 2026-03-14
|
||||
|
||||
## Dependencies
|
||||
- Requires: PROJ-1 (Authentifizierung) – nur Admins verwalten IMAP-Verbindungen
|
||||
- Requires: PROJ-5 (Speicherung & Indexierung) – importierte E-Mails werden gespeichert
|
||||
|
||||
## User Stories
|
||||
- Als Admin oder User möchte ich einen IMAP-Server konfigurieren (Host, Port, Zugangsdaten), damit das System E-Mails von dort abholen kann.
|
||||
- Als Admin oder User möchte ich beim ersten Verbinden alle vorhandenen E-Mails eines Postfachs importieren (Initial-Import).
|
||||
- Als System möchte ich beim Verbinden automatisch Junk- und Trash-Ordner per IMAP erkennen und ausschließen, damit kein Spam ins Archiv gelangt. Alle anderen Ordner werden importiert.
|
||||
- Als Admin oder User möchte ich den Verbindungsstatus sehen (verbunden, Fehler, letzter Sync), damit ich Probleme erkennen kann.
|
||||
- Als System möchte ich die IMAP-Verbindung testen bevor sie gespeichert wird, damit Konfigurationsfehler früh erkannt werden.
|
||||
- Als Admin möchte ich alle IMAP-Konten aller Nutzer sehen und verwalten können.
|
||||
- Als User möchte ich nur meine eigenen IMAP-Konten sehen und verwalten (keine fremden).
|
||||
|
||||
## Acceptance Criteria
|
||||
- [ ] Konfigurationsformular: Host, Port, TLS/SSL, Benutzername, Passwort
|
||||
- [ ] Verbindungstest vor dem Speichern (Timeout: 10 Sekunden)
|
||||
- [ ] Passwörter werden verschlüsselt in der Datenbank gespeichert (nie im Klartext)
|
||||
- [ ] Automatische Ordner-Erkennung via IMAP LIST-EXTENDED (RFC 6154 Special-Use Flags: `\Junk`, `\Trash`)
|
||||
- [ ] Fallback auf bekannte Ordnernamen wenn Special-Use Flags fehlen: `Junk`, `Spam`, `Trash`, `Deleted Items`, `Deleted Messages`, `Papierkorb`
|
||||
- [ ] Erkannte Ausschluss-Ordner werden dem Admin vor dem Import angezeigt (mit Option zur manuellen Korrektur)
|
||||
- [ ] Initial-Import: **alle Ordner außer Junk/Trash** – Ordnerstruktur wird verworfen, nur E-Mail-Inhalt archiviert
|
||||
- [ ] Fortschrittsanzeige während Initial-Import
|
||||
- [ ] Duplikate (Message-ID) werden übersprungen
|
||||
- [ ] Verbindungsstatus-Übersicht im Admin-Bereich
|
||||
|
||||
## Edge Cases
|
||||
- IMAP-Server nicht erreichbar → Fehlermeldung mit Retry-Option
|
||||
- Falsche Zugangsdaten → klare Fehlermeldung
|
||||
- IMAP-Server trennt Verbindung während Import → automatischer Reconnect
|
||||
- Postfach mit 200.000+ E-Mails → paginierter Import, kein Speicher-Overflow
|
||||
- OAuth2/XOAUTH2 für Gmail/Outlook → als spätere Erweiterung markiert (nicht MVP)
|
||||
|
||||
## Technical Requirements
|
||||
- **Verbindungsmodi:**
|
||||
- `SSL/TLS` – direkte TLS-Verbindung ab dem ersten Byte (Port 993)
|
||||
- `STARTTLS` – Verbindung startet unverschlüsselt, wird per STARTTLS-Befehl auf TLS hochgestuft (Port 143)
|
||||
- `None` – unverschlüsselt, nur für lokale/Testumgebungen
|
||||
- IMAP IDLE-Unterstützung für Echtzeit-Benachrichtigungen (optional)
|
||||
- Zugangsdaten AES-256-GCM verschlüsselt in der DB (gleicher Key wie Mail-Store)
|
||||
|
||||
---
|
||||
## Tech Design (Solution Architect)
|
||||
|
||||
### Komponentenstruktur
|
||||
|
||||
**Next.js Frontend (Admin-Bereich):**
|
||||
```
|
||||
/admin/imap
|
||||
├── IMAP-Verbindungsliste
|
||||
│ └── VerbindungsCard (pro Konto)
|
||||
│ ├── Name, Host, Status (OK / Fehler)
|
||||
│ ├── Letzter Import + Anzahl importierter Mails
|
||||
│ └── Aktionen: Bearbeiten / Löschen / Import starten
|
||||
├── Verbindung-Formular (anlegen / bearbeiten)
|
||||
│ ├── Host, Port, TLS-Auswahl
|
||||
│ ├── Benutzername, Passwort
|
||||
│ └── [Verbindung testen] Button
|
||||
├── Ordner-Vorschau (nach erfolgreichem Test)
|
||||
│ ├── Automatisch erkannte Ausschlüsse (Junk/Trash) – markiert
|
||||
│ └── Manuelle Korrektur möglich (Checkbox pro Ordner)
|
||||
└── Import-Fortschrittsanzeige
|
||||
├── Fortschrittsbalken (X von Y E-Mails)
|
||||
├── Aktueller Status
|
||||
└── Abschlussbericht (importiert / übersprungen / Fehler)
|
||||
```
|
||||
|
||||
**Go Backend:**
|
||||
```
|
||||
IMAP-Dienst
|
||||
├── Verbindungsverwaltung
|
||||
│ ├── POST /api/admin/imap ← Verbindung anlegen
|
||||
│ ├── POST /api/admin/imap/test ← testen + Ordner-Erkennung
|
||||
│ ├── GET /api/admin/imap ← alle Verbindungen auflisten
|
||||
│ └── DELETE /api/admin/imap/{id} ← Verbindung löschen
|
||||
│
|
||||
├── IMAP-Client
|
||||
│ ├── TLS/SSL + STARTTLS Handler
|
||||
│ ├── Ordner-Erkenner (Special-Use + Fallback)
|
||||
│ ├── SELECT + FETCH UID-basiert
|
||||
│ └── Reconnect-Handler
|
||||
│
|
||||
├── Import-Worker (Hintergrund-Goroutine)
|
||||
│ ├── Batch-weise FETCH (50 Mails pro Batch)
|
||||
│ ├── Duplikat-Check (Message-ID)
|
||||
│ ├── → Storage Coordinator (PROJ-5)
|
||||
│ └── Fortschritt in DB schreiben
|
||||
│
|
||||
└── GET /api/admin/imap/{id}/progress ← Polling durch Frontend
|
||||
```
|
||||
|
||||
### Ordner-Erkennungslogik
|
||||
|
||||
```
|
||||
IMAP-Verbindung aufgebaut
|
||||
│
|
||||
▼
|
||||
LIST-EXTENDED "" "*" RETURN (SPECIAL-USE) ← RFC 6154
|
||||
│
|
||||
├── \Junk gefunden? → Ordner ausschließen
|
||||
├── \Trash gefunden? → Ordner ausschließen
|
||||
└── Flags nicht unterstützt? → Fallback:
|
||||
Ordnernamen prüfen (case-insensitive):
|
||||
"junk", "spam", "trash", "deleted items",
|
||||
"deleted messages", "papierkorb"
|
||||
→ übereinstimmende Ordner ausschließen
|
||||
│
|
||||
▼
|
||||
Ordnerliste mit Markierungen an Frontend:
|
||||
INBOX ✓ (wird importiert)
|
||||
Sent ✓ (wird importiert)
|
||||
Drafts ✓ (wird importiert)
|
||||
Junk [\Junk erkannt] ✗ (ausgeschlossen)
|
||||
Trash [\ Trash erkannt] ✗ (ausgeschlossen)
|
||||
│
|
||||
▼
|
||||
Admin kann Ausschlüsse manuell korrigieren
|
||||
→ Speichern → Import starten
|
||||
```
|
||||
|
||||
### Importfluss
|
||||
|
||||
```
|
||||
Import-Worker startet
|
||||
│
|
||||
▼
|
||||
Für jeden nicht ausgeschlossenen Ordner:
|
||||
│
|
||||
├── IMAP UID SEARCH ALL → alle UIDs
|
||||
│
|
||||
└── Batch-weise (50 UIDs):
|
||||
├── IMAP FETCH RFC822
|
||||
├── Message-ID Duplikat? → überspringen
|
||||
└── Storage Coordinator (PROJ-5)
|
||||
→ verschlüsseln + speichern + indexieren
|
||||
│
|
||||
▼
|
||||
Fortschritt in DB → Frontend pollt alle 2 Sek.
|
||||
```
|
||||
|
||||
### Datenmodell
|
||||
|
||||
**Tabelle `imap_accounts`:**
|
||||
|
||||
| Feld | Beschreibung |
|
||||
|------|-------------|
|
||||
| `id` | Interne ID |
|
||||
| `name` | Bezeichnung |
|
||||
| `host` | IMAP-Hostname |
|
||||
| `port` | Port (143 / 993) |
|
||||
| `tls` | `ssl` / `starttls` / `none` |
|
||||
| `username` | IMAP-Benutzername |
|
||||
| `password_enc` | AES-256-GCM verschlüsseltes Passwort |
|
||||
| `excluded_folders` | JSON-Array ausgeschlossener Ordner |
|
||||
| `last_import_at` | Zeitpunkt des letzten Imports |
|
||||
| `last_import_count` | Anzahl importierter Mails |
|
||||
| `status` | `idle` / `running` / `error` |
|
||||
| `error_msg` | Letzter Fehler |
|
||||
|
||||
### Technische Entscheidungen
|
||||
|
||||
| Entscheidung | Begründung |
|
||||
|---|---|
|
||||
| **RFC 6154 Special-Use Flags** | Standard-Weg für Junk/Trash-Erkennung – funktioniert bei Dovecot, Exchange, Gmail |
|
||||
| **Fallback auf Ordnernamen** | Ältere oder nicht-standardkonforme Server kennen Special-Use nicht – Fallback deckt die gängigsten Namen ab |
|
||||
| **Admin-Korrektur möglich** | Automatik kann irren – Admin sieht die Erkennungsergebnisse und kann vor dem Import eingreifen |
|
||||
| **Nur Ausschlüsse konfigurieren** | Einfacher als Whitelist: alle Ordner importieren außer den erkannten Ausreißern |
|
||||
| **UID-basierter Fetch** | Bei Reconnect kann genau dort weitergemacht werden wo abgebrochen wurde |
|
||||
| **Batch-Größe 50** | Balance zwischen RAM-Verbrauch und IMAP-Roundtrips |
|
||||
|
||||
### Abhängigkeiten
|
||||
|
||||
| Paket | Zweck |
|
||||
|---|---|
|
||||
| `github.com/emersion/go-imap` | IMAP-Client (RFC 6154 LIST-EXTENDED, TLS, FETCH) |
|
||||
|
||||
## Implementation Notes (2026-03-14)
|
||||
|
||||
### Go Backend
|
||||
- **`internal/imap/store.go`**: DB CRUD for `imap_accounts` table with AES-256-GCM password encryption. Auto-migrates table on startup. Index on `owner` column.
|
||||
- **`internal/imap/client.go`**: IMAP client wrapper using `go-imap/v2` (beta.8). Supports SSL/STARTTLS/plaintext. Folder detection via RFC 6154 special-use flags with name-based fallback.
|
||||
- **`internal/imap/importer.go`**: Background import worker. Fetches all UIDs per folder, processes in batches of 50, stores via `storage.Store.Save()` (SHA256 dedup), indexes via `index.Indexer.IndexSync()`. Progress written to DB for frontend polling.
|
||||
- **`internal/api/server.go`**: 6 new IMAP endpoints (`GET/POST /api/imap`, `DELETE /api/imap/{id}`, `POST /api/imap/test`, `POST /api/imap/{id}/import`, `GET /api/imap/{id}/progress`). All auth-protected, ownership enforced (admin sees all, user sees own).
|
||||
- **`cmd/archivmail/main.go`**: Wires IMAP store and importer into API server.
|
||||
|
||||
### Frontend
|
||||
- **`src/app/imap/page.tsx`**: Full IMAP management page with account cards, add dialog (with connection test and folder preview), progress polling, delete confirmation.
|
||||
- **`src/lib/api.ts`**: IMAP types and 6 API functions.
|
||||
- **`src/components/navbar.tsx`**: Added "IMAP Import" link for all roles.
|
||||
|
||||
### Deviations from spec
|
||||
- Routes use `/api/imap` (not `/api/admin/imap`) since all authenticated users can manage their own IMAP accounts.
|
||||
- Using `go-imap/v2` beta.8 (latest available) instead of beta.5.
|
||||
- IMAP page at `/imap` (not `/admin/imap`) to match the route pattern.
|
||||
|
||||
## QA Test Results
|
||||
_To be added by /qa_
|
||||
|
||||
## Deployment
|
||||
Deployed to 192.168.1.131 on 2026-03-14. Both `archivmail` and `archivmail-web` services restarted and active. Database table `imap_accounts` auto-created with index.
|
||||
@@ -0,0 +1,164 @@
|
||||
# PROJ-4: E-Mail-Import: SMTP-Eingang (primär via BCC)
|
||||
|
||||
## Status: In Progress
|
||||
**Created:** 2026-03-12
|
||||
**Last Updated:** 2026-03-12
|
||||
|
||||
## Hinweis
|
||||
**Dies ist der primäre Eingangsweg.** archivmail enthält einen eingebetteten SMTP-Daemon, der **ausschließlich E-Mails empfängt** – kein Versand, keine Weiterleitung, kein MTA. Postfix (oder ein anderer Mailserver) wird per BCC-Mapping oder Always-BCC-Regel so konfiguriert, dass er eine Kopie jeder E-Mail an archivmails SMTP-Daemon zustellt.
|
||||
|
||||
```
|
||||
Absender → Postfix (MTA) → Empfänger
|
||||
│
|
||||
└── BCC/always_bcc → archivmail SMTP-Daemon (nur Empfang)
|
||||
│
|
||||
▼
|
||||
Storage Coordinator
|
||||
```
|
||||
|
||||
IMAP und EML/MBOX-Upload sind sekundäre/ergänzende Methoden (z.B. für Altbestände).
|
||||
|
||||
## Dependencies
|
||||
- Requires: PROJ-5 (Speicherung & Indexierung) – eingehende E-Mails werden gespeichert
|
||||
- Kein Login nötig für den Empfang – SMTP-Eingang läuft unabhängig vom HTTP-Server
|
||||
|
||||
## User Stories
|
||||
- Als Mailserver möchte ich E-Mails per BCC an archivmail zustellen, damit diese automatisch archiviert werden.
|
||||
- Als Admin möchte ich den eingebetteten SMTP-Server konfigurieren (Port, TLS, erlaubte Absender-IPs).
|
||||
- Als Admin möchte ich festlegen, welche Absender-IPs/Domains akzeptiert werden, damit nur der eigene Mailserver zustellen darf.
|
||||
- Als System möchte ich eingehende E-Mails sofort nach Empfang indexieren, damit sie innerhalb von Sekunden durchsuchbar sind.
|
||||
- Als Admin möchte ich den Status des SMTP-Empfängers sehen (läuft, Port, letzte empfangene E-Mail).
|
||||
|
||||
## Acceptance Criteria
|
||||
- [ ] Eingebetteter SMTP-Server lauscht auf konfigurierbarem Port (Standard: 25 oder 2525)
|
||||
- [ ] TLS/STARTTLS-Unterstützung für verschlüsselte Übertragung
|
||||
- [ ] IP-Allowlist: nur eingetragene Mailserver-IPs dürfen zustellen (Standard: nur localhost/127.0.0.1)
|
||||
- [ ] Optionale Domain-Allowlist als zusätzliche Prüfebene
|
||||
- [ ] E-Mails werden sofort nach Empfang gespeichert und indexiert
|
||||
- [ ] SMTP-Quittierung (250 OK) erst nach erfolgreicher Speicherung
|
||||
- [ ] Admin-UI zeigt: Port, TLS-Status, Anzahl empfangener E-Mails, letzte Aktivität
|
||||
- [ ] Fehlerhafte/abgelehnte E-Mails werden geloggt
|
||||
|
||||
## Edge Cases
|
||||
- E-Mail ohne Absender (Envelope-From leer) → annehmen aber markieren
|
||||
- Sehr große E-Mail (> 50 MB) → konfigurierbare Maximalgröße, Ablehnung mit 552-Fehlercode
|
||||
- SMTP-Server-Port bereits belegt → klare Fehlermeldung beim Start
|
||||
- Parallele Verbindungen (viele E-Mails gleichzeitig) → Connection-Pooling
|
||||
- Duplicate Message-ID → überspringen wie bei anderen Import-Methoden
|
||||
|
||||
## Technical Requirements
|
||||
- RFC 5321 (SMTP) konformer **reiner Empfänger** – kein SMTP-Versand, keine Queue, kein Relay
|
||||
- Kein SMTP AUTH – Zugang ausschließlich über IP-Allowlist (nur Postfix-IP eingetragen)
|
||||
- Maximale Nachrichtengröße konfigurierbar (Standard: 50 MB)
|
||||
- Startet als eigenständiger Goroutine/Service neben dem HTTP-Server
|
||||
- Postfix-Konfiguration (außerhalb von archivmail, Dokumentation in README):
|
||||
- `always_bcc = archiv@archivmail-host` in Postfix `main.cf`, oder
|
||||
- Sender/Recipient BCC-Maps für granulare Kontrolle
|
||||
|
||||
---
|
||||
## Tech Design (Solution Architect)
|
||||
|
||||
### Systemübersicht
|
||||
|
||||
```
|
||||
Absender → Postfix (MTA) → Empfänger (normale Zustellung)
|
||||
│
|
||||
└── always_bcc / BCC-Map
|
||||
│
|
||||
▼ SMTP (Port 2525)
|
||||
archivmail SMTP-Daemon
|
||||
(nur Empfang, kein Versand)
|
||||
│
|
||||
▼
|
||||
Storage Coordinator (PROJ-5)
|
||||
(speichern + indexieren)
|
||||
```
|
||||
|
||||
### Komponentenstruktur
|
||||
|
||||
```
|
||||
archivmail (Go-Binary)
|
||||
│
|
||||
├── HTTP-Server (Web-GUI + API)
|
||||
│
|
||||
└── SMTP-Daemon ← startet parallel zum HTTP-Server
|
||||
├── TCP Acceptor ← lauscht auf Port 2525 (konfigurierbar)
|
||||
├── IP Allowlist Guard ← prüft Absender-IP vor SMTP-Dialog
|
||||
├── Session Handler (pro Verbindung, eigene Goroutine)
|
||||
│ ├── TLS/STARTTLS Handler ← optional, Zertifikat aus config.yml
|
||||
│ └── Size Limiter ← bricht DATA-Phase bei Überschreitung ab
|
||||
└── Handoff → Storage Coordinator ← übergibt E-Mail nach vollständigem Empfang
|
||||
```
|
||||
|
||||
### SMTP-Dialogfluss
|
||||
|
||||
```
|
||||
Postfix
|
||||
│ TCP-Verbindung auf Port 2525
|
||||
▼
|
||||
IP Allowlist Guard
|
||||
├─ IP unbekannt → Verbindung trennen (kein SMTP-Dialog)
|
||||
└─ IP erlaubt → weiter
|
||||
│
|
||||
▼
|
||||
220 archivmail SMTP ready
|
||||
│
|
||||
EHLO mail.firma.de
|
||||
250 OK (kein AUTH angeboten – reiner Empfänger)
|
||||
│
|
||||
MAIL FROM: <absender@firma.de>
|
||||
250 OK
|
||||
│
|
||||
RCPT TO: <archiv@archivmail>
|
||||
250 OK
|
||||
│
|
||||
DATA
|
||||
354 Start input
|
||||
… E-Mail-Inhalt … (max. 50 MB)
|
||||
.
|
||||
│
|
||||
├─ Zu groß → 552 Message size exceeds limit
|
||||
├─ Duplikat (Message-ID) → 250 OK (still, kein Fehler – Postfix soll nicht retrying)
|
||||
└─ Neu → Storage Coordinator → verschlüsselt speichern + indexieren
|
||||
│
|
||||
▼
|
||||
250 OK ← erst nach erfolgreicher Speicherung
|
||||
│
|
||||
QUIT
|
||||
```
|
||||
|
||||
### Technische Entscheidungen
|
||||
|
||||
| Entscheidung | Begründung |
|
||||
|---|---|
|
||||
| **Reiner Empfänger, kein MTA** | archivmail ist kein Mailserver – keine ausgehende Queue, kein Relay-Risiko, kein Open-Relay |
|
||||
| **Kein SMTP AUTH** | Vertrauen basiert auf IP, nicht auf Passwort – Postfix und archivmail laufen im gleichen Netz |
|
||||
| **250 OK bei Duplikat** | Postfix würde bei Fehler die Mail in die Retry-Queue stellen – sinnlos, da Duplikat bereits archiviert |
|
||||
| **250 OK erst nach Speicherung** | Solange Postfix keine Bestätigung hat, behält er die Mail und versucht erneut – kein Datenverlust |
|
||||
| **Port 2525** | Port 25 erfordert root-Rechte; 2525 läuft als unprivilegierter `archivmail`-Systembenutzer |
|
||||
| **Eine Goroutine pro Session** | Viele parallele Verbindungen ohne Blocking; jede Session ist isoliert |
|
||||
|
||||
### Postfix-Konfiguration (Dokumentation, außerhalb von archivmail)
|
||||
|
||||
```
|
||||
# /etc/postfix/main.cf – einfachste Variante (alle Mails)
|
||||
always_bcc = archiv@archivmail-host
|
||||
|
||||
# Oder granular per Sender-BCC-Map:
|
||||
# sender_bcc_maps = hash:/etc/postfix/sender_bcc
|
||||
# empfänger@firma.de archiv@archivmail-host
|
||||
```
|
||||
|
||||
### Go-Abhängigkeiten
|
||||
|
||||
| Paket | Zweck |
|
||||
|---|---|
|
||||
| `github.com/emersion/go-smtp` | Eingebetteter SMTP-Daemon (RFC 5321, nur Empfang) |
|
||||
| `crypto/tls` | TLS/STARTTLS (Go Stdlib) |
|
||||
| `net` | IP-Prüfung (Go Stdlib) |
|
||||
|
||||
## QA Test Results
|
||||
_To be added by /qa_
|
||||
|
||||
## Deployment
|
||||
_To be added by /deploy_
|
||||
@@ -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_
|
||||
@@ -0,0 +1,128 @@
|
||||
# PROJ-6: Volltext-Suche & Filterung
|
||||
|
||||
## Status: In Progress
|
||||
**Created:** 2026-03-12
|
||||
**Last Updated:** 2026-03-12
|
||||
|
||||
## Dependencies
|
||||
- Requires: PROJ-1 (Authentifizierung) – Suche nur für eingeloggte Nutzer
|
||||
- Requires: PROJ-5 (Speicherung & Indexierung) – Suchergebnisse kommen aus dem Index
|
||||
|
||||
## User Stories
|
||||
- Als Nutzer möchte ich nach Schlüsselwörtern suchen, damit ich relevante E-Mails schnell finden kann.
|
||||
- Als Nutzer möchte ich Suchergebnisse nach Absender, Empfänger, Datum und Anhang filtern, damit ich die Treffermenge eingrenzen kann.
|
||||
- Als Nutzer möchte ich Suchergebnisse nach Datum sortieren können (neueste/älteste zuerst).
|
||||
- Als Nutzer möchte ich Suchbegriffe in den Ergebnissen hervorgehoben sehen (Highlighting).
|
||||
- Als Nutzer sehe ich nur E-Mails, auf die ich Zugriffsrecht habe, damit Datenschutz gewahrt bleibt.
|
||||
|
||||
## Acceptance Criteria
|
||||
- [ ] Sucheingabe mit Echtzeit-Vorschau (oder sofortiger Submit)
|
||||
- [ ] Suche in: Betreff, Absender, Empfänger, Body-Text
|
||||
- [ ] Filteroptionen: Datum (von–bis), Absender-Domain, hat Anhang (ja/nein), Label
|
||||
- [ ] Sortierung: nach Relevanz, nach Datum (auf-/absteigend)
|
||||
- [ ] Suchergebnisse paginiert (Standard: 25 pro Seite)
|
||||
- [ ] Suchbegriff in Betreff und Body-Snippet hervorgehoben
|
||||
- [ ] Suchanfragen liefern Ergebnisse in < 2 Sekunden (bei 100.000+ E-Mails)
|
||||
- [ ] Nutzer sehen nur E-Mails in ihren zugewiesenen Postfächern
|
||||
|
||||
## Edge Cases
|
||||
- Suchanfrage ohne Ergebnisse → "Keine Ergebnisse" Meldung mit Vorschlägen
|
||||
- Sonderzeichen in Suchanfrage (", *, ?) → Escaping oder Query-Syntax erlauben
|
||||
- Suche bei sehr großem Index (1M+ Mails) → Performance-Test erforderlich
|
||||
- Gleichzeitige Suchanfragen von vielen Nutzern → kein Query-Blocking
|
||||
|
||||
## Technical Requirements
|
||||
- **Such-Engine: Xapian** – die Web-GUI sucht ausschließlich über den Xapian-Index
|
||||
- Kein SQL-Fulltext-Query gegen PostgreSQL – DB wird nur für Metadaten-Lookup nach Treffern genutzt
|
||||
- Suchfluss: Web-GUI → API → Xapian-Query → Treffer-IDs → PostgreSQL-Metadaten-Lookup → Antwort
|
||||
- Xapian QueryParser: AND, OR, NOT, Phrasen (`"exakter Text"`), Wildcards (`word*`), Feldpräfixe (`from:`, `subject:`)
|
||||
- Relevanz-Ranking über Xapian BM25Weight
|
||||
- Snippet/Highlighting über `Xapian::MSet::snippet()`
|
||||
- `ReadonlyDatabase` für parallele Lesezugriffe (mehrere Nutzer gleichzeitig möglich)
|
||||
- Antwortzeit < 2 Sekunden für Volltext-Suche über 100.000 E-Mails
|
||||
- Suchanfragen werden für Audit-Log erfasst (optional, konfigurierbar)
|
||||
|
||||
---
|
||||
## Tech Design (Solution Architect)
|
||||
|
||||
### Komponentenstruktur
|
||||
|
||||
**Next.js Frontend:**
|
||||
```
|
||||
/search
|
||||
├── SearchBar ← Eingabefeld, Submit bei Enter oder Button
|
||||
├── FilterPanel ← aufklappbar
|
||||
│ ├── DateRangePicker ← von–bis Datum
|
||||
│ ├── DomainFilter ← Absender-Domain Freitext
|
||||
│ ├── AttachmentToggle ← nur Mails mit Anhang
|
||||
│ └── LabelFilter ← Label-Auswahl (Mehrfachauswahl)
|
||||
├── SortControls ← Relevanz / Datum aufsteigend / absteigend
|
||||
├── ResultsList
|
||||
│ └── MailCard (pro Treffer)
|
||||
│ ├── Betreff (Suchbegriff hervorgehoben)
|
||||
│ ├── Von / An / Datum / Größe
|
||||
│ ├── Body-Snippet (Suchbegriff hervorgehoben)
|
||||
│ └── Anhang-Indikator
|
||||
├── Pagination ← Seiten-Navigation
|
||||
└── EmptyState ← "Keine Ergebnisse" mit Suchtipps
|
||||
```
|
||||
|
||||
**Go Backend:**
|
||||
```
|
||||
GET /api/search
|
||||
├── Session Middleware ← Auth prüfen
|
||||
├── Role Filter Builder ← user: nur eigene Postfächer / auditor: alle
|
||||
├── Xapian QueryParser ← Nutzer-Query parsen (AND/OR/NOT/Wildcards)
|
||||
├── Xapian ReadonlyDatabase ← Query ausführen, MSet zurückgeben
|
||||
│ ├── BM25 Relevanz-Ranking ← beste Treffer zuerst
|
||||
│ └── MSet::snippet() ← Highlighting-Snippets erzeugen
|
||||
├── PostgreSQL Metadaten-Lookup ← From, To, Subject, Date, Size, Attachments
|
||||
└── JSON Response Assembly ← Ergebnis zusammenbauen
|
||||
```
|
||||
|
||||
### Suchfluss
|
||||
|
||||
```
|
||||
Next.js (Browser) Go Backend
|
||||
│ │
|
||||
│ GET /api/search │
|
||||
│ ?q=Rechnung&date_from=2024-01 │
|
||||
│ &has_attachments=true&page=2 │
|
||||
│ ────────────────────────────────► │
|
||||
│ Session prüfen
|
||||
│ Rolle ermitteln:
|
||||
│ user → Filter: nur eigene Postfach-IDs
|
||||
│ auditor → kein Filter
|
||||
│ │
|
||||
│ Xapian QueryParser
|
||||
│ → Query + Datumsfilter + Anhang-Filter
|
||||
│ │
|
||||
│ Xapian ReadonlyDatabase
|
||||
│ → MSet: [doc_id_1, doc_id_5, ...]
|
||||
│ → Snippets mit Highlighting
|
||||
│ │
|
||||
│ PostgreSQL
|
||||
│ → Metadaten für doc_ids laden
|
||||
│ │
|
||||
│ ◄────────────────────────────────
|
||||
│ { total, page, mails: [...] } │
|
||||
│ MailCards rendern + highlighten │
|
||||
```
|
||||
|
||||
### Technische Entscheidungen
|
||||
|
||||
| Entscheidung | Begründung |
|
||||
|---|---|
|
||||
| **Xapian für alles, kein SQL-Fulltext** | Optimiert für Volltext – PostgreSQL LIKE-Suche wäre bei 100k+ Mails zu langsam |
|
||||
| **ReadonlyDatabase** | Beliebig viele parallele Lesezugriffe – kein Blocking bei gleichzeitigen Nutzern |
|
||||
| **Rolle als Xapian-Term** | Postfach-ID beim Indexieren als Term gespeichert – Rollenfilter läuft in Xapian, nicht nachträglich in der DB |
|
||||
| **Snippets aus Xapian** | `MSet::snippet()` hebt Suchbegriff im Originaltext hervor – kein separates Rendering nötig |
|
||||
| **Paginierung über Xapian Offset** | Nur angefragter Seitenausschnitt zurückgegeben – kein Full-Scan pro Seite |
|
||||
| **PostgreSQL nur für Metadaten** | Nach Xapian-Suche werden nur gefundene IDs nachgeschlagen – minimale DB-Last |
|
||||
| **URL-State mit `nuqs`** | Suchparameter in der URL → Back-Button funktioniert, Suchergebnisse sind verlinkbar |
|
||||
|
||||
## QA Test Results
|
||||
_To be added by /qa_
|
||||
|
||||
## Deployment
|
||||
_To be added by /deploy_
|
||||
@@ -0,0 +1,184 @@
|
||||
# PROJ-7: E-Mail-Ansicht (Lesen & Anhänge)
|
||||
|
||||
## Status: In Progress
|
||||
**Created:** 2026-03-12
|
||||
**Last Updated:** 2026-03-12
|
||||
|
||||
## Dependencies
|
||||
- Requires: PROJ-1 (Authentifizierung) – nur eingeloggte Nutzer mit Zugriffsrecht
|
||||
- Requires: PROJ-5 (Speicherung & Indexierung) – E-Mail-Daten aus der Datenbank
|
||||
|
||||
## User Stories
|
||||
- Als Nutzer möchte ich eine E-Mail aus den Suchergebnissen öffnen und lesen, damit ich den vollständigen Inhalt sehe.
|
||||
- Als Nutzer möchte ich Anhänge herunterladen, damit ich auf angefügte Dokumente zugreifen kann.
|
||||
- Als Nutzer möchte ich die originalen E-Mail-Header einsehen (technische Details), damit ich Routing und Authentizität prüfen kann.
|
||||
- Als Nutzer möchte ich E-Mails im HTML-Format sehen (mit sanitizierten externen Inhalten), damit die Formatierung erhalten bleibt.
|
||||
- Als Nutzer möchte ich die Originalmail als EML herunterladen können.
|
||||
|
||||
## Acceptance Criteria
|
||||
- [ ] E-Mail-Detailansicht zeigt: Von, An, CC, Datum, Betreff, Body
|
||||
- [ ] HTML-Body wird **original** dargestellt – kein Entfernen oder Verändern von Inhalten
|
||||
- [ ] Darstellung in einem vollständig isolierten `<iframe sandbox>` – kein JavaScript aus der Mail kann ausgeführt werden
|
||||
- [ ] Externe Bilder/Ressourcen standardmäßig blockiert, Nutzer kann per Button "externe Inhalte laden" freischalten
|
||||
- [ ] Fallback auf Plain-Text wenn kein HTML vorhanden
|
||||
- [ ] Anhang-Liste mit Dateiname, Typ und Größe
|
||||
- [ ] Anhänge einzeln herunterladbar
|
||||
- [ ] Header-Ansicht (klappbar) zeigt alle Original-MIME-Header
|
||||
- [ ] Download der Original-E-Mail als .eml Datei
|
||||
- [ ] Zugriffsschutz: Nutzer kann nur E-Mails aus eigenen Postfächern öffnen
|
||||
- [ ] Jeder Zugriff auf eine E-Mail wird im Audit-Log erfasst
|
||||
|
||||
## Edge Cases
|
||||
- E-Mail mit nur Plain-Text → normales Rendering ohne HTML
|
||||
- HTML mit JavaScript → Script wird durch iframe-Sandbox blockiert, HTML-Inhalt bleibt unverändert sichtbar
|
||||
- Externe Tracker (Pixel, Links) → standardmäßig blockiert durch CSP, auf Wunsch des Nutzers freischaltbar
|
||||
- E-Mail mit sehr großen Anhängen (> 100 MB) → Download-Streaming, kein Speicher-Overflow
|
||||
- E-Mail mit verschachteltem MIME (E-Mail in E-Mail als Anhang) → als EML-Anhang anzeigen
|
||||
- Nicht unterstützte Zeichenkodierung → graceful Fallback mit Hinweis
|
||||
|
||||
## Technical Requirements
|
||||
- **Kein HTML-Sanitizing** – originale Darstellung ohne Veränderung des Inhalts
|
||||
- Isolation über `<iframe sandbox="allow-same-origin">` – JavaScript blockiert, Inhalt originalgetreu
|
||||
- Externe Ressourcen über CSP (`Content-Security-Policy`) serverseitig blockiert, opt-in per Nutzer-Aktion
|
||||
- Anhang-Downloads als Stream (kein vollständiges In-Memory-Laden)
|
||||
- Audit-Log-Eintrag: Nutzer-ID, E-Mail-ID, Zeitstempel bei jedem Lesezugriff
|
||||
|
||||
---
|
||||
## Tech Design (Solution Architect)
|
||||
|
||||
### Komponentenstruktur
|
||||
|
||||
**Next.js Frontend:**
|
||||
```
|
||||
/mail/[message_id]
|
||||
├── MailHeader
|
||||
│ ├── Betreff
|
||||
│ ├── Von / An / CC / Datum / Größe
|
||||
│ └── HeaderToggle (klappbar)
|
||||
│ └── RawHeaderView ← alle Original-MIME-Header als Text
|
||||
├── ActionBar
|
||||
│ ├── EML-Download Button ← lädt Original-Mail herunter
|
||||
│ ├── Externe Inhalte laden ← Button, standardmäßig deaktiviert
|
||||
│ └── Zurück zur Suche
|
||||
├── MailBody
|
||||
│ ├── HtmlView ← originales HTML in <iframe sandbox>
|
||||
│ │ └── ExternalContentBanner ← Hinweis "Externe Inhalte blockiert [Laden]"
|
||||
│ └── PlainTextView ← Fallback wenn kein HTML vorhanden
|
||||
└── AttachmentList
|
||||
└── AttachmentItem (pro Anhang)
|
||||
├── Icon (nach MIME-Type)
|
||||
├── Dateiname + Typ + Größe
|
||||
└── Download-Button ← direkter Stream vom Go-Backend
|
||||
```
|
||||
|
||||
**Go Backend:**
|
||||
```
|
||||
GET /api/mails/{message_id}
|
||||
├── Session Middleware ← Auth prüfen
|
||||
├── Zugriffsrecht prüfen ← user: nur eigenes Postfach / auditor: alle
|
||||
├── .m-Datei von Disk lesen ← Pfad aus PostgreSQL
|
||||
├── AES-256-GCM entschlüsseln ← Schlüssel aus Prozessspeicher
|
||||
├── MIME-Parser ← Body + Header + Anhang-Metadaten extrahieren
|
||||
└── JSON-Antwort ← Metadaten + originaler HTML-Body + Anhang-Liste
|
||||
|
||||
GET /api/mails/{message_id}/attachments/{index}
|
||||
├── Session Middleware
|
||||
├── Zugriffsrecht prüfen
|
||||
├── Hash aus PostgreSQL ← welche astore/-Datei?
|
||||
├── astore/-Datei öffnen
|
||||
├── AES-256-GCM entschlüsseln (stream)
|
||||
└── HTTP-Streaming-Response ← Content-Disposition: attachment
|
||||
|
||||
GET /api/mails/{message_id}/raw
|
||||
├── Session Middleware
|
||||
├── Zugriffsrecht prüfen
|
||||
├── .m-Datei entschlüsseln
|
||||
└── HTTP-Streaming-Response ← Content-Type: message/rfc822
|
||||
```
|
||||
|
||||
### Datenabruf-Fluss
|
||||
|
||||
```
|
||||
Browser klickt MailCard aus Suchergebnissen
|
||||
│
|
||||
│ GET /api/mails/<message_id>
|
||||
▼
|
||||
Zugriffsrecht prüfen
|
||||
│
|
||||
▼
|
||||
PostgreSQL → store_path
|
||||
│
|
||||
▼
|
||||
.m-Datei lesen + AES-256-GCM entschlüsseln
|
||||
│
|
||||
▼
|
||||
MIME-Parser → body_html (original, unverändert), body_plain, headers[], attachments[]
|
||||
│
|
||||
▼
|
||||
JSON-Antwort an Next.js
|
||||
│
|
||||
▼
|
||||
Next.js:
|
||||
├── HTML → <iframe sandbox="allow-same-origin">
|
||||
│ └── CSP-Header blockiert externe Ressourcen
|
||||
│ Nutzer klickt "Externe Inhalte laden"
|
||||
│ → iframe neu laden ohne CSP-Restriction
|
||||
└── Anhang-Liste → Download-Links
|
||||
```
|
||||
|
||||
### Technische Entscheidungen
|
||||
|
||||
| Entscheidung | Begründung |
|
||||
|---|---|
|
||||
| **Kein HTML-Sanitizing** | Originale Darstellung – kein Inhalt wird verändert oder entfernt |
|
||||
| **`<iframe sandbox>`** | JavaScript aus der Mail wird blockiert ohne den HTML-Inhalt zu verändern – Inhalt bleibt originalgetreu |
|
||||
| **CSP für externe Ressourcen** | Tracking-Pixel und externe Bilder standardmäßig blockiert – Nutzer kann bewusst freischalten |
|
||||
| **Entschlüsselung nur im Backend** | Verschlüsselte Rohdaten verlassen den Server nie |
|
||||
| **Anhang-Download als Stream** | Große Anhänge (>100 MB) nie komplett in RAM – direkt von Disk zum Browser |
|
||||
| **Kein Audit-Log bei Lesezugriff** | Bewusste Entscheidung (PROJ-11): Lesezugriffe werden nicht geloggt |
|
||||
|
||||
### Abhängigkeiten
|
||||
|
||||
**Go Backend:**
|
||||
|
||||
| Paket | Zweck |
|
||||
|---|---|
|
||||
| `mime`, `mime/multipart` | MIME-Parsing (Stdlib) |
|
||||
|
||||
**Next.js Frontend:** Nur shadcn/ui (bereits installiert), kein zusätzliches Paket nötig.
|
||||
|
||||
## QA Test Results
|
||||
_To be added by /qa_
|
||||
|
||||
## Deployment
|
||||
|
||||
### Lokal bauen
|
||||
|
||||
```bash
|
||||
# Im Projektverzeichnis
|
||||
npm run build
|
||||
```
|
||||
|
||||
Build-Artefakt liegt danach in `.next/`.
|
||||
|
||||
### Auf Server übertragen (192.168.1.131)
|
||||
|
||||
```bash
|
||||
# Next.js-Build + Abhängigkeiten übertragen
|
||||
rsync -avz --delete \
|
||||
.next/ \
|
||||
package.json \
|
||||
package-lock.json \
|
||||
next.config.ts \
|
||||
root@192.168.1.131:/opt/archivmail/frontend/
|
||||
|
||||
# Auf dem Server: Abhängigkeiten installieren & Dienst neu starten
|
||||
ssh root@192.168.1.131 "cd /opt/archivmail/frontend && npm ci --omit=dev && systemctl restart archivmail-frontend"
|
||||
```
|
||||
|
||||
### Voraussetzungen auf dem Server
|
||||
|
||||
- Node.js ≥ 20 installiert (`node -v`)
|
||||
- Verzeichnis `/opt/archivmail/frontend/` existiert
|
||||
- Systemd-Unit `archivmail-frontend` läuft `npm run start` (Port 3000)
|
||||
- Go-Backend läuft auf Port 8080, Next.js proxied `/api/*` dorthin (siehe `next.config.ts`)
|
||||
@@ -0,0 +1,154 @@
|
||||
# PROJ-8: Automatischer IMAP-Sync (Cron-Job)
|
||||
|
||||
## Status: In Progress
|
||||
**Created:** 2026-03-12
|
||||
**Last Updated:** 2026-03-12
|
||||
|
||||
## Dependencies
|
||||
- Requires: PROJ-3 (IMAP-Import) – IMAP-Verbindungen müssen konfiguriert sein
|
||||
- Requires: PROJ-5 (Speicherung & Indexierung)
|
||||
|
||||
## User Stories
|
||||
- Als Admin möchte ich ein Sync-Intervall konfigurieren (z.B. alle 15 Minuten), damit neue E-Mails automatisch archiviert werden.
|
||||
- Als Admin möchte ich den letzten Sync-Zeitpunkt und -Status pro IMAP-Verbindung sehen.
|
||||
- Als Admin möchte ich den Sync manuell auslösen können, damit ich nicht auf den nächsten Intervall warten muss.
|
||||
- Als System möchte ich beim Sync nur neue E-Mails (seit letztem Sync) abholen, damit kein unnötiger Traffic entsteht.
|
||||
|
||||
## Acceptance Criteria
|
||||
- [ ] Sync-Intervall pro IMAP-Verbindung konfigurierbar (min. 5 Minuten, max. 24 Stunden)
|
||||
- [ ] IMAP UID-basierter inkrementeller Sync (nur neue E-Mails seit letztem Sync)
|
||||
- [ ] Admin-UI zeigt: letzter Sync, Status (Erfolg/Fehler), Anzahl importierter E-Mails
|
||||
- [ ] Manueller "Sync jetzt"-Button im Admin-Bereich
|
||||
- [ ] Bei Sync-Fehler: Retry mit exponential backoff (max. 3 Versuche)
|
||||
- [ ] Sync-Fehler nach allen Versuchen → Fehlermeldung im Admin-Dashboard
|
||||
|
||||
## Edge Cases
|
||||
- IMAP-Server temporär nicht erreichbar → Retry ohne Abbruch des gesamten Sync-Jobs
|
||||
- Sync läuft noch wenn neuer Intervall beginnt → kein paralleler Sync für dieselbe Verbindung
|
||||
- E-Mails auf dem Server wurden gelöscht → im Archiv behalten (Archiv ist immutable)
|
||||
- Zeitzonenprobleme beim Datum-Vergleich → immer UTC intern verwenden
|
||||
|
||||
## Technical Requirements
|
||||
- Cron-Scheduler eingebettet (z.B. robfig/cron für Go)
|
||||
- Sync-Status persistent in DB gespeichert (überlebt Server-Neustart)
|
||||
|
||||
---
|
||||
## Tech Design (Solution Architect)
|
||||
|
||||
### Komponentenstruktur
|
||||
|
||||
**Next.js Frontend (Admin-Bereich):**
|
||||
```
|
||||
/admin/imap (integriert in IMAP-Verbindungsliste aus PROJ-3)
|
||||
└── VerbindungsCard (pro Konto)
|
||||
├── Sync-Intervall (Dropdown: 5min / 15min / 1h / 6h / 24h)
|
||||
├── Letzter Sync: Zeitpunkt + Status (✓ OK / ✗ Fehler)
|
||||
├── Anzahl importierter Mails beim letzten Sync
|
||||
├── Fehlermeldung (wenn letzter Sync fehlgeschlagen)
|
||||
└── [Sync jetzt] Button
|
||||
```
|
||||
|
||||
**Go Backend:**
|
||||
```
|
||||
Sync-Scheduler (startet beim Binary-Start)
|
||||
├── Cron-Loop ← prüft jede Minute alle IMAP-Accounts
|
||||
│ └── Für jeden Account:
|
||||
│ ├── Intervall abgelaufen? → Sync-Worker starten
|
||||
│ └── Sync läuft bereits? → überspringen (kein Parallel-Sync)
|
||||
│
|
||||
├── Sync-Worker (pro Account, Goroutine)
|
||||
│ ├── IMAP verbinden (gleicher Client wie PROJ-3)
|
||||
│ ├── Letzte bekannte UID aus DB laden
|
||||
│ ├── UID SEARCH UID <last_uid>:* → nur neue Mails
|
||||
│ ├── FETCH neue Mails
|
||||
│ ├── → Storage Coordinator (PROJ-5)
|
||||
│ ├── Letzte UID + Zeitstempel in DB speichern
|
||||
│ └── Bei Fehler: Retry mit Exponential Backoff
|
||||
│ (1. Versuch: sofort, 2.: +1min, 3.: +5min → dann Fehler)
|
||||
│
|
||||
└── POST /api/admin/imap/{id}/sync ← manueller Trigger
|
||||
└── Sync-Worker sofort starten (ignoriert Intervall)
|
||||
```
|
||||
|
||||
### Sync-Fluss
|
||||
|
||||
```
|
||||
Cron-Loop (jede Minute)
|
||||
│
|
||||
└── Account "Firmen-Postfach" – Intervall: 15 min
|
||||
last_sync_at = vor 16 Minuten → fällig
|
||||
sync_running = false → starten
|
||||
│
|
||||
▼
|
||||
IMAP verbinden
|
||||
│
|
||||
▼
|
||||
last_uid = 4821 (aus DB)
|
||||
UID SEARCH UID 4822:*
|
||||
→ [4822, 4823, 4830, 4831] (4 neue Mails)
|
||||
│
|
||||
▼
|
||||
FETCH 4822:4831 RFC822
|
||||
│
|
||||
▼
|
||||
Für jede Mail:
|
||||
Duplikat? → überspringen
|
||||
→ Storage Coordinator
|
||||
│
|
||||
▼
|
||||
last_uid = 4831 in DB speichern
|
||||
last_sync_at = NOW() (UTC)
|
||||
sync_status = "ok"
|
||||
sync_count = 4
|
||||
```
|
||||
|
||||
### Exponential Backoff bei Fehlern
|
||||
|
||||
```
|
||||
Sync-Fehler (z.B. IMAP nicht erreichbar)
|
||||
│
|
||||
├── Versuch 1: sofort → Fehler
|
||||
├── Versuch 2: +1 Minute → Fehler
|
||||
├── Versuch 3: +5 Minuten → Fehler
|
||||
└── Aufgeben:
|
||||
sync_status = "error"
|
||||
error_msg = "Connection refused after 3 attempts"
|
||||
→ Admin-Dashboard zeigt Fehler
|
||||
→ nächster regulärer Intervall versucht es erneut
|
||||
```
|
||||
|
||||
### Datenmodell (Ergänzung zu `imap_accounts`)
|
||||
|
||||
| Feld | Beschreibung |
|
||||
|------|-------------|
|
||||
| `sync_interval_min` | Sync-Intervall in Minuten (5–1440) |
|
||||
| `last_sync_at` | Zeitpunkt des letzten Syncs (UTC) |
|
||||
| `last_sync_count` | Anzahl importierter Mails beim letzten Sync |
|
||||
| `last_uid` | Höchste bekannte IMAP-UID (Startpunkt für nächsten Sync) |
|
||||
| `sync_running` | `true` wenn Sync gerade läuft (verhindert parallelen Sync) |
|
||||
| `sync_status` | `ok` / `error` / `running` |
|
||||
| `sync_error_msg` | Letzte Fehlermeldung |
|
||||
|
||||
### Technische Entscheidungen
|
||||
|
||||
| Entscheidung | Begründung |
|
||||
|---|---|
|
||||
| **UID-basierter inkrementeller Sync** | Nur neue Mails seit letzter bekannter UID werden abgeholt – minimaler Traffic, kein Re-Download |
|
||||
| **`sync_running`-Flag in DB** | Verhindert parallelen Sync derselben Verbindung auch nach Server-Neustart |
|
||||
| **Cron-Loop jede Minute** | Einfacher als individuelle Timer pro Account – skaliert auf viele Accounts ohne Overhead |
|
||||
| **Exponential Backoff** | Temporäre Ausfälle (Netz, Server-Neustart) werden automatisch überbrückt ohne Admin-Eingriff |
|
||||
| **Status persistent in DB** | Server-Neustart verliert keinen Sync-Fortschritt – Scheduler macht nahtlos weiter |
|
||||
| **Manueller Trigger** | Admin kann sofortigen Sync anstoßen ohne auf Intervall zu warten |
|
||||
|
||||
### Abhängigkeiten
|
||||
|
||||
| Paket | Zweck |
|
||||
|---|---|
|
||||
| `github.com/robfig/cron` | Eingebetteter Cron-Scheduler |
|
||||
| `github.com/emersion/go-imap` | IMAP-Client (bereits PROJ-3) |
|
||||
|
||||
## QA Test Results
|
||||
_To be added by /qa_
|
||||
|
||||
## Deployment
|
||||
_To be added by /deploy_
|
||||
@@ -0,0 +1,157 @@
|
||||
# PROJ-9: Ordner- & Label-Verwaltung
|
||||
|
||||
## Status: In Progress
|
||||
**Created:** 2026-03-12
|
||||
**Last Updated:** 2026-03-12
|
||||
|
||||
## Dependencies
|
||||
- Requires: PROJ-1 (Authentifizierung)
|
||||
- Requires: PROJ-5 (Speicherung & Indexierung)
|
||||
|
||||
## User Stories
|
||||
- Als Nutzer möchte ich E-Mails mit Labels versehen, damit ich sie thematisch organisieren kann.
|
||||
- Als Admin möchte ich globale Labels definieren, die automatisch beim Import vergeben werden (z.B. nach Absender-Domain oder Import-Quelle).
|
||||
- Als Nutzer möchte ich meine Suchergebnisse auf ein bestimmtes Label einschränken.
|
||||
- Als Nutzer möchte ich Labels erstellen, umbenennen und löschen.
|
||||
|
||||
## Acceptance Criteria
|
||||
- [ ] Nutzer können Labels erstellen (Name, Farbe)
|
||||
- [ ] E-Mails können mit mehreren Labels versehen werden
|
||||
- [ ] Label-Filter in der Suche verfügbar
|
||||
- [ ] **Keine IMAP-Ordnerstruktur** – das System ist ein Archiv, keine Ordnerhierarchie wird übernommen
|
||||
- [ ] Admin kann Regeln für automatische Label-Vergabe beim Import definieren (z.B. nach Absender-Domain)
|
||||
- [ ] Admin kann globale Labels definieren (für alle Nutzer sichtbar)
|
||||
- [ ] Löschen eines Labels entfernt es von allen E-Mails, löscht E-Mails nicht
|
||||
- [ ] Label-Übersicht in der Seitenleiste mit E-Mail-Anzahl pro Label
|
||||
|
||||
## Edge Cases
|
||||
- Label-Name bereits vergeben → Fehlermeldung
|
||||
- E-Mail wird gelöscht aber Labels bleiben → Labels bleiben erhalten, E-Mail-Referenz entfernt
|
||||
- Sehr viele Labels (> 100) → Suchfeld in der Label-Auswahl
|
||||
|
||||
## Technical Requirements
|
||||
- Labels: n:m-Beziehung zwischen E-Mails und Labels
|
||||
- Performance: Label-Filter darf Suchantwortzeit nicht verdoppeln
|
||||
|
||||
---
|
||||
## Tech Design (Solution Architect)
|
||||
|
||||
### Komponentenstruktur
|
||||
|
||||
**Next.js Frontend:**
|
||||
```
|
||||
Seitenleiste (global, alle Seiten)
|
||||
└── LabelList
|
||||
├── Label-Eintrag (Name, Farbe, Anzahl) ← klickbar → filtert Suche
|
||||
├── [+ Label erstellen] Button
|
||||
└── Suchfeld (bei > 10 Labels)
|
||||
|
||||
Label-Verwaltung (Inline / Modal)
|
||||
├── LabelForm
|
||||
│ ├── Name (Textfeld)
|
||||
│ └── Farbe (Color-Picker, 8 Vorschläge)
|
||||
└── LabelItem-Aktionen
|
||||
├── Umbenennen
|
||||
└── Löschen (mit Bestätigung)
|
||||
|
||||
E-Mail-Ansicht (PROJ-7, Erweiterung)
|
||||
└── LabelPicker
|
||||
├── Aktuelle Labels der Mail (als Badges)
|
||||
├── Dropdown: Labels hinzufügen/entfernen
|
||||
└── [+ Neues Label] Shortcut
|
||||
|
||||
Admin-Bereich (/admin/labels)
|
||||
├── Globale Labels verwalten
|
||||
└── Auto-Label-Regeln
|
||||
├── RegelListe
|
||||
└── RegelForm
|
||||
├── Bedingung: from-Domain / Import-Quelle / Betreff enthält
|
||||
└── Aktion: Label zuweisen
|
||||
```
|
||||
|
||||
**Go Backend:**
|
||||
```
|
||||
Label-API
|
||||
├── GET /api/labels ← alle Labels des Nutzers + globale
|
||||
├── POST /api/labels ← neues Label anlegen
|
||||
├── PATCH /api/labels/{id} ← umbenennen / Farbe ändern
|
||||
├── DELETE /api/labels/{id} ← löschen (entfernt von allen Mails)
|
||||
├── POST /api/mails/{id}/labels ← Label einer Mail zuweisen
|
||||
└── DELETE /api/mails/{id}/labels/{label_id} ← Label entfernen
|
||||
|
||||
Admin Label-API
|
||||
├── POST /api/admin/labels ← globales Label anlegen
|
||||
├── GET /api/admin/label-rules ← Auto-Label-Regeln
|
||||
├── POST /api/admin/label-rules ← Regel anlegen
|
||||
└── DELETE /api/admin/label-rules/{id}
|
||||
|
||||
Label-Filter in Suche (Erweiterung PROJ-6)
|
||||
└── Xapian-Term "label:<label_id>" pro Mail
|
||||
→ Label-Filter läuft direkt in Xapian
|
||||
```
|
||||
|
||||
### Datenmodell
|
||||
|
||||
**Tabelle `labels`:**
|
||||
|
||||
| Feld | Beschreibung |
|
||||
|------|-------------|
|
||||
| `id` | Interne ID |
|
||||
| `name` | Label-Name (eindeutig pro Nutzer) |
|
||||
| `color` | Hex-Farbe (z.B. `#e74c3c`) |
|
||||
| `owner_id` | Nutzer-ID (NULL = globales Admin-Label) |
|
||||
| `created_at` | Erstellungszeitpunkt |
|
||||
|
||||
**Tabelle `email_labels`** – n:m Verknüpfung:
|
||||
|
||||
| Feld | Beschreibung |
|
||||
|------|-------------|
|
||||
| `email_id` | Referenz auf `emails` |
|
||||
| `label_id` | Referenz auf `labels` |
|
||||
| `assigned_at` | Zeitpunkt der Zuweisung |
|
||||
| `assigned_by` | `user` / `auto-rule` / `import` |
|
||||
|
||||
**Tabelle `label_rules`** – Auto-Label beim Import:
|
||||
|
||||
| Feld | Beschreibung |
|
||||
|------|-------------|
|
||||
| `id` | Interne ID |
|
||||
| `condition_field` | `from_domain` / `source` / `subject_contains` |
|
||||
| `condition_value` | z.B. `example.com` oder `imap-account-1` |
|
||||
| `label_id` | Welches Label vergeben |
|
||||
|
||||
### Label-Filter in Xapian
|
||||
|
||||
Beim Indexieren einer Mail werden ihre Labels als Xapian-Terms gespeichert:
|
||||
```
|
||||
Label "Kunde" → Term: "label:42"
|
||||
Label "Projekt" → Term: "label:17"
|
||||
```
|
||||
|
||||
Suche mit Label-Filter läuft vollständig in Xapian – kein zusätzlicher DB-Join nötig. Labels werden beim Zuweisen/Entfernen sofort im Xapian-Dokument aktualisiert.
|
||||
|
||||
### Technische Entscheidungen
|
||||
|
||||
| Entscheidung | Begründung |
|
||||
|---|---|
|
||||
| **Labels statt Ordner** | Archiv hat keine Hierarchie – eine Mail kann mehrere Labels haben, aber nicht in mehreren Ordnern gleichzeitig sein |
|
||||
| **Label-Terms in Xapian** | Filter läuft direkt bei der Suche – kein nachträglicher DB-Join, keine Verdopplung der Antwortzeit |
|
||||
| **Globale Labels (owner_id NULL)** | Admin definiert unternehmensweite Labels – Nutzer können sie nicht löschen, nur zuweisen |
|
||||
| **Auto-Label-Regeln** | Importierte Mails werden sofort kategorisiert – kein manueller Aufwand für Bulk-Importe |
|
||||
| **`assigned_by`-Feld** | Nachvollziehbar ob Label manuell, per Regel oder beim Import vergeben wurde |
|
||||
|
||||
### Abhängigkeiten
|
||||
|
||||
**Next.js Frontend:**
|
||||
|
||||
| Paket | Zweck |
|
||||
|---|---|
|
||||
| `shadcn/ui` | Badge, Popover, Color-Picker-Basis (bereits installiert) |
|
||||
|
||||
**Go Backend:** Nur Stdlib + pgx (bereits vorhanden).
|
||||
|
||||
## QA Test Results
|
||||
_To be added by /qa_
|
||||
|
||||
## Deployment
|
||||
_To be added by /deploy_
|
||||
Reference in New Issue
Block a user