19a55a3166
- Neues Package internal/imapserver: vollständiger IMAP4rev1-Server (~700 Zeilen) - Auth via bcrypt (userstore.VerifyPassword), Multi-Tenant-Isolation - INBOX + INBOX/LabelName Ordnerstruktur - FETCH mit BODY[], ENVELOPE, RFC822.SIZE, INTERNALDATE, FLAGS, UID - SEARCH: ALL, FROM, TO, SUBJECT, SINCE, BEFORE + UID FETCH/SEARCH - Read-Only: STORE, DELETE, COPY, MOVE, APPEND → NO [CANNOT] - \Seen-Flag nicht persistent (GoBD-konform) - Max 5 gleichzeitige Verbindungen pro User, 30min Idle-Timeout - Audit-Log: imap_login / imap_login_failed Events - Config: imap_server.enabled + imap_server.bind (default: 127.0.0.1:1143) - Externe Ports: 9993 (primär) und 993 (alternativ) via nginx TLS-Terminierung Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
113 lines
6.8 KiB
Markdown
113 lines
6.8 KiB
Markdown
# PROJ-26: IMAP-Server-Schnittstelle (Read-Only Archivzugriff)
|
|
|
|
## Status: In Progress
|
|
**Created:** 2026-03-18
|
|
**Last Updated:** 2026-03-18
|
|
|
|
## Dependencies
|
|
- Requires: PROJ-1 (Authentifizierung & Rollen) — Login via Benutzername/Passwort
|
|
- Requires: PROJ-5 (Speicherung & Indexierung) — E-Mails aus dem Archiv lesen
|
|
- Requires: PROJ-9 (Labels) — Labels als IMAP-Ordner abbilden
|
|
- Requires: PROJ-21 (Multi-Tenancy) — Nutzer sieht nur eigene Mails
|
|
|
|
## User Stories
|
|
- Als Nutzer möchte ich mein E-Mail-Archiv mit Thunderbird, Outlook oder Apple Mail öffnen können, damit ich ohne Webinterface auf archivierte Mails zugreifen kann.
|
|
- Als Nutzer möchte ich mich mit meinem normalen Benutzername und Passwort am IMAP-Server anmelden, damit ich keine separaten Zugangsdaten brauche.
|
|
- Als Nutzer möchte ich meine Labels als IMAP-Ordner sehen, damit ich archivierte Mails thematisch organisiert abrufen kann.
|
|
- Als Admin möchte ich, dass Nutzer das Archiv nur lesen können (kein Löschen, kein Verschieben), damit die GoBD-Konformität und Archivintegrität gewahrt bleibt.
|
|
- Als Nutzer möchte ich den IMAP-Zugang von außen über Port 993 (Standard IMAPS) oder alternativ Port 9993 (archivmail-spezifisch) nutzen können, damit ich auch unterwegs auf das Archiv zugreifen kann und Port-Konflikte mit einem ggf. vorhandenen regulären Mailserver vermieden werden.
|
|
|
|
## Acceptance Criteria
|
|
- [ ] Eingebetteter IMAP4rev1-Server läuft als Teil des Go-Backends (Port 1143 intern)
|
|
- [ ] Externer Zugriff via IMAPS Port **993** (Standard) oder alternativ Port **9993** (archivmail-spezifisch) — nginx terminiert TLS, leitet an internen Port 1143 weiter
|
|
- [ ] Beide Ports können gleichzeitig aktiv sein (parallele nginx-Listener)
|
|
- [ ] Authentifizierung mit Benutzername + Passwort (bcrypt-Vergleich, wie Webinterface)
|
|
- [ ] LDAP-Nutzer können sich ebenfalls per IMAP anmelden (LDAP-Auth-Pfad)
|
|
- [ ] Jeder Nutzer sieht ausschließlich seine eigenen archivierten E-Mails (Multi-Tenant-Isolation)
|
|
- [ ] Ordnerstruktur: `INBOX` (alle Mails) + Labels als Unterordner (z.B. `INBOX/Rechnungen`)
|
|
- [ ] Globale Labels (Admin-Labels) erscheinen ebenfalls als Ordner
|
|
- [ ] Vollständig Read-Only: keine DELETE, STORE \Deleted, COPY oder MOVE-Operationen erlaubt
|
|
- [ ] `\Seen`-Flag darf NICHT persistent gesetzt werden (Archivintegrität)
|
|
- [ ] IMAP-Kommandos implementiert: `LOGIN`, `LIST`, `SELECT`, `FETCH`, `SEARCH`, `LOGOUT`, `NOOP`, `CAPABILITY`
|
|
- [ ] `FETCH` liefert vollständige RFC-2822 E-Mail (Header + Body + Anhänge)
|
|
- [ ] `SEARCH` unterstützt mindestens: `ALL`, `FROM`, `TO`, `SUBJECT`, `SINCE`, `BEFORE`
|
|
- [ ] Verbindungen werden nach Inaktivität getrennt (Timeout 30 Min.)
|
|
- [ ] Maximale gleichzeitige Verbindungen pro Nutzer: 5
|
|
- [ ] Audit-Log-Eintrag bei jedem Login (Erfolg + Fehlschlag)
|
|
|
|
## Edge Cases
|
|
- Nutzer hat keine E-Mails → `INBOX` ist leer, `SELECT INBOX` antwortet mit 0 EXISTS
|
|
- Label wurde gelöscht aber IMAP-Client cached den Ordner → leerer Ordner, keine Fehlermeldung
|
|
- Falsches Passwort → nach 5 Fehlversuchen temporäre Sperre (30 Min.), Audit-Log-Eintrag
|
|
- Sehr großes Archiv (> 100.000 Mails) → `SELECT` liefert korrekte EXISTS-Zahl, `FETCH` paginiert
|
|
- IMAP-Client versucht APPEND (Mail hochladen) → `NO [CANNOT] Read-only archive`
|
|
- Gleichzeitige Verbindungen vom gleichen Client → erlaubt bis Limit (5), danach `BYE`
|
|
- LDAP-Nutzer dessen LDAP-Server nicht erreichbar ist → Login verweigert, Fehlermeldung im Audit-Log
|
|
- Nutzer wird während aktiver IMAP-Session gelöscht → Session wird beim nächsten Kommando beendet
|
|
- Port 993 bereits durch anderen Mailserver belegt → Betrieb ausschließlich auf Port 9993 möglich (nginx-Config anpassen)
|
|
|
|
## Technical Requirements
|
|
- **Protokoll:** IMAP4rev1 (RFC 3501)
|
|
- **Port intern:** 1143 (plaintext, nur localhost/LAN)
|
|
- **Port extern:** 993 (Standard IMAPS) **und/oder** 9993 (archivmail-spezifisch, vermeidet Konflikte mit existierenden Mailservern) — beide via nginx als TLS-Terminator
|
|
- **TLS:** Pflicht für externe Verbindungen — Zertifikat von `/etc/letsencrypt/` oder selbstsigniert
|
|
- **Authentifizierung:** LOGIN-Mechanismus (Benutzername/Passwort), PLAIN über TLS
|
|
- **Performance:** SELECT auf 10.000 Mails < 500ms, FETCH einer einzelnen Mail < 200ms
|
|
- **Bibliothek:** `github.com/emersion/go-imap` (v1 oder v2) — battle-tested IMAP-Server-Bibliothek für Go
|
|
- **Integration:** IMAP-Server startet als Goroutine im bestehenden Go-Backend (wie smtpd)
|
|
- **Konfiguration:** Neuer Abschnitt `imap_server:` in `/etc/archivmail/config.yml`
|
|
```yaml
|
|
imap_server:
|
|
enabled: true
|
|
bind: "0.0.0.0:1143"
|
|
tls: false # TLS-Terminierung durch nginx
|
|
# Externe Ports (nginx-Konfiguration):
|
|
# 993 → Standard IMAPS (ggf. mit vorhandenem Mailserver kollidierend)
|
|
# 9993 → archivmail-spezifisch (empfohlen wenn 993 bereits belegt)
|
|
```
|
|
- **Sicherheit:** Kein STARTTLS auf 1143 (nginx übernimmt TLS) — Rate-Limiting bei Login-Fehlern
|
|
|
|
---
|
|
## Implementation Notes
|
|
|
|
### What was built (2026-03-18):
|
|
|
|
**New package:** `internal/imapserver/server.go`
|
|
- Custom IMAP4rev1 protocol implementation over raw TCP (no dependency on unstable go-imap/v2 server API)
|
|
- Architecture mirrors `internal/smtpd/smtpd.go` (goroutine-based, background listener)
|
|
- Authentication via `userstore.VerifyPassword()` (bcrypt, bypasses TOTP for protocol access)
|
|
- Mailbox listing: INBOX (all tenant mails) + INBOX/LabelName per user label
|
|
- FETCH: loads full RFC-2822 via `storage.Store.Load()`, supports BODY[], ENVELOPE, RFC822.SIZE, INTERNALDATE, BODYSTRUCTURE
|
|
- SEARCH: ALL, FROM, TO, SUBJECT, SINCE, BEFORE criteria
|
|
- UID FETCH and UID SEARCH supported
|
|
- Read-only enforcement: STORE, DELETE, COPY, MOVE, APPEND, EXPUNGE, CREATE, RENAME all return `NO [CANNOT]`
|
|
- `\Seen` flag NOT persisted (always reported as set for client compatibility)
|
|
- Multi-tenant isolation via `storage.GetAllIDsByTenant()` filtered by `user.TenantID`
|
|
- Connection limit: 5 concurrent per user (atomic counter with acquire/release)
|
|
- Idle timeout: 30 minutes
|
|
- IDLE command support (waits for DONE)
|
|
- Audit log: `imap_login` on success, `imap_login_failed` on failure
|
|
|
|
**Config change:** `config/config.go`
|
|
- Added `IMAPServer IMAPServerConfig` to Config struct
|
|
- New `IMAPServerConfig` struct with `enabled` and `bind` fields
|
|
|
|
**Wiring:** `cmd/archivmail/main.go`
|
|
- IMAP server starts after label store init, before SMTP daemon
|
|
- Controlled by `imap_server.enabled` config flag
|
|
- Default bind: `127.0.0.1:1143`
|
|
|
|
### Design decisions:
|
|
- Used raw TCP IMAP implementation instead of go-imap/v2 `imapserver` package because the v2 library is still in beta.8 and the server-side API is unstable
|
|
- TOTP is bypassed for IMAP access (standard IMAP LOGIN does not support 2FA)
|
|
- UIDs equal sequence numbers for simplicity (UIDVALIDITY=1, stable within session)
|
|
|
|
## Tech Design (Solution Architect)
|
|
_To be added by /architecture_
|
|
|
|
## QA Test Results
|
|
_To be added by /qa_
|
|
|
|
## Deployment
|
|
_To be added by /deploy_
|