Files
archivmail/features/PROJ-70-imap-rueckholung-self-service.md
T
sysopsandClaude Sonnet 5 c00bf27fea docs(PROJ-70): Feature-Spec-Datei nachtragen (versehentlich nicht committed)
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>
2026-07-07 00:37:32 +02:00

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ßlich storage.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):

  1. Gehört die angeforderte Mail-ID (email_refs) dem anfragenden User (user_id/tenant_id Abgleich)?
  2. Gehört das Ziel-IMAP-Konto (imap_accounts.owner) demselben User/Tenant?

Acceptance Criteria

  • Neues User-Setting imap_restore_enabled (default false), umschaltbar per Schieberegler (z.B. in /imap oder 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 IMAP APPEND, Wiederverwendung von Connect()/GetPassword() aus client.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 wenn imap_restore_enabled = true und 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: Spalte imap_restore_enabled BOOLEAN NOT NULL DEFAULT false via idempotenter initSchema-Migration, plus GetRestoreEnabled/ 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 }, hinter requireMailAccess (schließt superadmin/domain_admin explizit aus, SEC-29-Muster). Lehnt mit 403 ab, wenn imap_restore_enabled=false. Führt beide Ownership-Checks (Mail- und Account-Zugehörigkeit) in restoreAccessAllowed() 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 bestehenden Connect()/Conn-Typ, 2-Minuten-Deadline für den gesamten Connect+Login+Append-Vorgang. classifyAppendErr erkennt serverseitige Ablehnungen (CANNOT/PERMISSIONDENIED/ NOPERM/OVERQUOTA/READ-ONLY) und wrapped sie um ErrAppendRejected.
  • internal/audit/audit.go: neuer EventRestore = "restore", geloggt bei jedem Versuch (Erfolg und Fehlschlag) inkl. Mail-ID.
  • Kein Schreibzugriff auf internal/imapserver (Archiv-Server) — nur lesendes storage.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/me liefert bereits imap_restore_enabled im Response (siehe auth_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.