q.sysops.de (Konto patrick@sysops.de) beantwortet "UID SEARCH UID N:*" auf
Mailboxen mit weniger Nachrichten als N fehlerhaft und liefert die letzte
existierende UID statt eines leeren Ergebnisses zurück (verifiziert per
Debug-Client: "UID SEARCH UID 22:*" auf 21-Nachrichten-Mailbox -> [21]).
Ohne Filterung wurde dieselbe UID bei jedem 5-Minuten-Sync erneut als
"neu" behandelt und re-indexiert - dabei driftete auch date_ts bei
Mails ohne Date-Header ständig auf "jetzt". Server-Antwort wird jetzt
client-seitig re-gefiltert, nur UIDs >= angefragtem Minimum zählen als neu.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UPFC6Jk2ke1Pq9XcuVGP1R
Alte Bulk-importierte Archive (kein Date:-Header, nur Received:-Zeilen)
bekamen bislang das Import-Datum (received_at) als date_ts zugewiesen -
storage.Save() ignoriert den pm.Date-Parameter komplett und setzt
received_at immer auf NOW(). Jetzt wird bei fehlendem/unparsbarem
Date:-Header die Received:-Kette geparst (origin-nächster Hop zuerst,
via net/mail.Header["Received"] rückwärts iteriert) - liefert das
tatsächliche Mail-Datum statt des Import-Zeitpunkts.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UPFC6Jk2ke1Pq9XcuVGP1R
Ein IMAP-Konto blieb wegen sync_running=true seit 2026-07-05 (Prozess-Crash
vor dem defer in runSyncWithRetry) für den Scheduler auf ewig "beschäftigt" -
checkAccounts überspringt Konten mit SyncRunning=true. Neue Store-Methode
ClearStaleSyncRunning() wird jetzt bei jedem Scheduler.Start() aufgerufen und
setzt verwaiste Flags zurück, bevor der Scheduler-Loop startet.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UPFC6Jk2ke1Pq9XcuVGP1R
192.168.1.131 gehört seit 2026-09-01 nicht mehr zum archivmail-Projekt (User-Bestätigung).
Alle Referenzen in CLAUDE.md und Agent-Defs auf den verbleibenden Server 192.168.1.132
korrigiert (Test/Prod-Paar existiert nicht mehr).
Zusätzlich Erkenntnisse aus PROJ-86-Audit (Subagenten db-migrator + mailarchiv-architect)
in die Agent-Defs eingearbeitet: emails.tenant_id vs. email_refs Dual-Source-Falle,
MailDocument-Fan-out-Drift-Muster, emails_global-Mirror-Pflicht, --tenant-Reindex-Lücke.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UPFC6Jk2ke1Pq9XcuVGP1R
emails_global bekam nie Tenant-Mails gespiegelt, Superadmin-Suche sah nur
~30% aller Mails. Zusätzlich sanken Mails ohne gültigen Date-Header
(date_ts=0) beim datumssortierten Listing ans Ende aller Ergebnisse und
waren dadurch praktisch unauffindbar ("GUI zeigt neue Mails nicht") -
Import/Indexierung liefen technisch korrekt, nur Sortierung/Spiegelung
waren kaputt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UPFC6Jk2ke1Pq9XcuVGP1R
Getrennt von PROJ-84: Header (v.a. Subject) mit rohen 8-Bit-Bytes ohne
RFC-2047-Encoded-Word-Syntax wurden nicht auf tatsächliches Charset
geprüft, landeten als ungültiges UTF-8 in emails.subject und Manticore.
Gleiche Lücke bei decodeCharset() für den Body ohne verwertbaren
Content-Type.
RepairUTF8/RepairUTF8Bytes (charset_repair.go): bytegenaue Reparatur,
gültiges UTF-8 bleibt Identität, nur ungültige Byte-Sequenzen fallen
auf Windows-1252 zurück. Attachment.Data bewusst ausgenommen (bleibt
byte-exakt für Downloads). fix-subjects-Kommando erkennt jetzt beide
Fälle (HasEncodedWord || NeedsCharsetRepair).
Verifiziert auf 192.168.1.132: 54 zusätzliche Subject-Fälle, 219
Body-Fälle behoben (bodyInvalidUTF8 219 -> 0). --apply noch nicht
ausgeführt, Body-Korrektur braucht zusätzlich reindex.
Die ursprünglich gemeldete Amazon-Mail bleibt bewusst unverändert:
Encoding-Fehler kam bereits so vom Absender (=3F statt =DC im
Original-Encoded-Word), GoBD verbietet nachträgliche Korrektur
archivierter Originalinhalte.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WapWkrQusDuBMhaN8WyuXB
package.json-Version stand seit Projektstart unverändert auf 1.0.0.
Neues Schema in CLAUDE.md dokumentiert: Minor = aktive Features aus
features/INDEX.md / 10, abgerundet. 82 aktive Features -> 1.8.0.
Veralteten "nächste freie PROJ-44"-Hinweis auf dynamischen Verweis
auf INDEX.md korrigiert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WapWkrQusDuBMhaN8WyuXB
CLI-Subkommando "archivmail fix-subjects" korrigiert Bestandsmails mit
undekodiertem RFC-2047-Encoded-Word im Betreff (Folge des Fixes in
31d7113). Dry-Run per Default, echte Änderung nur mit --apply.
Original-EML im Storage bleibt unangetastet - nur Postgres emails.subject
wird korrigiert, danach Manticore-Reindex inkl. Erhalt von OCR-Text.
Jeder Lauf erzeugt einen Audit-Log-Eintrag (metadata_backfill).
Verifiziert auf 192.168.1.132: 543 Kandidaten, 541 reparierbar, 2 zu
Recht übersprungen (RFC-2047-Verletzung im Original). --apply noch
nicht ausgeführt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WapWkrQusDuBMhaN8WyuXB
decodeMIMEHeader() nutzte mime.WordDecoder ohne CharsetReader. Go kennt
dort nativ nur UTF-8/US-ASCII/ISO-8859-1, alles andere (z.B.
Windows-1252) ließ DecodeHeader() scheitern, Betreff blieb roh
("=?Windows-1252?Q?...?="). CharsetReader ergänzt, nutzt dieselbe
htmlindex-Logik wie decodeCharset() für den Body (vgl. PROJ-57).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WapWkrQusDuBMhaN8WyuXB
Seit dem BUG-81-3-Fix läuft PDF-Parsing über pdfjs-dist im App-Origin
statt in einer Browser-Sandbox. Sicherheitsniveau hängt damit an der
Code-Qualität der Dependency - Update-Pflicht bei Security-Releases
dokumentiert, nicht erst beim nächsten großen Dependency-Sweep.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WapWkrQusDuBMhaN8WyuXB
Chromium/Brave zeigte leeren PDF-Bereich (PDFium/MimeHandlerView
verweigert Rendering in sandbox="allow-scripts" ohne allow-same-origin,
Firefox war nicht betroffen). allow-same-origin nachzurüsten kam nicht
infrage: Blob-URLs erben den App-Origin, die Kombination hätte die
Sandbox faktisch aufgehoben (PROJ-61-Kontext).
Neu: PdfCanvasPreview.tsx rendert PDF via pdf.js selbst nach <canvas>,
kein iframe/eingebettetes Dokument mehr nötig. Parsing im Worker, kein
PDF-eigenes JavaScript, kein Text-/Annotationslayer im DOM. Seitenlimit
30, eigener Fehlerzweig für passwortgeschützte/beschädigte PDFs.
Neue Dependency pdfjs-dist@6.2.108 - Restrisiko: PDF-Parsing läuft
jetzt im App-Origin statt im Browser-Viewer-Prozess.
Noch nicht im Browser verifiziert, Status bleibt In Review.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WapWkrQusDuBMhaN8WyuXB
Nebenfund aus PROJ-81-QA: handleGetAttachment protokolliert Zugriffe
nicht im Audit-Log. Nicht blockierend für PROJ-81-Deploy — Vorschau
ändert das gespeicherte Original nicht, nur die Zugriffs-Nachvollzieh-
barkeit fehlt (GoBD/DSGVO Art. 30).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WapWkrQusDuBMhaN8WyuXB
- AC1-Text auf tatsächlichen Vorschau-Button-Ansatz umformuliert
- resolveMimeType(): Server-content_type nur als Hinweis bei fehlender
Dateiendung, kann vorhandene Endung nicht überstimmen; Rendering bleibt
strikt auf clientseitige MIME-Whitelist beschränkt (Security-Modell
unverändert, kein SVG/HTML)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WapWkrQusDuBMhaN8WyuXB
- PROJ-80: hartkodierte Farben in ModulesTab/TenantsTab/TenantLDAPTab durch
Theme-Tokens ersetzt, Mail-HTML-Container bewusst hell isoliert, Print
im Dark Mode auf lesbare (nicht farbidentische) Ausgabe umgestellt
- PROJ-81: neuer AttachmentPreviewDialog für PDF/Bild-Anhänge in /mail/[id],
MIME-Whitelist ohne SVG, sandboxed iframe ohne allow-same-origin,
Auto-Retry-Schleife bei Fehlern behoben
- PROJ-82 angelegt: Print-Farbparität als Folge-Ticket (BUG-80-1-Rest)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WapWkrQusDuBMhaN8WyuXB
Duplizierte "Keine Commits"-Eintraege der heutigen Session (Hook lief
mehrfach) durch einen konsolidierten Eintrag mit den echten PROJ-79-
Commits ersetzt.
TS 7.0.2 getestet und verworfen: typescript-eslint@8.66.0 wirft explizit
"does not support TS 7.0" (Peer-Range <6.1.0, siehe typescript-eslint#10940).
tsc/lint/build auf 6.0.3 gruen.
Alle 5 Teilschritte von PROJ-79 bearbeitet - lucide-react, tailwindcss v4
+ tailwind-merge v3 und @types/node abgeschlossen, eslint 10 und
typescript 7 an Oekosystem-Blockern (Plugin-Support fehlt) dokumentiert
zurueckgestellt. Status auf Deployed.
Offizielles @tailwindcss/upgrade-Tool durchgelaufen: tailwind.config.ts
entfernt, Theme nach globals.css als @theme-Block (CSS-first-Config),
PostCSS auf @tailwindcss/postcss umgestellt. tailwind-merge im selben
Schritt auf v3 (Pflicht-Kopplung an Tailwind v4).
Codemod-Bug korrigiert: variant="outline" (Prop-Wert) wurde faelschlich
als CSS-Klasse zu "outline-solid" umbenannt in 4 Call-Sites - zurueckgesetzt.
Neue Lint-Regel (no-restricted-syntax) gegen *-opacity-* Utilities, die
in v4 nicht mehr existieren und von tailwind-merge v3 lautlos verschluckt
wuerden statt einen Fehler zu werfen.
tsc/lint/build gruen. Visueller Regressionstest offen (kein Browser-Tool
in dieser Session) - folgt auf 132 vor 131-Deploy.
EXTERNAL_URL_RE verlangte zwei Forward-Slashes, aber der WHATWG-URL-Parser
akzeptiert für http(s) auch Backslash als Authority-Slash: "https:/\/evil.com"
und "https:\\evil.com" lösen im Browser identisch zu "https://evil.com" auf,
wurden vom PROJ-76-Fix aber nicht erkannt. Ein Tracking-Pixel mit dieser
Schreibweise hätte trotz aktivierter Blockierung geladen und dem Absender
eine Lesebestätigung samt IP verraten.
Gefunden durch Security-Review der heutigen Session-Commits.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019j28kGcaJAhBnrYX34hGdt
ProtectSystem=strict + ReadOnlyPaths=.../etc/ssl/archivmail blockierte den
Cert-Handler (internal/api/cert_handlers.go) beim Erstellen/Erneuern von
Zertifikaten mit "read-only file system" — Fehler kam vom systemd-Namespace,
nicht von einem echten ro-Mount oder Owner-Problem. SSL_DIR gehört in
ReadWritePaths statt ReadOnlyPaths, da die Anwendung dort aktiv schreibt.
Fix im Unit-Generator in update.sh, nicht nur manuell am Server, da die
Unit bei jedem Deploy neu geschrieben wird.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019j28kGcaJAhBnrYX34hGdt
npm run lint rief next lint auf, das es in Next.js 16 nicht mehr gibt —
seit dem Next-16-Upgrade lief effektiv gar kein Lint mehr. Umgestellt auf
eslint . mit Flat-Config (eslint.config.mjs statt .eslintrc.json). Der
dadurch wieder sichtbare Lint-Lauf zeigte 30 Findings (25 Fehler,
5 Warnungen), alle gefixt:
- 19x react-hooks/set-state-in-effect: Loading-States wo möglich als
echte Ableitung statt eigenem Effect-State (use-mobile.tsx komplett auf
useSyncExternalStore umgebaut), sonst async-Wrapper mit Cancel-Guard um
bestehende Loader — Timing/Ladeanzeige unverändert.
- react-hooks/refs (useSearch.ts): Ref-Schreibzugriff aus dem Render in
einen Effect verschoben.
- 4x no-html-link-for-pages: <a href> durch next/link ersetzt in
admin/login, forgot-password, signup.
- Rest (exhaustive-deps, no-img-element, unused disable) einzeln gefixt.
- 4 bewusst belassene disable-Kommentare mit Begründung (shadcn/ui-Datei,
QR-Code-data-URL, Full-Reload nach Auth laut Projektregel).
eslint-Major-Upgrade auf 10 selbst bleibt blockiert: eslint-plugin-react/
jsx-a11y/import unterstützen ESLint 10 in ihrer aktuellen Latest-Version
noch nicht (Crash beim Laden), siehe Feature-Spec PROJ-79.
Verifiziert auf 132 (Build-Sandbox, kein Live-Deploy): npm ci/tsc/lint/
build grün, 8 Kern-Routen per Standalone-Server auf HTTP 200 geprüft.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019j28kGcaJAhBnrYX34hGdt
lucide-react 0.562.0 auf 1.28.0 (kein Codeumbau nötig, alle genutzten
Icon-Namen kanonisch unverändert). npm audit fix behebt alle 6 gefundenen
Vulnerabilities ohne Breaking Change (next minor 16.2.9->16.3.0 im
bestehenden ^16.1.1-Range). caniuse-lite/Browserslist-DB aktualisiert
(war 8 Monate alt).
update.sh: npm audit --audit-level=high läuft jetzt als nicht-blockierendes
Warn-Gate nach npm ci. Bewusst kein Auto-Fix während des laufenden Deploys
— ein npm audit fix live auf dem Produktivsystem könnte unbemerkt Versionen
ändern, ohne vorherige Verifikation. Bei Fund wird gewarnt und auf lokales
audit fix + Test + Commit verwiesen.
Restliche PROJ-79-Schritte (eslint, @types/node, tailwindcss v4 +
tailwind-merge v3, typescript) bleiben offen, siehe Feature-Spec.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019j28kGcaJAhBnrYX34hGdt
Jeder Tastendruck im Suchfeld setzte bisher sofort State in SearchPage,
was über eine instabile doSearch-Referenz einen vollen Re-Render der
Ergebnistabelle auslöste (bei page_size 100: 100 Zeilen inkl.
sanitizeSnippet pro Tastendruck). Suchbegriff liegt jetzt lokal in der
Filterbar und wird debounced (300ms) nach oben propagiert; doSearch liest
Filterwerte aus einem Ref statt sie als Dependencies zu führen und bleibt
dadurch referenzstabil; SearchResultsTable ist jetzt memoisiert.
Enter-Verhalten bleibt sofort (Debounce wird vor dem Submit geflusht),
übrige Filter unverändert direkt-kontrolliert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019j28kGcaJAhBnrYX34hGdt
18 Admin-Tab-Komponenten wurden bisher alle statisch in src/app/admin/page.tsx
importiert, unabhängig davon welcher Tab tatsächlich geöffnet wird. Umstellung
auf next/dynamic mit gemeinsamem TabSkeleton-Loading-State reduziert das
initiale JS-Bundle der Admin-Route um 204,9 KB (-19,8%). Initial aktiver Tab
bleibt weiterhin serverseitig vorgerendert (ssr: true), kein Skeleton-Flash
beim ersten Laden.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019j28kGcaJAhBnrYX34hGdt
blockExternalSrcs() filterte bisher nur src= bei img/video/audio/source und
ließ Tracking-Pixel via <link href>, background=, CSS url() (Style-Block und
inline) sowie srcset durch — DSGVO-relevantes Read-Tracking trotz aktivierter
Blockierung. Zusätzlich deckt die neue Erkennung protokollrelative URLs
(//host/px.gif) ab, die die alte https?:-Prüfung durchließ.
data:/cid:-URIs bleiben unangetastet, <a href> weiterhin klickbar. CSP-Header
als robustere Ergänzung (Blocklist-Regex bleibt grundsätzlich umgehbar) folgt
separat mit dem firewall-security-Skill.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019j28kGcaJAhBnrYX34hGdt
Der index-pending-Cron (PROJ-58) scannte bei jedem Lauf die komplette
emails-Tabelle, weil indexed_at unindiziert war — skaliert linear mit
Archivgröße. Partial Index auf (received_at DESC) WHERE indexed_at IS NULL
verifiziert auf 192.168.1.132: Seq Scan (13.3ms, 52895 Zeilen) → Index Scan
(0.086ms).
Restlicher PROJ-75-Audit (pgxpool-Tuning, reconciliation N+1) bewusst
zurückgestellt, siehe Feature-Spec.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019j28kGcaJAhBnrYX34hGdt
archivmail-export nutzte noch die alte storage.New(path string)-Signatur
statt storage.Config (fehlender Keyfile/Compress hätte Rohbytes statt
Klartext-EML exportiert). Toten Self-Assignment-Code in storage.go entfernt.
Testdateien (storage, audit, api, userstore, auth) an aktuelle Signaturen
angeglichen; auth-Tests liefen bisher gegen einen SQLite-Pfad statt Postgres-
DSN und wurden auf das TEST_DATABASE_URL-Schema-Isolationsmuster der übrigen
Pakete umgestellt. api_test.go las den Login-Token noch aus dem JSON-Body
statt aus dem httpOnly-Cookie (Auth-Contract-Drift).
TestParseMissingDate an tatsächliches Verhalten angepasst: der Parser lässt
das Datum bewusst als Zero-Value, der time.Now()-Fallback sitzt in der
Storage-Schicht — damit bleibt nachvollziehbar ob ein Datum aus der Mail
stammt oder vom Archiv gesetzt wurde (GoBD).
Verifiziert auf 192.168.1.132: go build/vet/test ./... komplett grün,
kein Skip (Postgres + Manticore erreichbar).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019j28kGcaJAhBnrYX34hGdt
Upload-Job bleibt bei einem Panic im Verarbeitungspfad nicht mehr auf
"running" hängen, sondern zeigt "error" mit generischer Meldung (Panic-
Rohwert nur im Server-Log, ErrMsg geht ans Frontend und könnte sonst
Mail-Inhalt-Fragmente transportieren). Zusätzlich fail-closed nil-Guards
an allen 13 GetByUsername-Aufrufstellen in internal/api/, die das
Ergebnis bisher ungeprüft dereferenzierten.
Verifiziert auf 192.168.1.132: Build und go vet fehlerfrei für die
geänderten Dateien.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019j28kGcaJAhBnrYX34hGdt
Backend: recover() in allen langlebigen Goroutinen (neues internal/safego-
Paket), MIME-Multipart-Tiefenlimit gegen Stack-Overflow, IMAP-Zeilenlängen-
und FETCH-Result-Limits gegen OOM, MBOX-Buffer-Aliasing-Bug (Datenkorruption
beim Import), ungeprüfte Type Assertions abgesichert, Data Race im
API-Key-Rate-Limiter behoben, SMTP-Session-Panic führt jetzt zu 451-Retry
statt Prozessabsturz. Performance: O(n²)-String-Concat in Mailparser und
IMAP-Parser durch strings.Builder ersetzt.
Frontend: Error Boundaries für Root und Mail-Detailansicht ergänzt (gab es
vorher nicht), zahlreiche Guards gegen nil-Slices aus dem Backend-JSON die
sonst .map()/.length-Crashes/White-Screens auslösten, defekte JSON-Antworten
in api/core.ts abgefangen, zwei React-Key-Bugs bei löschbaren Listen
korrigiert.
Verifiziert auf 192.168.1.132: Build und Tests der geänderten Pakete
fehlerfrei, keine Regressionen gegenüber vorbestehendem Stand.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019j28kGcaJAhBnrYX34hGdt
- devops-deploy/mailarchiv-architect/db-migrator sagten teils "direkt auf 131 (Produktiv)"
deployen/migrieren, widersprüchlich zur Test-first-Konvention (132 zuerst validieren)
- sub-frist.md hatte kaputtes Frontmatter (description = kompletter Prompt-Body dupliziert
als Einzeiler) statt Kurzbeschreibung + Beispiele wie bei anderen Agenten — dadurch
vermutlich nicht als regulärer subagent_type registriert
- db-migrator/devops-deploy/sub-frist bisher nie getrackt (.gitignore blockt .claude/),
jetzt force-added wie mailarchiv-architect/manticore-admin
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016RmCVQZ9qtzfUtU6a7F4GR
Feature-Spec, INDEX.md und features.ts für PROJ-72 (Superadmin-Peer-Patch-Fix)
ergänzt inkl. QA-Ergebnisse. Zusätzlich: abgelehnte Privilege-Escalation-/
Tenant-Isolation-Versuche (403) werden jetzt als Success:false im Audit-Log
protokolliert (vorher nur erfolgreiche Updates) - Härtungspunkt aus QA-Runde.
Wichtig für PROJ-32-Duplikatserkennung. Bereits erfüllt durch 1:1-Weitergabe
der rohen RFC-2822-Bytes an AppendToMailbox() ohne Reparse.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Beide Features auf 132 und 131 deployed (2026-07-07). PROJ-69/PROJ-70
ohne vollständigen Browser-/E2E-Test, nur Smoke-Tests — offene QA-Punkte
in den Spec-Dateien dokumentiert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Datei war durch .git/info/exclude (features/) von "git add -u" ausgenommen,
INDEX-Eintrag existierte bereits, die eigentliche Spec-Datei fehlte im
Commit. Enthält jetzt auch die Implementation Notes zum Backend.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Neuer Opt-in-Endpunkt, mit dem User archivierte Mails per IMAP APPEND
zurück in ihr eigenes externes Postfach (INBOX) kopieren können. Das
Archiv selbst bleibt read-only (nur storage.Load(), kein Schreibzugriff
auf internal/imapserver).
- PATCH /api/auth/imap-restore: Opt-in-Flag umschalten, Aktivierung
erfordert Passwort-Reverifikation.
- POST /api/mails/{id}/restore: lädt Mail lesend, prüft Mail- und
Account-Ownership in restoreAccessAllowed() (PROJ-61-Muster), APPEND
via neuer internal/imap/append.go, kein Admin-Override.
- Audit-Log-Eintrag (EventRestore) pro Versuch, erfolgreich und
fehlgeschlagen.
- imap_restore_enabled-Spalte (default false) via idempotenter
initSchema-Migration.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Selbst-Registrierung ist nicht möglich, /signup nur über gültigen
Einladungslink erreichbar. Route bleibt für Invite-Flow bestehen,
nur der irreführende Link auf der Login-Seite fällt weg.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Manticore-Upgrade (PROJ-67) und sudo-Provisionierung (PROJ-68) auf 131
und 132 verifiziert abgeschlossen; QA/Deployment-Notizen ergänzt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
docs/BACKUP_RESTORE_RUNBOOK.md deckt beide Sicherungsebenen ab: App-eigenes
CLI-Backup (archivmail backup/restore, PROJ-66) und Infrastruktur-Ebene
(PBS + Sync auf Zweitserver, pct restore). Enthaelt Bitwarden-Keyfile-
Escrow-Anleitung, Pflicht-Verifikationsschritte (reconcile/reindex/
Stichprobe), Rotation-vs-DSGVO-Hinweis und den offenen Punkt "noch kein
Restore-Test durchgefuehrt" als groesste verbleibende Luecke.
AC 8 aus PROJ-66 damit erfuellt; AC 6 (Restore-Test) und AC 7 (Alerting)
bleiben offen.
0.9.1 stand seit Einführung des Versionsschemas fest, obwohl seitdem
PROJ-19 bis PROJ-68 dazugekommen sind (inkl. Breaking Change PROJ-46).
1.0.0 markiert diesen Stand als erstes stabiles Release nach eigenem
Schema (MAJOR: Breaking Changes / große Architekturänderungen).
Modul-Versionen für die zuletzt geänderten Pakete nachgezogen:
storage 1.9->1.10 (PROJ-65/66), api 1.8->1.9 (PROJ-67/68),
auth 1.3->2.0 (PROJ-46 Breaking Change), userstore 1.3->1.4 (PROJ-46).
QA-Re-Diagnose auf 132 fand einen zweiten, unabhängigen Blocker: die
archivmail.service-Unit setzt CapabilityBoundingSet=CAP_NET_BIND_SERVICE
CAP_NET_ADMIN. Das kappt das Capability-Set des GESAMTEN Prozessbaums
inklusive der von exec.Command("sudo", ...) geforkten Kindprozesse - sudo
braucht aber zusaetzlich CAP_SETUID/CAP_SETGID um auf uid/gid 0 zu
wechseln, sonst bricht es ab ("unable to change to root gid: Operation not
permitted"), selbst wenn sudoers korrekt konfiguriert ist.
Ein erster Verifikationsversuch per `runuser -u archivmail -- sudo ...`
hatte faelschlich Erfolg gemeldet, weil runuser den restriktiven
Bounding-Set der Unit nicht traegt - nur ein Test im echten
systemd-Unit-Kontext (systemd-run oder echter API-Call) ist aussagekraeftig.
Fix: CapabilityBoundingSet in update.sh und install.sh um CAP_SETUID
CAP_SETGID erweitert. AmbientCapabilities bleibt unveraendert (der Go-Daemon
selbst soll diese Rechte nie dauerhaft halten, nur der sudo-Kindprozess
braucht sie im Bounding-Set verfuegbar).
Root Cause für den HTTP-500-Bug beim Manticore-Neustart über den Dienste-Tab:
sudo war auf 132 gar nicht installiert, admin_services_handlers.go ruft aber
für JEDE Dienst-Aktion sudo systemctl auf. Betraf nicht nur Manticore,
sondern alle Dienste in der Whitelist - PROJ-67 hat es nur erstmals sichtbar
gemacht.
install.sh/update.sh installieren jetzt sudo (falls fehlend) und legen
/etc/sudoers.d/archivmail mit NOPASSWD-Regeln für genau die Service-
Whitelist an (archivmail, archivmail-web, manticore, postgresql@17-main,
postfix, nginx) - validiert per visudo -c vor dem Einspielen, damit ein
Bug im Generator nie eine kaputte sudoers-Datei live schaltet. update.sh
zieht das als Backfill für Bestandsinstallationen nach, analog zum
PROJ-67-Upgrade-Pfad-Muster.
archivmail-nft-Helper (externe Zugriffskontrolle) fehlt auf 132 ebenfalls,
aber bewusst nicht Teil dieses Tickets (vermutlich firewall-security-Skill-
Verantwortung).
features.ts hatte nur 18 von 67 PROJ-Einträgen mit größtenteils falschem
Status (z.B. PROJ-1 zeigte "In Review" statt "Deployed", PROJ-9 zeigte
"In Progress" statt "Removed") — komplett losgelöst von features/INDEX.md,
offenbar seit den frühen Tagen des Projekts nie mehr aktualisiert.
Vollständig mit INDEX.md abgeglichen (alle PROJ-1 bis PROJ-67 inkl. PROJ-56c),
FeatureStatus um "Removed" erweitert. Neues Feld "version" (manuell gepflegt
pro PROJ, Baseline 1.0) + Tabellenspalte im Modulübersicht-Tab.
Bislang war die einzige Möglichkeit, die laufende Manticore-/PostgreSQL-/
Postfix-/nginx-Version zu sehen, SSH + <binary> --version. serviceVersion()
löst das best-effort pro Dienst auf (archivmail/-web: appVersion-Konstante,
manticore: searchd --version, postgresql: psql --version, postfix: postconf
mail_version, nginx: nginx -v). Fehler werden verschluckt (leerer String),
eine unbekannte Version darf den Dienst-Status nicht auf "Fehler" kippen.
manticore war bisher gar nicht in der Dienste-Whitelist (allowedServices) —
jetzt ergänzt, damit es überhaupt in der Liste auftaucht und
start/stop/restart wie die anderen Dienste möglich ist.
Statt Version hart zu pinnen, wird bei jedem install.sh-/update.sh-Lauf die
Packages-Datei von repo.manticoresearch.com abgefragt und der letzte
"Package: manticore"-Block (Version+Filename) daraus extrahiert. Bleibt
dadurch automatisch aktuell, ohne den Code bei jeder neuen Manticore-Version
manuell anfassen zu müssen. _MANTICORE_VERSION_FALLBACK greift nur, wenn die
Packages-Datei nicht erreichbar/parsbar ist (Netzwerkfehler, Repo down).
Parser-Bug beim ersten Entwurf gefunden und gefixt: die awk-Regex
"/(^|\n)Package: manticore$/" matchte nie, weil $ in einem Multi-Zeilen-
Record das Ende des GESAMTEN Records verankert, nicht das einer Zeile.
Gefixt durch Split der ersten Zeile und direkten String-Vergleich. Gegen die
echte Packages-Datei getestet (liefert korrekt Version 27.1.5-...).
install.sh/update.sh übersprangen den Manticore-Block komplett, sobald
irgendeine Version schon lief — der Versions-Pin aus dem letzten Fix wirkte
dadurch nie auf existierende Installationen (nur Neuinstallationen). Jetzt
per dpkg --compare-versions: ist die installierte Version älter als
_MANTICORE_VERSION, wird der Dienst gestoppt, das neue Paket eingespielt und
neu gestartet. Downgrade-Schutz durch "lt"-Vergleich.
Spec PROJ-67 dokumentiert die Recherche zum Versionssprung 25.0.0 -> 27.1.5
(u.a. offene Frage zur neuen v27-Auth-Funktionalität) und den geplanten
Produktiv-Test auf 131.
Manticore hat das Debian-Paketschema geändert: früher mehrere
manticoresearch_*-Pakete unter pool/main/m/manticoresearch/, seit ~v25 ein
einziges "manticore"-Paket unter dists/bookworm/main/binary-ARCH/. Beide
Skripte waren zudem hart auf die veraltete Version 6.3.6 gepinnt (aktuell:
27.1.5). GitHub-Release-Fallback entfernt, da GitHub-Releases inzwischen
keine .deb-Assets mehr anhängen (leeres assets[], per API verifiziert) —
repo.manticoresearch.com ist der einzige funktionierende Weg.
Neue Download-URL + Paket-Metadaten (Name/Version/Architektur) gegen den
echten Server verifiziert, beide Skripte mit bash -n auf Syntaxfehler
geprüft. Betrifft nur Neuinstallationen — bestehende Installationen mit
bereits laufendem Manticore überspringen den Installationsblock unverändert.
BUG-1 (QA): Backticks in der restore-Hilfezeile schlossen das Raw-String-
Literal von printHelp() vorzeitig, Build brach komplett. Backticks durch
einfache Anführungszeichen ersetzt.
BUG-2 (QA): -force bei restore scheiterte an bereits vorhandenen
Hardlink-Zieldateien (os.Link: file exists) — für den Normalfall (Restore
gegen einen PROJ-65-Tenant-Hardlink-Bestand) war -force damit funktionslos.
copyTreePreservingHardlinks() entfernt jetzt eine vorhandene Zieldatei vor
os.Link.
install.sh gibt das frisch generierte Keyfile nach der Erzeugung einmalig
aus (beide Installationspfade), mit Aufforderung es sofort in einen
Passwort-Safe (Bitwarden) zu verschieben. Backup ist ohne dieses Keyfile
wertlos — der Hinweis darf nicht vom "mach ich später" verschluckt werden,
da der Wert danach nirgends mehr sichtbar abrufbar ist.
Sichert Store (Hardlinks erhalten, PROJ-65-tauglich ohne Abhängigkeit von
rsync -H), Keyfile, config.yml und PostgreSQL (pg_dump -Fc) konsistent in
ein Zielverzeichnis. Rotation läuft nur nach erfolgreichem Lauf, ein
fehlgeschlagener Backup rotiert nie ein gutes altes Backup weg.
restore ist bewusst konservativ: bricht bei nicht-leerem Store/Keyfile ohne
-force ab, stoppt/startet den Dienst nicht selbst, gibt am Ende die
Pflicht-Verifikationsschritte (reconcile, reindex, Stichprobe) aus.
Ergänzt die vorhandene PBS+Sync-Infrastruktur um einen App-eigenen,
selektiven Restore-Weg. Kein automatischer Cron-Eintrag aktiv (Zielpfad
noch offen), nur als Vorlage in deploy/cron.d/archivmail auskommentiert.
Kein lokaler go build möglich, QA folgt auf Testserver.
PROJ-66 (Planned): kein App- oder Cron-Backup für /var/archivmail/store,
Keyfile, PostgreSQL auf 131 und 132 vorhanden. Nutzer-Rückmeldung ergänzt:
Infra-Ebene sichert bereits per Proxmox Backup Server + Sync auf Zweitserver,
das entschärft den Off-Site-Blocker deutlich - verbleibende Kernlücke ist
ein nie getesteter Restore, nicht das fehlende Backup-Ziel. Bitwarden-
Keyfile-Escrow als zusätzlicher, von PBS unabhängiger Wiederherstellungsweg
ergänzt.
PROJ-65: Deploy-Agent hatte Status/QA-Ergebnisse bereits lokal geschrieben,
aber noch nicht committed.
Jeder Tenant bekommt ein eigenes Verzeichnis store/tenant_<id>/, das per
Hardlink auf die kanonische content-adressierte Datei zeigt — das bestehende
Cross-Tenant-Dedup-Modell (email_refs M:N, PROJ-32/37) bleibt dadurch
erhalten, kein Speicherplatz-Mehrverbrauch. Neues CLI-Subcommand
`archivmail migrate-tenant-dirs` zieht Bestandsdaten einmalig nach
(idempotent). Zusätzlich neuer Status-Check checkStoragePermissions
(warnt bei zu offenen store_path-Rechten, analog checkEncryption/PROJ-49).
DB-gestützte Zugriffskontrolle bleibt der maßgebliche Zugriffspfad im Code;
die Tenant-Ordner sind eine zusätzliche Defense-in-Depth-Ebene für manuelle
Dateisystem-Audits. Kein lokaler go build möglich, QA folgt auf Testserver.
Backend/Frontend erfolgreich auf 192.168.1.131 deployt (update.sh, code war
bereits committed). Deployment-Abschnitt mit Smoke-Test-Ergebnis ergänzt,
Breaking-Change-Hinweis (Tenant-User Login nur noch per E-Mail) ins DEVLOG.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Screenshots dürfen nicht auf Gitea landen, waren in af16138 versehentlich
mit committet. Aus Tracking entfernt (git rm --cached, Dateien bleiben
lokal erhalten) und in .gitignore aufgenommen.
Bewertet drei Optionen (Hardlink-Farm pro Tenant, Metadaten-Trennung
dokumentieren, Dedup nur noch pro Tenant) gegen das bestehende
Cross-Tenant-Dedup-Modell (email_refs M:N). Empfehlung: Option B
zuerst, Option A nur bei konkreter Kundenanforderung.
Baseline/Post-Vergleich, PROJ-18-Integritätscheck, Manticore-Konsistenz,
PROJ-52-Reconciliation und Byte-Vergleich als Ablauf nach jeder Schema-/
Index-/Tenant-Migration. Checklist-Punkt 10 auf Erfüllt gesetzt.
Checklist war seit 2026-06-13 nicht aktualisiert: PROJ-48/49/50/51/52 waren
laengst deployt, standen aber noch als fehlend/teilweise drin. PROJ-56c
(Cron-Purge mit Markierungspflicht) war im Code implementiert, hatte aber
nie eine eigene Feature-Spec — nachtraeglich dokumentiert.
Punkt 1 (Vollständigkeit) war noch als "Teilweise" markiert, obwohl PROJ-52
(Reconciliation-Report) das inzwischen abdeckt. Punkt 7 (Löschsperre)
beschrieb noch den alten Purge() ohne Markierungspflicht; ergänzt um
PROJ-56c Cron-Purge, der nur retain_until < NOW() UND marked_for_deletion
löscht.
Beide Features wurden auf 192.168.1.131 deployt (Commit 4c92587), inkl. der
zugehörigen QA-Bugfixes (Dry-Run-Adressmatching PROJ-43, days-Clamp PROJ-52).
Deployment-Abschnitte in den Feature-Specs und INDEX.md aktualisiert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
dryRunCondition() erwartete faelschlich die Winkelklammer-Form "Name <addr>",
waehrend mail_from/mail_to die Adresse bare speichern - Dry-Run zeigte dadurch
immer 0 Treffer fuer Adress-Regeln, obwohl der Live-Matcher (routeBareAddr)
korrekt matcht. QA-Ergebnisse (Bug-1) in die Feature-Spec uebernommen.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
strings.Contains(nftStr, "dport 443") fand den Substring nicht, wenn
nftables mehrere Ports als Set ausgibt ("tcp dport { 80, 443 } accept" statt
"tcp dport 443 accept") — das ist bei archivmail-Installationen der
Normalfall (80+443 stehen zusammen in einer Regel). Dashboard zeigte
dadurch fälschlich "Kein HTTPS — Verbindungen unverschlüsselt", obwohl
Port 443 korrekt offen war (bestätigt auf 131 und 132).
Neue Helper-Funktion nftHasPort() per Regex erkennt beide Formen
(Einzelport und Set). Betrifft die Checks für HTTPS/443, Port 3000 und
Port 8080.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Bisher schrieb nur install.sh die Unit-Dateien (einmalig beim Erst-Setup).
Server, die vor einer Unit-Änderung installiert wurden, blieben dauerhaft
auf altem Stand — konkret fehlte auf 192.168.1.132 CAP_NET_ADMIN (wurde in
install.sh ergänzt, aber nie auf bereits laufende Installationen
nachgezogen), wodurch die Security-Tab-Aktion "Firewall aktivieren"
(nft -f /etc/nftables.conf) mit "Operation not permitted" fehlschlug.
update.sh schreibt jetzt bei jedem Deploy beide Unit-Dateien neu (identisch
zum install.sh-Template) und lädt sie per daemon-reload nach, bevor die
Dienste gestartet werden.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Tenant-User (tenant_id IS NOT NULL) melden sich künftig per E-Mail an statt
per Username — behebt Verwechslungen wie im Support-Fall vom 2026-06-13
(Login schlug trotz Passwort-Reset fehl, weil E-Mail statt Username
verwendet wurde). Nicht-Tenant-User (Superadmin/System) können weiterhin
Username ODER E-Mail nutzen.
Neue Store.VerifyLogin() prüft erst per E-Mail (alle User), fällt dann auf
Username zurück (nur tenant_id IS NULL). VerifyPassword() bleibt für den
IMAP-Server-Login-Pfad (PROJ-26) unverändert. Bewusster Breaking Change für
Tenant-User, Datenqualität vorab geprüft (0 Kollisionen).
Security-Nachtrag: bcrypt-Dummy-Compare im "user not found"-Pfad ergänzt,
um Timing-basierte Identifier-Enumeration zu verhindern.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Backend liefert bei GET /api/admin/dsgvo ein Objekt {"requests":[...]},
Frontend erwartete rohes Array → .map() auf Nicht-Array warf Exception,
angezeigt als "DSGVO-Anträge konnten nicht geladen werden".
Zusätzlich stimmten DSGVOResultSummary/DSGVOMailResult-Feldnamen nicht mit
dem Go-JSON überein (total vs. total_hits; id/status/received_at existieren
im Backend nicht, stattdessen mail_id/deletable/deleted/date). Status wird
jetzt im Frontend aus deletable/deleted abgeleitet (mailStatus()).
Bug bestand seit Feature-Implementierung (2026-06-13), fiel erst jetzt beim
ersten Live-Test auf.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Läuft nachts um 04:10 Uhr, nach dem Purge-Job und außerhalb der
OCR-Pause. Ohne diesen Eintrag existierte der neue Subcommand
"archivmail reconcile" nur manuell aufrufbar, ohne produktiven Betrieb.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Security-Audit-Nachtrag (siehe PROJ-64):
- internal/api/v1_handlers.go: handleV1SearchMails fehlte der fail-closed
Tenant-Post-Filter-Fallback für den Fall idxMgr==nil (gleiches Muster wie
bereits in search_handlers.go). Aktuell nicht ausnutzbar, da idxMgr in
main.go immer gesetzt wird, aber strukturelle Absicherung gegen künftige
Regressionen (analog PROJ-55 BUG-1).
- internal/smtpoutconfig/store.go: Verschlüsselungsschlüssel wird jetzt aus
dem HKDF-abgeleiteten aesKey gebildet statt aus dem rohen cfg.API.Secret,
konsistent mit internal/ldapconfig und internal/imap/store.go (SEC-08).
Verifiziert: smtp_out_config auf Produktiv (131) war leer, kein
Breaking Change für bestehend gespeicherte Zugangsdaten.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Täglicher Cron-Job (archivmail reconcile) berechnet pro Tenant/Quelle
(SMTP-Journal, IMAP-Konto, POP3-Konto, Datei-Import) archivierte Mail-Zahlen,
für IMAP zusätzlich einen Soll/Ist-Vergleich via UID-Tracking. Abweichungen
über Schwellenwert erzeugen Audit-Log-Warnung. Neue Admin-Dashboard-Kachel
"Vollständigkeits-Check" (letzte 7 Tage, Warn-Badge, CSV-Export).
Schließt die "teilweise erfüllt"-Lücke bei Vollständigkeit im
GoBD/DSGVO-Compliance-Check (VOI-Grundsatz 2).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Security-Audit deckte zwei Medium-Findings auf: JWTs blieben bis zu 8h nach
Passwort-Change/-Reset oder Admin-TOTP-Reset gültig (kein Session-Invalidation),
und archivierte Mails/Anhänge wurden mit 0644/0755 statt 0600/0700 geschrieben.
- users.tokens_valid_after (neue Spalte) wird bei SetPassword() und
InvalidateTokensBefore() gesetzt; ValidateToken() lehnt JWTs mit iat davor ab.
- Admin-TOTP-Reset revoked jetzt aktive Sessions des Zielnutzers.
- Mail-/Attachment-Dateien und ihre Verzeichnisse nur noch für den
archivmail-Service-Account lesbar.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Bei NextPart()-Fehler (EOF, malformed header) wird der bereits geparste Inhalt
zurückgegeben statt der gesamte Parse abgebrochen. parseMultipart() gibt keinen
Fehler mehr zurück — partieller Text/HTML ist besser als gar kein Index-Eintrag.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- OS-Check: bricht ab wenn nicht Debian 13 erkannt
- Go: apt-golang-go entfernt, Go 1.24.4 von dl.google.com (arm64/amd64)
- Manticore: bookworm-Paket hardcodiert (kein natives trixie-Paket), in
install.sh und update.sh konsistent
- Native config: batch_mode: true für index + ocr (wie PROJ-58 vorgesehen)
- systemd: NEXT_PUBLIC_API_URL entfernt (Next.js baked bei Build, nicht Runtime)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Neuer Tab im Admin-Bereich: Antrags-Liste, Formular (Adresse + Zeitraum),
Detail-Dialog mit Mail-Tabelle, PDF-Export und Löschen mit Bestätigung.
Rollen-Gating: admin/auditor sehen den Tab, nur admin kann anlegen/löschen.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bug-1: GetDSGVOMailMeta mit tenant_id-Filter (Defense-in-Depth an DB-Schicht).
Bug-2: Verwaiste open-Einträge bei Auswertungsfehler werden auf failed markiert.
Bug-3: Fehlgeschlagene Suchen werden im Audit-Log protokolliert.
Bug-6: Ungültige date_from/date_to-Eingaben liefern HTTP 400.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
tenantAccessAllowed()-Check in allen {id}-Handlern von tenant_handlers.go,
tenant_domain_handlers.go und tenant_logo_handlers.go ergänzt — No-op für
globale Admins, zweite Verteidigungslinie für hypothetische tenant-gebundene
Admin-Sessions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Dockerfile und Makefile bauten noch mit CGO_ENABLED=1 -tags xapian und
installierten libxapian-dev/libxapian30, obwohl der Xapian-Code bereits
entfernt wurde. PRD.md und api-v1.md erwähnten Xapian noch im Tech-Stack
bzw. als Such-Syntax-Referenz.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
handleDeletePop3 und handleStartPop3Import prüften nur Owner/Rollen-Level,
nicht den Tenant-Scope (anders als das korrekte IMAP-Pendant). Ein
domain_admin konnte dadurch POP3-Konten eines fremden Tenants löschen
oder deren Import anstoßen. Fix: tenantAccessAllowed(sess, acc.TenantID)
ergänzt, analog zum IMAP-Handler. Gefunden bei gezielter Nachsuche nach
Geschwister-Bugs zu PROJ-61.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
GET /api/tenants/{id}/logo prüfte nur die Authentifizierung, aber keinen
Tenant-Scope — jeder eingeloggte Nutzer konnte das Logo jedes beliebigen
Tenants lesen. Kombiniert mit dem bisher erlaubten SVG-Upload (kann
eingebettetes JavaScript enthalten) ergab das einen Cross-Tenant Stored-XSS:
ein domain_admin konnte ein bösartiges SVG als eigenes Logo hochladen und
Opfer aus beliebigen anderen Tenants per direktem Link darauf locken.
Fix: tenantAccessAllowed()-Scope-Check beim Logo-Lesepfad (analog PROJ-55),
SVG aus erlaubten Upload-Typen entfernt, X-Content-Type-Options: nosniff
als Defense-in-Depth ergänzt.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Xapian war seit der Manticore-Migration (PROJ-30) nur noch ein
build-tag-gatetes, in Produktion nie genutztes Legacy-Backend.
internal/index/xapian.go, xapian_stub.go, xapian_wrapper.cpp/.h entfernt;
index.New() unterstützt jetzt nur noch "manticore". Default-Fallbacks in
Nebenwerkzeugen (archivmail-import/-export, cmd_export/cmd_reindex/...)
von "xapian" auf "manticore" korrigiert, Xapian-spezifische Tests entfernt,
Kommentare bereinigt.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
xapian_path/backend: xapian waren toter Datenmüll (StorageConfig hat kein
xapian_path-Feld, wird beim YAML-Unmarshal stillschweigend ignoriert).
Auf den tatsächlich genutzten Manticore-Default umgestellt
(backend: manticore, manticore_dsn). Behebt das in PROJ-56 QA als
INFO 3 dokumentierte, außerhalb des Scopes liegende Finding.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Root Cause für ausbleibende Lastsenkung: update.sh hat /etc/cron.d/archivmail
nie auf den Server kopiert, daher fehlten die PROJ-58-Cronzeilen trotz
aktivem batch_mode komplett. Jetzt kopiert update.sh die Cron-Datei und alle
Wrapper-Skripte bei jedem Deploy automatisch ein.
Zusätzlich: Wrapper-Skripte (analog mailpiler indexer.delta.sh) verhindern
per Lockfile, dass sich Cron-Läufe bei großem Backlog überlappen.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
config.docker.yml.example setzt index.batch_mode/ocr.batch_mode jetzt
auf true statt es nur kommentiert zu zeigen, damit neue Installationen
direkt mit Cron-Batch-Verarbeitung statt Dauerbetrieb starten. Bestehende
Installationen (131/132) sind unberührt, da ihre config.yml unabhängig
vom Repo-Beispiel ist.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Neues config.yml-Feld batch_mode (index/ocr, Default false = unverändertes
Verhalten). Bei batch_mode:true verarbeiten neue Cron-Jobs (index-pending,
ocr-reprocess) die Backlogs in größeren Abständen statt sofort bei jedem
Mail-Import, um Schreiblast auf der Festplatte zu glätten. Zeiten in
/etc/cron.d/archivmail frei anpassbar.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Der Mail-Parser ignorierte das charset-Parameter aus Content-Type und
interpretierte Bytes immer als UTF-8, wodurch iso-8859-1/windows-1252
kodierte Mails (z.B. mit Umlauten) als Mojibake gespeichert wurden.
Zusätzlich fehlte das Charset für die Manticore-MySQL-Verbindung und
der charset-Parameter im JSON-Response-Header.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
archivmail import --dir/--file ordnete importierte Mails bisher immer
ohne Mandant zu (Save(..., nil) fest verdrahtet). Neuer --tenant <id> Flag
übergibt die Tenant-ID an Store.Save() und an idxMgr.ForTenant(), sodass
Mail sowohl in der DB als auch im richtigen Tenant-Suchindex landet —
und automatisch die Retention-Regeln des Tenants greifen (applyRetention
nutzt bereits die übergebene tenantID). Relevant für den geplanten
Watch-Folder-Import pro Mandant.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Der automatische Purge-Cron darf nicht allein anhand des abgelaufenen
retain_until löschen — eine Mail muss zusätzlich von einem Admin im UI
zur Löschung markiert worden sein. Dafür: neue Spalten
marked_for_deletion(_by/_at) auf emails, Store.ListExpiredMarkedMailIDs()
(retain_until abgelaufen UND markiert) für den Cron-Pfad, und
Store.SetMarkedForDeletion() zum Setzen/Löschen der Markierung.
Neue Endpoints (domain_admin+, tenant-scoped):
- GET /api/admin/retention/expired Metadaten abgelaufener Mails
(kein Body-Zugriff, SEC-29)
- PUT /api/admin/mails/{id}/mark-deletion Markierung setzen/entfernen,
mit Audit-Log-Eintrag
RetentionTab.tsx zeigt abgelaufene Mails mit Checkbox zum Markieren.
Der bestehende manuelle "Jetzt löschen"-Button (/api/admin/purge) bleibt
unverändert und löscht weiterhin alle abgelaufenen Mails auf einen Klick —
nur der unbeaufsichtigte Cron-Job ist jetzt auf markierte Mails beschränkt.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
archivmail purge ist ein neuer CLI-Befehl, der Mails mit abgelaufener
retain_until löscht, aus dem Suchindex entfernt und pro Mail einen
Audit-Eintrag (mail_purged) schreibt — analog zu Pilers purge.sh, nachts
03:40 Uhr über deploy/cron.d/archivmail. Nur Mails mit explizit gesetztem
und abgelaufenem retain_until werden angefasst; ohne retain_until bleibt
alles unberührt, die Löschsperre (PROJ-34) greift weiterhin.
Beim Testen aufgedeckt: Store.Delete() entfernte die Datei vor dem
DB-Delete und verschluckte den Fehler, wenn email_refs/email_attachments
per Fremdschlüssel die Löschung blockierten — Ergebnis war ein DB-Eintrag
ohne zugehörige Datei. Jetzt läuft die DB-Löschung (inkl. abhängiger
Zeilen) zuerst in einer Transaktion, die Datei wird erst nach
erfolgreichem Commit entfernt.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>