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:
co-authored by
Claude Sonnet 5
parent
0fa4fad49d
commit
798cb2817c
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"archivmail/internal/auth"
|
||||
imapstore "archivmail/internal/imap"
|
||||
"archivmail/internal/safego"
|
||||
"archivmail/internal/userstore"
|
||||
)
|
||||
|
||||
@@ -211,7 +212,9 @@ func (s *Server) handleStartImport(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
go s.imapImporter.Run(context.Background(), id)
|
||||
safego.Go(s.logger, "imap import", func() {
|
||||
s.imapImporter.Run(context.Background(), id)
|
||||
})
|
||||
|
||||
// Return current account state (status will switch to "running" shortly)
|
||||
acc.Status = "running"
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"archivmail/internal/audit"
|
||||
"archivmail/internal/mailer"
|
||||
"archivmail/internal/safego"
|
||||
"archivmail/internal/tokenstore"
|
||||
"archivmail/internal/userstore"
|
||||
)
|
||||
@@ -55,11 +56,12 @@ func (s *Server) handleCreateInvite(w http.ResponseWriter, r *http.Request) {
|
||||
tenantName = t.Name
|
||||
}
|
||||
}
|
||||
go func() {
|
||||
email := body.Email
|
||||
safego.Go(s.logger, "send invite mail", func() {
|
||||
html := mailer.InviteHTML(s.fqdn, token, tenantName)
|
||||
txt := mailer.InviteText(s.fqdn, token, tenantName)
|
||||
_ = s.mailer.Send(body.Email, "Einladung zu archivmail", html, txt)
|
||||
}()
|
||||
_ = s.mailer.Send(email, "Einladung zu archivmail", html, txt)
|
||||
})
|
||||
}
|
||||
|
||||
if s.audlog != nil {
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"archivmail/internal/audit"
|
||||
"archivmail/internal/mailer"
|
||||
"archivmail/internal/safego"
|
||||
"archivmail/internal/tokenstore"
|
||||
"archivmail/internal/userstore"
|
||||
)
|
||||
@@ -79,11 +80,12 @@ func (s *Server) handleSignup(w http.ResponseWriter, r *http.Request) {
|
||||
if err != nil {
|
||||
// SEC: token already consumed — response is identical to success to prevent
|
||||
// enumeration of whether the email address already existed.
|
||||
go func() {
|
||||
email := body.Email
|
||||
safego.Go(s.logger, "send already-registered mail", func() {
|
||||
html := mailer.AlreadyRegisteredHTML(s.fqdn)
|
||||
txt := mailer.AlreadyRegisteredText(s.fqdn)
|
||||
_ = s.mailer.Send(body.Email, "archivmail – Registrierungsversuch", html, txt)
|
||||
}()
|
||||
_ = s.mailer.Send(email, "archivmail – Registrierungsversuch", html, txt)
|
||||
})
|
||||
writeJSON(w, http.StatusOK, map[string]string{"message": signupMsg})
|
||||
return
|
||||
}
|
||||
@@ -100,11 +102,11 @@ func (s *Server) handleSignup(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Send verification email.
|
||||
go func() {
|
||||
safego.Go(s.logger, "send verify mail", func() {
|
||||
html := mailer.VerifyEmailHTML(s.fqdn, token, u.Username)
|
||||
txt := mailer.VerifyEmailText(s.fqdn, token, u.Username)
|
||||
_ = s.mailer.Send(u.Email, "archivmail – E-Mail bestätigen", html, txt)
|
||||
}()
|
||||
})
|
||||
|
||||
if s.audlog != nil {
|
||||
s.audlog.Log(audit.Entry{
|
||||
@@ -193,11 +195,11 @@ func (s *Server) handleForgotPassword(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
safego.Go(s.logger, "send password-reset mail", func() {
|
||||
html := mailer.ResetPasswordHTML(s.fqdn, token, u.Username)
|
||||
txt := mailer.ResetPasswordText(s.fqdn, token, u.Username)
|
||||
_ = s.mailer.Send(u.Email, "archivmail – Passwort zurücksetzen", html, txt)
|
||||
}()
|
||||
})
|
||||
|
||||
if s.audlog != nil {
|
||||
s.audlog.Log(audit.Entry{
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"archivmail/internal/auth"
|
||||
pop3store "archivmail/internal/pop3"
|
||||
"archivmail/internal/safego"
|
||||
"archivmail/internal/userstore"
|
||||
)
|
||||
|
||||
@@ -212,7 +213,9 @@ func (s *Server) handleStartPop3Import(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
go s.pop3Importer.Run(context.Background(), id)
|
||||
safego.Go(s.logger, "pop3 import", func() {
|
||||
s.pop3Importer.Run(context.Background(), id)
|
||||
})
|
||||
|
||||
// Return current account state (status will switch to "running" shortly)
|
||||
acc.Status = "running"
|
||||
|
||||
+15
-3
@@ -9,6 +9,7 @@ import (
|
||||
"sync"
|
||||
|
||||
"archivmail/internal/index"
|
||||
"archivmail/internal/safego"
|
||||
"archivmail/pkg/mailparser"
|
||||
)
|
||||
|
||||
@@ -94,7 +95,12 @@ func (s *Server) handleUpload(w http.ResponseWriter, r *http.Request) {
|
||||
var allMessages [][]byte
|
||||
for _, e := range entries {
|
||||
if e.isMbox {
|
||||
msgs := mailparser.SplitMbox(e.data)
|
||||
msgs, err := mailparser.SplitMboxErr(e.data)
|
||||
if err != nil {
|
||||
// Partial result: import what was parsed, but make the
|
||||
// truncation visible instead of losing mails silently.
|
||||
s.logger.Warn("upload: mbox scan incomplete", "messages", len(msgs), "err", err)
|
||||
}
|
||||
allMessages = append(allMessages, msgs...)
|
||||
} else {
|
||||
allMessages = append(allMessages, e.data)
|
||||
@@ -113,7 +119,9 @@ func (s *Server) handleUpload(w http.ResponseWriter, r *http.Request) {
|
||||
tenantID := tenantFromCtx(r.Context())
|
||||
|
||||
// Run import in background
|
||||
go s.runUploadJob(job, allMessages, tenantID)
|
||||
safego.Go(s.logger, "upload import job", func() {
|
||||
s.runUploadJob(job, allMessages, tenantID)
|
||||
})
|
||||
|
||||
writeJSON(w, http.StatusAccepted, map[string]string{"job_id": jobID})
|
||||
}
|
||||
@@ -126,7 +134,11 @@ func (s *Server) handleUploadProgress(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, http.StatusNotFound, "job not found")
|
||||
return
|
||||
}
|
||||
job := val.(*UploadJob)
|
||||
job, ok := val.(*UploadJob)
|
||||
if !ok || job == nil {
|
||||
writeError(w, http.StatusNotFound, "job not found")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, job.snapshot())
|
||||
}
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@ type APIKeyRow struct {
|
||||
|
||||
// tokenBucket implements a simple per-key token-bucket rate limiter.
|
||||
type tokenBucket struct {
|
||||
mu sync.Mutex // guards tokens/lastCheck — concurrent requests share one bucket
|
||||
tokens float64
|
||||
limit float64
|
||||
lastCheck time.Time
|
||||
@@ -128,7 +129,14 @@ func (m *APIKeyMiddleware) allow(keyID int64, limitPerMin int) bool {
|
||||
limit: limit,
|
||||
lastCheck: now,
|
||||
})
|
||||
bucket := val.(*tokenBucket)
|
||||
bucket, ok := val.(*tokenBucket)
|
||||
if !ok || bucket == nil {
|
||||
// Should never happen — fail closed rather than panic.
|
||||
return false
|
||||
}
|
||||
|
||||
bucket.mu.Lock()
|
||||
defer bucket.mu.Unlock()
|
||||
|
||||
elapsed := now.Sub(bucket.lastCheck).Seconds()
|
||||
bucket.lastCheck = now
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"time"
|
||||
|
||||
"archivmail/internal/audit"
|
||||
"archivmail/internal/safego"
|
||||
|
||||
imapv2 "github.com/emersion/go-imap/v2"
|
||||
"github.com/emersion/go-imap/v2/imapclient"
|
||||
@@ -107,7 +108,9 @@ func (s *Scheduler) TriggerSync(ctx context.Context, accountID int64) error {
|
||||
s.running[accountID] = true
|
||||
s.mu.Unlock()
|
||||
|
||||
go s.runSyncWithRetry(context.Background(), accountID)
|
||||
safego.Go(s.logger, "imap manual sync", func() {
|
||||
s.runSyncWithRetry(context.Background(), accountID)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -121,7 +124,10 @@ func (s *Scheduler) loop(ctx context.Context) {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
s.checkAccounts(ctx)
|
||||
// A panic in one tick must not stop the scheduler permanently.
|
||||
safego.Run(s.logger, "imap scheduler tick", func() {
|
||||
s.checkAccounts(ctx)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -166,7 +172,10 @@ func (s *Scheduler) checkAccounts(ctx context.Context) {
|
||||
|
||||
s.logger.Info("imap scheduler: starting scheduled sync",
|
||||
"account_id", acc.ID, "name", acc.Name)
|
||||
go s.runSyncWithRetry(context.Background(), acc.ID)
|
||||
accID := acc.ID
|
||||
safego.Go(s.logger, "imap scheduled sync", func() {
|
||||
s.runSyncWithRetry(context.Background(), accID)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
@@ -31,6 +32,9 @@ const (
|
||||
maxConnsPerUser = 5
|
||||
readBufferSize = 8192
|
||||
maxLineLength = 65536
|
||||
// maxFetchResults caps how many messages a single sequence set may expand
|
||||
// to, bounding per-request memory for "FETCH 1:*" on huge mailboxes.
|
||||
maxFetchResults = 50000
|
||||
)
|
||||
|
||||
// tenantIMAPModeGetter is satisfied by tenantstore.Store.
|
||||
@@ -199,6 +203,7 @@ func (s *Server) handleConnection(conn net.Conn) {
|
||||
defer conn.Close()
|
||||
|
||||
remoteAddr := conn.RemoteAddr().String()
|
||||
|
||||
s.logger.Debug("imapserver: new connection", "remote", remoteAddr)
|
||||
|
||||
sess := &session{
|
||||
@@ -209,6 +214,19 @@ func (s *Server) handleConnection(conn net.Conn) {
|
||||
state: stateNotAuth,
|
||||
}
|
||||
|
||||
// A panic while parsing a malformed/hostile client command must only kill
|
||||
// this connection, never the whole archivmail process. The per-user
|
||||
// connection counter is released here as well so it cannot leak.
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
if sess.username != "" {
|
||||
s.releaseConn(sess.username)
|
||||
}
|
||||
s.logger.Error("imapserver: recovered from panic in connection handler",
|
||||
"remote", remoteAddr, "panic", fmt.Sprintf("%v", r))
|
||||
}
|
||||
}()
|
||||
|
||||
// Send greeting — use FQDN if configured (RFC 3501 §7.1)
|
||||
fqdn := s.cfg.FQDN
|
||||
if fqdn == "" {
|
||||
@@ -282,12 +300,26 @@ type mailEntry struct {
|
||||
HasAttach bool
|
||||
}
|
||||
|
||||
// readLine reads a single CRLF-terminated command line. The length is capped at
|
||||
// maxLineLength: a client that never sends a newline would otherwise make the
|
||||
// buffer grow without bound until the process is killed by the OOM killer.
|
||||
func (sess *session) readLine() (string, error) {
|
||||
line, err := sess.reader.ReadString('\n')
|
||||
if err != nil {
|
||||
var buf []byte
|
||||
for {
|
||||
chunk, err := sess.reader.ReadSlice('\n')
|
||||
if len(buf)+len(chunk) > maxLineLength {
|
||||
return "", fmt.Errorf("imapserver: command line exceeds %d bytes", maxLineLength)
|
||||
}
|
||||
buf = append(buf, chunk...)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
if errors.Is(err, bufio.ErrBufferFull) {
|
||||
continue
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimRight(line, "\r\n"), nil
|
||||
return strings.TrimRight(string(buf), "\r\n"), nil
|
||||
}
|
||||
|
||||
func (sess *session) writeResponse(line string) {
|
||||
@@ -949,26 +981,29 @@ func parseFetchItems(items string) []string {
|
||||
items = strings.TrimSuffix(items, ")")
|
||||
|
||||
var result []string
|
||||
current := ""
|
||||
// strings.Builder instead of `current += string(ch)`: the latter reallocates
|
||||
// and copies on every rune, i.e. O(n²) for a command line that may be up to
|
||||
// maxLineLength bytes long.
|
||||
var current strings.Builder
|
||||
bracketDepth := 0
|
||||
for _, ch := range items {
|
||||
if ch == '[' {
|
||||
bracketDepth++
|
||||
current += string(ch)
|
||||
current.WriteRune(ch)
|
||||
} else if ch == ']' {
|
||||
bracketDepth--
|
||||
current += string(ch)
|
||||
current.WriteRune(ch)
|
||||
} else if ch == ' ' && bracketDepth == 0 {
|
||||
if current != "" {
|
||||
result = append(result, current)
|
||||
current = ""
|
||||
if current.Len() > 0 {
|
||||
result = append(result, current.String())
|
||||
current.Reset()
|
||||
}
|
||||
} else {
|
||||
current += string(ch)
|
||||
current.WriteRune(ch)
|
||||
}
|
||||
}
|
||||
if current != "" {
|
||||
result = append(result, current)
|
||||
if current.Len() > 0 {
|
||||
result = append(result, current.String())
|
||||
}
|
||||
|
||||
return result
|
||||
@@ -976,15 +1011,22 @@ func parseFetchItems(items string) []string {
|
||||
|
||||
// parseSequenceSet parses an IMAP sequence set (e.g. "1:*", "1,3:5", "1")
|
||||
// and returns the expanded list of numbers.
|
||||
//
|
||||
// The expansion is capped at maxFetchResults: "FETCH 1:*" on a mailbox with
|
||||
// millions of mails would otherwise allocate a huge slice plus dedup map per
|
||||
// request — several concurrent clients could exhaust the machine's memory.
|
||||
func parseSequenceSet(set string, maxVal uint32) []uint32 {
|
||||
if maxVal == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var result []uint32
|
||||
result := make([]uint32, 0, 64)
|
||||
seen := make(map[uint32]bool)
|
||||
|
||||
for _, part := range strings.Split(set, ",") {
|
||||
if len(result) >= maxFetchResults {
|
||||
break
|
||||
}
|
||||
part = strings.TrimSpace(part)
|
||||
if part == "" {
|
||||
continue
|
||||
@@ -998,6 +1040,9 @@ func parseSequenceSet(set string, maxVal uint32) []uint32 {
|
||||
start, end = end, start
|
||||
}
|
||||
for i := start; i <= end; i++ {
|
||||
if len(result) >= maxFetchResults {
|
||||
break
|
||||
}
|
||||
if !seen[i] {
|
||||
result = append(result, i)
|
||||
seen[i] = true
|
||||
@@ -1151,23 +1196,24 @@ func stripQuotes(s string) string {
|
||||
|
||||
func splitIMAPArgs(s string) []string {
|
||||
var parts []string
|
||||
current := ""
|
||||
// See parseFetchItems: strings.Builder avoids O(n²) rune-wise concatenation.
|
||||
var current strings.Builder
|
||||
inQuote := false
|
||||
for _, ch := range s {
|
||||
if ch == '"' {
|
||||
inQuote = !inQuote
|
||||
current += string(ch)
|
||||
current.WriteRune(ch)
|
||||
} else if ch == ' ' && !inQuote {
|
||||
if current != "" {
|
||||
parts = append(parts, current)
|
||||
current = ""
|
||||
if current.Len() > 0 {
|
||||
parts = append(parts, current.String())
|
||||
current.Reset()
|
||||
}
|
||||
} else {
|
||||
current += string(ch)
|
||||
current.WriteRune(ch)
|
||||
}
|
||||
}
|
||||
if current != "" {
|
||||
parts = append(parts, current)
|
||||
if current.Len() > 0 {
|
||||
parts = append(parts, current.String())
|
||||
}
|
||||
return parts
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package index
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"sync"
|
||||
)
|
||||
@@ -83,7 +84,20 @@ func (w *TenantIndexWorker) QueueLen() int {
|
||||
}
|
||||
|
||||
func (w *TenantIndexWorker) indexDoc(doc MailDocument) {
|
||||
// A panic while indexing a single document must not kill the long-running
|
||||
// worker goroutine (and with it the whole process).
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
w.logger.Error("tenant index worker: recovered from panic",
|
||||
"id", doc.ID, "tenant_id", doc.TenantID, "panic", fmt.Sprintf("%v", r))
|
||||
}
|
||||
}()
|
||||
|
||||
idx := w.mgr.ForTenant(doc.TenantID)
|
||||
if idx == nil {
|
||||
w.logger.Error("tenant index worker: no indexer for tenant", "id", doc.ID, "tenant_id", doc.TenantID)
|
||||
return
|
||||
}
|
||||
if err := idx.IndexSync(doc); err != nil {
|
||||
w.logger.Error("tenant index worker: index failed", "id", doc.ID, "tenant_id", doc.TenantID, "err", err)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package index
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"sync"
|
||||
)
|
||||
@@ -53,9 +54,7 @@ func (w *IndexWorker) Start() {
|
||||
// Channel closed, drain complete
|
||||
return
|
||||
}
|
||||
if err := w.idx.IndexSync(doc); err != nil {
|
||||
w.logger.Error("index worker: index failed", "id", doc.ID, "err", err)
|
||||
}
|
||||
w.indexDoc(doc, "")
|
||||
case <-w.done:
|
||||
// Drain remaining items in the queue before exiting
|
||||
for {
|
||||
@@ -64,9 +63,7 @@ func (w *IndexWorker) Start() {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if err := w.idx.IndexSync(doc); err != nil {
|
||||
w.logger.Error("index worker: index failed (drain)", "id", doc.ID, "err", err)
|
||||
}
|
||||
w.indexDoc(doc, " (drain)")
|
||||
default:
|
||||
return
|
||||
}
|
||||
@@ -76,6 +73,21 @@ func (w *IndexWorker) Start() {
|
||||
}()
|
||||
}
|
||||
|
||||
// indexDoc indexes a single document. A panic in the backend must not kill the
|
||||
// long-running worker goroutine (and with it the whole process).
|
||||
func (w *IndexWorker) indexDoc(doc MailDocument, phase string) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
w.logger.Error("index worker: recovered from panic"+phase,
|
||||
"id", doc.ID, "panic", fmt.Sprintf("%v", r))
|
||||
}
|
||||
}()
|
||||
|
||||
if err := w.idx.IndexSync(doc); err != nil {
|
||||
w.logger.Error("index worker: index failed"+phase, "id", doc.ID, "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Stop signals the worker to drain remaining items and stop. It blocks until
|
||||
// the worker goroutine has exited.
|
||||
func (w *IndexWorker) Stop() {
|
||||
|
||||
@@ -3,6 +3,7 @@ package ocr
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -199,6 +200,19 @@ func (w *Worker) run(ctx context.Context, id int) {
|
||||
}
|
||||
|
||||
func (w *Worker) process(ctx context.Context, job Job) {
|
||||
// A panic while processing a single mail (malformed attachment, external
|
||||
// tool output, index backend) must not kill the worker goroutine — the
|
||||
// whole process would go down with it.
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
w.logger.Error("ocr worker: recovered from panic",
|
||||
"mail_id", job.MailID, "panic", fmt.Sprintf("%v", r))
|
||||
if w.store != nil {
|
||||
_ = w.store.SetOCRResult(ctx, job.MailID, "failed", 0)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// PROJ-44: The canonical source of truth for a mail's tenant assignment
|
||||
// is emails.tenant_id in PostgreSQL — never the submitter's context.
|
||||
// Re-imports via IMAP/POP3 scheduler may submit the same mail with a
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
// Package safego starts background goroutines that cannot take the process
|
||||
// down. A panic inside a detached goroutine is fatal for the whole binary in
|
||||
// Go — there is no per-goroutine boundary like net/http provides for request
|
||||
// handlers. Every "fire and forget" goroutine in archivmail (mail sending,
|
||||
// importer runs, boot-resume jobs) therefore goes through Go/Run.
|
||||
package safego
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
// Run executes fn synchronously and recovers from a panic, logging it with the
|
||||
// given name. It returns true when fn completed without panicking.
|
||||
func Run(logger *slog.Logger, name string, fn func()) (ok bool) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
ok = false
|
||||
if logger != nil {
|
||||
logger.Error("recovered from panic in background task",
|
||||
"task", name, "panic", fmt.Sprintf("%v", r))
|
||||
}
|
||||
}
|
||||
}()
|
||||
fn()
|
||||
return true
|
||||
}
|
||||
|
||||
// Go runs fn in a new goroutine, recovering and logging any panic.
|
||||
func Go(logger *slog.Logger, name string, fn func()) {
|
||||
go Run(logger, name, fn) //nolint:errcheck // result is irrelevant for detached tasks
|
||||
}
|
||||
+17
-1
@@ -365,7 +365,23 @@ func (s *session) Rcpt(to string, _ *smtp.RcptOptions) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *session) Data(r io.Reader) error {
|
||||
func (s *session) Data(r io.Reader) (err error) {
|
||||
// go-smtp does not guard session handlers: a panic while processing a
|
||||
// malformed mail would take the whole archivmail process down. Convert it
|
||||
// into a temporary failure so the sending MTA retries.
|
||||
defer func() {
|
||||
if rec := recover(); rec != nil {
|
||||
s.daemon.stats.Rejected.Add(1)
|
||||
s.daemon.logger.Error("SMTP: recovered from panic while processing message",
|
||||
"from", s.from, "ip", s.remoteIP, "panic", fmt.Sprintf("%v", rec))
|
||||
err = &smtp.SMTPError{
|
||||
Code: 451,
|
||||
EnhancedCode: smtp.EnhancedCode{4, 3, 0},
|
||||
Message: "Temporary processing failure, please retry",
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
var buf bytes.Buffer
|
||||
if _, err := io.Copy(&buf, r); err != nil {
|
||||
s.daemon.stats.Rejected.Add(1)
|
||||
|
||||
Reference in New Issue
Block a user