Files
archivmail/features/PROJ-81-anhang-online-vorschau.md
sysopsandClaude Sonnet 5 2aad899dfc docs(PROJ-81): Pflegehinweis für pdfjs-dist in Spec ergänzt
Seit dem BUG-81-3-Fix läuft PDF-Parsing über pdfjs-dist im App-Origin
statt in einer Browser-Sandbox. Sicherheitsniveau hängt damit an der
Code-Qualität der Dependency - Update-Pflicht bei Security-Releases
dokumentiert, nicht erst beim nächsten großen Dependency-Sweep.

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

302 lines
27 KiB
Markdown

# 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
- [ ] Jede Anhang-Zeile hat einen eigenen "Vorschau"-Button, der die Vorschau als Modal/Dialog über der aktuellen Mail-Ansicht öffnet (bewusst ein separater Button statt Klick auf die ganze Zeile, damit "Vorschau" und "Herunterladen" eindeutig unterscheidbar bleiben — siehe BUG-81-5)
- [ ] 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
- **Pflege: `pdfjs-dist` regelmäßig aktuell halten.** Seit dem Fix zu BUG-81-3 läuft das PDF-Parsing über die Bibliothek `pdfjs-dist` im App-Origin (Web Worker, kein iframe/Browser-Sandbox mehr, siehe Implementation Notes unten). Das Sicherheitsniveau der PDF-Vorschau hängt damit direkt an der Code-Qualität dieser Dependency — ein Parser-Bug dort landet im App-Origin, nicht in einer isolierten Browser-Komponente. `pdfjs-dist` ist aktiv gepflegt (Basis des Firefox-eigenen PDF-Viewers) und regelmäßiges CVE-Ziel. Bei Major-/Security-Updates zeitnah aktualisieren, nicht auf den nächsten großen Dependency-Sweep (vgl. PROJ-79) warten.
---
<!-- Sections below are added by subsequent skills -->
## 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): **`pdfjs-dist@6.2.108`**, ergänzt im dritten Fix-Durchgang (BUG-81-3). Der zunächst genutzte browsereigene Viewer im iframe war nicht browserübergreifend nutzbar. 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: `pdf``application/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`
```tsx
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/0``401` |
| 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: nosniff`**ja, 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: attachment`**ja**, `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.
### BUG-81-5 (Low) — AC1-Wortlaut vs. Umsetzung — **fixed (Spec-Text)**
Kein Code-Fix. AC1 oben umformuliert: statt "Klick auf Anhang öffnet Vorschau" beschreibt es jetzt den tatsächlich umgesetzten separaten "Vorschau"-Button je Anhang-Zeile, inkl. Begründung (Vorschau und Herunterladen bleiben eindeutig unterscheidbar). Spec und Code stimmen damit überein.
### BUG-81-6 (Low) — Anhänge ohne Dateiendung bekamen nie Vorschau — **fixed**
`AttachmentPreviewDialog.tsx`: Die Typ-Erkennung läuft jetzt über `resolveMimeType()`:
1. Dateiendung `pdf` bzw. bekannte Bild-Endung → gewinnt weiterhin, unverändert.
2. **Nur bei komplett fehlender Endung** (`scan`, `Rechnung` — typisch für Fax-/Scanner-Gateways) wird der Server-`content_type` als Hinweis herangezogen, über die neue Konstante `ALLOWED_MIME_TYPES` (Parameter wie `; charset=…` werden abgeschnitten, Vergleich case-insensitiv).
3. Unbekannte Endung → weiterhin "keine Vorschau"; der Server-Wert kann eine vorhandene Endung **nicht** überstimmen.
**Das Security-Modell bleibt unverändert:** der Server-Wert entscheidet ausschließlich, *welcher* Eintrag aus der festen Whitelist gewählt wird. Gerendert wird nach wie vor nur mit dem clientseitig erzwungenen MIME-Typ aus dieser Liste. Ein Wert außerhalb der Whitelist — insbesondere `image/svg+xml` (PROJ-61-Vektor) oder `text/html` — führt zu "keine Vorschau", nicht zum Rendern. SVG ist in `ALLOWED_MIME_TYPES` bewusst nicht enthalten.
Ein Angreifer gewinnt dadurch nichts: er könnte über den Content-Type höchstens erreichen, dass seine endungslose Datei als PDF oder Bild *interpretiert* wird — genau die beiden Pfade, die ohnehin als sicher ausgelegt sind (erzwungener MIME-Typ, PDF im Sandbox-iframe ohne `allow-same-origin`).
### BUG-81-3 (Medium) — PDF-Vorschau blank in Chromium/Brave — **fixed**
**Befund aus dem Live-Test auf 132:** Firefox rendert die PDF-Vorschau einwandfrei, Chromium/Brave zeigt eine leere weiße Fläche — kein Fehler, kein Download-Prompt. Ursache bestätigt: Chromiums PDFium läuft als MimeHandlerView-Extension und benötigt dafür Same-Origin-Zugriff; in einem iframe mit `sandbox="allow-scripts"` ohne `allow-same-origin` verweigert er sich still. Firefox' pdf.js-basierter Viewer kommt damit klar. Kein Spec-Verstoß, sondern Chromium-spezifisches Verhalten.
#### Bewertung Option 1 (`allow-same-origin` ergänzen) — **verworfen**
Die entscheidende Frage war, ob eine Blob-URL einen eigenen opaken Origin hat. **Hat sie nicht.** `URL.createObjectURL()` erzeugt eine URL der Form `blob:https://host/uuid`; der Origin des daraus geladenen Dokuments ist per Spezifikation der Origin des erzeugenden Kontexts, also unser App-Origin. Opak wird ein Frame nur durch die Sandbox selbst (`allow-same-origin` weggelassen) — nicht durch das Blob.
Daraus folgt: mit `sandbox="allow-scripts allow-same-origin"` auf same-origin-Inhalt wäre der Frame vollwertig App-Origin. Diese Kombination hebt die Sandbox praktisch auf, weil das eingebettete Dokument über `parent.document` an das Sandbox-Attribut selbst herankommt und sich per Re-Navigation entsandboxen kann; die HTML-Spezifikation warnt genau vor dieser Kombination. Der Frame hätte damit Zugriff auf DOM, nicht-httpOnly-Cookies, `localStorage` und credential-behaftete API-Aufrufe.
Man könnte einwenden, dass ein getarntes HTML-Dokument wegen des erzwungenen Blob-MIME-Typs (`application/pdf`) ohnehin nie als HTML geparst wird — das stimmt. Aber damit hinge die gesamte Absicherung an genau einer Kontrolle. Für ein Archiv mit fremdem, unkontrolliertem Mail-Inhalt und der PROJ-61-Vorgeschichte ist das die falsche Richtung.
#### Umgesetzt: Option 2 (pdf.js gebündelt)
Neue Komponente `src/components/mail/PdfCanvasPreview.tsx`, neue Dependency `pdfjs-dist@6.2.108`.
- PDF wird von pdf.js **selbst nach `<canvas>` gerendert** — kein iframe, kein eingebettetes Dokument, damit entfällt die Sandbox-Frage vollständig statt sie aufzuweichen.
- Verhalten ist in Chromium und Firefox identisch, weil nicht mehr vom Viewer des Browsers abhängig.
- Parsing läuft im Worker; das Worker-Bundle wird vom Build als statisches Asset emittiert (verifiziert: `.next/static/media/pdf.worker.min.*.mjs`).
- PDF-eigenes JavaScript wird nicht ausgeführt: das Scripting-Sandbox-Bundle (`pdf.sandbox`) wird nicht geladen. `isEvalSupported` gibt es in pdfjs-dist v6 nicht mehr, weil `eval()` dort bereits vollständig entfernt wurde.
- Es wird ausschließlich in Canvas gezeichnet, kein Text-/Annotationslayer — es gelangt also kein HTML aus dem PDF ins DOM.
- Seitenlimit `MAX_RENDERED_PAGES = 30` mit Hinweis, damit ein PDF mit sehr vielen Seiten den Tab nicht blockiert.
- Eigener Fehlerzweig für passwortgeschützte/beschädigte PDFs (Edge Case aus der Spec) mit Download-Hinweis; `doc.destroy()` beim Unmount.
Im Dialog hält jetzt `pdfBlob` das PDF, Bilder laufen unverändert über die Blob-URL (Bildpfad nicht angefasst, er funktioniert live). Der Guard gegen die BUG-81-1-Schleife nutzt jetzt `loadedRef` statt `urlRef`, damit er beide Pfade abdeckt.
**Restrisiko / ehrlich:** die Verlagerung des PDF-Parsings in unseren Origin heißt, dass eine Schwachstelle in pdf.js im App-Origin landet statt im Viewer-Prozess des Browsers. pdf.js ist genau für feindliche PDFs ausgelegt, parst im Worker und ohne `eval`, und die Alternative (Option 1) hätte den App-Origin ohnehin direkt preisgegeben. Die Dependency muss aber mitgepflegt werden — pdf.js ist ein regelmäßiges CVE-Ziel.
### Offen, wie abgestimmt (kein Code-Fix in diesem Durchgang)
- **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.
### Stand nach drittem Fix-Durchgang
Alle im QA-Bericht gemeldeten Bugs sind erledigt: BUG-81-1 (High/Blocker), BUG-81-2 (Medium), BUG-81-3 (Medium), BUG-81-4, BUG-81-5, BUG-81-6 (Low).
**Deployment-Empfehlung: erneut auf 132 deployen und nachtesten.** Der Status bleibt auf **In Review**, weil der PDF-Pfad komplett ausgetauscht wurde und der Fix bisher nur durch Build und statische Analyse belegt ist, nicht im Browser. Nach dem Deploy zu verifizieren:
- **PDF-Vorschau in Chromium/Brave** — das war das ursprüngliche Symptom, hier muss jetzt gerendert werden.
- **PDF-Vorschau in Firefox** — Regressionsprobe, dort funktionierte es vorher schon.
- Mehrseitiges PDF: Seiten scrollbar, ab 30 Seiten erscheint der Kürzungshinweis.
- Passwortgeschütztes/beschädigtes PDF: sichtbarer Fehlertext statt leerer Fläche.
- Bild-Vorschau (JPG) unverändert funktionsfähig — der Pfad wurde bewusst nicht angefasst.
- Gegenprobe zu BUG-81-1: fehlgeschlagene Vorschau (z.B. 404) erzeugt genau **einen** Request, keine Schleife — im Netzwerk-Tab prüfen.
- Gegenprobe zu BUG-81-6: endungsloser PDF-Anhang bekommt einen Vorschau-Button und rendert.
- Ladezeit gegen das Technical Requirement (< 2 s bei normaler Dateigröße) gegenprüfen — pdf.js rendert langsamer als der native Viewer.
## Deployment
_To be added by /deploy_