diff --git a/.gitignore b/.gitignore deleted file mode 100644 index e8ee96e..0000000 --- a/.gitignore +++ /dev/null @@ -1,53 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js -.yarn/install-state.gz - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# local env files -.env*.local - -# vercel -.vercel - -# claude code – komplett ignorieren -.claude/ - -# AI Coding Starter Kit – Template-Docs -docs/production/ -features/README.md - -# AI Coding Starter Kit – Supabase Template (nicht verwendet) -src/lib/supabase.ts - -# Next.js Default-Assets (AI Coding Starter Kit) -public/file.svg -public/globe.svg -public/next.svg -public/vercel.svg -public/window.svg - -# typescript -*.tsbuildinfo -next-env.d.ts diff --git a/features/INDEX.md b/features/INDEX.md deleted file mode 100644 index 656f023..0000000 --- a/features/INDEX.md +++ /dev/null @@ -1,38 +0,0 @@ -# Feature Index - -> Central tracking for all features. Updated by skills automatically. - -## Status Legend -- **Planned** - Requirements written, ready for development -- **In Progress** - Currently being built -- **In Review** - QA testing in progress -- **Deployed** - Live in production - -## Features - -| ID | Feature | Status | Spec | Created | -|----|---------|--------|------|---------| -| PROJ-1 | Nutzer-Authentifizierung & Rollen (User/Admin) | Deployed | [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 | Deployed | [PROJ-3](PROJ-3-import-imap.md) | 2026-03-12 | -| PROJ-4 | E-Mail-Import: SMTP-Eingang via BCC (primär) | Deployed | [PROJ-4](PROJ-4-import-smtp.md) | 2026-03-12 | -| PROJ-5 | E-Mail-Speicherung & Volltext-Indexierung | Deployed | [PROJ-5](PROJ-5-speicherung-und-indexierung.md) | 2026-03-12 | -| PROJ-6 | Volltext-Suche & Filterung | Deployed | [PROJ-6](PROJ-6-volltext-suche.md) | 2026-03-12 | -| PROJ-7 | E-Mail-Ansicht (Lesen & Anhänge) | Deployed | [PROJ-7](PROJ-7-email-ansicht.md) | 2026-03-12 | -| PROJ-8 | Automatischer IMAP-Sync (Cron-Job) | Deployed | [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 | Deployed | [PROJ-10](PROJ-10-admin-bereich.md) | 2026-03-12 | -| PROJ-11 | Audit-Log & Compliance-Berichte | Deployed | [PROJ-11](PROJ-11-audit-log.md) | 2026-03-12 | -| PROJ-12 | E-Mail-Export (EML/PDF) | Deployed | [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) | Deployed | [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 | Deployed | [PROJ-17](PROJ-17-system-dashboard.md) | 2026-03-14 | -| PROJ-18 | E-Mail Integritätsprüfung | Deployed | [PROJ-18](PROJ-18-integritaetspruefung.md) | 2026-03-14 | -| PROJ-19 | Mailpiler → archivmail Migrationstool | Deployed | [PROJ-19](PROJ-19-import-piler.md) | 2026-03-17 | - - - -## Next Available ID: PROJ-20 diff --git a/features/PROJ-1-authentifizierung-und-rollen.md b/features/PROJ-1-authentifizierung-und-rollen.md deleted file mode 100644 index de7aee5..0000000 --- a/features/PROJ-1-authentifizierung-und-rollen.md +++ /dev/null @@ -1,185 +0,0 @@ -# PROJ-1: Nutzer-Authentifizierung & Rollen - -## Status: Deployed -**Created:** 2026-03-12 -**Last Updated:** 2026-03-17 - -## 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 - ---- - - -## 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_ diff --git a/features/PROJ-10-admin-bereich.md b/features/PROJ-10-admin-bereich.md deleted file mode 100644 index a326f80..0000000 --- a/features/PROJ-10-admin-bereich.md +++ /dev/null @@ -1,161 +0,0 @@ -# PROJ-10: Admin-Bereich: Nutzer- & Postfachverwaltung - -## Status: Deployed -**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_ diff --git a/features/PROJ-11-audit-log.md b/features/PROJ-11-audit-log.md deleted file mode 100644 index a85abdc..0000000 --- a/features/PROJ-11-audit-log.md +++ /dev/null @@ -1,145 +0,0 @@ -# PROJ-11: Audit-Log & Compliance-Berichte - -## Status: Deployed -**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_ diff --git a/features/PROJ-12-export.md b/features/PROJ-12-export.md deleted file mode 100644 index 011c231..0000000 --- a/features/PROJ-12-export.md +++ /dev/null @@ -1,149 +0,0 @@ -# PROJ-12: E-Mail-Export (EML / PDF) - -## Status: Deployed -**Created:** 2026-03-12 -**Last Updated:** 2026-03-17 - -## 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: .eml oder .pdf - │ ├── Anhänge: attachments// (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) | - -## Implementation Notes (2026-03-14) - -### What was built - -**Go Backend — `internal/api/export.go` (new file):** -- `GET /api/export/pdf/{id}` — generates a stdlib-only PDF (no external deps) using raw PDF 1.4 syntax with Helvetica core font. Renders header fields, plain-text body (with HTML-strip fallback), and attachment list. `toLatinSafe()` converts umlauts for Latin-1 compatibility. -- `POST /api/export/zip` — streaming ZIP via `archive/zip` stdlib. Accepts `{"ids":[...], "attachments": true}`, max 500 IDs. Adds `{id[:16]}.eml`, optional `attachments/{id[:8]}/{filename}` entries, and `manifest.csv` (CSV with filename/message_id/from/to/subject/date). Audit-logged as `export: zip: N mails`. -- Both handlers use `requireMailAccess` middleware (blocks admin role). RoleUser is filtered to own mails via `mailBelongsToUser`; RoleAuditor can export all. - -**Deviation from spec:** PDF generation uses a hand-rolled stdlib PDF writer instead of `go-wkhtmltopdf` or `go-pdf/fpdf` — avoids adding an external dependency that would require `go get` on the server. - -**Routes added to `internal/api/server.go`:** -- `GET /api/export/pdf/{id}` -- `POST /api/export/zip` - -**Frontend `src/lib/api.ts`:** -- Added `exportMailPDF(id)` and `exportMailsZIP(ids, attachments)` export functions. - -**Frontend `src/app/mail/[id]/page.tsx`:** -- Added "Als PDF exportieren" button next to "Als .eml herunterladen". - -**Frontend `src/app/search/page.tsx`:** -- Added per-row Checkbox column + select-all header checkbox. -- Export toolbar appears when ≥1 mail is selected. -- ZIP export dialog with attachments toggle (Switch component). -- Selection cleared when search results change. - -## QA Test Results -_To be added by /qa_ - -## Deployment -_To be added by /deploy_ diff --git a/features/PROJ-13-rest-api-crm.md b/features/PROJ-13-rest-api-crm.md deleted file mode 100644 index 53e4c19..0000000 --- a/features/PROJ-13-rest-api-crm.md +++ /dev/null @@ -1,178 +0,0 @@ -# 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 ` -- [ ] 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 - ▼ -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_ diff --git a/features/PROJ-14-import-pop3.md b/features/PROJ-14-import-pop3.md deleted file mode 100644 index 6aa8cfb..0000000 --- a/features/PROJ-14-import-pop3.md +++ /dev/null @@ -1,147 +0,0 @@ -# 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 → 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_ diff --git a/features/PROJ-15-cli-import-export.md b/features/PROJ-15-cli-import-export.md deleted file mode 100644 index 0c44626..0000000 --- a/features/PROJ-15-cli-import-export.md +++ /dev/null @@ -1,203 +0,0 @@ -# PROJ-15: CLI Import & Export - -## Status: Deployed -**Created:** 2026-03-13 -**Last Updated:** 2026-03-17 - -## 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 -- [x] `archivmail import --file /pfad/zu/datei.eml` – einzelne EML importieren -- [x] `archivmail import --file /pfad/zu/archiv.mbox` – MBOX importieren -- [x] `archivmail import --dir /pfad/zum/verzeichnis/` – alle EML-Dateien in einem Verzeichnis importieren (rekursiv optional: `--recursive`) -- [x] Fortschrittsausgabe auf stdout (eine Zeile pro 100 Mails) -- [x] Exit-Code 0 bei Erfolg, 1 bei Fehler -- [x] Duplikate werden übersprungen (SHA256-Dedup im Store), kein Fehler -- [x] `--dry-run` Flag: zeigt was importiert würde ohne tatsächlich zu speichern - -### Export -- [x] `archivmail export --out /pfad/ziel/` – alle Mails als EML-Dateien exportieren -- [x] `archivmail export --out /pfad/archiv.mbox` – alle Mails als MBOX exportieren -- [x] `archivmail export --from alice@firma.de --out /pfad/` – Filter nach Absender -- [x] `archivmail export --date-from 2024-01-01 --date-to 2024-12-31 --out /pfad/` – Filter nach Datum -- [x] `archivmail export --query "Rechnung" --out /pfad/` – Filter per Volltext-Suche (Xapian) -- [x] Exportierte Mails als Klartext EML auf Disk -- [x] `--format eml` (Standard) oder `--format mbox` - -### Allgemein -- [x] CLI läuft als Systembenutzer `archivmail` – Config aus `/etc/archivmail/config.yml` -- [x] Fehler werden auf stderr ausgegeben -- [x] `archivmail help` zeigt Übersicht aller Befehle -- [x] `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 ` -- 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 [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) | - -## Implementation Notes -- Subcommands in `cmd/archivmail/main.go` via `os.Args[1]` Router (kein cobra nötig) -- `cmd_import.go`: EML + MBOX Import, `--file`, `--dir`, `--recursive`, `--dry-run`, `--json` -- `cmd_export.go`: EML + MBOX Export, alle Filter, `--force`, `--json` -- MBOX Parser in `pkg/mailparser/mbox.go` (`SplitMbox`) -- MBOX Export mit korrektem `>From ` Escaping -- Deployed auf `root@192.168.1.131`, Daemon läuft - -## QA Test Results -_To be added by /qa_ - -## Deployment -_To be added by /deploy_ diff --git a/features/PROJ-16-ldap-active-directory.md b/features/PROJ-16-ldap-active-directory.md deleted file mode 100644 index da2a69a..0000000 --- a/features/PROJ-16-ldap-active-directory.md +++ /dev/null @@ -1,109 +0,0 @@ ---- -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) diff --git a/features/PROJ-17-system-dashboard.md b/features/PROJ-17-system-dashboard.md deleted file mode 100644 index b62ce83..0000000 --- a/features/PROJ-17-system-dashboard.md +++ /dev/null @@ -1,86 +0,0 @@ -# PROJ-17: Admin Dashboard – Systemauslastung & Archiv-Übersicht - -## Status: Deployed -**Created:** 2026-03-14 -**Last Updated:** 2026-03-17 - -## 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` diff --git a/features/PROJ-18-integritaetspruefung.md b/features/PROJ-18-integritaetspruefung.md deleted file mode 100644 index aecb0f3..0000000 --- a/features/PROJ-18-integritaetspruefung.md +++ /dev/null @@ -1,19 +0,0 @@ -# PROJ-18: E-Mail Integritätsprüfung - -## Status: Deployed -**Created:** 2026-03-14 -**Last Updated:** 2026-03-17 - -## User Stories -- Als Admin möchte ich sehen ob eine archivierte E-Mail unverändert ist, damit ich Manipulationen erkennen kann. - -## Acceptance Criteria -- [x] Hintergrund-Job läuft alle 5 Minuten und prüft alle E-Mails -- [x] Prüfung: SHA-256 der entschlüsselten Datei == gespeicherte ID -- [x] Ergebnis wird in DB gespeichert (verify_ok, verified_at) -- [x] Mail-Ansicht zeigt grünen Haken (verifiziert OK), graues X (noch nicht geprüft) oder rotes X (Manipulation erkannt) - -## Implementation Notes -- verify_ok BOOLEAN + verified_at TIMESTAMPTZ in emails-Tabelle -- Background worker in main.go, Ticker 5 Minuten -- GET /api/mails/{id} gibt verified_ok + verified_at zurück diff --git a/features/PROJ-19-import-piler.md b/features/PROJ-19-import-piler.md deleted file mode 100644 index 1debd82..0000000 --- a/features/PROJ-19-import-piler.md +++ /dev/null @@ -1,62 +0,0 @@ -# PROJ-19: Mailpiler → archivmail Migrationstool - -## Status: Deployed -**Created:** 2026-03-17 -**Last Updated:** 2026-03-17 - -## Dependencies -- Requires: PROJ-5 (Speicherung & Indexierung) -- Requires: PROJ-15 (CLI Import) - -## User Stories -- Als Admin möchte ich alle E-Mails aus einem bestehenden mailpiler-Archiv nach archivmail migrieren, damit ich die Plattform wechseln kann ohne E-Mails zu verlieren. -- Als Admin möchte ich den Fortschritt der Migration in Echtzeit sehen. -- Als Admin möchte ich Duplikate automatisch überspringen, damit bei Teil-Migrationen oder Wiederholungsläufen keine Daten doppelt archiviert werden. - -## Acceptance Criteria -- [x] Methode 1: `pilerexport`-Wrapper – ruft das mailpiler-eigene Export-Tool auf, importiert die EML-Dateien -- [x] Methode 2: Direkt – liest `.m`-Dateien aus dem mailpiler Store-Verzeichnis, entschlüsselt (AES-256-CBC) und dekomprimiert (zlib) -- [x] Automatische Methodenwahl (`--method auto`): pilerexport → direct -- [x] Fortschrittsanzeige (importiert / übersprungen / Fehler) -- [x] `--dry-run` Modus -- [x] JSON-Ausgabe für Skripting -- [x] Datums-Filter (`--date-from`, `--date-to`) für pilerexport-Methode - -## Aufruf - -```bash -# Auf dem mailpiler-Server (pilerexport-Methode, empfohlen): -archivmail import-piler \ - --config /etc/archivmail/config.yml \ - --method pilerexport - -# Mit Datumsfilter: -archivmail import-piler \ - --config /etc/archivmail/config.yml \ - --date-from 2020-01-01 \ - --date-to 2024-12-31 - -# Direkte Methode (kein mailpiler nötig, kein MySQL nötig): -archivmail import-piler \ - --config /etc/archivmail/config.yml \ - --method direct \ - --store-dir /var/piler/store \ - --key-file /var/piler/store/piler.key - -# Nur Simulation (kein Speichern): -archivmail import-piler --dry-run - -# JSON-Ausgabe für Skripte: -archivmail import-piler --json -``` - -## Technische Details - -| Aspekt | Detail | -|--------|--------| -| pilerexport-Ausgabe | EML-Dateien im temp-Verzeichnis, jede `.eml` = eine E-Mail | -| mailpiler-Dateiformat | `{storedir}/**/{piler_id}.m` – AES-256-CBC verschlüsselt, zlib komprimiert | -| Entschlüsselung | Erste 16 Bytes = IV, Rest = CBC-Ciphertext, Key aus `piler.key` (32 Bytes) | -| Dekomprimierung | zlib (ohne AES falls kein Key vorhanden) | -| Duplikat-Erkennung | Storage.Save() + IsIndexed() – identische Inhalts-Hashes werden übersprungen | -| Keine ext. Abhängigkeiten | Nur Go stdlib (compress/zlib, crypto/aes) + vorhandene archivmail-Pakete | diff --git a/features/PROJ-2-import-eml-mbox.md b/features/PROJ-2-import-eml-mbox.md deleted file mode 100644 index 112df65..0000000 --- a/features/PROJ-2-import-eml-mbox.md +++ /dev/null @@ -1,146 +0,0 @@ -# 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_ diff --git a/features/PROJ-3-import-imap.md b/features/PROJ-3-import-imap.md deleted file mode 100644 index e8320c0..0000000 --- a/features/PROJ-3-import-imap.md +++ /dev/null @@ -1,205 +0,0 @@ -# PROJ-3: E-Mail-Import: IMAP-Verbindung - -## Status: Deployed -**Created:** 2026-03-12 -**Last Updated:** 2026-03-17 - -## 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. diff --git a/features/PROJ-4-import-smtp.md b/features/PROJ-4-import-smtp.md deleted file mode 100644 index 337e92d..0000000 --- a/features/PROJ-4-import-smtp.md +++ /dev/null @@ -1,164 +0,0 @@ -# PROJ-4: E-Mail-Import: SMTP-Eingang (primär via BCC) - -## Status: Deployed -**Created:** 2026-03-12 -**Last Updated:** 2026-03-17 - -## 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: -250 OK - │ -RCPT TO: -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_ diff --git a/features/PROJ-5-speicherung-und-indexierung.md b/features/PROJ-5-speicherung-und-indexierung.md deleted file mode 100644 index cc9e7f6..0000000 --- a/features/PROJ-5-speicherung-und-indexierung.md +++ /dev/null @@ -1,242 +0,0 @@ -# PROJ-5: E-Mail-Speicherung & Volltext-Indexierung - -## Status: Deployed -**Created:** 2026-03-12 -**Last Updated:** 2026-03-17 - -## 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////xxxxx.m` (AES-256-GCM verschlüsselt) -- [ ] Anhänge gespeichert unter `/var/archivmail/astore/` (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////xxxxx.m - ``` - - Anhänge dedupliziert in separatem Store (ein Anhang = eine Datei, unabhängig wie oft er vorkommt): - ``` - /var/archivmail/astore/ - ``` - - 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 - ///x.m └─ /var/archivmail/astore/ - + 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 | - -## Implementation Notes (2026-03-14) - -### What was built - -1. **AES-256-GCM encryption** in `internal/storage/storage.go`: - - Key loaded from file at `cfg.Storage.Keyfile` path or `ARCHIVMAIL_KEY` env var - - Supports base64-encoded or raw 32-byte key files - - If no keyfile configured, stores unencrypted (backwards compatible for dev) - - `Save()` encrypts with random 12-byte nonce prepended to ciphertext - - `Load()` decrypts transparently; falls back to raw read if decryption fails (pre-encryption files) - - SHA-256 dedup based on **plaintext** content (hash before encrypt) - - Same flat file path `store/{id[:2]}/{id}` - -2. **PostgreSQL `emails` metadata table** auto-created at startup: - - Schema: `id TEXT PK, received_at, mail_from, mail_to, subject, size_bytes, has_attach, indexed_at` - - Indexes on `received_at`, `mail_from`, and GIN on `subject` - - `Save()` inserts metadata via mailparser after writing file (ON CONFLICT DO NOTHING) - - `Delete()` also removes DB row - - `Stats()` and `FirstAndLastMail()` use DB queries when available (fast), fall back to FS walk - - New methods: `SaveMeta()`, `SetIndexedAt()`, `IsIndexed()`, `WalkStore()` - -3. **Storage constructor changed** from `New(dir string)` to `New(cfg storage.Config)`: - - `Config` struct: `Dir`, `Keyfile`, `DSN` - - All callers updated: `main.go`, `cmd_import.go`, `cmd_export.go` - - `Close()` method added to release DB pool - -4. **Async Index Worker** in `internal/index/worker.go`: - - Buffered channel queue (configurable size via `config.Index.AsyncQueueSize`) - - `Submit()` is non-blocking; drops + warns if queue full - - `Start()` launches background goroutine; `Stop()` drains queue and blocks until done - - Serialises Xapian writes (one writer at a time) - -5. **SMTP daemon integration**: `SetIndexCallback()` on `smtpd.Daemon` - - After each successfully stored mail, callback submits to async worker - - Wired in `main.go` - -6. **Backfill at startup** in `main.go`: - - Runs in background goroutine - - Walks store directory, parses each file, upserts DB metadata - - Submits un-indexed emails (`indexed_at IS NULL`) to the async worker - - Logs progress every 100 files - -### Deviations from spec -- Store path kept flat `store/{id[:2]}/{id}` (no `server_id/customer_id` hierarchy) per user decision -- Attachment dedup store (`astore/`) not yet implemented (body + attachments stored together in `.m` files as before) -- No separate `attachments` or `email_attachments` DB tables yet (deferred to future iteration) -- IMAP importer still uses synchronous `IndexSync()` directly (not routed through async worker yet) - -## QA Test Results -_To be added by /qa_ - -## Deployment -_To be added by /deploy_ diff --git a/features/PROJ-6-volltext-suche.md b/features/PROJ-6-volltext-suche.md deleted file mode 100644 index e82bf56..0000000 --- a/features/PROJ-6-volltext-suche.md +++ /dev/null @@ -1,128 +0,0 @@ -# PROJ-6: Volltext-Suche & Filterung - -## Status: Deployed -**Created:** 2026-03-12 -**Last Updated:** 2026-03-17 - -## 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_ diff --git a/features/PROJ-7-email-ansicht.md b/features/PROJ-7-email-ansicht.md deleted file mode 100644 index 0d77df1..0000000 --- a/features/PROJ-7-email-ansicht.md +++ /dev/null @@ -1,184 +0,0 @@ -# PROJ-7: E-Mail-Ansicht (Lesen & Anhänge) - -## Status: Deployed -**Created:** 2026-03-12 -**Last Updated:** 2026-03-17 - -## 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 `