Datei war durch .git/info/exclude (features/) von "git add -u" ausgenommen, INDEX-Eintrag existierte bereits, die eigentliche Spec-Datei fehlte im Commit. Enthält jetzt auch die Implementation Notes zum Backend. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
6.3 KiB
id, title, status, created
| id | title | status | created |
|---|---|---|---|
| PROJ-70 | User-Self-Service IMAP-Rückholung (Archiv-Mail zurück ins Postfach) | Planned | 2026-07-07 |
Problem
User können archivierte Mails nicht selbst wieder in ihr eigenes IMAP-Postfach
zurückholen. Aktuell existiert nur die Import-Richtung (Postfach → Archiv,
internal/imap/). Der eingebettete IMAP-Server des Archivs (internal/imapserver/)
ist bewusst read-only (server.go:342, server.go:609 — blockt APPEND/COPY/
MOVE/STORE) und muss read-only bleiben.
Lösung
Neue Rückholung schreibt NICHT ins Archiv, sondern liest eine archivierte Mail
lesend aus internal/storage und schreibt eine Kopie per IMAP APPEND in das
externe, echte Postfach des Users (Gmail/eigener Server), unter Nutzung der
bereits hinterlegten IMAP-Account-Credentials (internal/imap/, imap_accounts
Tabelle).
Vorgaben
- Archiv bleibt read-only — kein Schreibzugriff auf
internal/imapserver, ausschließlichstorage.Store.Load()lesend. - Standardmäßig deaktiviert. Rückholung ist pro User ein Opt-in-Feature: Schieberegler in den User-Einstellungen, den der User selbst aktivieren muss.
- Passwort-Bestätigung beim Aktivieren. Nach Umlegen des Schiebereglers muss der User sein aktuelles Login-Passwort erneut eingeben, bevor die Rückholung-Funktion freigeschaltet wird (analog sensiblen Aktionen wie Passwort-Änderung).
- Kein Admin-Override. Nur der Account-Owner selbst darf seine eigenen Mails zurückholen — kein Admin/domain_admin-Zugriff auf fremde Postfächer.
- Zielordner: fix
INBOX(kein Ordner-Picker in v1).
Tenant-/Ownership-Isolation (kritisch, siehe PROJ-61-Muster)
Zwei unabhängige Checks in EINER gemeinsamen Hilfsfunktion (nicht getrennt in Mail- und IMAP-Handler dupliziert):
- Gehört die angeforderte Mail-ID (
email_refs) dem anfragenden User (user_id/tenant_idAbgleich)? - Gehört das Ziel-IMAP-Konto (
imap_accounts.owner) demselben User/Tenant?
Acceptance Criteria
- Neues User-Setting
imap_restore_enabled(defaultfalse), umschaltbar per Schieberegler (z.B. in/imapoder Profil-Einstellungen). - Aktivierung erfordert erneute Passwort-Eingabe (bcrypt-Vergleich gegen hinterlegtes Passwort), sonst bleibt Regler aus/wird zurückgesetzt.
- Solange
imap_restore_enabled = false: Rückholung-Button/Endpoint nicht sichtbar bzw. Backend lehnt Anfrage ab (403), auch bei direktem API-Call. - Neuer Endpoint (z.B.
POST /api/mail/{id}/restore) mit den zwei Ownership-Checks aus obigem Abschnitt. - Neue Funktion in
internal/imap/(z.B.append.go) für IMAPAPPEND, Wiederverwendung vonConnect()/GetPassword()ausclient.go/store.go. - Fehlerfall abgefangen: Ziel-IMAP-Konto erlaubt kein Schreiben
(
NO [CANNOT]/NO [PERMISSIONDENIED]) → klare Fehlermeldung im Frontend, kein 500 mit leerem Body. - Jeder Rückhol-Vorgang wird im Audit-Log erfasst (neuer Entry-Typ
restore/retrieve), analog Export-Zugriff. - Archiv selbst bleibt unverändert nach Rückholung (keine Löschung, keine Änderung an SHA-256/Retention/Löschsperre) — reine Kopie.
- Frontend: Button in
/mail/[id](Detailansicht), nur sichtbar wennimap_restore_enabled = trueund mindestens ein IMAP-Konto hinterlegt. - Tenant-Isolation-Test: User A kann Mail von User B nicht in sein (A's) Postfach zurückholen, auch nicht bei manipulierter Mail-ID im Request.
Non-Goals
- Kein Ordner-Picker für Zielordner (v1: fix INBOX).
- Kein Admin-Zugriff/Support-Override auf fremde Rückholungen.
- Keine Änderung am read-only-Verhalten des eingebetteten IMAP-Archivservers.
Offene Punkte
- Credentials-Reichweite: Ob das gespeicherte IMAP-Passwort auch Schreibrechte beim Provider hat, ist nicht vom Datenmodell garantiert — nur als Fehlerfall behandelbar, nicht vorab prüfbar ohne Testschreibversuch.
Implementation Notes
Backend (Commit da79a56):
internal/userstore/userstore.go: Spalteimap_restore_enabled BOOLEAN NOT NULL DEFAULT falsevia idempotenterinitSchema-Migration, plusGetRestoreEnabled/SetRestoreEnabled.internal/api/restore_handlers.go(neu):PATCH /api/auth/imap-restore— Body{ "enabled": bool, "current_password": string }, Aktivierung erfordert bcrypt-Gegenprobe des aktuellen Passworts (LDAP-Accounts abgelehnt, da kein lokales Passwort). Antwort:{ "ok": true, "imap_restore_enabled": bool }.POST /api/mails/{id}/restore— Body{ "account_id": number }, hinterrequireMailAccess(schließt superadmin/domain_admin explizit aus, SEC-29-Muster). Lehnt mit 403 ab, wennimap_restore_enabled=false. Führt beide Ownership-Checks (Mail- und Account-Zugehörigkeit) inrestoreAccessAllowed()zusammen (PROJ-61-Muster). Antwort bei Erfolg:{ "ok": true, "mailbox": "INBOX", "account": <id> }. Fehlerfälle: 400 (ungültige ID/fehlender account_id), 403 (Opt-in aus oder Zugriff verweigert), 404 (Mail/Account nicht gefunden), 422 (Zielpostfach lehnt APPEND ab —ErrAppendRejected, Klartext-Fehlermeldung), 502 (Verbindung zum Zielpostfach fehlgeschlagen).
internal/imap/append.go(neu):AppendToMailbox(host, port, tlsMode, username, password, mailbox, raw []byte) error, nutzt bestehendenConnect()/Conn-Typ, 2-Minuten-Deadline für den gesamten Connect+Login+Append-Vorgang.classifyAppendErrerkennt serverseitige Ablehnungen (CANNOT/PERMISSIONDENIED/ NOPERM/OVERQUOTA/READ-ONLY) und wrapped sie umErrAppendRejected.internal/audit/audit.go: neuerEventRestore = "restore", geloggt bei jedem Versuch (Erfolg und Fehlschlag) inkl. Mail-ID.- Kein Schreibzugriff auf
internal/imapserver(Archiv-Server) — nur lesendesstorage.Load()plus schreibender Client gegen das externe Postfach. - Offen (Frontend, noch nicht umgesetzt): Schieberegler + Passwort-Dialog für
Opt-in, Restore-Button in
/mail/[id], Auswahl des Ziel-IMAP-Kontos falls mehrere hinterlegt sind.GET /api/auth/meliefert bereitsimap_restore_enabledim Response (sieheauth_handlers.go), Frontend kann das für Sichtbarkeit nutzen. - Nicht lokal kompilierbar/testbar (kein Go-Toolchain lokal) — Verifikation von
go vet/Kompilierung steht auf Test-Server (132) noch aus.
QA Test Results
wird nach Umsetzung ergänzt.
Deployment
wird nach Abschluss ergänzt.