Files
archivmail/features/PROJ-81-anhang-online-vorschau.md
T
sysopsandClaude Sonnet 5 0a02bd7112 feat(PROJ-80,PROJ-81): Dark Mode Vervollständigung + Anhang-Online-Vorschau
- PROJ-80: hartkodierte Farben in ModulesTab/TenantsTab/TenantLDAPTab durch
  Theme-Tokens ersetzt, Mail-HTML-Container bewusst hell isoliert, Print
  im Dark Mode auf lesbare (nicht farbidentische) Ausgabe umgestellt
- PROJ-81: neuer AttachmentPreviewDialog für PDF/Bild-Anhänge in /mail/[id],
  MIME-Whitelist ohne SVG, sandboxed iframe ohne allow-same-origin,
  Auto-Retry-Schleife bei Fehlern behoben
- PROJ-82 angelegt: Print-Farbparität als Folge-Ticket (BUG-80-1-Rest)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WapWkrQusDuBMhaN8WyuXB
2026-08-06 18:00:41 +02:00

20 KiB

PROJ-81: Anhang-Online-Vorschau (PDF, Bilder)

Status: In Review

Created: 2026-08-06 Last Updated: 2026-08-06

Kontext

Mail-Anhänge lassen sich in /mail/[id] aktuell nur herunterladen (downloadMailAttachment), keine Online-Ansicht im Browser. PROJ-81 ergänzt eine Vorschau direkt im Frontend, damit User Anhänge nicht erst lokal öffnen müssen.

Dependencies

  • Baut auf PROJ-7 (E-Mail-Ansicht) auf — Anhang-Liste existiert bereits in /mail/[id]

User Stories

  • Als User will ich einen PDF-Anhang direkt im Browser ansehen können, ohne ihn herunterzuladen.
  • Als User will ich Bild-Anhänge (jpg/png/gif) als Vorschau sehen, bevor ich sie herunterlade.
  • Als User will ich die Vorschau in einem Dialog/Overlay öffnen, ohne die Mail-Ansicht zu verlassen.
  • Als Admin/Auditor will ich, dass Vorschau nur für Anhänge funktioniert, auf die der jeweilige Tenant/User laut bestehender Zugriffsregeln berechtigt ist (keine neue Sicherheitslücke).

Acceptance Criteria

  • Klick auf Anhang öffnet Vorschau als Modal/Dialog über der aktuellen Mail-Ansicht
  • PDF-Anhänge werden inline gerendert (Browser-natives PDF-Rendering oder eingebetteter Viewer)
  • Bild-Anhänge (jpg, png, gif, webp) werden als Bildvorschau angezeigt
  • Nicht unterstützte Dateitypen (inkl. Office-Dokumente wie docx/xlsx) zeigen Hinweis "Keine Vorschau verfügbar" + Download-Button bleibt erhalten
  • Download-Option bleibt im Dialog zusätzlich verfügbar (Vorschau ersetzt Download nicht)
  • Vorschau respektiert bestehende Tenant-Isolation/Zugriffsrechte (kein direkter öffentlicher Link auf Rohdatei)
  • Große Anhänge (Performance-Grenze definieren, z.B. > 20 MB) zeigen Warnhinweis statt automatischem Laden

Edge Cases

  • Sehr große PDF/Bild-Dateien (>20 MB) → Warnhinweis statt automatischer Vorschau, User muss Laden explizit bestätigen
  • Passwortgeschützte/verschlüsselte PDFs → Vorschau schlägt fehl, Fehlermeldung + Download-Fallback
  • Beschädigte/korrupte Anhänge → Fehlermeldung statt Absturz des Dialogs
  • Sehr viele Anhänge in einer Mail → Performance beim Öffnen mehrerer Vorschauen nacheinander
  • Anhang wurde per DSGVO-Löschersuchen (PROJ-50) bereits entfernt → Vorschau zeigt "nicht mehr verfügbar" statt Fehler

Technical Requirements (optional)

  • Performance: PDF/Bild-Vorschau < 2s Ladezeit bei normaler Dateigröße
  • Security: Vorschau-Endpunkt muss denselben Auth-/Tenant-Check wie bestehender Download-Endpunkt durchlaufen
  • Browser Support: Chrome, Firefox, Safari, Edge

Tech Design (Solution Architect)

A) Komponentenstruktur (visuell)

Mail-Ansicht (/mail/[id])
+-- Anhang-Liste (bestehend)
|   +-- Anhang-Zeile
|       +-- "Vorschau"-Button (NEU)
|       +-- "Herunterladen"-Button (bestehend, bleibt)
+-- Vorschau-Dialog (NEU, Overlay über aktueller Seite)
    +-- Titelzeile (Dateiname, Schließen-Button)
    +-- Inhalt (je nach Typ):
    |   +-- PDF-Ansicht (eingebetteter Viewer)
    |   +-- Bild-Ansicht (Vollbild-Bild, zoombar)
    |   +-- "Keine Vorschau verfügbar"-Hinweis (Fallback, u.a. für Office-Dokumente)
    +-- Ladezustand (Spinner bei großer Datei)
    +-- Warnhinweis bei sehr großen Dateien ("trotzdem laden?")
    +-- "Herunterladen"-Button (bleibt zusätzlich verfügbar)

B) Datenfluss (Klartext, kein neues Datenmodell)

  • Vorschau nutzt denselben bestehenden Anhang-Endpunkt wie der heutige Download (/api/mails/{id}/attachments/{index}), der bereits Tenant-/Zugriffsprüfung durchläuft — kein neuer öffentlicher Link, keine neue Sicherheitsfläche.
  • PDF und Bilder: Browser stellt Datei direkt dar, keine Serververarbeitung nötig.
  • Kein neuer Datenbank-Eintrag nötig — Vorschau ist ein reiner Anzeige-Vorgang, keine dauerhaft gespeicherte Information.
  • Kein Server-seitiger Konvertierungsschritt (Office-Dokumente bewusst außerhalb des Scopes, siehe unten).

C) Tech-Entscheidungen (Begründung)

  • Vorschau als Modal statt neue Seite/Route: User bleibt im Kontext der Mail, kein Verlust der aktuellen Ansicht/Scroll-Position (laut Klärung des Nutzers gewünscht).
  • Bestehenden Anhang-Endpunkt wiederverwenden statt neuen zu bauen: Zugriffsrechte (Tenant-Isolation, Auth) sind dort bereits korrekt implementiert — Wiederverwendung vermeidet doppelte Sicherheitslogik und Inkonsistenzrisiko.
  • Office-Dokumente (docx/xlsx) bewusst außerhalb des Scopes: Nutzer hat Scope auf PDF+Bilder reduziert. Damit entfällt der größte Aufwandstreiber (Server-seitige Konvertierung, neue Systemabhängigkeit). Reines Frontend-Feature ohne neue Backend-Logik.
  • Größenlimit + Warnhinweis statt hartem Blocken: Verhindert, dass ein einzelner sehr großer Anhang den Browser blockiert, lässt dem User aber die Wahl, ihn trotzdem zu laden.

D) Abhängigkeiten (Pakete)

  • PDF-Viewer-Bibliothek im Frontend (Anzeige im Dialog, ohne Download). Bilder benötigen keine zusätzliche Bibliothek (natives <img>).
  • Keine neuen Server-/Systemabhängigkeiten.

Implementation Notes (Frontend, 2026-08-06)

Neue Komponente: src/components/mail/AttachmentPreviewDialog.tsx Angebunden in src/app/mail/[id]/page.tsx (AttachmentRow): pro Anhang erscheint ein "Vorschau"-Button, aber nur wenn der Typ vorschaufähig ist; "Herunterladen" bleibt unverändert daneben und zusätzlich im Dialog.

Keine neue API-Funktion nötig — der Dialog nutzt das bestehende downloadMailAttachment(mailId, index) und damit denselben Endpunkt inkl. bestehender Auth-/Tenant-Prüfung. src/lib/api/index.ts musste nicht ergänzt werden.

Security-Umsetzung (PROJ-61-Kontext)

Abweichung vom Tech Design in der Umsetzung, bewusst strenger:

  • Der Anhang wird per authentifiziertem fetch als Blob geholt, nicht per direkter Navigation auf die Anhang-URL.
  • Der vom Server gelieferte Content-Type wird nicht zum Rendern verwendet. Der Blob wird clientseitig mit einem erzwungenen MIME-Typ aus einer festen Whitelist neu verpackt. Eine als rechnung.pdf getarnte HTML-Datei kann dadurch nicht als aktives Dokument im App-Origin ausgeführt werden.
  • Whitelist: pdfapplication/pdf; jpg/jpeg/png/gif/webp/bmp → jeweiliger Bildtyp. SVG ist bewusst ausgeschlossen (kann Skripte enthalten — genau der PROJ-61-Vektor).
  • PDF rendert in <iframe sandbox="allow-scripts"> ohne allow-same-origin: eigener undurchsichtiger Origin, kein Zugriff auf Cookies/DOM der App. allow-scripts ist nötig, weil die browsereigenen PDF-Viewer sonst nicht rendern.
  • Blob-URL lebt nur im Tab und wird beim Schließen des Dialogs bzw. beim Unmount per URL.revokeObjectURL freigegeben — kein persistenter öffentlicher Link.

Damit ist die Vorschau unabhängig davon sicher, welche Header das Backend setzt.

Weitere Details

  • Größenlimit: PREVIEW_SIZE_WARN_BYTES = 20 MB. Darüber wird nicht automatisch geladen, sondern "Trotzdem laden" angeboten.
  • Zustände umgesetzt: Laden (Spinner), Fehler (Alert + Download-Fallback), "Keine Vorschau verfügbar" (u.a. docx/xlsx), Bild-onError fängt korrupte Dateien ab.
  • Bild-/PDF-Fläche auf neutral hellem Grund, damit transparente PNGs auch im Dark Mode sichtbar sind (Konsistenz mit PROJ-80).
  • Responsive: Dialog w-[95vw] max-w-4xl, Buttons stapeln unter sm.

Build: npx tsc --noEmit und npm run build fehlerfrei.

Offen / abzustimmen mit Backend Developer

Nicht blockierend für die Frontend-Funktion, aber zur Härtung des Endpunkts /api/mails/{id}/attachments/{index} bestätigen zu lassen:

  1. Wird X-Content-Type-Options: nosniff gesetzt?
  2. Wird Content-Disposition: attachment gesetzt (verhindert Inline-Rendering bei direktem Aufruf der URL im Browser)?
  3. Wird der gespeicherte Content-Type unverändert aus der Mail übernommen, oder serverseitig auf eine Whitelist normalisiert? Der Dialog ist unabhängig von den Antworten sicher, weil er den Server-Content-Type ignoriert — die Fragen betreffen den Fall, dass jemand die Anhang-URL direkt aufruft.

QA Test Results

Getestet: 2026-08-06 | Methode: Statische Code-Verifikation + Backend-Endpunkt-Live-Test gegen 192.168.1.132

Testbarkeits-Einschränkung (wichtig)

Der Frontend-Code ist uncommitted und nicht deployed (git status: ?? src/components/mail/AttachmentPreviewDialog.tsx, M src/app/mail/[id]/page.tsx). Auf 132 läuft der alte Stand. Ein visuelles/funktionales Durchklicken der Vorschau im Browser war daher NICHT möglich. Alle UI-Kriterien sind per Code-Review bewertet, nicht per Live-Test.

Acceptance Criteria

# Kriterium Ergebnis Anmerkung
1 Klick auf Anhang öffnet Vorschau als Modal PASS (mit Abweichung) Umgesetzt als separater "Vorschau"-Button, nicht als Klick auf die Zeile → BUG-81-5
2 PDF inline gerendert UNVERIFIZIERT Code vorhanden (<iframe sandbox="allow-scripts">), Rendering in FF/Safari fraglich → BUG-81-3
3 Bilder (jpg/png/gif/webp) als Vorschau PASS Whitelist inkl. bmp, <img> mit onError-Fallback
4 Nicht unterstützte Typen zeigen "Keine Vorschau verfügbar" FAIL Hinweis ist unerreichbar, Button wird gar nicht gerendert → BUG-81-2
5 Download bleibt im Dialog verfügbar PASS Download-Button im Dialog-Footer + unverändert in der Zeile
6 Tenant-Isolation / kein öffentlicher Link PASS Kein neuer Endpunkt; Blob-URL tab-lokal + revokeObjectURL
7 Große Anhänge (>20 MB) zeigen Warnhinweis PASS PREVIEW_SIZE_WARN_BYTES = 20 MB, "Trotzdem laden"-Bestätigung

Summe: 4 PASS, 1 FAIL, 1 PASS-mit-Abweichung, 1 unverifiziert

Edge Cases

Edge Case Ergebnis
>20 MB → Bestätigung statt Autoload PASS
Passwortgeschütztes PDF FAIL (indirekt) — Fehlerpfad löst BUG-81-1 aus
Beschädigter Anhang TEILWEISE — img onError greift; Fetch-Fehler löst BUG-81-1 aus
Viele Anhänge nacheinander PASS — Dialog wird pro Zeile nur bei previewOpen gemountet, Blob wird beim Unmount freigegeben
Anhang per DSGVO-Löschung entfernt (404) FAIL — löst BUG-81-1 (Endlosschleife) aus

Bugs

BUG-81-1 — Endlos-Retry-Schleife bei jedem Vorschau-Fehler — Severity: High — Priorität 1

AttachmentPreviewDialog.tsx:143-149

useEffect(() => {
  if (!open) return;
  if (kind === "unsupported") return;
  if (isLarge && !confirmedLarge) return;
  if (urlRef.current || loading) return;
  void load();
}, [open, kind, isLarge, confirmedLarge, loading, load]);

Der Effekt hat keinen error-Guard. Ablauf bei einem fehlgeschlagenen Laden:

  1. load() schlägt fehl → setError(...), setLoading(false)
  2. loading wechselt true → false → Effekt feuert erneut (steht in den Deps)
  3. urlRef.current ist null, loading ist false → alle Guards passieren → load() erneut
  4. → Endlosschleife

Reproduktion: Mail mit Anhang öffnen, Vorschau eines Anhangs starten, dessen Abruf fehlschlägt (Backend antwortet 403/404/500 — z.B. per DSGVO-Löschersuchen entfernter Anhang, oder Netzwerk offline schalten). Auswirkung: Browser-Tab blockiert/heizt, und der Client feuert unbegrenzt Requests gegen /api/mails/{id}/attachments/{index} — selbstverursachter API-Dauerbeschuss pro geöffnetem Dialog. Trifft genau die dokumentierten Edge Cases (korrupter Anhang, gelöschter Anhang, passwortgeschütztes PDF). Blocker für Produktion.

BUG-81-2 — AC4 nicht erfüllt: "Keine Vorschau verfügbar" ist toter Code — Severity: Medium — Priorität 2

src/app/mail/[id]/page.tsx:366 rendert den Vorschau-Button nur bei previewable === true (canPreview()). Für docx/xlsx/zip gibt es damit keinen Weg, den Dialog zu öffnen — der Zweig kind === "unsupported" in AttachmentPreviewDialog.tsx:175-183 ist unerreichbar. AC4 verlangt explizit, dass nicht unterstützte Typen den Hinweis zeigen. Entweder Button immer rendern (und Hinweis anzeigen) oder AC anpassen — die Spec muss mit der Implementierung übereinstimmen.

BUG-81-3 — PDF-Rendering im Sandbox-iframe browserübergreifend unverifiziert — Severity: Medium — Priorität 3

AttachmentPreviewDialog.tsx:241-248: <iframe src={blobUrl} sandbox="allow-scripts"> ohne allow-same-origin. Firefox' pdf.js und Safari rendern PDFs in sandboxed iframes ohne allow-same-origin erfahrungsgemäß nicht zuverlässig; zusätzlich weigert sich Safari häufig, blob:-PDFs in iframes darzustellen. Das Technical Requirement "Browser Support: Chrome, Firefox, Safari, Edge" ist damit nicht belegt. Erforderlich: manuelle Verifikation in allen vier Browsern nach dem Deploy. Falls FF/Safari leer bleiben, braucht es einen sichtbaren Fallback ("PDF kann in diesem Browser nicht angezeigt werden") statt eines weißen Rahmens.

BUG-81-4 — Sicherheitskommentar widerspricht dem Code — Severity: Low — Priorität 4

AttachmentPreviewDialog.tsx:28 behauptet: "PDFs laufen in einem sandboxed iframe ohne allow-same-origin/allow-scripts". Tatsächlich setzt Zeile 246 sandbox="allow-scripts". Der Inline-Kommentar bei Zeile 236-239 beschreibt es korrekt. Ein falscher Security-Kommentar im Datei-Header ist gefährlich, weil künftige Reviews darauf vertrauen.

BUG-81-5 — AC1-Wortlaut vs. Umsetzung — Severity: Low — Priorität 5

AC1: "Klick auf Anhang öffnet Vorschau". Umgesetzt: zusätzlicher "Vorschau"-Button. Funktional gleichwertig und arguably besser (Download bleibt eindeutig), aber Spec und Code sollten angeglichen werden.

BUG-81-6 — Anhänge ohne Dateiendung bekommen nie Vorschau — Severity: Low — Priorität 6

previewKindFor() entscheidet ausschließlich über die Dateiendung. Ein echtes PDF, das als scan oder Rechnung (ohne Endung) in der Mail steckt — bei Fax-/Scanner-Gateways nicht selten — bekommt keinen Vorschau-Button. Vorschlag: bei fehlender Endung zusätzlich den Server-content_type als Hinweis nutzen (weiterhin nicht zum Rendern, sondern nur zur Auswahl des erzwungenen MIME-Typs aus der Whitelist).


Security Audit (Red Team)

Ergebnis: keine neue Angriffsfläche durch PROJ-81. Die Umsetzung ist an den kritischen Stellen strenger als das Tech Design und korrekt.

Prüfpunkt Ergebnis
Neuer Endpunkt? Nein — /api/mails/{id}/attachments/{index} wiederverwendet, keine neue Route in internal/api/server.go
Auth ohne Cookie PASS — live gegen 132: GET /api/mails/abc/attachments/0401
Tenant-Isolation (PROJ-61-Klasse) PASS — search_handlers.go:406-413 vergleicht sess.TenantID gegen GetTenantForMail(); zusätzlich auditorIsGlobal()-Zweig (PROJ-55) und RoleUser-Ownership-Check
RBAC PASS — requireMailAccess() (server.go:423-438) blockt superadmin/domain_admin vom Mailinhalt (SEC-29)
Stored XSS via getarntem Anhang PASS — Server-Content-Type wird ignoriert, Blob mit erzwungenem MIME aus fester Whitelist neu verpackt; SVG bewusst ausgeschlossen; PDF-iframe ohne allow-same-origin (undurchsichtiger Origin, kein Cookie-/DOM-Zugriff)
Persistenter öffentlicher Link PASS — URL.revokeObjectURL bei Close und Unmount (urlRef-Muster deckt auch Unmount während des Ladens ab)

Antworten auf die offenen Backend-Fragen aus den Implementation Notes:

  1. X-Content-Type-Options: nosniffja, aber nur über nginx, nicht im Go-Handler. Live auf 132 verifiziert, greift auch auf /api/-Antworten. Empfehlung (Härtung, nicht blockierend): zusätzlich im Handler setzen — der Go-Dienst auf :8080 liefert den Header sonst ohne Proxy nicht, und andere Handler wie tenant_logo_handlers.go:44 setzen ihn bereits selbst.
  2. Content-Disposition: attachmentja, search_handlers.go:447. Direkter Aufruf der URL im Browser rendert also nicht inline.
  3. Content-Type-Normalisierung — nein. search_handlers.go:446 setzt w.Header().Set("Content-Type", a.ContentType) unverändert aus der Mail, ohne Whitelist. Risiko durch Punkt 1+2 gemindert, aber eine serverseitige Whitelist wäre die saubere Defense-in-Depth.

Bestandsfunde (nicht durch PROJ-81 verursacht, aber durch das Feature relevanter):

  • Anhang-Abrufe werden nicht ins Audit-Log geschrieben. handleGetAttachment (search_handlers.go:378-450) enthält keinen Audit-Aufruf. Da die Vorschau denselben Endpunkt nutzt, greifen User künftig deutlich häufiger auf Anhangsinhalte zu, ohne dass es nachvollziehbar ist — GoBD-/DSGVO-relevant. Severity: Medium, eigenes Ticket empfohlen.
  • Existenz-Orakel: unbekannte Mail-ID → 404, fremde Mail-ID → 403. Erlaubt Enumeration der Existenz fremder Mail-IDs. Severity: Low, Bestandsverhalten (auch in handleGetRaw).

Regression

  • npx tsc --noEmit fehlerfrei (Exit 0).
  • Keine Backend-Änderung → keine Regression an PROJ-55 (Auditor-Scoping), PROJ-50 (DSGVO-Löschung), SEC-29 zu erwarten.
  • src/app/mail/[id]/page.tsx (PROJ-7) angefasst: AttachmentRow umgebaut, Download-Pfad unverändert; HTML-Mail-Container zusätzlich bg-white (PROJ-80-Änderung, siehe dort).
  • Regression an PROJ-76 (Mail-HTML-Sanitizing) und PROJ-61 (Logo-XSS) nicht berührt.

Produktionsreife: NEIN

Blocker: BUG-81-1 (High). Zusätzlich sollte BUG-81-2 (AC4 nicht erfüllt) vor dem Deploy geklärt werden, und BUG-81-3 verlangt eine echte Browser-Verifikation nach dem Deploy auf 132.


Bugfixes nach QA (Frontend, 2026-08-06)

BUG-81-1 (High, Blocker) — Endlos-Retry-Schleife — fixed

AttachmentPreviewDialog.tsx: if (error) return; als Guard ergänzt, error in die Dep-Liste des Lade-Effekts aufgenommen. Nach einem Fehlschlag wird nicht mehr automatisch neu geladen — die Schleife (loading true→false → Effekt feuert → load() → Fehler → …) ist damit unterbrochen und der Dauerbeschuss von /api/mails/{id}/attachments/{index} beendet. Der User kommt weiterhin ans Ziel: Download-Button bleibt im Dialog, und beim Schließen wird error zurückgesetzt, ein erneutes Öffnen startet also einen sauberen neuen Versuch. Der Grund für den Guard steht als Kommentar direkt am Effekt, damit er bei künftigen Refactorings nicht wieder wegfällt.

BUG-81-2 (Medium) — AC4-Fallback war toter Code — fixed

src/app/mail/[id]/page.tsx: Der "Vorschau"-Button wird jetzt für alle Anhänge gerendert, nicht mehr nur bei canPreview(). docx/xlsx/zip öffnen den Dialog und erhalten dort den Hinweis "Keine Vorschau verfügbar" plus Download-Button — AC4 ist damit erfüllt und der Zweig erreichbar. canPreview wird in page.tsx nicht mehr importiert, bleibt aber als Export erhalten (intern weiter über previewKindFor genutzt).

BUG-81-4 (Low) — Sicherheitskommentar widersprach dem Code — fixed

Der Datei-Header behauptete "sandboxed iframe ohne allow-same-origin/allow-scripts", tatsächlich ist sandbox="allow-scripts" gesetzt. Header korrigiert: allow-scripts ja, allow-same-origin nein (undurchsichtiger Origin, kein Cookie-/DOM-Zugriff), inkl. Begründung warum allow-scripts nötig ist. Ein falscher Security-Kommentar ist gefährlich, weil künftige Reviews darauf vertrauen — daher trotz Low-Severity sofort mitgenommen.

Offen, wie abgestimmt (kein Code-Fix in diesem Durchgang)

  • BUG-81-3 — PDF-Sandbox in Firefox/Safari unbelegt: nach Deploy im Browser verifizieren. Fällt das Rendering aus, braucht es einen sichtbaren Hinweis statt eines leeren weißen Rahmens.
  • BUG-81-5 (AC1-Wortlaut "Klick auf Anhang" vs. Vorschau-Button) und BUG-81-6 (Anhänge ohne Dateiendung bekommen keinen Vorschau-Button) — nicht angefasst.
  • Audit-Logging für Anhang-Abrufe (Bestandsfund aus dem Security-Audit): bewusst nicht hier mitgefixt, bekommt ein eigenes Ticket.
  • Die drei Backend-Fragen sind vom QA-Lauf beantwortet (nosniff nur über nginx, Content-Disposition: attachment gesetzt, keine serverseitige Content-Type-Whitelist). Für die Vorschau unkritisch, weil der Dialog den Server-Content-Type ignoriert; die empfohlene Handler-seitige Härtung ist Backend-Scope.

Nach den Fixes: npx tsc --noEmit und npm run build fehlerfrei.

Deployment

To be added by /deploy