diff --git a/internal/mime/parse.go b/internal/mime/parse.go index f7b9d562e..afdcdc6e4 100644 --- a/internal/mime/parse.go +++ b/internal/mime/parse.go @@ -220,6 +220,7 @@ var dateFormats = []string{ "2006-01-02T15:04:05-07:00", // ISO 8601 with offset "2006-01-02 15:04:05 -0700", // SQL-like format "2006-01-02 15:04:05", // SQL-like without TZ + "Mon, Jan 2 2006 15:04:05 -0700", // Weekday, US month-day order, no comma after day } // numericOffsetRe matches numeric timezone offsets like +0000, -0700, +00:00, -07:00. @@ -257,6 +258,12 @@ func parseDate(s string) time.Time { // Normalize whitespace efficiently: split on whitespace runs and rejoin s = strings.Join(strings.Fields(s), " ") + // Some mbox-derived sources have a stray, unindented continuation line + // (a lone ".") directly after the Date header. enmime folds it onto the + // header value instead of treating it as a parse error, leaving a + // trailing " ." that no real Date header would ever contain. + s = strings.TrimSuffix(s, " .") + // Strip trailing timezone name in parentheses like "(UTC)" or "(PST)" // but keep the numeric offset for parsing baseStr := s diff --git a/internal/mime/parse_test.go b/internal/mime/parse_test.go index 18926c998..6a079233a 100644 --- a/internal/mime/parse_test.go +++ b/internal/mime/parse_test.go @@ -209,6 +209,15 @@ func TestParseDate(t *testing.T) { {"numeric offset with different paren TZ", "Mon, 02 Jan 2006 15:04:05 +0700 (UTC)", time.Date(2006, 1, 2, 8, 4, 5, 0, time.UTC)}, + // mbox-derived Date headers with a stray unindented continuation + // line ("."), which enmime folds onto the header value + {"trailing malformed continuation dot", "Thu, 27 May 2004 17:06:42 -0500 .", + time.Date(2004, 5, 27, 22, 6, 42, 0, time.UTC)}, + + // US-style weekday + month-day order, no comma after day + {"weekday US month-day order", "Tue, Oct 17 2000 02:15:24 -0700", + time.Date(2000, 10, 17, 9, 15, 24, 0, time.UTC)}, + // Invalid/unparseable dates should return zero time {"empty", "", time.Time{}}, {"garbage", "not a date", time.Time{}}, @@ -344,6 +353,25 @@ func TestParse_MinimalMessage(t *testing.T) { assert.Equal(t, "Body text", msg.BodyText) } +// TestParse_MalformedContinuationLineAfterDate reproduces an mbox-derived +// message where a stray, unindented "." line follows the Date header. +// enmime folds it onto the Date value as a continuation instead of erroring, +// so Parse must still recover the sent date. +func TestParse_MalformedContinuationLineAfterDate(t *testing.T) { + raw := []byte("X-Mozilla-Status: 7001\n" + + "X-Mozilla-Status2: 00000000\n" + + "Date: Thu, 27 May 2004 17:06:42 -0500\n" + + ".\n" + + "Content-Type: text/plain; charset=ISO-8859-1\n" + + "\n" + + "Body text.") + + msg := mustParse(t, raw) + + assert.True(t, msg.Date.Equal(time.Date(2004, 5, 27, 22, 6, 42, 0, time.UTC)), + "msg.Date = %v", msg.Date) +} + // TestParse_InvalidCharset verifies enmime handles malformed charsets gracefully. // Enmime should not fail on invalid charset - it attempts conversion and collects errors. func TestParse_InvalidCharset(t *testing.T) {