d360c9a5ba
- Systemauslastungs-Sektion wird immer gerendert (nicht nur bei Erfolg) - Fehlermeldung wenn /api/admin/system/stats nicht erreichbar ist - Feature-Status auf In Review gesetzt Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
185 lines
7.3 KiB
Markdown
185 lines
7.3 KiB
Markdown
# PROJ-7: E-Mail-Ansicht (Lesen & Anhänge)
|
||
|
||
## Status: In Progress
|
||
**Created:** 2026-03-12
|
||
**Last Updated:** 2026-03-12
|
||
|
||
## Dependencies
|
||
- Requires: PROJ-1 (Authentifizierung) – nur eingeloggte Nutzer mit Zugriffsrecht
|
||
- Requires: PROJ-5 (Speicherung & Indexierung) – E-Mail-Daten aus der Datenbank
|
||
|
||
## User Stories
|
||
- Als Nutzer möchte ich eine E-Mail aus den Suchergebnissen öffnen und lesen, damit ich den vollständigen Inhalt sehe.
|
||
- Als Nutzer möchte ich Anhänge herunterladen, damit ich auf angefügte Dokumente zugreifen kann.
|
||
- Als Nutzer möchte ich die originalen E-Mail-Header einsehen (technische Details), damit ich Routing und Authentizität prüfen kann.
|
||
- Als Nutzer möchte ich E-Mails im HTML-Format sehen (mit sanitizierten externen Inhalten), damit die Formatierung erhalten bleibt.
|
||
- Als Nutzer möchte ich die Originalmail als EML herunterladen können.
|
||
|
||
## Acceptance Criteria
|
||
- [ ] E-Mail-Detailansicht zeigt: Von, An, CC, Datum, Betreff, Body
|
||
- [ ] HTML-Body wird **original** dargestellt – kein Entfernen oder Verändern von Inhalten
|
||
- [ ] Darstellung in einem vollständig isolierten `<iframe sandbox>` – kein JavaScript aus der Mail kann ausgeführt werden
|
||
- [ ] Externe Bilder/Ressourcen standardmäßig blockiert, Nutzer kann per Button "externe Inhalte laden" freischalten
|
||
- [ ] Fallback auf Plain-Text wenn kein HTML vorhanden
|
||
- [ ] Anhang-Liste mit Dateiname, Typ und Größe
|
||
- [ ] Anhänge einzeln herunterladbar
|
||
- [ ] Header-Ansicht (klappbar) zeigt alle Original-MIME-Header
|
||
- [ ] Download der Original-E-Mail als .eml Datei
|
||
- [ ] Zugriffsschutz: Nutzer kann nur E-Mails aus eigenen Postfächern öffnen
|
||
- [ ] Jeder Zugriff auf eine E-Mail wird im Audit-Log erfasst
|
||
|
||
## Edge Cases
|
||
- E-Mail mit nur Plain-Text → normales Rendering ohne HTML
|
||
- HTML mit JavaScript → Script wird durch iframe-Sandbox blockiert, HTML-Inhalt bleibt unverändert sichtbar
|
||
- Externe Tracker (Pixel, Links) → standardmäßig blockiert durch CSP, auf Wunsch des Nutzers freischaltbar
|
||
- E-Mail mit sehr großen Anhängen (> 100 MB) → Download-Streaming, kein Speicher-Overflow
|
||
- E-Mail mit verschachteltem MIME (E-Mail in E-Mail als Anhang) → als EML-Anhang anzeigen
|
||
- Nicht unterstützte Zeichenkodierung → graceful Fallback mit Hinweis
|
||
|
||
## Technical Requirements
|
||
- **Kein HTML-Sanitizing** – originale Darstellung ohne Veränderung des Inhalts
|
||
- Isolation über `<iframe sandbox="allow-same-origin">` – JavaScript blockiert, Inhalt originalgetreu
|
||
- Externe Ressourcen über CSP (`Content-Security-Policy`) serverseitig blockiert, opt-in per Nutzer-Aktion
|
||
- Anhang-Downloads als Stream (kein vollständiges In-Memory-Laden)
|
||
- Audit-Log-Eintrag: Nutzer-ID, E-Mail-ID, Zeitstempel bei jedem Lesezugriff
|
||
|
||
---
|
||
## Tech Design (Solution Architect)
|
||
|
||
### Komponentenstruktur
|
||
|
||
**Next.js Frontend:**
|
||
```
|
||
/mail/[message_id]
|
||
├── MailHeader
|
||
│ ├── Betreff
|
||
│ ├── Von / An / CC / Datum / Größe
|
||
│ └── HeaderToggle (klappbar)
|
||
│ └── RawHeaderView ← alle Original-MIME-Header als Text
|
||
├── ActionBar
|
||
│ ├── EML-Download Button ← lädt Original-Mail herunter
|
||
│ ├── Externe Inhalte laden ← Button, standardmäßig deaktiviert
|
||
│ └── Zurück zur Suche
|
||
├── MailBody
|
||
│ ├── HtmlView ← originales HTML in <iframe sandbox>
|
||
│ │ └── ExternalContentBanner ← Hinweis "Externe Inhalte blockiert [Laden]"
|
||
│ └── PlainTextView ← Fallback wenn kein HTML vorhanden
|
||
└── AttachmentList
|
||
└── AttachmentItem (pro Anhang)
|
||
├── Icon (nach MIME-Type)
|
||
├── Dateiname + Typ + Größe
|
||
└── Download-Button ← direkter Stream vom Go-Backend
|
||
```
|
||
|
||
**Go Backend:**
|
||
```
|
||
GET /api/mails/{message_id}
|
||
├── Session Middleware ← Auth prüfen
|
||
├── Zugriffsrecht prüfen ← user: nur eigenes Postfach / auditor: alle
|
||
├── .m-Datei von Disk lesen ← Pfad aus PostgreSQL
|
||
├── AES-256-GCM entschlüsseln ← Schlüssel aus Prozessspeicher
|
||
├── MIME-Parser ← Body + Header + Anhang-Metadaten extrahieren
|
||
└── JSON-Antwort ← Metadaten + originaler HTML-Body + Anhang-Liste
|
||
|
||
GET /api/mails/{message_id}/attachments/{index}
|
||
├── Session Middleware
|
||
├── Zugriffsrecht prüfen
|
||
├── Hash aus PostgreSQL ← welche astore/-Datei?
|
||
├── astore/-Datei öffnen
|
||
├── AES-256-GCM entschlüsseln (stream)
|
||
└── HTTP-Streaming-Response ← Content-Disposition: attachment
|
||
|
||
GET /api/mails/{message_id}/raw
|
||
├── Session Middleware
|
||
├── Zugriffsrecht prüfen
|
||
├── .m-Datei entschlüsseln
|
||
└── HTTP-Streaming-Response ← Content-Type: message/rfc822
|
||
```
|
||
|
||
### Datenabruf-Fluss
|
||
|
||
```
|
||
Browser klickt MailCard aus Suchergebnissen
|
||
│
|
||
│ GET /api/mails/<message_id>
|
||
▼
|
||
Zugriffsrecht prüfen
|
||
│
|
||
▼
|
||
PostgreSQL → store_path
|
||
│
|
||
▼
|
||
.m-Datei lesen + AES-256-GCM entschlüsseln
|
||
│
|
||
▼
|
||
MIME-Parser → body_html (original, unverändert), body_plain, headers[], attachments[]
|
||
│
|
||
▼
|
||
JSON-Antwort an Next.js
|
||
│
|
||
▼
|
||
Next.js:
|
||
├── HTML → <iframe sandbox="allow-same-origin">
|
||
│ └── CSP-Header blockiert externe Ressourcen
|
||
│ Nutzer klickt "Externe Inhalte laden"
|
||
│ → iframe neu laden ohne CSP-Restriction
|
||
└── Anhang-Liste → Download-Links
|
||
```
|
||
|
||
### Technische Entscheidungen
|
||
|
||
| Entscheidung | Begründung |
|
||
|---|---|
|
||
| **Kein HTML-Sanitizing** | Originale Darstellung – kein Inhalt wird verändert oder entfernt |
|
||
| **`<iframe sandbox>`** | JavaScript aus der Mail wird blockiert ohne den HTML-Inhalt zu verändern – Inhalt bleibt originalgetreu |
|
||
| **CSP für externe Ressourcen** | Tracking-Pixel und externe Bilder standardmäßig blockiert – Nutzer kann bewusst freischalten |
|
||
| **Entschlüsselung nur im Backend** | Verschlüsselte Rohdaten verlassen den Server nie |
|
||
| **Anhang-Download als Stream** | Große Anhänge (>100 MB) nie komplett in RAM – direkt von Disk zum Browser |
|
||
| **Kein Audit-Log bei Lesezugriff** | Bewusste Entscheidung (PROJ-11): Lesezugriffe werden nicht geloggt |
|
||
|
||
### Abhängigkeiten
|
||
|
||
**Go Backend:**
|
||
|
||
| Paket | Zweck |
|
||
|---|---|
|
||
| `mime`, `mime/multipart` | MIME-Parsing (Stdlib) |
|
||
|
||
**Next.js Frontend:** Nur shadcn/ui (bereits installiert), kein zusätzliches Paket nötig.
|
||
|
||
## QA Test Results
|
||
_To be added by /qa_
|
||
|
||
## Deployment
|
||
|
||
### Lokal bauen
|
||
|
||
```bash
|
||
# Im Projektverzeichnis
|
||
npm run build
|
||
```
|
||
|
||
Build-Artefakt liegt danach in `.next/`.
|
||
|
||
### Auf Server übertragen (192.168.1.131)
|
||
|
||
```bash
|
||
# Next.js-Build + Abhängigkeiten übertragen
|
||
rsync -avz --delete \
|
||
.next/ \
|
||
package.json \
|
||
package-lock.json \
|
||
next.config.ts \
|
||
root@192.168.1.131:/opt/archivmail/frontend/
|
||
|
||
# Auf dem Server: Abhängigkeiten installieren & Dienst neu starten
|
||
ssh root@192.168.1.131 "cd /opt/archivmail/frontend && npm ci --omit=dev && systemctl restart archivmail-frontend"
|
||
```
|
||
|
||
### Voraussetzungen auf dem Server
|
||
|
||
- Node.js ≥ 20 installiert (`node -v`)
|
||
- Verzeichnis `/opt/archivmail/frontend/` existiert
|
||
- Systemd-Unit `archivmail-frontend` läuft `npm run start` (Port 3000)
|
||
- Go-Backend läuft auf Port 8080, Next.js proxied `/api/*` dorthin (siehe `next.config.ts`)
|