From: James Hillyerd <james@hillyerd.com>
Date: Thu, 2 Feb 2023 11:50:20 -0800
Subject: fix: Reject invalid MIME header name characters to support Go 1.20
 (#270)

* Test with go 1.20 release candidate
* header: Detect invalid header name characters for Go 1.20
* header: test for permitted special chars

Origin: backport, https://github.com/jhillyerd/enmime/commit/415ff72f
---
 header.go      | 59 +++++++++++++++++++++++++++++++++++++++++++++++++---------
 header_test.go | 18 +++++++++++++-----
 2 files changed, 63 insertions(+), 14 deletions(-)

diff --git a/header.go b/header.go
index cd9d28b..628e765 100644
--- a/header.go
+++ b/header.go
@@ -81,9 +81,9 @@ var AddressHeaders = map[string]bool{
 // ParseMediaType is a more tolerant implementation of Go's mime.ParseMediaType function.
 //
 // Tolerances accounted for:
-//   * Missing ';' between content-type and media parameters
-//   * Repeating media parameters
-//   * Unquoted values in media parameters containing 'tspecials' characters
+//   - Missing ';' between content-type and media parameters
+//   - Repeating media parameters
+//   - Unquoted values in media parameters containing 'tspecials' characters
 func ParseMediaType(ctype string) (mtype string, params map[string]string, invalidParams []string,
 	err error) {
 	// Export of internal function.
@@ -97,6 +97,7 @@ func readHeader(r *bufio.Reader, p *Part) (textproto.MIMEHeader, error) {
 	buf := &bytes.Buffer{}
 	tp := textproto.NewReader(r)
 	firstHeader := true
+line:
 	for {
 		// Pull out each line of the headers as a temporary slice s
 		s, err := tp.ReadLineBytes()
@@ -119,18 +120,29 @@ func readHeader(r *bufio.Reader, p *Part) (textproto.MIMEHeader, error) {
 			continue
 		}
 		if firstColon > 0 {
-			// Contains a colon, treat as a new header line
-			if !firstHeader {
-				// New Header line, end the previous
-				buf.Write([]byte{'\r', '\n'})
-			}
-
 			// Behavior change in net/textproto package in Golang 1.12.10 and 1.13.1:
 			// A space preceding the first colon in a header line is no longer handled
 			// automatically due to CVE-2019-16276 which takes advantage of this
 			// particular violation of RFC-7230 to exploit HTTP/1.1
 			if bytes.Contains(s[:firstColon+1], []byte{' ', ':'}) {
 				s = bytes.Replace(s, []byte{' ', ':'}, []byte{':'}, 1)
+				firstColon = bytes.IndexByte(s, ':')
+			}
+
+			// Behavior change in net/textproto package in Golang 1.20: invalid characters
+			// in header keys are no longer allowed; https://github.com/golang/go/issues/53188
+			for _, c := range s[:firstColon] {
+				if c != ' ' && !validHeaderFieldByte(c) {
+					p.addError(
+						ErrorMalformedHeader, "Header name %q contains invalid character %q", s, c)
+					continue line
+				}
+			}
+
+			// Contains a colon, treat as a new header line
+			if !firstHeader {
+				// New Header line, end the previous
+				buf.Write([]byte{'\r', '\n'})
 			}
 
 			s = textproto.TrimBytes(s)
@@ -213,3 +225,32 @@ func quotedDisplayName(s string) string {
 func whiteSpaceRune(r rune) bool {
 	return r == ' ' || r == '\t' || r == '\r' || r == '\n'
 }
+
+// Mirror of func in net/textproto/reader.go; from go 1.20.0-rc.2
+func validHeaderFieldByte(c byte) bool {
+	// mask is a 128-bit bitmap with 1s for allowed bytes,
+	// so that the byte c can be tested with a shift and an and.
+	// If c >= 128, then 1<<c and 1<<(c-64) will both be zero,
+	// and this function will return false.
+	const mask = 0 |
+		(1<<(10)-1)<<'0' |
+		(1<<(26)-1)<<'a' |
+		(1<<(26)-1)<<'A' |
+		1<<'!' |
+		1<<'#' |
+		1<<'$' |
+		1<<'%' |
+		1<<'&' |
+		1<<'\'' |
+		1<<'*' |
+		1<<'+' |
+		1<<'-' |
+		1<<'.' |
+		1<<'^' |
+		1<<'_' |
+		1<<'`' |
+		1<<'|' |
+		1<<'~'
+	return ((uint64(1)<<c)&(mask&(1<<64-1)) |
+		(uint64(1)<<(c-64))&(mask>>64)) != 0
+}
diff --git a/header_test.go b/header_test.go
index 01055be..5b18568 100644
--- a/header_test.go
+++ b/header_test.go
@@ -99,8 +99,6 @@ func TestReadHeader(t *testing.T) {
 		{
 			label:   "missing name",
 			input:   ": line1=foo\r\n",
-			hname:   "",
-			want:    "",
 			correct: false,
 		},
 		{
@@ -127,12 +125,17 @@ func TestReadHeader(t *testing.T) {
 			correct: true,
 		},
 		{
-			label:   "equals in name",
-			input:   "name=value:text\n",
-			hname:   "name=value",
+			label:   "permitted specials in name",
+			input:   "Begin!#$%&'*+-.^_`|~End: text\n",
+			hname:   "Begin!#$%&'*+-.^_`|~End",
 			want:    "text",
 			correct: true,
 		},
+		{
+			label:   "equals in name",
+			input:   "name=value:text\n",
+			correct: false,
+		},
 		{
 			label:   "no space before continuation",
 			input:   "X-Bad-Continuation: line1=foo;\nline2=bar\n",
@@ -236,6 +239,11 @@ func TestReadHeader(t *testing.T) {
 			gotErrs := len(p.Errors)
 			if gotErrs != wantErrs {
 				t.Errorf("Got %v p.Errors, want %v", gotErrs, wantErrs)
+				if gotErrs > 0 {
+					for _, e := range p.Errors {
+						t.Log(e)
+					}
+				}
 			}
 
 			// Check for extra headers by removing expected ones.
