# PROJ-9: Ordner- & Label-Verwaltung ## Status: In Review **Created:** 2026-03-12 **Last Updated:** 2026-03-18 ## 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:" 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). ## Implementation Notes (Backend) **Implemented 2026-03-18:** - `internal/labelstore/store.go` -- Full CRUD for labels, email-label assignments, and auto-label rules. Uses own `initSchema()` (same pattern as tenantstore). Includes `GetLabelsForEmails()` batch helper for search enrichment. - `internal/api/label_handlers.go` -- All 10 HTTP handlers + `SetLabels()` wiring method. Routes registered in setter (same pattern as SetLDAP/SetTenants). Input validation: color hex format, allowed condition fields, mail ID via SEC-22 regex. - `internal/storage/migrations/012_labels.sql` -- Reference SQL (actual schema applied by labelstore.initSchema). - `internal/index/index.go` -- `SearchRequest.LabelID` field added for label filtering. - `internal/api/server.go` -- Label post-filter in search handler (Go-level, not Xapian CGO). Search results enriched with `label_ids` array via batch query. `labels` field added to Server struct. - `cmd/archivmail/main.go` -- Labelstore wired via `srv.SetLabels(labelSt)`. **Design deviation from spec:** Label filtering uses Go-level post-filter on search results (same pattern as tenant isolation fallback) instead of Xapian boolean terms. Reason: the Xapian integration goes through CGO with a C wrapper; adding label terms would require modifying both the C wrapper and the Go bridge. The post-filter approach is simpler and consistent with existing patterns. ## QA Test Results _To be added by /qa_ ## Deployment _To be added by /deploy_