chore: .gitignore und features/ aus Git-Tracking entfernen (nur lokal)

Ignore-Regeln liegen jetzt in .git/info/exclude und werden
nie nach Gitea übertragen.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
sysops
2026-03-17 09:53:47 +01:00
parent ce10864e62
commit 861df83986
21 changed files with 0 additions and 2924 deletions
-53
View File
@@ -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
-38
View File
@@ -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 |
<!-- Add features above this line -->
## Next Available ID: PROJ-20
@@ -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
---
<!-- Sections below are added by subsequent skills -->
## Tech Design (Solution Architect)
### Systemübersicht: Two-Tier Architektur
```
Browser (Next.js App) Go REST API Backend
│ │
│ POST /api/auth/login │
│ {email, password} │
│ ─────────────────────────────────► │
│ 1. Lokaler Account? → bcrypt verify
│ 2. Nicht gefunden + LDAP aktiv?
│ → LDAP-Bind (Service Account)
│ → User-DN suchen
│ → User-Bind mit Passwort
│ → AD-Gruppen → Rolle bestimmen
│ → UpsertLDAPUser in PostgreSQL
│ 3. Session-Token erstellen
│ Session in PostgreSQL speichern
│ ◄─────────────────────────────────
│ Set-Cookie: session=TOKEN │
│ (httpOnly, Secure, SameSite) │
│ │
│ GET /api/search?q=... │
│ Cookie: session=TOKEN │
│ ─────────────────────────────────► │
│ Session-Middleware: Token prüfen
│ Role-Middleware: Route erlaubt?
│ ◄─────────────────────────────────
│ JSON-Antwort │
```
> **LDAP ist vollständig optional.** Wenn `ldap.enabled: false` (Standard), verhält sich das System exakt wie ohne LDAP. Lokale Accounts funktionieren immer — auch wenn LDAP aktiviert ist (Fallback bei LDAP-Fehler).
### Komponentenstruktur
**Next.js Frontend:**
```
src/app/
├── /login ← Login-Seite (öffentlich)
├── /search ← Suche + E-Mail-Ansicht (user + auditor)
├── /audit ← Audit-Log (nur auditor)
└── /admin ← Admin-Bereich (nur admin)
src/components/
├── LoginForm ← E-Mail + Passwort, Fehlermeldungen
├── RoleGuard ← Schützt Routen clientseitig, redirect auf /login
└── PasswordChangePrompt ← Hinweis bei Default-Passwort
```
**Go Backend:**
```
HTTP-Server
├── POST /api/auth/login ← Session ausstellen
├── POST /api/auth/logout ← Session löschen
├── Session Middleware ← prüft Token bei allen /api/* Routen
├── Role Middleware
│ ├── /api/admin/* → nur `admin`
│ ├── /api/audit/* → nur `auditor`
│ └── /api/* → `user` + `auditor` (admin → 403)
├── Password Manager ← bcrypt Hash + Verify
├── User Store ← PostgreSQL users-Tabelle
└── Bootstrap ← Default-User beim ersten Start
```
### Datenmodell
**Tabelle `users`:**
| Feld | Beschreibung |
|------|-------------|
| `id` | Interne ID |
| `email` | Login-E-Mail (eindeutig) |
| `password_hash` | bcrypt-Hash (Cost 12) — NULL bei LDAP-Usern |
| `role` | `user` / `auditor` / `admin` |
| `source` | `local` oder `ldap` — Herkunft des Accounts |
| `active` | Deaktivierte Nutzer können sich nicht einloggen |
| `created_at` | Erstellungszeitpunkt |
| `last_login_at` | Letzter erfolgreicher Login |
**Tabelle `sessions`:**
| Feld | Beschreibung |
|------|-------------|
| `token` | Zufälliger 32-Byte-Token |
| `user_id` | Referenz auf `users` |
| `expires_at` | Ablaufzeitpunkt (rollierend, +8h bei Aktivität) |
| `last_active_at` | Wird bei jeder Anfrage aktualisiert |
### Technische Entscheidungen
| Entscheidung | Begründung |
|---|---|
| **Next.js Frontend + Go REST API** | Klare Trennung: Next.js rendert die UI, Go verwaltet Daten und Sicherheit. Kein Java. |
| **Session-Cookie (httpOnly)** | Next.js sendet Cookie automatisch mit kein manuelles Token-Handling im Frontend-Code nötig |
| **Server-side Sessions (nicht JWT)** | Logout und Admin-Deaktivierung wirken sofort JWT wäre bis Ablauf weiterhin gültig |
| **Role-Check im Go-Backend** | Sicherheits-kritische Prüfung liegt im Backend, nicht im Next.js-Client (der wäre manipulierbar) |
| **RoleGuard in Next.js zusätzlich** | Verhindert kurzes Aufblitzen falscher Seiten rein UX, kein Sicherheits-Feature |
| **bcrypt Cost 12** | Ausreichend langsam gegen Brute-Force |
| **LDAP als optionaler Fallback** | Login versucht erst lokalen Account, dann LDAP Reihenfolge garantiert, dass lokale Admins immer funktionieren |
| **LDAP-User in PostgreSQL gespiegelt** | Nach erstem Login landet LDAP-User in `users`-Tabelle (`source: ldap`) einheitliche Session-Verwaltung, kein Sonder-Code |
| **AD-Gruppen → Rollen-Mapping** | Rolle wird bei jedem Login aus AD-Gruppenmitgliedschaft neu bestimmt Rollen-Änderung in AD wirkt beim nächsten Login |
### Abhängigkeiten
**Go Backend:**
| Paket | Zweck |
|---|---|
| `golang.org/x/crypto/bcrypt` | Passwort-Hashing |
| `crypto/rand` | Sichere Token-Generierung (Stdlib) |
| `github.com/go-ldap/ldap/v3` | LDAP/AD-Authentifizierung (PROJ-16) |
**Next.js Frontend:**
| Paket | Zweck |
|---|---|
| `react-hook-form` + `zod` | Login-Formular-Validierung (bereits im Template) |
| `shadcn/ui` | UI-Komponenten (bereits installiert) |
## QA Test Results
_To be added by /qa_
## Deployment
_To be added by /deploy_
-161
View File
@@ -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_
-145
View File
@@ -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 (vonbis), 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_
-149
View File
@@ -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: <message_id>.eml oder <message_id>.pdf
│ ├── Anhänge: attachments/<hash>/<filename> (wenn aktiviert)
│ └── manifest.csv (Message-ID, From, To, Subject, Date, Dateiname)
└── → Audit-Log: export_start + export_done (Anzahl, Format)
```
### Export-Fluss (Massenexport)
```
POST /api/export/zip
├── Zugriffsfilter: IDs auf user-eigene Mails beschränken
├── Audit-Log: export_start
├── ZIP-Stream öffnen (Content-Type: application/zip)
└── Für jede Mail:
.m-Datei lesen → AES-256-GCM entschlüsseln
→ Zu ZIP hinzufügen (kein vollständiges RAM-Buffering)
manifest.csv Zeile anhängen
ZIP schließen → Audit-Log: export_done
```
### Technische Entscheidungen
| Entscheidung | Begründung |
|---|---|
| **Streaming-ZIP** | 500 Mails mit Anhängen können mehrere GB sein — kein RAM-Overhead |
| **Serverseitiges PDF** | Browser-Print ist nicht reproduzierbar; serverseitiges PDF ist auditierbar und einheitlich |
| **Plain-Text-Fallback für PDF** | Komplexes HTML kann PDF-Renderer zum Absturz bringen — graceful degradation |
| **Zugriffscheck im Export-Handler** | Serverseitige Filterung verhindert Datenlecks durch manipulierte IDs |
| **manifest.csv im ZIP** | Nachvollziehbarkeit bei Behördenanfragen ohne jede EML einzeln öffnen zu müssen |
| **Audit-Log für jeden Export** | Compliance-Anforderung — wer hat wann was exportiert |
### Abhängigkeiten
| Paket | Zweck |
|---|---|
| `archive/zip` (Stdlib) | Streaming-ZIP-Erstellung ohne externe Abhängigkeit |
| `github.com/SebastiaanKlippert/go-wkhtmltopdf` | PDF-Generierung aus HTML (serverseitig) |
## 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_
-178
View File
@@ -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 <api-key>`
- [ ] Endpunkt: `GET /api/v1/mails?from=&to=&subject=&date_from=&date_to=` Suche/Filterung
- [ ] Endpunkt: `GET /api/v1/mails/{message_id}` einzelne E-Mail abrufen (Metadaten)
- [ ] Endpunkt: `GET /api/v1/mails/{message_id}/raw` Original-EML herunterladen
- [ ] Endpunkt: `GET /api/v1/mails?contact=email@firma.de` alle Mails eines Kontakts (From oder To)
- [ ] Antwortformat: JSON für Metadaten, `application/octet-stream` für Raw-EML
- [ ] Paginierung: `?page=1&limit=25` (max. 100 pro Anfrage)
- [ ] API-Zugriffe werden im Audit-Log erfasst (API-Key-Name, Endpunkt, Zeitstempel)
- [ ] OpenAPI/Swagger-Dokumentation unter `/api/v1/docs`
## Edge Cases
- Ungültiger oder deaktivierter API-Key → 401 Unauthorized
- API-Key mit `user`-Rolle fragt Mails ab, auf die er keinen Zugriff hat → 403
- Rate-Limiting: zu viele Anfragen pro API-Key → 429 Too Many Requests
- Sehr große Ergebnismengen (>10.000 Treffer) → Paginierung erzwingen, kein Full-Dump
- CRM fragt nicht existierende Message-ID ab → 404
## Technical Requirements
- **Reine Lese-API** ausschließlich `GET`-Endpunkte, keine Schreiboperationen
- Eigener Route-Prefix `/api/v1/` für externe API (getrennt von interner `/api/`)
- API-Keys: zufällig generiert (32 Byte, Base64), bcrypt-gehasht in der DB (nie im Klartext)
- Rate-Limiting pro API-Key konfigurierbar (Standard: 60 Anfragen/Minute)
- OpenAPI 3.0 Spec wird aus Code generiert oder manuell gepflegt
- Versionierung: `/api/v1/` spätere Versionen brechen bestehende Clients nicht
---
## Tech Design (Solution Architect)
### Systemübersicht
```
CRM / ERP / Helpdesk
│ GET /api/v1/mails?contact=kunde@example.com
│ Authorization: Bearer <api-key>
Go Backend Externer API-Router (/api/v1/*)
├── API-Key Middleware ← statt Session-Cookie
├── Rate Limiter
├── Shared Search Service ←──── dieselbe Logik wie interne Suche (PROJ-6)
└── Shared Mail Service ←──── dieselbe Logik wie Mail-Abruf (PROJ-7)
```
### Komponentenstruktur
**Go Backend:**
```
/api/v1/* (externer Prefix, getrennt von internem /api/*)
├── API-Key Middleware ← ersetzt Session-Middleware
│ ├── Bearer-Token aus Header lesen
│ ├── SHA-256-Hash → DB-Lookup ← schneller Lookup ohne bcrypt-Overhead
│ ├── Key deaktiviert? → 401
│ ├── Rolle laden (user/auditor)
│ └── Rate-Limit-Konfiguration laden
├── Rate Limiter ← Token-Bucket pro API-Key
│ └── Limit überschritten → 429 + Retry-After Header
├── Method Guard ← alles außer GET → 405
├── Shared Search Service ← identische Logik wie /api/search (PROJ-6)
│ ├── Xapian QueryParser
│ ├── Rollen-Filter (user/auditor)
│ └── PostgreSQL Metadaten-Lookup
├── Shared Mail Service ← identische Logik wie /api/mails (PROJ-7)
│ ├── .m-Datei lesen + entschlüsseln
│ ├── MIME-Parser
│ └── Anhang-Streaming
├── Audit Logger ← API-Key-Name + Endpunkt + Zeitstempel
└── API-Key-Verwaltung (Admin)
├── POST /api/admin/apikeys ← Key generieren (einmalige Anzeige)
├── GET /api/admin/apikeys ← Liste (Name + Rolle + letzter Zugriff)
└── DELETE /api/admin/apikeys/{id}
```
### API-Key Authentifizierungsfluss
```
CRM-System
│ Authorization: Bearer am_a1b2c3d4e5f6...
API-Key Middleware
├─ Präfix "am_" prüfen
├─ SHA-256(token) → DB-Lookup (indiziert)
├─ Key gefunden + aktiv? Nein → 401
└─ Ja → Rolle + Rate-Limit laden
Rate Limiter (Token-Bucket)
├─ Limit erreicht? → 429 + Retry-After: 30
└─ OK → weiter
Method Guard
├─ POST/PUT/DELETE? → 405
└─ GET → weiter
Shared Service Layer
Audit Logger → API-Key-Name + Endpunkt + Zeitstempel
```
### Datenmodell
**Tabelle `api_keys`:**
| Feld | Beschreibung |
|------|-------------|
| `id` | Interne ID |
| `name` | Bezeichnung (z.B. "CRM Salesforce") |
| `token_hash` | SHA-256 des Tokens (für schnellen Lookup, indiziert) |
| `role` | `user` oder `auditor` |
| `active` | `true` / `false` |
| `rate_limit` | Anfragen pro Minute (Standard: 60) |
| `created_at` | Erstellungszeitpunkt |
| `last_used_at` | Letzter erfolgreicher Zugriff |
**Key-Format:**
```
Generiert: am_<32-Byte-random-Base64>
Gespeichert: SHA-256(token) in DB
Angezeigt: einmalig im Admin-UI danach nicht mehr abrufbar
```
### Technische Entscheidungen
| Entscheidung | Begründung |
|---|---|
| **Shared Service Layer** | Suche und Mail-Abruf teilen dieselbe Go-Logik mit der internen API kein doppelter Code |
| **SHA-256 statt bcrypt** | API-Keys sind kryptografisch zufällig (32 Byte) SHA-256 reicht, bcrypt wäre bei jeder Anfrage zu langsam |
| **`am_`-Präfix** | Erkennungsmerkmal für archivmail-Keys einfach filterbar in Logs |
| **Token einmalig anzeigen** | Nur Hash gespeichert kein späteres Auslesen möglich (wie GitHub PAT) |
| **Token-Bucket Rate Limiter** | Gleichmäßige Anfragen erlaubt, kurze Bursts toleriert |
| **`/api/v1/` Prefix** | Klare Versionierung zukünftige `/api/v2/` bricht bestehende Clients nicht |
| **Audit-Log bei API-Zugriffen** | Externe Zugriffe werden geloggt (anders als interne Lesezugriffe) |
### Abhängigkeiten
Kein zusätzliches Paket Rate-Limiter und SHA-256 aus der Go-Stdlib (`crypto/sha256`, `sync`).
## QA Test Results
_To be added by /qa_
## Deployment
_To be added by /deploy_
-147
View File
@@ -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 <n> → rohe Mail (RFC 2822)
Message-ID Duplikat? → überspringen
→ Storage Coordinator (PROJ-5)
Fortschritt: n / 3842
Kein DELE → Mails bleiben auf dem Server
QUIT → Verbindung trennen
Abschlussbericht speichern
```
### Technische Entscheidungen
| Entscheidung | Begründung |
|---|---|
| **Kein DELE** | Archiv löscht nichts vom Quellserver nur lesen und archivieren |
| **Kein regelmäßiger Sync** | POP3 hat keine UIDs es gibt keine zuverlässige Möglichkeit festzustellen welche Mails bereits importiert wurden |
| **Synthetische Message-ID bei Fehlen** | POP3-Mails ohne Message-ID bekommen SHA-256(Inhalt) als ID Duplikatserkennung bleibt konsistent |
| **Gleiche Codebasis wie IMAP-Worker** | Import-Worker-Struktur identisch nur POP3-Client statt IMAP-Client |
### Abhängigkeiten
| Paket | Zweck |
|---|---|
| `github.com/emersion/go-message` | POP3-Client mit TLS/STARTTLS |
## QA Test Results
_To be added by /qa_
## Deployment
_To be added by /deploy_
-203
View File
@@ -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 <command>`
- Zugriff auf Storage Coordinator direkt (kein HTTP-Umweg über den laufenden Daemon)
- Key-Datei muss lesbar sein (`/etc/archivmail/keyfile`, `chmod 400`, Owner `archivmail`)
- Kann parallel zum laufenden Daemon betrieben werden (Xapian WritableDatabase: Lock beachten)
- Strukturierte Ausgabe optional: `--json` Flag für maschinenlesbare Ausgabe
---
## Tech Design (Solution Architect)
### CLI-Struktur
```
archivmail <command> [flags]
Commands:
import E-Mails importieren (EML, MBOX, Verzeichnis)
export E-Mails exportieren (EML, MBOX)
version Version anzeigen
help Hilfe anzeigen
archivmail import
--file /pfad/datei.eml oder .mbox
--dir /pfad/verzeichnis/
--recursive Unterverzeichnisse einschließen (mit --dir)
--dry-run Simulation ohne Speichern
--json Maschinenlesbare Ausgabe (JSON)
archivmail export
--out /pfad/ziel/ oder /pfad/archiv.mbox (Pflicht)
--format eml (Standard) | mbox
--from Absender-Filter
--to Empfänger-Filter
--date-from Datum von (ISO 8601: 2024-01-01)
--date-to Datum bis (ISO 8601: 2024-12-31)
--query Volltext-Suche (Xapian QueryParser)
--force Zieldatei überschreiben
--json Maschinenlesbare Ausgabe (JSON)
```
### Komponentenstruktur
```
archivmail (Go-Binary)
├── main.go ← Subcommand-Router (import / export / ...)
├── cmd/import.go
│ ├── Flag-Parsing
│ ├── Dateityp-Erkennung (.eml / .mbox / Verzeichnis)
│ ├── EML-Parser
│ ├── MBOX-Parser (zeilenweise)
│ └── → Storage Coordinator (PROJ-5, direkt, kein HTTP)
└── cmd/export.go
├── Flag-Parsing
├── Filter-Builder (from, to, date, query)
├── → Xapian ReadonlyDatabase (Suche/Filter)
├── → PostgreSQL Metadaten-Lookup
├── → .m-Datei lesen + AES-256-GCM entschlüsseln
└── Schreiben als EML-Dateien oder MBOX
```
### Import-Fluss
```
$ archivmail import --dir /backup/mails/ --recursive
Key laden aus /etc/archivmail/keyfile
Verzeichnis scannen → 3.842 .eml-Dateien gefunden
[████████░░] 2.150 / 3.842 (übersprungen: 12 Duplikate)
Fertig:
Importiert: 2.130
Übersprungen: 12 (Duplikate)
Fehler: 0
```
### Export-Fluss
```
$ archivmail export --from alice@firma.de \
--date-from 2024-01-01 \
--out /backup/export/
Key laden aus /etc/archivmail/keyfile
Xapian: 847 Mails gefunden (Filter: from=alice, date>=2024-01-01)
Exportiere nach /backup/export/
[████████████] 847 / 847
Fertig:
Exportiert: 847 EML-Dateien
Ziel: /backup/export/
```
### JSON-Ausgabe (--json Flag)
```json
{
"status": "done",
"imported": 2130,
"skipped": 12,
"errors": 0,
"duration_sec": 42
}
```
### Xapian-Lock beim parallelen Betrieb
```
Daemon läuft (WritableDatabase hält Lock für Index-Worker)
CLI export → ReadonlyDatabase → kein Lock-Konflikt ✓
CLI import → Storage Coordinator → WritableDatabase
└── Lock bereits gehalten?
→ Warten (max. 30 Sek.) → dann Fehlermeldung:
"Index locked by running daemon. Stop daemon or retry."
```
### Technische Entscheidungen
| Entscheidung | Begründung |
|---|---|
| **Gleiche Binary, Subcommands** | Kein separates CLI-Tool `archivmail import` und `archivmail serve` teilen Code und Storage Coordinator |
| **Direkter Speicherzugriff, kein HTTP** | CLI läuft als `archivmail`-User mit Dateisystem-Zugriff kein laufender Daemon nötig für Import/Export |
| **`--dry-run`** | Sicher testen ohne Daten zu verändern wichtig für große Bulk-Imports |
| **`--json` Flag** | Maschinenlesbar für Cron-Jobs, Monitoring-Skripte, Ansible-Playbooks |
| **Exit-Codes** | 0 = Erfolg, 1 = Fehler Standard für Shell-Skripting |
| **Xapian ReadonlyDatabase für Export** | Export kann parallel zum Daemon laufen ohne Lock-Konflikte |
### Abhängigkeiten
| Paket | Zweck |
|---|---|
| `github.com/spf13/cobra` | Subcommand-CLI-Framework |
| Xapian CGo-Bindings | Volltext-Filter beim Export (bereits PROJ-5) |
## 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_
-109
View File
@@ -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)
-86
View File
@@ -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`
-19
View File
@@ -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
-62
View File
@@ -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 |
-146
View File
@@ -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_
-205
View File
@@ -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.
-164
View File
@@ -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: <absender@firma.de>
250 OK
RCPT TO: <archiv@archivmail>
250 OK
DATA
354 Start input
… E-Mail-Inhalt … (max. 50 MB)
.
├─ Zu groß → 552 Message size exceeds limit
├─ Duplikat (Message-ID) → 250 OK (still, kein Fehler Postfix soll nicht retrying)
└─ Neu → Storage Coordinator → verschlüsselt speichern + indexieren
250 OK ← erst nach erfolgreicher Speicherung
QUIT
```
### Technische Entscheidungen
| Entscheidung | Begründung |
|---|---|
| **Reiner Empfänger, kein MTA** | archivmail ist kein Mailserver keine ausgehende Queue, kein Relay-Risiko, kein Open-Relay |
| **Kein SMTP AUTH** | Vertrauen basiert auf IP, nicht auf Passwort Postfix und archivmail laufen im gleichen Netz |
| **250 OK bei Duplikat** | Postfix würde bei Fehler die Mail in die Retry-Queue stellen sinnlos, da Duplikat bereits archiviert |
| **250 OK erst nach Speicherung** | Solange Postfix keine Bestätigung hat, behält er die Mail und versucht erneut kein Datenverlust |
| **Port 2525** | Port 25 erfordert root-Rechte; 2525 läuft als unprivilegierter `archivmail`-Systembenutzer |
| **Eine Goroutine pro Session** | Viele parallele Verbindungen ohne Blocking; jede Session ist isoliert |
### Postfix-Konfiguration (Dokumentation, außerhalb von archivmail)
```
# /etc/postfix/main.cf einfachste Variante (alle Mails)
always_bcc = archiv@archivmail-host
# Oder granular per Sender-BCC-Map:
# sender_bcc_maps = hash:/etc/postfix/sender_bcc
# empfänger@firma.de archiv@archivmail-host
```
### Go-Abhängigkeiten
| Paket | Zweck |
|---|---|
| `github.com/emersion/go-smtp` | Eingebetteter SMTP-Daemon (RFC 5321, nur Empfang) |
| `crypto/tls` | TLS/STARTTLS (Go Stdlib) |
| `net` | IP-Prüfung (Go Stdlib) |
## QA Test Results
_To be added by /qa_
## Deployment
_To be added by /deploy_
@@ -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/<server_id>/<customer_id>/<hash>/xxxxx.m` (AES-256-GCM verschlüsselt)
- [ ] Anhänge gespeichert unter `/var/archivmail/astore/<hash>` (AES-256-GCM verschlüsselt)
- [ ] Anhänge werden dedupliziert: gleicher Hash → eine Datei, mehrere Referenzen in der DB
- [ ] Verschlüsselungsschlüssel wird beim Start aus `/etc/archivmail/keyfile` geladen (Pfad konfigurierbar)
- [ ] Key-Datei: `chmod 400`, Owner `archivmail`-Systembenutzer
- [ ] Server verweigert Start wenn Key-Datei fehlt, nicht lesbar oder Schlüssel ≠ 32 Byte nach Base64-Dekodierung
- [ ] Xapian-Index enthält keinen vollständigen E-Mail-Body (nur Terme/Tokens)
- [ ] PostgreSQL speichert ausschließlich Metadaten + Dateipfade kein E-Mail-Body in der DB
## Edge Cases
- E-Mail ohne Body (nur Anhang) → Body als leer speichern, Anhang indexieren
- HTML-Body ohne Plain-Text-Alternative → HTML zu Plain-Text konvertieren für Index
- E-Mail mit sehr vielen Empfängern (> 500) → TO/CC/BCC werden vollständig gespeichert
- Sonderzeichen und Nicht-ASCII in Headern (RFC 2047 encoded) → dekodieren
- Anhang-Deduplizierung: gleicher Inhalt in 1000 E-Mails → nur eine Datei in `astore/`, DB zählt Referenzen; Löschen einer E-Mail dekrementiert Referenzzähler, Datei erst bei 0 gelöscht
- Speicherplatz voll → Import-Fehler mit klarer Meldung, keine partiellen Einträge
- Verschlüsselungsschlüssel fehlt beim Start → Server startet nicht, klare Fehlermeldung
- Schlüssel-Rotation: alte `.enc`-Dateien müssen mit neuem Schlüssel re-verschlüsselt werden (Admin-Tool, nicht automatisch)
## Technical Requirements
- **Volltext-Index: Xapian** (via CGo-Bindings, z.B. `github.com/rcaught/go-xapian` oder direkte CGo-Integration)
- Xapian-Datenbank liegt auf dem Dateisystem (kein externer Dienst nötig)
- Felder als Xapian-Terms und -Values indexiert: Subject, From, To, CC, BCC, Body
- Stemming für Deutsch und Englisch (Xapian Snowball Stemmer)
- Anhang-Dateinamen als zusätzliche Terms indexiert
- **Speicherung: Verschlüsselt im Dateisystem (AES-256-GCM)**
- Mailkörper (ohne Anhänge) als `.m`-Datei:
```
/var/archivmail/store/<server_id>/<customer_id>/<hash>/xxxxx.m
```
- Anhänge dedupliziert in separatem Store (ein Anhang = eine Datei, unabhängig wie oft er vorkommt):
```
/var/archivmail/astore/<hash>
```
- Hash = SHA-256 des Inhalts → dient gleichzeitig als Pfad und Integritätsprüfung
- Beide Stores AES-256-GCM verschlüsselt auf Disk
- Verschlüsselungsschlüssel (32 Byte) aus dedizierter Key-Datei: `/etc/archivmail/keyfile`
- Dateiformat: Base64-kodierter 32-Byte-Schlüssel, eine Zeile
- Dateiberechtigungen: `chmod 400`, Owner: `archivmail` (Systembenutzer des Dienstes)
- Pfad zur Key-Datei konfigurierbar in `config.yml` (`encryption.keyfile`)
- Schlüssel wird beim Start einmalig in den Prozessspeicher geladen danach keine Disk-Zugriffe mehr auf die Key-Datei
- Server verweigert Start wenn Key-Datei fehlt, nicht lesbar oder Inhalt nicht exakt 32 Byte (nach Base64-Dekodierung)
- PostgreSQL speichert folgende Metadaten (kein Mail-Body):
- `message_id`, `from`, `to`, `cc`, `subject`, `date`, `size`
- Attachment-Tabelle: `filename`, `mime_type`, `size`, `hash` (→ Pfad in `astore/`)
- Pfadreferenz zur `.m`-Datei in `store/`
- Xapian-Datenbank liegt unverschlüsselt auf Disk (enthält nur Text-Terme, keinen vollständigen Body)
- Xapian-Schreibzugriffe serialisiert (WritableDatabase nicht thread-safe) Background-Worker-Queue
- Indexierung innerhalb 5 Sekunden nach E-Mail-Eingang
- Retention-Policy: konfigurierbare automatische Löschung alter E-Mails (DSGVO) löscht sowohl DB-Eintrag als auch Xapian-Dokument
---
## Tech Design (Solution Architect)
### Komponentenstruktur
```
archivmail (Go-Binary)
├── Storage Coordinator ← Einziger Eintrittspunkt für alle Schreibvorgänge
│ ├── MIME Parser ← Zerlegt eingehende E-Mail in Body + Anhänge
│ ├── Mail Store ← Schreibt .m-Datei verschlüsselt auf Disk
│ │ └── Encryption Layer ← AES-256-GCM (Schlüssel aus /etc/archivmail/keyfile)
│ ├── Attachment Store ← Schreibt Anhänge in astore/, prüft Duplikate per Hash
│ │ └── Encryption Layer ← gleiche AES-256-GCM Instanz
│ └── Metadata Writer ← Schreibt Metadaten in PostgreSQL
├── Index Worker (Hintergrund) ← Serialisierte Warteschlange für Xapian-Schreibzugriffe
│ ├── Text Extractor ← HTML → Plain-Text, RFC 2047 Header-Dekodierung
│ └── Xapian WritableDatabase ← Ein Schreiber gleichzeitig (Queue verhindert Konflikte)
└── Xapian ReadonlyDatabase ← Beliebig viele parallele Lesezugriffe (Suche)
```
### Datenfluss: E-Mail eingehend
```
E-Mail (RFC 2822) primär via SMTP-BCC
MIME Parser
┌────┴──────────────────────────────────┐
│ │
▼ ▼
Body (ohne Anhänge) Anhänge (0..n)
│ │
├─ SHA-256(Body) → Hash ├─ SHA-256(Anhang) → Hash
├─ AES-256-GCM verschlüsseln ├─ Hash in astore/ vorhanden? → nur ref_count++
└─ /var/archivmail/store/ ├─ AES-256-GCM verschlüsseln
<server>/<customer>/<hash>/x.m └─ /var/archivmail/astore/<hash>
+ ref_count++ in DB
│ │
└──────────────┬────────────────────────┘
PostgreSQL (Metadaten)
message_id, from, to, cc,
subject, date, size,
store_path, sha256,
indexed_at = NULL
Index Worker Queue (Channel)
Text Extractor
(HTML→Text, Encoding-Normalisierung)
Xapian WritableDatabase
Subject, From, To, CC, Body als Terms
indexed_at = NOW() in PostgreSQL
```
### Datenmodell
**Tabelle `emails`** eine Zeile pro archivierter E-Mail:
| Feld | Beschreibung |
|------|-------------|
| `message_id` | RFC-2822 Message-ID (Primärschlüssel, Duplikatschutz) |
| `from` | Absender |
| `to` | Empfänger |
| `cc` | CC-Empfänger |
| `subject` | Betreff |
| `date` | Sendedatum (UTC) |
| `size` | Größe des Originals in Bytes |
| `store_path` | Pfad zur .m-Datei |
| `sha256` | Hash des Originals (Integritätsprüfung) |
| `indexed_at` | Zeitpunkt der Xapian-Indexierung (NULL = ausstehend) |
**Tabelle `attachments`** ein Eintrag pro einzigartigem Anhang:
| Feld | Beschreibung |
|------|-------------|
| `hash` | SHA-256 des Inhalts (= Dateiname in astore/) |
| `filename` | Originaldateiname |
| `mime_type` | z.B. application/pdf |
| `size` | Größe in Bytes |
| `ref_count` | Anzahl E-Mails die diesen Anhang referenzieren |
**Tabelle `email_attachments`** Verknüpfung E-Mail ↔ Anhang (n:m)
### Technische Entscheidungen
| Entscheidung | Begründung |
|---|---|
| Body und Anhänge getrennt | Anhang-Deduplizierung: gleicher PDF in 1000 Mails = eine Datei auf Disk |
| SHA-256 als Dateipfad | Hash dient gleichzeitig als Pfad und Integritätsprüfung kein separates Mapping |
| AES-256-GCM | Authentifizierte Verschlüsselung erkennt Dateimanipulationen (Tamper Detection) |
| Index Worker Queue | Xapian erlaubt nur einen Schreiber Queue serialisiert ohne Datenverlust |
| `indexed_at` NULL-Flag | Nach Absturz können nicht-indexierte Mails beim Neustart nachindexiert werden |
| Metadaten in PostgreSQL, Body auf Disk | Filterabfragen (Datum, Absender) ohne Disk-Zugriff; Body nur bei Bedarf lesen |
| Storage Coordinator als Single Entry Point | Alle Importwege (SMTP, IMAP, EML/MBOX) rufen dieselbe Schreiblogik auf |
### Go-Abhängigkeiten
| Paket | Zweck |
|---|---|
| Xapian CGo-Bindings | Volltext-Index |
| `pgx` | PostgreSQL-Treiber |
| `crypto/aes`, `crypto/cipher` | AES-256-GCM (Go Stdlib) |
| `crypto/sha256` | Hashing (Go Stdlib) |
| `mime`, `mime/multipart` | MIME-Parsing (Go Stdlib) |
| `golang.org/x/net/html` | HTML → Plain-Text für Index |
## 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_
-128
View File
@@ -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 (vonbis), 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 ← vonbis 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_
-184
View File
@@ -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 `<iframe sandbox>` kein JavaScript aus der Mail kann ausgeführt werden
- [ ] Externe Bilder/Ressourcen standardmäßig blockiert, Nutzer kann per Button "externe Inhalte laden" freischalten
- [ ] Fallback auf Plain-Text wenn kein HTML vorhanden
- [ ] Anhang-Liste mit Dateiname, Typ und Größe
- [ ] Anhänge einzeln herunterladbar
- [ ] Header-Ansicht (klappbar) zeigt alle Original-MIME-Header
- [ ] Download der Original-E-Mail als .eml Datei
- [ ] Zugriffsschutz: Nutzer kann nur E-Mails aus eigenen Postfächern öffnen
- [ ] Jeder Zugriff auf eine E-Mail wird im Audit-Log erfasst
## Edge Cases
- E-Mail mit nur Plain-Text → normales Rendering ohne HTML
- HTML mit JavaScript → Script wird durch iframe-Sandbox blockiert, HTML-Inhalt bleibt unverändert sichtbar
- Externe Tracker (Pixel, Links) → standardmäßig blockiert durch CSP, auf Wunsch des Nutzers freischaltbar
- E-Mail mit sehr großen Anhängen (> 100 MB) → Download-Streaming, kein Speicher-Overflow
- E-Mail mit verschachteltem MIME (E-Mail in E-Mail als Anhang) → als EML-Anhang anzeigen
- Nicht unterstützte Zeichenkodierung → graceful Fallback mit Hinweis
## Technical Requirements
- **Kein HTML-Sanitizing** originale Darstellung ohne Veränderung des Inhalts
- Isolation über `<iframe sandbox="allow-same-origin">` JavaScript blockiert, Inhalt originalgetreu
- Externe Ressourcen über CSP (`Content-Security-Policy`) serverseitig blockiert, opt-in per Nutzer-Aktion
- Anhang-Downloads als Stream (kein vollständiges In-Memory-Laden)
- Audit-Log-Eintrag: Nutzer-ID, E-Mail-ID, Zeitstempel bei jedem Lesezugriff
---
## Tech Design (Solution Architect)
### Komponentenstruktur
**Next.js Frontend:**
```
/mail/[message_id]
├── MailHeader
│ ├── Betreff
│ ├── Von / An / CC / Datum / Größe
│ └── HeaderToggle (klappbar)
│ └── RawHeaderView ← alle Original-MIME-Header als Text
├── ActionBar
│ ├── EML-Download Button ← lädt Original-Mail herunter
│ ├── Externe Inhalte laden ← Button, standardmäßig deaktiviert
│ └── Zurück zur Suche
├── MailBody
│ ├── HtmlView ← originales HTML in <iframe sandbox>
│ │ └── ExternalContentBanner ← Hinweis "Externe Inhalte blockiert [Laden]"
│ └── PlainTextView ← Fallback wenn kein HTML vorhanden
└── AttachmentList
└── AttachmentItem (pro Anhang)
├── Icon (nach MIME-Type)
├── Dateiname + Typ + Größe
└── Download-Button ← direkter Stream vom Go-Backend
```
**Go Backend:**
```
GET /api/mails/{message_id}
├── Session Middleware ← Auth prüfen
├── Zugriffsrecht prüfen ← user: nur eigenes Postfach / auditor: alle
├── .m-Datei von Disk lesen ← Pfad aus PostgreSQL
├── AES-256-GCM entschlüsseln ← Schlüssel aus Prozessspeicher
├── MIME-Parser ← Body + Header + Anhang-Metadaten extrahieren
└── JSON-Antwort ← Metadaten + originaler HTML-Body + Anhang-Liste
GET /api/mails/{message_id}/attachments/{index}
├── Session Middleware
├── Zugriffsrecht prüfen
├── Hash aus PostgreSQL ← welche astore/-Datei?
├── astore/-Datei öffnen
├── AES-256-GCM entschlüsseln (stream)
└── HTTP-Streaming-Response ← Content-Disposition: attachment
GET /api/mails/{message_id}/raw
├── Session Middleware
├── Zugriffsrecht prüfen
├── .m-Datei entschlüsseln
└── HTTP-Streaming-Response ← Content-Type: message/rfc822
```
### Datenabruf-Fluss
```
Browser klickt MailCard aus Suchergebnissen
│ GET /api/mails/<message_id>
Zugriffsrecht prüfen
PostgreSQL → store_path
.m-Datei lesen + AES-256-GCM entschlüsseln
MIME-Parser → body_html (original, unverändert), body_plain, headers[], attachments[]
JSON-Antwort an Next.js
Next.js:
├── HTML → <iframe sandbox="allow-same-origin">
│ └── CSP-Header blockiert externe Ressourcen
│ Nutzer klickt "Externe Inhalte laden"
│ → iframe neu laden ohne CSP-Restriction
└── Anhang-Liste → Download-Links
```
### Technische Entscheidungen
| Entscheidung | Begründung |
|---|---|
| **Kein HTML-Sanitizing** | Originale Darstellung kein Inhalt wird verändert oder entfernt |
| **`<iframe sandbox>`** | JavaScript aus der Mail wird blockiert ohne den HTML-Inhalt zu verändern Inhalt bleibt originalgetreu |
| **CSP für externe Ressourcen** | Tracking-Pixel und externe Bilder standardmäßig blockiert Nutzer kann bewusst freischalten |
| **Entschlüsselung nur im Backend** | Verschlüsselte Rohdaten verlassen den Server nie |
| **Anhang-Download als Stream** | Große Anhänge (>100 MB) nie komplett in RAM direkt von Disk zum Browser |
| **Kein Audit-Log bei Lesezugriff** | Bewusste Entscheidung (PROJ-11): Lesezugriffe werden nicht geloggt |
### Abhängigkeiten
**Go Backend:**
| Paket | Zweck |
|---|---|
| `mime`, `mime/multipart` | MIME-Parsing (Stdlib) |
**Next.js Frontend:** Nur shadcn/ui (bereits installiert), kein zusätzliches Paket nötig.
## QA Test Results
_To be added by /qa_
## Deployment
### Lokal bauen
```bash
# Im Projektverzeichnis
npm run build
```
Build-Artefakt liegt danach in `.next/`.
### Auf Server übertragen (192.168.1.131)
```bash
# Next.js-Build + Abhängigkeiten übertragen
rsync -avz --delete \
.next/ \
package.json \
package-lock.json \
next.config.ts \
root@192.168.1.131:/opt/archivmail/frontend/
# Auf dem Server: Abhängigkeiten installieren & Dienst neu starten
ssh root@192.168.1.131 "cd /opt/archivmail/frontend && npm ci --omit=dev && systemctl restart archivmail-frontend"
```
### Voraussetzungen auf dem Server
- Node.js ≥ 20 installiert (`node -v`)
- Verzeichnis `/opt/archivmail/frontend/` existiert
- Systemd-Unit `archivmail-frontend` läuft `npm run start` (Port 3000)
- Go-Backend läuft auf Port 8080, Next.js proxied `/api/*` dorthin (siehe `next.config.ts`)
-163
View File
@@ -1,163 +0,0 @@
# PROJ-8: Automatischer IMAP-Sync (Cron-Job)
## Status: Deployed
**Created:** 2026-03-12
**Last Updated:** 2026-03-17
## Dependencies
- Requires: PROJ-3 (IMAP-Import) IMAP-Verbindungen müssen konfiguriert sein
- Requires: PROJ-5 (Speicherung & Indexierung)
## User Stories
- Als Admin möchte ich ein Sync-Intervall konfigurieren (z.B. alle 15 Minuten), damit neue E-Mails automatisch archiviert werden.
- Als Admin möchte ich den letzten Sync-Zeitpunkt und -Status pro IMAP-Verbindung sehen.
- Als Admin möchte ich den Sync manuell auslösen können, damit ich nicht auf den nächsten Intervall warten muss.
- Als System möchte ich beim Sync nur neue E-Mails (seit letztem Sync) abholen, damit kein unnötiger Traffic entsteht.
## Acceptance Criteria
- [x] Sync-Intervall pro IMAP-Verbindung konfigurierbar (min. 5 Minuten, max. 24 Stunden)
- [x] IMAP UID-basierter inkrementeller Sync (nur neue E-Mails seit letztem Sync)
- [x] Admin-UI zeigt: letzter Sync, Status (Erfolg/Fehler), Anzahl importierter E-Mails
- [x] Manueller "Sync jetzt"-Button im Admin-Bereich
- [x] Bei Sync-Fehler: Retry mit exponential backoff (max. 3 Versuche)
- [x] Sync-Fehler nach allen Versuchen → Fehlermeldung im Admin-Dashboard
## Edge Cases
- IMAP-Server temporär nicht erreichbar → Retry ohne Abbruch des gesamten Sync-Jobs
- Sync läuft noch wenn neuer Intervall beginnt → kein paralleler Sync für dieselbe Verbindung
- E-Mails auf dem Server wurden gelöscht → im Archiv behalten (Archiv ist immutable)
- Zeitzonenprobleme beim Datum-Vergleich → immer UTC intern verwenden
## Technical Requirements
- Kein externer Cron-Scheduler — `time.NewTicker(1 * time.Minute)` + Goroutine (YAGNI, keine neue Abhängigkeit)
- Sync-Status persistent in DB gespeichert (überlebt Server-Neustart)
## Implementation Notes (2026-03-17)
- `internal/imap/store.go`: Account-Struct um 7 Sync-Felder erweitert; `migrationSQL` mit `ADD COLUMN IF NOT EXISTS`; neue Methoden: `ListAll`, `UpdateSyncInterval`, `SetSyncRunning`, `UpdateSyncResult`; einheitliche `scanRow(scanner)`-Funktion mit eigenem Interface statt `pgx.Row`
- `internal/imap/scheduler.go`: Neues Paket; `Scheduler` mit `sync.Mutex`-geschützter `running`-Map; `Start/Stop/TriggerSync`; `runSyncWithRetry` mit 3 Versuchen (Backoffs: 1s, 60s, 300s); `doSync` delegiert `storeAndIndex` an den vorhandenen `Importer`
- `internal/api/server.go`: `imapScheduler`-Feld; `SetImap`-Signatur erweitert; neue Routen `POST /api/imap/{id}/sync` und `PATCH /api/imap/{id}`
- `src/lib/api.ts`: ImapAccount um 6 Felder erweitert; `triggerImapSync`, `updateImapInterval` hinzugefügt
- `src/app/imap/page.tsx`: Polling auch für `sync_running`; Dropdown für Sync-Intervall; "Sync jetzt"-Button; Sync-Status-Badge + letzter Sync-Zeitstempel pro Account-Card
- `cmd/archivmail/main.go`: `NewScheduler`, `Start`, `Stop`, `SetImap` mit Scheduler verdrahtet
---
## Tech Design (Solution Architect)
### Komponentenstruktur
**Next.js Frontend (Admin-Bereich):**
```
/admin/imap (integriert in IMAP-Verbindungsliste aus PROJ-3)
└── VerbindungsCard (pro Konto)
├── Sync-Intervall (Dropdown: 5min / 15min / 1h / 6h / 24h)
├── Letzter Sync: Zeitpunkt + Status (✓ OK / ✗ Fehler)
├── Anzahl importierter Mails beim letzten Sync
├── Fehlermeldung (wenn letzter Sync fehlgeschlagen)
└── [Sync jetzt] Button
```
**Go Backend:**
```
Sync-Scheduler (startet beim Binary-Start)
├── Cron-Loop ← prüft jede Minute alle IMAP-Accounts
│ └── Für jeden Account:
│ ├── Intervall abgelaufen? → Sync-Worker starten
│ └── Sync läuft bereits? → überspringen (kein Parallel-Sync)
├── Sync-Worker (pro Account, Goroutine)
│ ├── IMAP verbinden (gleicher Client wie PROJ-3)
│ ├── Letzte bekannte UID aus DB laden
│ ├── UID SEARCH UID <last_uid>:* → nur neue Mails
│ ├── FETCH neue Mails
│ ├── → Storage Coordinator (PROJ-5)
│ ├── Letzte UID + Zeitstempel in DB speichern
│ └── Bei Fehler: Retry mit Exponential Backoff
│ (1. Versuch: sofort, 2.: +1min, 3.: +5min → dann Fehler)
└── POST /api/admin/imap/{id}/sync ← manueller Trigger
└── Sync-Worker sofort starten (ignoriert Intervall)
```
### Sync-Fluss
```
Cron-Loop (jede Minute)
└── Account "Firmen-Postfach" Intervall: 15 min
last_sync_at = vor 16 Minuten → fällig
sync_running = false → starten
IMAP verbinden
last_uid = 4821 (aus DB)
UID SEARCH UID 4822:*
→ [4822, 4823, 4830, 4831] (4 neue Mails)
FETCH 4822:4831 RFC822
Für jede Mail:
Duplikat? → überspringen
→ Storage Coordinator
last_uid = 4831 in DB speichern
last_sync_at = NOW() (UTC)
sync_status = "ok"
sync_count = 4
```
### Exponential Backoff bei Fehlern
```
Sync-Fehler (z.B. IMAP nicht erreichbar)
├── Versuch 1: sofort → Fehler
├── Versuch 2: +1 Minute → Fehler
├── Versuch 3: +5 Minuten → Fehler
└── Aufgeben:
sync_status = "error"
error_msg = "Connection refused after 3 attempts"
→ Admin-Dashboard zeigt Fehler
→ nächster regulärer Intervall versucht es erneut
```
### Datenmodell (Ergänzung zu `imap_accounts`)
| Feld | Beschreibung |
|------|-------------|
| `sync_interval_min` | Sync-Intervall in Minuten (51440) |
| `last_sync_at` | Zeitpunkt des letzten Syncs (UTC) |
| `last_sync_count` | Anzahl importierter Mails beim letzten Sync |
| `last_uid` | Höchste bekannte IMAP-UID (Startpunkt für nächsten Sync) |
| `sync_running` | `true` wenn Sync gerade läuft (verhindert parallelen Sync) |
| `sync_status` | `ok` / `error` / `running` |
| `sync_error_msg` | Letzte Fehlermeldung |
### Technische Entscheidungen
| Entscheidung | Begründung |
|---|---|
| **UID-basierter inkrementeller Sync** | Nur neue Mails seit letzter bekannter UID werden abgeholt minimaler Traffic, kein Re-Download |
| **`sync_running`-Flag in DB** | Verhindert parallelen Sync derselben Verbindung auch nach Server-Neustart |
| **Cron-Loop jede Minute** | Einfacher als individuelle Timer pro Account skaliert auf viele Accounts ohne Overhead |
| **Exponential Backoff** | Temporäre Ausfälle (Netz, Server-Neustart) werden automatisch überbrückt ohne Admin-Eingriff |
| **Status persistent in DB** | Server-Neustart verliert keinen Sync-Fortschritt Scheduler macht nahtlos weiter |
| **Manueller Trigger** | Admin kann sofortigen Sync anstoßen ohne auf Intervall zu warten |
### Abhängigkeiten
| Paket | Zweck |
|---|---|
| `github.com/robfig/cron` | Eingebetteter Cron-Scheduler |
| `github.com/emersion/go-imap` | IMAP-Client (bereits PROJ-3) |
## QA Test Results
_To be added by /qa_
## Deployment
_To be added by /deploy_
-157
View File
@@ -1,157 +0,0 @@
# PROJ-9: Ordner- & Label-Verwaltung
## Status: In Progress
**Created:** 2026-03-12
**Last Updated:** 2026-03-12
## Dependencies
- Requires: PROJ-1 (Authentifizierung)
- Requires: PROJ-5 (Speicherung & Indexierung)
## User Stories
- Als Nutzer möchte ich E-Mails mit Labels versehen, damit ich sie thematisch organisieren kann.
- Als Admin möchte ich globale Labels definieren, die automatisch beim Import vergeben werden (z.B. nach Absender-Domain oder Import-Quelle).
- Als Nutzer möchte ich meine Suchergebnisse auf ein bestimmtes Label einschränken.
- Als Nutzer möchte ich Labels erstellen, umbenennen und löschen.
## Acceptance Criteria
- [ ] Nutzer können Labels erstellen (Name, Farbe)
- [ ] E-Mails können mit mehreren Labels versehen werden
- [ ] Label-Filter in der Suche verfügbar
- [ ] **Keine IMAP-Ordnerstruktur** das System ist ein Archiv, keine Ordnerhierarchie wird übernommen
- [ ] Admin kann Regeln für automatische Label-Vergabe beim Import definieren (z.B. nach Absender-Domain)
- [ ] Admin kann globale Labels definieren (für alle Nutzer sichtbar)
- [ ] Löschen eines Labels entfernt es von allen E-Mails, löscht E-Mails nicht
- [ ] Label-Übersicht in der Seitenleiste mit E-Mail-Anzahl pro Label
## Edge Cases
- Label-Name bereits vergeben → Fehlermeldung
- E-Mail wird gelöscht aber Labels bleiben → Labels bleiben erhalten, E-Mail-Referenz entfernt
- Sehr viele Labels (> 100) → Suchfeld in der Label-Auswahl
## Technical Requirements
- Labels: n:m-Beziehung zwischen E-Mails und Labels
- Performance: Label-Filter darf Suchantwortzeit nicht verdoppeln
---
## Tech Design (Solution Architect)
### Komponentenstruktur
**Next.js Frontend:**
```
Seitenleiste (global, alle Seiten)
└── LabelList
├── Label-Eintrag (Name, Farbe, Anzahl) ← klickbar → filtert Suche
├── [+ Label erstellen] Button
└── Suchfeld (bei > 10 Labels)
Label-Verwaltung (Inline / Modal)
├── LabelForm
│ ├── Name (Textfeld)
│ └── Farbe (Color-Picker, 8 Vorschläge)
└── LabelItem-Aktionen
├── Umbenennen
└── Löschen (mit Bestätigung)
E-Mail-Ansicht (PROJ-7, Erweiterung)
└── LabelPicker
├── Aktuelle Labels der Mail (als Badges)
├── Dropdown: Labels hinzufügen/entfernen
└── [+ Neues Label] Shortcut
Admin-Bereich (/admin/labels)
├── Globale Labels verwalten
└── Auto-Label-Regeln
├── RegelListe
└── RegelForm
├── Bedingung: from-Domain / Import-Quelle / Betreff enthält
└── Aktion: Label zuweisen
```
**Go Backend:**
```
Label-API
├── GET /api/labels ← alle Labels des Nutzers + globale
├── POST /api/labels ← neues Label anlegen
├── PATCH /api/labels/{id} ← umbenennen / Farbe ändern
├── DELETE /api/labels/{id} ← löschen (entfernt von allen Mails)
├── POST /api/mails/{id}/labels ← Label einer Mail zuweisen
└── DELETE /api/mails/{id}/labels/{label_id} ← Label entfernen
Admin Label-API
├── POST /api/admin/labels ← globales Label anlegen
├── GET /api/admin/label-rules ← Auto-Label-Regeln
├── POST /api/admin/label-rules ← Regel anlegen
└── DELETE /api/admin/label-rules/{id}
Label-Filter in Suche (Erweiterung PROJ-6)
└── Xapian-Term "label:<label_id>" pro Mail
→ Label-Filter läuft direkt in Xapian
```
### Datenmodell
**Tabelle `labels`:**
| Feld | Beschreibung |
|------|-------------|
| `id` | Interne ID |
| `name` | Label-Name (eindeutig pro Nutzer) |
| `color` | Hex-Farbe (z.B. `#e74c3c`) |
| `owner_id` | Nutzer-ID (NULL = globales Admin-Label) |
| `created_at` | Erstellungszeitpunkt |
**Tabelle `email_labels`** n:m Verknüpfung:
| Feld | Beschreibung |
|------|-------------|
| `email_id` | Referenz auf `emails` |
| `label_id` | Referenz auf `labels` |
| `assigned_at` | Zeitpunkt der Zuweisung |
| `assigned_by` | `user` / `auto-rule` / `import` |
**Tabelle `label_rules`** Auto-Label beim Import:
| Feld | Beschreibung |
|------|-------------|
| `id` | Interne ID |
| `condition_field` | `from_domain` / `source` / `subject_contains` |
| `condition_value` | z.B. `example.com` oder `imap-account-1` |
| `label_id` | Welches Label vergeben |
### Label-Filter in Xapian
Beim Indexieren einer Mail werden ihre Labels als Xapian-Terms gespeichert:
```
Label "Kunde" → Term: "label:42"
Label "Projekt" → Term: "label:17"
```
Suche mit Label-Filter läuft vollständig in Xapian kein zusätzlicher DB-Join nötig. Labels werden beim Zuweisen/Entfernen sofort im Xapian-Dokument aktualisiert.
### Technische Entscheidungen
| Entscheidung | Begründung |
|---|---|
| **Labels statt Ordner** | Archiv hat keine Hierarchie eine Mail kann mehrere Labels haben, aber nicht in mehreren Ordnern gleichzeitig sein |
| **Label-Terms in Xapian** | Filter läuft direkt bei der Suche kein nachträglicher DB-Join, keine Verdopplung der Antwortzeit |
| **Globale Labels (owner_id NULL)** | Admin definiert unternehmensweite Labels Nutzer können sie nicht löschen, nur zuweisen |
| **Auto-Label-Regeln** | Importierte Mails werden sofort kategorisiert kein manueller Aufwand für Bulk-Importe |
| **`assigned_by`-Feld** | Nachvollziehbar ob Label manuell, per Regel oder beim Import vergeben wurde |
### Abhängigkeiten
**Next.js Frontend:**
| Paket | Zweck |
|---|---|
| `shadcn/ui` | Badge, Popover, Color-Picker-Basis (bereits installiert) |
**Go Backend:** Nur Stdlib + pgx (bereits vorhanden).
## QA Test Results
_To be added by /qa_
## Deployment
_To be added by /deploy_