fix: Date-Parsing-Fallback für nicht-standard MTA-Datumsformate

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>
This commit is contained in:
sysops
2026-05-11 23:36:18 +02:00
parent 1f7e02dc53
commit a1c4e59fff
3 changed files with 39 additions and 1 deletions
+4 -1
View File
@@ -173,12 +173,13 @@ func (s *Server) handleSearch(w http.ResponseWriter, r *http.Request) {
} }
} }
// Batch-load thread info for all hits // Batch-load thread info and received_at fallback for all hits
hitIDs := make([]string, len(result.Hits)) hitIDs := make([]string, len(result.Hits))
for i, h := range result.Hits { for i, h := range result.Hits {
hitIDs[i] = h.ID hitIDs[i] = h.ID
} }
threadInfo, _ := s.store.GetThreadInfo(r.Context(), hitIDs) threadInfo, _ := s.store.GetThreadInfo(r.Context(), hitIDs)
receivedAts := s.store.GetReceivedAts(r.Context(), hitIDs)
enriched := make([]enrichedHit, 0, len(result.Hits)) enriched := make([]enrichedHit, 0, len(result.Hits))
for _, h := range result.Hits { for _, h := range result.Hits {
@@ -193,6 +194,8 @@ func (s *Server) handleSearch(w http.ResponseWriter, r *http.Request) {
eh.Subject = pm.Subject eh.Subject = pm.Subject
if !pm.Date.IsZero() { if !pm.Date.IsZero() {
eh.Date = pm.Date.UTC().Format(time.RFC3339) eh.Date = pm.Date.UTC().Format(time.RFC3339)
} else if t, ok := receivedAts[h.ID]; ok && !t.IsZero() {
eh.Date = t.UTC().Format(time.RFC3339)
} }
eh.HasAttachments = len(pm.Attachments) > 0 eh.HasAttachments = len(pm.Attachments) > 0
+25
View File
@@ -682,6 +682,31 @@ func (s *Store) Delete(id string) error {
return nil return nil
} }
// GetReceivedAts returns the received_at timestamp for each mail ID in the
// provided slice. Used as a date fallback when the email Date header cannot
// be parsed. Missing IDs are silently omitted from the result map.
func (s *Store) GetReceivedAts(ctx context.Context, ids []string) map[string]time.Time {
if s.db == nil || len(ids) == 0 {
return nil
}
// pgx supports $1 = []string via ANY
rows, err := s.db.Query(ctx,
`SELECT id, received_at FROM emails WHERE id = ANY($1)`, ids)
if err != nil {
return nil
}
defer rows.Close()
result := make(map[string]time.Time, len(ids))
for rows.Next() {
var id string
var t time.Time
if err := rows.Scan(&id, &t); err == nil {
result[id] = t
}
}
return result
}
// Purge deletes all mails whose retain_until has passed. // Purge deletes all mails whose retain_until has passed.
// Returns the number of successfully deleted mails. // Returns the number of successfully deleted mails.
// Mails that fail to delete (e.g. file missing) are skipped silently. // Mails that fail to delete (e.g. file missing) are skipped silently.
+10
View File
@@ -117,6 +117,16 @@ func Parse(raw []byte) (*ParsedMail, error) {
"02 Jan 2006 15:04:05 -0700", "02 Jan 2006 15:04:05 -0700",
"Mon, 2 Jan 2006 15:04:05 MST", "Mon, 2 Jan 2006 15:04:05 MST",
"Mon, 02 Jan 2006 15:04:05 MST", "Mon, 02 Jan 2006 15:04:05 MST",
// Colon in timezone offset (e.g. "+02:00") used by some MTA versions
"Mon, 2 Jan 2006 15:04:05 -07:00",
"Mon, 02 Jan 2006 15:04:05 -07:00",
"2 Jan 2006 15:04:05 -07:00",
"02 Jan 2006 15:04:05 -07:00",
// Without seconds
"Mon, 2 Jan 2006 15:04 -0700",
"Mon, 02 Jan 2006 15:04 -0700",
"2 Jan 2006 15:04 -0700",
// Go stdlib aliases
time.RFC1123Z, time.RFC1123Z,
time.RFC1123, time.RFC1123,
} { } {