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:
-53
@@ -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
|
|
||||||
@@ -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_
|
|
||||||
@@ -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_
|
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
# PROJ-11: Audit-Log & Compliance-Berichte
|
|
||||||
|
|
||||||
## Status: Deployed
|
|
||||||
**Created:** 2026-03-12
|
|
||||||
**Last Updated:** 2026-03-13
|
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
- Requires: PROJ-1 (Authentifizierung) – Audit-Einträge sind an Nutzer geknüpft
|
|
||||||
|
|
||||||
## User Stories
|
|
||||||
- Als Admin möchte ich sicherheitsrelevante Ereignisse im Audit-Log einsehen, damit ich Zugriffe und Änderungen nachvollziehen kann.
|
|
||||||
- Als Admin möchte ich den Audit-Log nach Datum, Nutzer und Ereignistyp filtern.
|
|
||||||
- Als Admin möchte ich den Audit-Log als CSV exportieren, damit ich ihn für externe Prüfungen verwenden kann.
|
|
||||||
|
|
||||||
## Erfasste Ereignisse
|
|
||||||
**Ja – wird geloggt:**
|
|
||||||
- Login (Erfolg und Fehlschlag) inkl. IP-Adresse
|
|
||||||
- Logout
|
|
||||||
- Suchanfragen (Suchbegriff, Anzahl Treffer, Nutzer)
|
|
||||||
- Import gestartet / abgeschlossen / fehlgeschlagen (Quelle, Anzahl E-Mails)
|
|
||||||
- Export gestartet / abgeschlossen (Format, Anzahl E-Mails)
|
|
||||||
|
|
||||||
**Nein – wird nicht geloggt:**
|
|
||||||
- Lesezugriff auf einzelne E-Mails (kein per-Mail-Leselogging)
|
|
||||||
|
|
||||||
## Acceptance Criteria
|
|
||||||
- [ ] Jeder Log-Eintrag enthält: Zeitstempel (UTC), Nutzer-ID, Ereignistyp, Details (z.B. Suchbegriff, Import-Quelle), IP-Adresse
|
|
||||||
- [ ] Audit-Log-Ansicht **nur für Auditoren** unter `/audit/` – kein Zugriff für `admin` oder `user`
|
|
||||||
- [ ] Filterung nach: Datum (von–bis), Nutzer, Ereignistyp
|
|
||||||
- [ ] Paginierung (50 Einträge pro Seite)
|
|
||||||
- [ ] Export als CSV (gefilterte oder vollständige Ansicht, Streaming-Download)
|
|
||||||
- [ ] Einträge sind unveränderlich: kein UPDATE/DELETE durch Admin oder Anwendung möglich
|
|
||||||
- [ ] Retention konfigurierbar in `config.yml` (`audit.retention_days`), kein Standardwert erzwungen
|
|
||||||
- [ ] Doppelte Speicherung: PostgreSQL (für GUI-Abfragen) + Append-only Logdatei auf Disk (als unveränderliches Backup)
|
|
||||||
- [ ] Logdatei-Format: JSON Lines (ein Eintrag pro Zeile)
|
|
||||||
|
|
||||||
## Edge Cases
|
|
||||||
- Audit-Log über Jahre sehr groß → paginierte DB-Abfragen mit Index auf `(timestamp, event_type, user_id)`, kein Full-Table-Scan
|
|
||||||
- Nutzer DSGVO-gelöscht → Audit-Einträge behalten, `user_id` durch `"anonymized"` ersetzen, IP-Adresse löschen
|
|
||||||
- Logdatei nicht beschreibbar beim Start → Warnung loggen, Dienst läuft weiter (DB-Log bleibt aktiv)
|
|
||||||
- Gleichzeitige Schreibzugriffe auf Logdatei → Append mit file lock
|
|
||||||
|
|
||||||
## Technical Requirements
|
|
||||||
- PostgreSQL: separate Tabelle `audit_log`, kein DELETE/UPDATE per DB-Constraint (Row-Level Security oder Trigger)
|
|
||||||
- Logdatei: `/var/log/archivmail/audit.log` (Pfad konfigurierbar), append-only, JSON Lines
|
|
||||||
- Log-Rotation über `logrotate` (extern konfiguriert), Datei wird nie vom Dienst selbst rotiert oder gelöscht
|
|
||||||
- Zeitstempel immer UTC (RFC 3339)
|
|
||||||
|
|
||||||
---
|
|
||||||
## Tech Design (Solution Architect)
|
|
||||||
|
|
||||||
### Komponentenstruktur
|
|
||||||
|
|
||||||
**Next.js Frontend (`/audit/*`):**
|
|
||||||
```
|
|
||||||
/audit ← nur für Auditoren (RoleGuard)
|
|
||||||
└── AuditTable
|
|
||||||
├── Spalten: Zeitstempel, Nutzer, Ereignis, Details, IP
|
|
||||||
├── Filter-Leiste
|
|
||||||
│ ├── Datepicker: von – bis
|
|
||||||
│ ├── Dropdown: Nutzer auswählen
|
|
||||||
│ └── Dropdown: Ereignistyp
|
|
||||||
├── Paginierung (50 pro Seite)
|
|
||||||
└── [CSV exportieren] Button → Streaming-Download
|
|
||||||
```
|
|
||||||
|
|
||||||
**Go Backend:**
|
|
||||||
```
|
|
||||||
Audit-Service (intern, kein eigener HTTP-Handler)
|
|
||||||
├── WriteEvent(event AuditEvent)
|
|
||||||
│ ├── → INSERT INTO audit_log (kein UPDATE/DELETE je möglich)
|
|
||||||
│ └── → Append zu /var/log/archivmail/audit.log (JSON Line)
|
|
||||||
│ └── File-Lock beim Schreiben (sync.Mutex)
|
|
||||||
│
|
|
||||||
└── Audit-API (nur für Auditoren)
|
|
||||||
├── GET /api/audit/events ← Paginiert, gefiltert
|
|
||||||
└── GET /api/audit/export ← Streaming-CSV-Download
|
|
||||||
```
|
|
||||||
|
|
||||||
### Datenmodell
|
|
||||||
|
|
||||||
**Tabelle `audit_log`** (append-only via DB-Trigger):
|
|
||||||
|
|
||||||
| Feld | Beschreibung |
|
|
||||||
|------|-------------|
|
|
||||||
| `id` | Sequentielle ID |
|
|
||||||
| `timestamp` | UTC, RFC 3339 |
|
|
||||||
| `user_id` | Nutzer-ID (NULL nach DSGVO-Löschung) |
|
|
||||||
| `user_email` | E-Mail zum Zeitpunkt des Events (für Lesbarkeit nach Anonymisierung) |
|
|
||||||
| `event_type` | `login_ok`, `login_fail`, `logout`, `search`, `import_start`, `import_done`, `import_fail`, `export_start`, `export_done` |
|
|
||||||
| `details` | JSON: Suchbegriff / Import-Quelle / Anzahl / etc. |
|
|
||||||
| `ip_address` | IPv4/IPv6 (NULL nach DSGVO-Löschung) |
|
|
||||||
|
|
||||||
**DB-Constraint:** PostgreSQL-Trigger verhindert `UPDATE` und `DELETE` auf der gesamten Tabelle → physische Unveränderlichkeit.
|
|
||||||
|
|
||||||
**Logdatei-Format** (`/var/log/archivmail/audit.log`, JSON Lines):
|
|
||||||
```
|
|
||||||
{"ts":"2024-03-01T10:00:00Z","user":"alice@firma.de","event":"search","details":{"q":"Rechnung","hits":42},"ip":"192.168.1.1"}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Schreibfluss
|
|
||||||
|
|
||||||
```
|
|
||||||
Beliebige Aktion (Login, Suche, Import...)
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
audit.WriteEvent() aufgerufen
|
|
||||||
│
|
|
||||||
├── PostgreSQL INSERT (non-blocking, Goroutine)
|
|
||||||
│
|
|
||||||
└── File-Append mit sync.Mutex
|
|
||||||
(Datei nicht beschreibbar? → Warnung auf stderr, Dienst läuft weiter)
|
|
||||||
```
|
|
||||||
|
|
||||||
### DSGVO-Löschfluss (Nutzer anonymisieren)
|
|
||||||
|
|
||||||
```
|
|
||||||
DELETE /api/admin/users/{id}
|
|
||||||
│
|
|
||||||
├── audit_log: user_id → NULL, ip_address → NULL
|
|
||||||
│ user_email → "anonymized"
|
|
||||||
└── Logdatei: bleibt unverändert (tamper-evident)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Technische Entscheidungen
|
|
||||||
|
|
||||||
| Entscheidung | Begründung |
|
|
||||||
|---|---|
|
|
||||||
| **Audit-Ansicht nur für Auditoren** | Strict role separation — Admin hat keine Einsicht in Zugriffsprotokolle |
|
|
||||||
| **DB-Trigger für Unveränderlichkeit** | Applikationscode kann versehentlich löschen — Trigger ist eine härtere Garantie |
|
|
||||||
| **Doppelte Speicherung** | DB für GUI-Abfragen; Logdatei als tamper-evident Backup für externe Prüfungen |
|
|
||||||
| **logrotate extern** | Dienst rotiert nie selbst — Logdatei bleibt unter Systemadmin-Kontrolle |
|
|
||||||
| **DSGVO: IP-Adresse löschen, Event behalten** | Personenbezug entfernen, Compliance-Nachweis bleibt erhalten |
|
|
||||||
| **Composite Index `(timestamp, event_type, user_id)`** | Schnelle gefilterte Abfragen auch bei sehr großem Log über Jahre |
|
|
||||||
|
|
||||||
### Abhängigkeiten
|
|
||||||
|
|
||||||
**Go Backend:** Nur Stdlib + pgx (bereits vorhanden).
|
|
||||||
**Next.js:** shadcn/ui Table, Select, DatePicker (bereits installiert).
|
|
||||||
|
|
||||||
## QA Test Results
|
|
||||||
_To be added by /qa_
|
|
||||||
|
|
||||||
## Deployment
|
|
||||||
_To be added by /deploy_
|
|
||||||
@@ -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_
|
|
||||||
@@ -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_
|
|
||||||
@@ -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_
|
|
||||||
@@ -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_
|
|
||||||
@@ -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)
|
|
||||||
@@ -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`
|
|
||||||
@@ -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
|
|
||||||
@@ -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 |
|
|
||||||
@@ -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_
|
|
||||||
@@ -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.
|
|
||||||
@@ -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_
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
# PROJ-6: Volltext-Suche & Filterung
|
|
||||||
|
|
||||||
## Status: Deployed
|
|
||||||
**Created:** 2026-03-12
|
|
||||||
**Last Updated:** 2026-03-17
|
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
- Requires: PROJ-1 (Authentifizierung) – Suche nur für eingeloggte Nutzer
|
|
||||||
- Requires: PROJ-5 (Speicherung & Indexierung) – Suchergebnisse kommen aus dem Index
|
|
||||||
|
|
||||||
## User Stories
|
|
||||||
- Als Nutzer möchte ich nach Schlüsselwörtern suchen, damit ich relevante E-Mails schnell finden kann.
|
|
||||||
- Als Nutzer möchte ich Suchergebnisse nach Absender, Empfänger, Datum und Anhang filtern, damit ich die Treffermenge eingrenzen kann.
|
|
||||||
- Als Nutzer möchte ich Suchergebnisse nach Datum sortieren können (neueste/älteste zuerst).
|
|
||||||
- Als Nutzer möchte ich Suchbegriffe in den Ergebnissen hervorgehoben sehen (Highlighting).
|
|
||||||
- Als Nutzer sehe ich nur E-Mails, auf die ich Zugriffsrecht habe, damit Datenschutz gewahrt bleibt.
|
|
||||||
|
|
||||||
## Acceptance Criteria
|
|
||||||
- [ ] Sucheingabe mit Echtzeit-Vorschau (oder sofortiger Submit)
|
|
||||||
- [ ] Suche in: Betreff, Absender, Empfänger, Body-Text
|
|
||||||
- [ ] Filteroptionen: Datum (von–bis), Absender-Domain, hat Anhang (ja/nein), Label
|
|
||||||
- [ ] Sortierung: nach Relevanz, nach Datum (auf-/absteigend)
|
|
||||||
- [ ] Suchergebnisse paginiert (Standard: 25 pro Seite)
|
|
||||||
- [ ] Suchbegriff in Betreff und Body-Snippet hervorgehoben
|
|
||||||
- [ ] Suchanfragen liefern Ergebnisse in < 2 Sekunden (bei 100.000+ E-Mails)
|
|
||||||
- [ ] Nutzer sehen nur E-Mails in ihren zugewiesenen Postfächern
|
|
||||||
|
|
||||||
## Edge Cases
|
|
||||||
- Suchanfrage ohne Ergebnisse → "Keine Ergebnisse" Meldung mit Vorschlägen
|
|
||||||
- Sonderzeichen in Suchanfrage (", *, ?) → Escaping oder Query-Syntax erlauben
|
|
||||||
- Suche bei sehr großem Index (1M+ Mails) → Performance-Test erforderlich
|
|
||||||
- Gleichzeitige Suchanfragen von vielen Nutzern → kein Query-Blocking
|
|
||||||
|
|
||||||
## Technical Requirements
|
|
||||||
- **Such-Engine: Xapian** – die Web-GUI sucht ausschließlich über den Xapian-Index
|
|
||||||
- Kein SQL-Fulltext-Query gegen PostgreSQL – DB wird nur für Metadaten-Lookup nach Treffern genutzt
|
|
||||||
- Suchfluss: Web-GUI → API → Xapian-Query → Treffer-IDs → PostgreSQL-Metadaten-Lookup → Antwort
|
|
||||||
- Xapian QueryParser: AND, OR, NOT, Phrasen (`"exakter Text"`), Wildcards (`word*`), Feldpräfixe (`from:`, `subject:`)
|
|
||||||
- Relevanz-Ranking über Xapian BM25Weight
|
|
||||||
- Snippet/Highlighting über `Xapian::MSet::snippet()`
|
|
||||||
- `ReadonlyDatabase` für parallele Lesezugriffe (mehrere Nutzer gleichzeitig möglich)
|
|
||||||
- Antwortzeit < 2 Sekunden für Volltext-Suche über 100.000 E-Mails
|
|
||||||
- Suchanfragen werden für Audit-Log erfasst (optional, konfigurierbar)
|
|
||||||
|
|
||||||
---
|
|
||||||
## Tech Design (Solution Architect)
|
|
||||||
|
|
||||||
### Komponentenstruktur
|
|
||||||
|
|
||||||
**Next.js Frontend:**
|
|
||||||
```
|
|
||||||
/search
|
|
||||||
├── SearchBar ← Eingabefeld, Submit bei Enter oder Button
|
|
||||||
├── FilterPanel ← aufklappbar
|
|
||||||
│ ├── DateRangePicker ← von–bis Datum
|
|
||||||
│ ├── DomainFilter ← Absender-Domain Freitext
|
|
||||||
│ ├── AttachmentToggle ← nur Mails mit Anhang
|
|
||||||
│ └── LabelFilter ← Label-Auswahl (Mehrfachauswahl)
|
|
||||||
├── SortControls ← Relevanz / Datum aufsteigend / absteigend
|
|
||||||
├── ResultsList
|
|
||||||
│ └── MailCard (pro Treffer)
|
|
||||||
│ ├── Betreff (Suchbegriff hervorgehoben)
|
|
||||||
│ ├── Von / An / Datum / Größe
|
|
||||||
│ ├── Body-Snippet (Suchbegriff hervorgehoben)
|
|
||||||
│ └── Anhang-Indikator
|
|
||||||
├── Pagination ← Seiten-Navigation
|
|
||||||
└── EmptyState ← "Keine Ergebnisse" mit Suchtipps
|
|
||||||
```
|
|
||||||
|
|
||||||
**Go Backend:**
|
|
||||||
```
|
|
||||||
GET /api/search
|
|
||||||
├── Session Middleware ← Auth prüfen
|
|
||||||
├── Role Filter Builder ← user: nur eigene Postfächer / auditor: alle
|
|
||||||
├── Xapian QueryParser ← Nutzer-Query parsen (AND/OR/NOT/Wildcards)
|
|
||||||
├── Xapian ReadonlyDatabase ← Query ausführen, MSet zurückgeben
|
|
||||||
│ ├── BM25 Relevanz-Ranking ← beste Treffer zuerst
|
|
||||||
│ └── MSet::snippet() ← Highlighting-Snippets erzeugen
|
|
||||||
├── PostgreSQL Metadaten-Lookup ← From, To, Subject, Date, Size, Attachments
|
|
||||||
└── JSON Response Assembly ← Ergebnis zusammenbauen
|
|
||||||
```
|
|
||||||
|
|
||||||
### Suchfluss
|
|
||||||
|
|
||||||
```
|
|
||||||
Next.js (Browser) Go Backend
|
|
||||||
│ │
|
|
||||||
│ GET /api/search │
|
|
||||||
│ ?q=Rechnung&date_from=2024-01 │
|
|
||||||
│ &has_attachments=true&page=2 │
|
|
||||||
│ ────────────────────────────────► │
|
|
||||||
│ Session prüfen
|
|
||||||
│ Rolle ermitteln:
|
|
||||||
│ user → Filter: nur eigene Postfach-IDs
|
|
||||||
│ auditor → kein Filter
|
|
||||||
│ │
|
|
||||||
│ Xapian QueryParser
|
|
||||||
│ → Query + Datumsfilter + Anhang-Filter
|
|
||||||
│ │
|
|
||||||
│ Xapian ReadonlyDatabase
|
|
||||||
│ → MSet: [doc_id_1, doc_id_5, ...]
|
|
||||||
│ → Snippets mit Highlighting
|
|
||||||
│ │
|
|
||||||
│ PostgreSQL
|
|
||||||
│ → Metadaten für doc_ids laden
|
|
||||||
│ │
|
|
||||||
│ ◄────────────────────────────────
|
|
||||||
│ { total, page, mails: [...] } │
|
|
||||||
│ MailCards rendern + highlighten │
|
|
||||||
```
|
|
||||||
|
|
||||||
### Technische Entscheidungen
|
|
||||||
|
|
||||||
| Entscheidung | Begründung |
|
|
||||||
|---|---|
|
|
||||||
| **Xapian für alles, kein SQL-Fulltext** | Optimiert für Volltext – PostgreSQL LIKE-Suche wäre bei 100k+ Mails zu langsam |
|
|
||||||
| **ReadonlyDatabase** | Beliebig viele parallele Lesezugriffe – kein Blocking bei gleichzeitigen Nutzern |
|
|
||||||
| **Rolle als Xapian-Term** | Postfach-ID beim Indexieren als Term gespeichert – Rollenfilter läuft in Xapian, nicht nachträglich in der DB |
|
|
||||||
| **Snippets aus Xapian** | `MSet::snippet()` hebt Suchbegriff im Originaltext hervor – kein separates Rendering nötig |
|
|
||||||
| **Paginierung über Xapian Offset** | Nur angefragter Seitenausschnitt zurückgegeben – kein Full-Scan pro Seite |
|
|
||||||
| **PostgreSQL nur für Metadaten** | Nach Xapian-Suche werden nur gefundene IDs nachgeschlagen – minimale DB-Last |
|
|
||||||
| **URL-State mit `nuqs`** | Suchparameter in der URL → Back-Button funktioniert, Suchergebnisse sind verlinkbar |
|
|
||||||
|
|
||||||
## QA Test Results
|
|
||||||
_To be added by /qa_
|
|
||||||
|
|
||||||
## Deployment
|
|
||||||
_To be added by /deploy_
|
|
||||||
@@ -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`)
|
|
||||||
@@ -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 (5–1440) |
|
|
||||||
| `last_sync_at` | Zeitpunkt des letzten Syncs (UTC) |
|
|
||||||
| `last_sync_count` | Anzahl importierter Mails beim letzten Sync |
|
|
||||||
| `last_uid` | Höchste bekannte IMAP-UID (Startpunkt für nächsten Sync) |
|
|
||||||
| `sync_running` | `true` wenn Sync gerade läuft (verhindert parallelen Sync) |
|
|
||||||
| `sync_status` | `ok` / `error` / `running` |
|
|
||||||
| `sync_error_msg` | Letzte Fehlermeldung |
|
|
||||||
|
|
||||||
### Technische Entscheidungen
|
|
||||||
|
|
||||||
| Entscheidung | Begründung |
|
|
||||||
|---|---|
|
|
||||||
| **UID-basierter inkrementeller Sync** | Nur neue Mails seit letzter bekannter UID werden abgeholt – minimaler Traffic, kein Re-Download |
|
|
||||||
| **`sync_running`-Flag in DB** | Verhindert parallelen Sync derselben Verbindung auch nach Server-Neustart |
|
|
||||||
| **Cron-Loop jede Minute** | Einfacher als individuelle Timer pro Account – skaliert auf viele Accounts ohne Overhead |
|
|
||||||
| **Exponential Backoff** | Temporäre Ausfälle (Netz, Server-Neustart) werden automatisch überbrückt ohne Admin-Eingriff |
|
|
||||||
| **Status persistent in DB** | Server-Neustart verliert keinen Sync-Fortschritt – Scheduler macht nahtlos weiter |
|
|
||||||
| **Manueller Trigger** | Admin kann sofortigen Sync anstoßen ohne auf Intervall zu warten |
|
|
||||||
|
|
||||||
### Abhängigkeiten
|
|
||||||
|
|
||||||
| Paket | Zweck |
|
|
||||||
|---|---|
|
|
||||||
| `github.com/robfig/cron` | Eingebetteter Cron-Scheduler |
|
|
||||||
| `github.com/emersion/go-imap` | IMAP-Client (bereits PROJ-3) |
|
|
||||||
|
|
||||||
## QA Test Results
|
|
||||||
_To be added by /qa_
|
|
||||||
|
|
||||||
## Deployment
|
|
||||||
_To be added by /deploy_
|
|
||||||
@@ -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_
|
|
||||||
Reference in New Issue
Block a user