fix: Crash-Robustheit + Performance in Backend und Frontend härten

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
This commit is contained in:
sysops
2026-08-05 13:39:00 +02:00
co-authored by Claude Sonnet 5
parent 0fa4fad49d
commit 798cb2817c
35 changed files with 502 additions and 112 deletions
+5 -1
View File
@@ -183,7 +183,11 @@ func runImport(args []string) {
var messages [][]byte
if fe.isMbox {
messages = mailparser.SplitMbox(raw)
var splitErr error
messages, splitErr = mailparser.SplitMboxErr(raw)
if splitErr != nil {
fmt.Fprintf(os.Stderr, "warning: %s: %v\n", fe.path, splitErr)
}
if len(messages) == 0 {
continue
}
+14 -7
View File
@@ -32,6 +32,7 @@ import (
"archivmail/internal/ocr"
pop3store "archivmail/internal/pop3"
"archivmail/internal/reconciliation"
"archivmail/internal/safego"
"archivmail/internal/smtpoutconfig"
"archivmail/internal/smtpd"
"archivmail/internal/storage"
@@ -246,7 +247,7 @@ func main() {
// queries only return genuinely outstanding jobs.
// PROJ-58: skipped in batch mode — the cron job drains the backlog instead.
if !cfg.OCR.BatchMode {
go func() {
safego.Go(logger, "ocr boot-resume", func() {
ctx := context.Background()
queueCap := 1000 // matches ocr.Options.QueueSize above
processed := 0
@@ -281,7 +282,7 @@ func main() {
logger.Info("ocr boot-resume: enqueued batch",
"batch", len(pending), "total_so_far", processed)
}
}()
})
}
// User store
@@ -519,11 +520,15 @@ func main() {
if cfg.OCR.BatchMode {
backfillOCR = nil
}
go runBackfill(context.Background(), mailStore, idx, tenantWorker, logger, backfillOCR)
safego.Go(logger, "backfill", func() {
runBackfill(context.Background(), mailStore, idx, tenantWorker, logger, backfillOCR)
})
}
// Background integrity verification — runs every 5 minutes
go runIntegrityCheck(context.Background(), mailStore, logger)
safego.Go(logger, "integrity check", func() {
runIntegrityCheck(context.Background(), mailStore, logger)
})
// Start HTTP API
go func() {
@@ -538,11 +543,13 @@ func main() {
// (which would otherwise drop SMTP/IMAP connections unnecessarily).
reload := make(chan os.Signal, 1)
signal.Notify(reload, syscall.SIGHUP)
go func() {
safego.Go(logger, "sighup reload", func() {
for range reload {
reloadOCRPauseWindow(*configPath, ocrWorker, logger)
safego.Run(logger, "ocr pause-window reload", func() {
reloadOCRPauseWindow(*configPath, ocrWorker, logger)
})
}
}()
})
// Graceful shutdown
quit := make(chan os.Signal, 1)