diff --git a/src/net/mail/message.go b/src/net/mail/message.go index 75207db434..0781310ed3 100644 --- a/src/net/mail/message.go +++ b/src/net/mail/message.go @@ -79,7 +79,7 @@ func buildDateLayouts() { years := [...]string{"2006", "06"} // year = 4*DIGIT / 2*DIGIT seconds := [...]string{":05", ""} // second // "-0700 (MST)" is not in RFC 5322, but is common. - zones := [...]string{"-0700", "MST", "-0700 (MST)"} // zone = (("+" / "-") 4DIGIT) / "GMT" / ... + zones := [...]string{"-0700", "MST"} // zone = (("+" / "-") 4DIGIT) / "GMT" / ... for _, dow := range dows { for _, day := range days { @@ -98,6 +98,29 @@ func buildDateLayouts() { // ParseDate parses an RFC 5322 date string. func ParseDate(date string) (time.Time, error) { dateLayoutsBuildOnce.Do(buildDateLayouts) + // CR and LF must match and are tolerated anywhere in the date field. + date = strings.ReplaceAll(date, "\r\n", "") + if strings.Index(date, "\r") != -1 { + return time.Time{}, errors.New("mail: header has a CR without LF") + } + // Re-using some addrParser methods which support obsolete text, i.e. non-printable ASCII + p := addrParser{date, nil} + p.skipSpace() + + // RFC 5322: zone = (FWS ( "+" / "-" ) 4DIGIT) / obs-zone + // zone length is always 5 chars unless obsolete (obs-zone) + if ind := strings.IndexAny(p.s, "+-"); ind != -1 && len(p.s) >= ind+5 { + date = p.s[:ind+5] + p.s = p.s[ind+5:] + } else if ind := strings.Index(p.s, "T"); ind != -1 && len(p.s) >= ind+1 { + // The last letter T of the obsolete time zone is checked when no standard time zone is found. + // If T is misplaced, the date to parse is garbage. + date = p.s[:ind+1] + p.s = p.s[ind+1:] + } + if !p.skipCFWS() { + return time.Time{}, errors.New("mail: misformatted parenthetical comment") + } for _, layout := range dateLayouts { t, err := time.Parse(layout, date) if err == nil { diff --git a/src/net/mail/message_test.go b/src/net/mail/message_test.go index 2950bc4de9..fbdc4f70f8 100644 --- a/src/net/mail/message_test.go +++ b/src/net/mail/message_test.go @@ -124,6 +124,151 @@ func TestDateParsing(t *testing.T) { } } +func TestDateParsingCFWS(t *testing.T) { + tests := []struct { + dateStr string + exp time.Time + valid bool + }{ + // FWS-only. No date. + { + " ", + // nil is not allowed + time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)), + false, + }, + // FWS is allowed before optional day of week. + { + " Fri, 21 Nov 1997 09:55:06 -0600", + time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)), + true, + }, + { + "21 Nov 1997 09:55:06 -0600", + time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)), + true, + }, + { + "Fri 21 Nov 1997 09:55:06 -0600", + time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)), + false, // missing , + }, + // FWS is allowed before day of month but HTAB fails. + { + "Fri, 21 Nov 1997 09:55:06 -0600", + time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)), + true, + }, + // FWS is allowed before and after year but HTAB fails. + { + "Fri, 21 Nov 1997 09:55:06 -0600", + time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)), + true, + }, + // FWS is allowed before zone but HTAB is not handled. Obsolete timezone is handled. + { + "Fri, 21 Nov 1997 09:55:06 CST", + time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("CST", 0)), + true, + }, + // FWS is allowed after date and a CRLF is already replaced. + { + "Fri, 21 Nov 1997 09:55:06 CST (no leading FWS and a trailing CRLF) \r\n", + time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("CST", 0)), + true, + }, + // CFWS is a reduced set of US-ASCII where space and accentuated are obsolete. No error. + { + "Fri, 21 Nov 1997 09:55:06 -0600 (MDT and non-US-ASCII signs éèç )", + time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)), + true, + }, + // CFWS is allowed after zone including a nested comment. + // Trailing FWS is allowed. + { + "Fri, 21 Nov 1997 09:55:06 -0600 \r\n (thisisa(valid)cfws) \t ", + time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)), + true, + }, + // CRLF is incomplete and misplaced. + { + "Fri, 21 Nov 1997 \r 09:55:06 -0600 \r\n (thisisa(valid)cfws) \t ", + time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)), + false, + }, + // CRLF is complete but misplaced. No error is returned. + { + "Fri, 21 Nov 199\r\n7 09:55:06 -0600 \r\n (thisisa(valid)cfws) \t ", + time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)), + true, // should be false in the strict interpretation of RFC 5322. + }, + // Invalid ASCII in date. + { + "Fri, 21 Nov 1997 ù 09:55:06 -0600 \r\n (thisisa(valid)cfws) \t ", + time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)), + false, + }, + // CFWS chars () in date. + { + "Fri, 21 Nov () 1997 09:55:06 -0600 \r\n (thisisa(valid)cfws) \t ", + time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)), + false, + }, + // Timezone is invalid but T is found in comment. + { + "Fri, 21 Nov 1997 09:55:06 -060 \r\n (Thisisa(valid)cfws) \t ", + time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)), + false, + }, + // Date has no month. + { + "Fri, 21 1997 09:55:06 -0600", + time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)), + false, + }, + // Invalid month : OCT iso Oct + { + "Fri, 21 OCT 1997 09:55:06 CST", + time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)), + false, + }, + // A too short time zone. + { + "Fri, 21 Nov 1997 09:55:06 -060", + time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)), + false, + }, + // A too short obsolete time zone. + { + "Fri, 21 1997 09:55:06 GT", + time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)), + false, + }, + } + for _, test := range tests { + hdr := Header{ + "Date": []string{test.dateStr}, + } + date, err := hdr.Date() + if err != nil && test.valid { + t.Errorf("Header(Date: %s).Date(): %v", test.dateStr, err) + } else if err == nil && !date.Equal(test.exp) && test.valid { + t.Errorf("Header(Date: %s).Date() = %+v, want %+v", test.dateStr, date, test.exp) + } else if err == nil && !test.valid { // an invalid expression was tested + t.Errorf("Header(Date: %s).Date() did not return an error but %v", test.dateStr, date) + } + + date, err = ParseDate(test.dateStr) + if err != nil && test.valid { + t.Errorf("ParseDate(%s): %v", test.dateStr, err) + } else if err == nil && !test.valid { // an invalid expression was tested + t.Errorf("ParseDate(%s) did not return an error but %v", test.dateStr, date) + } else if err == nil && test.valid && !date.Equal(test.exp) { + t.Errorf("ParseDate(%s) = %+v, want %+v", test.dateStr, date, test.exp) + } + } +} + func TestAddressParsingError(t *testing.T) { mustErrTestCases := [...]struct { text string