storage.loadKey() startet bei fehlendem/unlesbarem/ungültigem Keyfile
weiterhin unverschlüsselt (kein Hard-Fail), aber:
- einmalige WARN-Logzeile beim Start mit konkretem Grund
- neuer Healthcheck-Prüfpunkt "Encryption" in archivmail status
- Dashboard-API liefert encryption.enabled
- README: GoBD-Hinweis zu storage.keyfile
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Gleiches Muster wie bei IMAP (730099d): domain_admin konnte POP3-Konten
fremder Tenants auflisten, löschen und Importe/Progress fremder Tenants
ansehen, da pop3_accounts keine tenant_id hatte und Store.List() für
Admins ungefiltert alle Konten lieferte.
- pop3_accounts: neue Spalte tenant_id (ALTER TABLE ADD COLUMN IF NOT EXISTS)
- Store.List() filtert nach tenant_id, außer für superadmin
- Store.Create() setzt tenant_id beim Anlegen
- delete/start-import/progress prüfen zusätzlich tenantAccessAllowed()
domain_admin sah und konnte IMAP-Konten (inkl. Credentials) fremder
Tenants auflisten, löschen, synchronisieren und umkonfigurieren, da
Store.List() für Admins ungefiltert alle Konten lieferte und die
Einzelhandler nur den Owner, nicht den Tenant prüften.
- Store.List() filtert jetzt nach tenant_id, außer für superadmin
- Store.Create() setzt tenant_id beim Anlegen
- Alle Einzelhandler (delete/start-import/progress/sync/update)
prüfen zusätzlich tenantAccessAllowed()
- Signup ohne Invite-Token gibt 400 zurück (war: optional)
- Use() statt Peek() vor User-Erstellung: verhindert TOCTOU bei parallelen
Requests mit demselben Token und Enumeration via "Token noch gültig?"
- invite_used Audit-Eintrag ergänzt
- Doppeltes IsConfigured()-Check entfernt
- Frontend: ohne ?invite= im URL wird Formular nicht gerendert
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Serves the static OpenAPI YAML via go:embed. Completes the last
open acceptance criterion for PROJ-13. PROJ-44 marked Deployed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- SEC: requireMailAccess auf GET /api/threads/{threadID} — superadmin/domain_admin konnten Mail-Metadaten lesen
- SEC: requireMailAccess auf POST /api/export/ediscovery — superadmin/domain_admin konnten bis zu 10k EML exportieren
- SEC: V1-API user-role Keys müssen 'contact=' angeben — verhindert vollständige Tenant-Enumeration
- SEC: Domain-Regex-Validierung in handleCertACME vor filepath.Join und certbot-Aufruf
- docs: README und config.test.yml auf Manticore Search aktualisiert (kein Xapian mehr)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
mailparser: weitere Layouts (Timezone +02:00 mit Doppelpunkt, ohne Sekunden)
storage: GetReceivedAts() für Batch-Lookup von received_at
search_handlers: received_at als Fallback wenn pm.Date.IsZero()
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Strukturbug auf 132 gefunden: Tenant-User (Rolle user) sahen ihren
OCR-Text nicht, obwohl ocr_chars>0 in PostgreSQL stand. Ursache:
- OCR-Worker hat in den per Job.TenantID gewaehlten Index geschrieben.
Beim Reprocess via CLI kam TenantID aus dem Submitter-Kontext und
konnte vom in emails.tenant_id gespeicherten Wert abweichen.
- /ocr-text-Endpoint hat fuer die Index-Auswahl session.TenantID
benutzt. Bei Admin/Auditor (nil Session-Tenant) wurde immer global
gelesen, auch wenn die Mail einem Tenant gehoert.
Fix: Beide Stellen lesen jetzt die TenantID **immer** aus
storage.GetTenantForMail(emails.tenant_id) und routen den
Manticore-Index entsprechend. ACL-Check im Endpoint bleibt
unveraendert auf session.TenantID == mail.tenant_id — die
Tenant-Isolation wird nicht aufgeweicht.
Edge cases:
- Mail mit tenant_id NULL: GetTenantForMail liefert nil -> globaler
Index (vorher und nachher gleich).
- DB-Fehler beim Lookup: faellt auf nil zurueck -> globaler Index,
liefert leeren Text fuer Tenant-Mails -> 404. Safe (keine
Querleckage zwischen Tenants).
Die enrichedHit-Struct in search_handlers.go fehlten die PROJ-44-Felder
Snippet und MatchField, sodass die vom Index berechneten Snippets in der
API-Response verworfen wurden.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
GET /api/mails/{id} liefert jetzt ocr_status + ocr_chars fuer das GUI-
Badge und den konditionalen Download-Button. Die externe v1-API
(/api/v1/mails/{id}) bekommt zusaetzlich ocr_status, damit CRM-
Integrationen entscheiden koennen, ob ein OCR-Text-Pull lohnt.
Hardcodierte 192.168.1.131 in settings/page.tsx ersetzt durch
dynamischen API-Call GET /api/system/info → {fqdn, imap_port}.
Fallback auf window.location.hostname wenn API nicht antwortet.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
sanitizeFilename war doppelt deklariert (server.go + ediscovery.go).
Funktion in ediscovery.go zu sanitizeExportFilename umbenannt.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
auditor-Rolle hat evtl. tenant_id gesetzt (historisch), soll aber
trotzdem immer den globalen Index durchsuchen und nur No-Tenant-Mails
sehen. tenant_id auf auditor-User per DB-Migration auf NULL gesetzt.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- auditor-Rolle sieht jetzt Mails wo tenant_id IS NULL und kein
email_refs-Eintrag existiert (statt nur eigene Mails)
- Neues storage.IsWithoutTenant() für effizienten Direktzugriff
- Neues storage.GetAllIDsWithoutTenant() für Suche + ZIP-Export
- Konsistente Prüfung in Search, GetMail, GetAttachment, GetRaw, Export
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- emailsFromHeader gibt bei Parse-Fehler nil zurück (fail-closed) statt raw-Header-String;
verhindert Authorization-Bypass via malformiertem From-Header
- mailBelongsToUser: strings.Contains-Fallback entfernt (war dead code nach dem fix-closed-Fix)
- handleSearch: domain_auditor ohne TenantID wird mit 403 abgewiesen, bevor der globale Index
abgefragt wird
- manticoreTableName: Regex-Validierung ^emails_(global|tenant_\d+)$ mit panic bei Abweichung
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- mailBelongsToUser: net/mail.ParseAddressList statt strings.Contains
verhindert False-Positives durch Display-Namen in Mail-Headern
- LDAP mail-Attribut: net/mail.ParseAddress-Validierung vor Übernahme,
Fallback auf username / username@ldap.local bei ungültiger Adresse
- handleSearch: Auditor-Rolle in userEmailFilter-Check eingeschlossen,
sodass Auditoren im Search-Pfad dieselbe Mail-Isolation erhalten wie User
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bei doppeltem Signup wird eine "bereits registriert"-Mail gesendet,
sodass jeder Signup-Versuch eine ausgehende E-Mail erzeugt.
Side-Channel-Angriff zur Account-Enumeration nicht mehr möglich.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Ersetzt nicht-existente getSession(r) durch sessionFromCtx(r.Context()) und
passt den audlog.Log-Aufruf auf die korrekte audit.Entry-Struct-Signatur an.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- version.go: AppVersion + Modules-Map (pro Modul interne Versionsnummer)
- GET /api/version: liefert App- und Modulversionen (ohne Auth)
- archivmail version: zeigt App- und Modulversionen in CLI
- version-Konstante aus cmd_import.go entfernt (war falsche Stelle)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- superadmin + domain_admin haben keinen Mail-Zugriff mehr (requireMailAccess)
- Neue Rolle domain_auditor: alle Tenant-Mails, kein Admin-Zugriff
- auditor + user: nur eigene Mails
- ZIP-Export: kein separates Attachment-Entpacken mehr, nur EML
- roleLevel() um domain_auditor (Level 3) erweitert
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Neuer Endpoint POST /api/admin/tenants/{id}/ldap/sync importiert alle
LDAP-User (source=ldap) per UpsertLDAPUser in die Tenant-Benutzerliste.
Im Nutzer-Dialog erscheint ein "LDAP-Benutzer synchronisieren"-Button
wenn LDAP für den Mandanten aktiv ist. Unterstützt Univention UCS
(mailPrimaryAddress, inetOrgPerson). Benutzertabelle zeigt jetzt auch
die Quelle (local/ldap).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
W-02: Cookie Secure-Flag ist nun über config.yml steuerbar.
api.secure_cookies: true/false — default false (kein Breaking Change).
Alle 3 SetCookie-Aufrufe (Login, Logout, TOTP) nutzen s.cfg.SecureCookies.
W-03: remoteIP() ist jetzt eine Methode und prüft api.trusted_proxies.
X-Forwarded-For wird nur ausgewertet wenn der direkte Peer in der
trusted_proxies-Liste steht (IP oder CIDR). Sonst wird r.RemoteAddr
verwendet — kein Spoofing mehr möglich.
Neue Hilfsfunktion: isTrustedProxy(ip, proxies).
config.go: APIConfig um SecureCookies bool + TrustedProxies []string erweitert.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- BUG-1 (P0): domain_admin kann keine Rollen > auditor in default_role/
group_mappings setzen — serverseitige Allowlist-Prüfung in
handleSaveTenantLDAP (user/auditor) und handleAdminSaveTenantLDAP
(user/auditor/domain_admin)
- WARN-1: Login-Fallback-Reihenfolge korrigiert — tenant_ldap wird
jetzt VOR globalem ldap_config geprüft (Spec: tenant > global > local)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>