fix(PROJ-86): Date-Fallback auf Received-Header-Kette wenn Date-Header fehlt
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
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
e1f39ce56f
commit
160b28099e
+87
-61
@@ -51,6 +51,69 @@ type ParsedMail struct {
|
|||||||
// fatal error for the whole process.
|
// fatal error for the whole process.
|
||||||
const maxMultipartDepth = 20
|
const maxMultipartDepth = 20
|
||||||
|
|
||||||
|
// parseDateFlexible tries the RFC 2822 date format plus the non-standard
|
||||||
|
// variants seen in real-world Date: and Received: header tails (missing
|
||||||
|
// leading zero, colon in timezone offset, no seconds, MTA timezone name,
|
||||||
|
// localised weekday prefix, trailing "(TZ)" comment). Returns ok=false if
|
||||||
|
// none of them match.
|
||||||
|
func parseDateFlexible(raw string) (time.Time, bool) {
|
||||||
|
raw = strings.TrimSpace(raw)
|
||||||
|
if raw == "" {
|
||||||
|
return time.Time{}, false
|
||||||
|
}
|
||||||
|
// Strip parenthesised timezone comment: "... +0100 (CET)" → "... +0100"
|
||||||
|
if idx := strings.LastIndex(raw, "("); idx > 0 {
|
||||||
|
raw = strings.TrimSpace(raw[:idx])
|
||||||
|
}
|
||||||
|
for _, layout := range []string{
|
||||||
|
"Mon, 2 Jan 2006 15:04:05 -0700",
|
||||||
|
"Mon, 02 Jan 2006 15:04:05 -0700",
|
||||||
|
"2 Jan 2006 15:04:05 -0700",
|
||||||
|
"02 Jan 2006 15:04:05 -0700",
|
||||||
|
"Mon, 2 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.RFC1123,
|
||||||
|
} {
|
||||||
|
if t, err := time.Parse(layout, raw); err == nil {
|
||||||
|
return t, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Some MTAs (e.g. PMG with German locale) use localised weekday names:
|
||||||
|
// "So, 24 Aug 2025 00:05:17 +0200" instead of "Sun, 24 Aug 2025...".
|
||||||
|
// Strip the "Weekday, " prefix (≤3 chars before the first comma) and retry.
|
||||||
|
commaIdx := strings.Index(raw, ",")
|
||||||
|
if commaIdx <= 0 || commaIdx > 3 {
|
||||||
|
return time.Time{}, false
|
||||||
|
}
|
||||||
|
noWeekday := strings.TrimSpace(raw[commaIdx+1:])
|
||||||
|
for _, layout := range []string{
|
||||||
|
"2 Jan 2006 15:04:05 -0700",
|
||||||
|
"02 Jan 2006 15:04:05 -0700",
|
||||||
|
"2 Jan 2006 15:04:05 -07:00",
|
||||||
|
"02 Jan 2006 15:04:05 -07:00",
|
||||||
|
"2 Jan 2006 15:04:05 MST",
|
||||||
|
"02 Jan 2006 15:04:05 MST",
|
||||||
|
"2 Jan 2006 15:04 -0700",
|
||||||
|
"02 Jan 2006 15:04 -0700",
|
||||||
|
} {
|
||||||
|
if t, err := time.Parse(layout, noWeekday); err == nil {
|
||||||
|
return t, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return time.Time{}, false
|
||||||
|
}
|
||||||
|
|
||||||
// Parse parses a raw RFC 2822 / MIME email and returns a ParsedMail.
|
// Parse parses a raw RFC 2822 / MIME email and returns a ParsedMail.
|
||||||
//
|
//
|
||||||
// Input is untrusted (IMAP/POP3/SMTP/upload). A panic inside the MIME/charset
|
// Input is untrusted (IMAP/POP3/SMTP/upload). A panic inside the MIME/charset
|
||||||
@@ -123,72 +186,35 @@ func Parse(raw []byte) (pmOut *ParsedMail, errOut error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Date — try go-message parser first, then fallback formats, then zero
|
// Date — try go-message parser first, then fallback formats, then Received:
|
||||||
|
// trace headers, then zero.
|
||||||
if d, err := msg.Header.Date(); err == nil {
|
if d, err := msg.Header.Date(); err == nil {
|
||||||
pm.Date = d
|
pm.Date = d
|
||||||
|
} else if t, ok := parseDateFlexible(msg.Header.Get("Date")); ok {
|
||||||
|
pm.Date = t
|
||||||
} else {
|
} else {
|
||||||
// Some MUAs emit non-standard variants (e.g. "+0100 (CET)" suffix).
|
// No usable Date: header at all (missing, or unparseable in every
|
||||||
// Try common RFC 2822 / non-standard formats before giving up.
|
// format we know) — some very old/legacy MTAs never set one. Fall
|
||||||
raw := strings.TrimSpace(msg.Header.Get("Date"))
|
// back to the Received: trace, which every relay hop adds a
|
||||||
// Strip parenthesised timezone comment: "... +0100 (CET)" → "... +0100"
|
// timestamp to. net/mail.Header["Received"] preserves header order
|
||||||
if idx := strings.LastIndex(raw, "("); idx > 0 {
|
// (topmost/latest hop first, origin-closest hop last) — iterate
|
||||||
raw = strings.TrimSpace(raw[:idx])
|
// back-to-front so we try the origin-closest hop first, which is
|
||||||
}
|
// our best proxy for when the mail actually originated. Far better
|
||||||
parsed := false
|
// than defaulting to "now"/import time for archived mail.
|
||||||
for _, layout := range []string{
|
|
||||||
"Mon, 2 Jan 2006 15:04:05 -0700",
|
|
||||||
"Mon, 02 Jan 2006 15:04:05 -0700",
|
|
||||||
"2 Jan 2006 15:04:05 -0700",
|
|
||||||
"02 Jan 2006 15:04:05 -0700",
|
|
||||||
"Mon, 2 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.RFC1123,
|
|
||||||
} {
|
|
||||||
if t, err := time.Parse(layout, raw); err == nil {
|
|
||||||
pm.Date = t
|
|
||||||
parsed = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !parsed {
|
|
||||||
// Some MTAs (e.g. PMG with German locale) use localised weekday names:
|
|
||||||
// "So, 24 Aug 2025 00:05:17 +0200" instead of "Sun, 24 Aug 2025...".
|
|
||||||
// Strip the "Weekday, " prefix (≤3 chars before the first comma) and retry.
|
|
||||||
if commaIdx := strings.Index(raw, ","); commaIdx > 0 && commaIdx <= 3 {
|
|
||||||
noWeekday := strings.TrimSpace(raw[commaIdx+1:])
|
|
||||||
for _, layout := range []string{
|
|
||||||
"2 Jan 2006 15:04:05 -0700",
|
|
||||||
"02 Jan 2006 15:04:05 -0700",
|
|
||||||
"2 Jan 2006 15:04:05 -07:00",
|
|
||||||
"02 Jan 2006 15:04:05 -07:00",
|
|
||||||
"2 Jan 2006 15:04:05 MST",
|
|
||||||
"02 Jan 2006 15:04:05 MST",
|
|
||||||
"2 Jan 2006 15:04 -0700",
|
|
||||||
"02 Jan 2006 15:04 -0700",
|
|
||||||
} {
|
|
||||||
if t, err := time.Parse(layout, noWeekday); err == nil {
|
|
||||||
pm.Date = t
|
|
||||||
parsed = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !parsed {
|
|
||||||
// Leave pm.Date as zero — storage will use DB DEFAULT NOW()
|
|
||||||
pm.Date = time.Time{}
|
pm.Date = time.Time{}
|
||||||
|
received := msg.Header["Received"]
|
||||||
|
for i := len(received) - 1; i >= 0; i-- {
|
||||||
|
semi := strings.LastIndex(received[i], ";")
|
||||||
|
if semi < 0 {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
if t, ok := parseDateFlexible(received[i][semi+1:]); ok {
|
||||||
|
pm.Date = t
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Leave pm.Date as zero if nothing parsed — storage will use DB
|
||||||
|
// DEFAULT NOW() / EffectiveDate's caller-supplied fallback.
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse body / MIME parts
|
// Parse body / MIME parts
|
||||||
|
|||||||
Reference in New Issue
Block a user