Files
archivmail/features/PROJ-26-imap-server-schnittstelle.md
T
sysops 19a55a3166 feat(PROJ-26): IMAP-Archive-Server Read-Only Zugriff auf archivierte Mails
- 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>
2026-03-18 11:42:35 +01:00

6.8 KiB

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
    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