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:
@@ -23,6 +23,7 @@ import (
|
|||||||
"github.com/archivmail/internal/audit"
|
"github.com/archivmail/internal/audit"
|
||||||
"github.com/archivmail/internal/auth"
|
"github.com/archivmail/internal/auth"
|
||||||
imapstore "github.com/archivmail/internal/imap"
|
imapstore "github.com/archivmail/internal/imap"
|
||||||
|
"github.com/archivmail/internal/imapserver"
|
||||||
"github.com/archivmail/internal/index"
|
"github.com/archivmail/internal/index"
|
||||||
"github.com/archivmail/internal/labelstore"
|
"github.com/archivmail/internal/labelstore"
|
||||||
ldapcfg "github.com/archivmail/internal/ldapconfig"
|
ldapcfg "github.com/archivmail/internal/ldapconfig"
|
||||||
@@ -201,6 +202,21 @@ func main() {
|
|||||||
defer labelSt.Close()
|
defer labelSt.Close()
|
||||||
srv.SetLabels(labelSt)
|
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
|
// Start SMTP daemon with index worker integration
|
||||||
if cfg.SMTP.Bind == "" {
|
if cfg.SMTP.Bind == "" {
|
||||||
cfg.SMTP.Bind = fmt.Sprintf(":%d", cfg.Server.SMTPPort)
|
cfg.SMTP.Bind = fmt.Sprintf(":%d", cfg.Server.SMTPPort)
|
||||||
|
|||||||
@@ -29,6 +29,13 @@ type Config struct {
|
|||||||
Index IndexConfig `yaml:"index"`
|
Index IndexConfig `yaml:"index"`
|
||||||
Audit AuditConfig `yaml:"audit"`
|
Audit AuditConfig `yaml:"audit"`
|
||||||
Logging LoggingConfig `yaml:"logging"`
|
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.
|
// ServerConfig holds port settings for the main services.
|
||||||
|
|||||||
+1
-1
@@ -39,7 +39,7 @@
|
|||||||
| PROJ-24 | TOTP Zwei-Faktor-Authentifizierung (2FA) | Deployed | [PROJ-24](PROJ-24-totp-zwei-faktor.md) | 2026-03-18 |
|
| 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-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 -->
|
<!-- Add features above this line -->
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# PROJ-26: IMAP-Server-Schnittstelle (Read-Only Archivzugriff)
|
# PROJ-26: IMAP-Server-Schnittstelle (Read-Only Archivzugriff)
|
||||||
|
|
||||||
## Status: Planned
|
## Status: In Progress
|
||||||
**Created:** 2026-03-18
|
**Created:** 2026-03-18
|
||||||
**Last Updated:** 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 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 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 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
|
## Acceptance Criteria
|
||||||
- [ ] Eingebetteter IMAP4rev1-Server läuft als Teil des Go-Backends (Port 1143 intern)
|
- [ ] 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)
|
- [ ] Authentifizierung mit Benutzername + Passwort (bcrypt-Vergleich, wie Webinterface)
|
||||||
- [ ] LDAP-Nutzer können sich ebenfalls per IMAP anmelden (LDAP-Auth-Pfad)
|
- [ ] 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)
|
- [ ] 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`
|
- 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
|
- 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
|
- 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
|
## Technical Requirements
|
||||||
- **Protokoll:** IMAP4rev1 (RFC 3501)
|
- **Protokoll:** IMAP4rev1 (RFC 3501)
|
||||||
- **Port intern:** 1143 (plaintext, nur localhost/LAN)
|
- **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
|
- **TLS:** Pflicht für externe Verbindungen — Zertifikat von `/etc/letsencrypt/` oder selbstsigniert
|
||||||
- **Authentifizierung:** LOGIN-Mechanismus (Benutzername/Passwort), PLAIN über TLS
|
- **Authentifizierung:** LOGIN-Mechanismus (Benutzername/Passwort), PLAIN über TLS
|
||||||
- **Performance:** SELECT auf 10.000 Mails < 500ms, FETCH einer einzelnen Mail < 200ms
|
- **Performance:** SELECT auf 10.000 Mails < 500ms, FETCH einer einzelnen Mail < 200ms
|
||||||
@@ -59,10 +61,47 @@
|
|||||||
enabled: true
|
enabled: true
|
||||||
bind: "0.0.0.0:1143"
|
bind: "0.0.0.0:1143"
|
||||||
tls: false # TLS-Terminierung durch nginx
|
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
|
- **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)
|
## Tech Design (Solution Architect)
|
||||||
_To be added by /architecture_
|
_To be added by /architecture_
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user