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>
This commit is contained in:
sysops
2026-03-18 11:42:35 +01:00
parent 5c25e3a7e7
commit 19a55a3166
5 changed files with 1324 additions and 13 deletions
+16
View File
@@ -23,6 +23,7 @@ import (
"github.com/archivmail/internal/audit"
"github.com/archivmail/internal/auth"
imapstore "github.com/archivmail/internal/imap"
"github.com/archivmail/internal/imapserver"
"github.com/archivmail/internal/index"
"github.com/archivmail/internal/labelstore"
ldapcfg "github.com/archivmail/internal/ldapconfig"
@@ -201,6 +202,21 @@ func main() {
defer labelSt.Close()
srv.SetLabels(labelSt)
// PROJ-26: IMAP Archive Server (read-only access for IMAP clients)
if cfg.IMAPServer.Enabled {
imapSrv := imapserver.New(cfg.IMAPServer, mailStore, users, labelSt, audlog, authMgr, logger)
if err := imapSrv.Start(); err != nil {
logger.Error("IMAP server failed to start", "err", err)
os.Exit(1)
}
defer imapSrv.Stop()
imapBind := cfg.IMAPServer.Bind
if imapBind == "" {
imapBind = "127.0.0.1:1143"
}
logger.Info("IMAP archive server started", "addr", imapBind)
}
// Start SMTP daemon with index worker integration
if cfg.SMTP.Bind == "" {
cfg.SMTP.Bind = fmt.Sprintf(":%d", cfg.Server.SMTPPort)
+7
View File
@@ -29,6 +29,13 @@ type Config struct {
Index IndexConfig `yaml:"index"`
Audit AuditConfig `yaml:"audit"`
Logging LoggingConfig `yaml:"logging"`
IMAPServer IMAPServerConfig `yaml:"imap_server"`
}
// IMAPServerConfig holds settings for the embedded read-only IMAP archive server.
type IMAPServerConfig struct {
Enabled bool `yaml:"enabled"`
Bind string `yaml:"bind"` // default: "127.0.0.1:1143"
}
// ServerConfig holds port settings for the main services.
+1 -1
View File
@@ -39,7 +39,7 @@
| PROJ-24 | TOTP Zwei-Faktor-Authentifizierung (2FA) | Deployed | [PROJ-24](PROJ-24-totp-zwei-faktor.md) | 2026-03-18 |
| PROJ-25 | User-Profil & Einstellungen | Deployed | [PROJ-25](PROJ-25-user-profil-einstellungen.md) | 2026-03-18 |
| PROJ-26 | IMAP-Server-Schnittstelle (Read-Only Archivzugriff) | Planned | [PROJ-26](PROJ-26-imap-server-schnittstelle.md) | 2026-03-18 |
| PROJ-26 | IMAP-Server-Schnittstelle (Read-Only Archivzugriff) | In Progress | [PROJ-26](PROJ-26-imap-server-schnittstelle.md) | 2026-03-18 |
<!-- Add features above this line -->
+43 -4
View File
@@ -1,6 +1,6 @@
# PROJ-26: IMAP-Server-Schnittstelle (Read-Only Archivzugriff)
## Status: Planned
## Status: In Progress
**Created:** 2026-03-18
**Last Updated:** 2026-03-18
@@ -15,11 +15,12 @@
- 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 (Port 993, SSL) nutzen können, damit ich auch unterwegs auf das Archiv zugreifen kann.
- 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 — nginx oder stunnel terminiert TLS, leitet an 1143 weiter
- [ ] 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)
@@ -43,11 +44,12 @@
- 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 (IMAPS via nginx/stunnel als TLS-Terminator)
- **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
@@ -59,10 +61,47 @@
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_
File diff suppressed because it is too large Load Diff