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
+36 -8
View File
@@ -3,12 +3,30 @@ package mailparser
import (
"bufio"
"bytes"
"strings"
"fmt"
)
var (
mboxSeparator = []byte("From ")
mboxHeaderTag = []byte("From: ")
mboxQuoted = []byte(">From ")
)
// SplitMbox splits a raw mbox file into individual RFC 2822 message bytes.
// Each message starts with a "From " separator line which is skipped.
//
// Scan errors (e.g. a single line larger than the 10 MB scanner limit) are
// ignored here for backwards compatibility — use SplitMboxErr to detect a
// truncated result instead of silently importing partial data.
func SplitMbox(data []byte) [][]byte {
messages, _ := SplitMboxErr(data)
return messages
}
// SplitMboxErr behaves like SplitMbox but also reports scanner failures.
// On error the already-collected messages are still returned so the caller can
// decide between aborting and importing a partial file.
func SplitMboxErr(data []byte) ([][]byte, error) {
var messages [][]byte
var current bytes.Buffer
@@ -17,11 +35,16 @@ func SplitMbox(data []byte) [][]byte {
inMessage := false
for scanner.Scan() {
line := scanner.Text()
// Bytes() avoids a string allocation per line — mbox files routinely
// have millions of lines.
line := scanner.Bytes()
// mbox separator: line starts with "From " but not "From:" header
if strings.HasPrefix(line, "From ") && !strings.HasPrefix(line, "From: ") {
if bytes.HasPrefix(line, mboxSeparator) && !bytes.HasPrefix(line, mboxHeaderTag) {
if inMessage && current.Len() > 0 {
messages = append(messages, bytes.TrimSpace(current.Bytes()))
// Copy: current.Bytes() aliases the buffer, which is reused
// after Reset() and would corrupt already-collected messages.
msg := bytes.TrimSpace(current.Bytes())
messages = append(messages, append([]byte(nil), msg...))
current.Reset()
}
inMessage = true
@@ -29,15 +52,20 @@ func SplitMbox(data []byte) [][]byte {
}
if inMessage {
// unescape ">From " lines (mbox quoting)
if strings.HasPrefix(line, ">From ") {
if bytes.HasPrefix(line, mboxQuoted) {
line = line[1:]
}
current.WriteString(line)
current.Write(line)
current.WriteByte('\n')
}
}
if inMessage && current.Len() > 0 {
messages = append(messages, bytes.TrimSpace(current.Bytes()))
msg := bytes.TrimSpace(current.Bytes())
messages = append(messages, append([]byte(nil), msg...))
}
return messages
if err := scanner.Err(); err != nil {
return messages, fmt.Errorf("mailparser: mbox scan aborted after %d messages: %w", len(messages), err)
}
return messages, nil
}