net/mail: added support to trailing CFWS in date

RFC 5322 date format allows CFWS after the timezone.
If CFWS is valid, it is discarded and parsing is done as before
using time.Parse().
Existing test is extended with limit cases and invalid strings.

Fixes #22661

Change-Id: I54b96d7bc384b751962a76690e7e4786217a7941
Reviewed-on: https://go-review.googlesource.com/c/go/+/117596
Run-TryBot: Ian Lance Taylor <iant@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Ian Lance Taylor <iant@golang.org>
diff --git a/src/net/mail/message.go b/src/net/mail/message.go
index 75207db..0781310 100644
--- a/src/net/mail/message.go
+++ b/src/net/mail/message.go
@@ -79,7 +79,7 @@
 	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 @@
 // 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 2950bc4..fbdc4f7 100644
--- a/src/net/mail/message_test.go
+++ b/src/net/mail/message_test.go
@@ -124,6 +124,151 @@
 	}
 }
 
+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