Repository: jhillyerd/enmime
Pull request #211 (2021-08-08 19:13:22)

Move decodeHeader to internal/coding
- create internal version of decodeHeader
- Update decodeHeader refs, delete old tests
- update existing test table syntax
- update passthrough tests
- Update qp and b64 tests



Patch:
diff --git a/envelope.go b/envelope.go
index dc3d97d..8c2fd5e 100644
--- a/envelope.go
+++ b/envelope.go
@@ -45,7 +45,7 @@ func (e *Envelope) GetHeader(name string) string {
 	if e.header == nil {
 		return ""
 	}
-	return decodeHeader(e.header.Get(name))
+	return coding.DecodeExtHeader(e.header.Get(name))
 }
 
 // GetHeaderValues processes the specified header for RFC 2047 encoded words and returns all existing
@@ -58,7 +58,7 @@ func (e *Envelope) GetHeaderValues(name string) []string {
 	rawValues := (*e.header)[textproto.CanonicalMIMEHeaderKey(name)]
 	var values []string
 	for _, v := range rawValues {
-		values = append(values, decodeHeader(v))
+		values = append(values, coding.DecodeExtHeader(v))
 	}
 	return values
 }
diff --git a/header.go b/header.go
index 290a0bd..579267e 100644
--- a/header.go
+++ b/header.go
@@ -153,22 +153,6 @@ func readHeader(r *bufio.Reader, p *Part) (textproto.MIMEHeader, error) {
 	return header, errors.WithStack(err)
 }
 
-// decodeHeader decodes a single line (per RFC 2047) using Golang's mime.WordDecoder
-func decodeHeader(input string) string {
-	if !strings.Contains(input, "=?") {
-		// Don't scan if there is nothing to do here
-		return input
-	}
-
-	dec := new(mime.WordDecoder)
-	dec.CharsetReader = coding.NewCharsetReader
-	header, err := dec.DecodeHeader(input)
-	if err != nil {
-		return input
-	}
-	return header
-}
-
 // decodeToUTF8Base64Header decodes a MIME header per RFC 2047, reencoding to =?utf-8b?
 func decodeToUTF8Base64Header(input string) string {
 	if !strings.Contains(input, "=?") {
@@ -201,7 +185,9 @@ func decodeToUTF8Base64Header(input string) string {
 				token = token[:len(token)-1]
 			}
 			// Base64 encode token
-			output[i] = prefix + mime.BEncoding.Encode("UTF-8", decodeHeader(token)) + suffix
+			output[i] = prefix +
+				mime.BEncoding.Encode("UTF-8", coding.DecodeExtHeader(token)) +
+				suffix
 		} else {
 			output[i] = token
 		}
diff --git a/inspect.go b/inspect.go
index 591dbf0..fc5da53 100644
--- a/inspect.go
+++ b/inspect.go
@@ -8,6 +8,7 @@ import (
 	"net/textproto"
 	"strings"
 
+	"github.com/jhillyerd/enmime/internal/coding"
 	"github.com/pkg/errors"
 )
 
@@ -122,8 +123,8 @@ func rfc2047recurse(s string) (string, error) {
 	}
 
 	var val string
-	if val = decodeHeader(s); val == s {
-		if val = decodeHeader(fixRFC2047String(val)); val == s {
+	if val = coding.DecodeExtHeader(s); val == s {
+		if val = coding.DecodeExtHeader(fixRFC2047String(val)); val == s {
 			return val, io.EOF
 		}
 	}
diff --git a/internal/coding/headerext.go b/internal/coding/headerext.go
new file mode 100644
index 0000000..def8ee4
--- /dev/null
+++ b/internal/coding/headerext.go
@@ -0,0 +1,23 @@
+package coding
+
+import (
+	"mime"
+	"strings"
+)
+
+// DecodeExtHeader decodes a single line (per RFC 2047, aka Message Header Extensions) using Golang's
+// mime.WordDecoder
+func DecodeExtHeader(input string) string {
+	if !strings.Contains(input, "=?") {
+		// Don't scan if there is nothing to do here
+		return input
+	}
+
+	dec := new(mime.WordDecoder)
+	dec.CharsetReader = NewCharsetReader
+	header, err := dec.DecodeHeader(input)
+	if err != nil {
+		return input
+	}
+	return header
+}
diff --git a/part.go b/part.go
index d693430..4a9db2f 100644
--- a/part.go
+++ b/part.go
@@ -143,13 +143,13 @@ func (p *Part) setupContentHeaders(mediaParams map[string]string) {
 	if err == nil {
 		// Disposition is optional
 		p.Disposition = disposition
-		p.FileName = decodeHeader(dparams[hpFilename])
+		p.FileName = coding.DecodeExtHeader(dparams[hpFilename])
 	}
 	if p.FileName == "" && mediaParams[hpName] != "" {
-		p.FileName = decodeHeader(mediaParams[hpName])
+		p.FileName = coding.DecodeExtHeader(mediaParams[hpName])
 	}
 	if p.FileName == "" && mediaParams[hpFile] != "" {
-		p.FileName = decodeHeader(mediaParams[hpFile])
+		p.FileName = coding.DecodeExtHeader(mediaParams[hpFile])
 	}
 	if p.Charset == "" {
 		p.Charset = mediaParams[hpCharset]


Test patch:
diff --git a/header_test.go b/header_test.go
index 55e36dd..4c4cb57 100644
--- a/header_test.go
+++ b/header_test.go
@@ -7,135 +7,6 @@ import (
 	"testing"
 )
 
-// Ensure that a single plain text token passes unharmed
-func TestPlainSingleToken(t *testing.T) {
-	in := "Test"
-	want := in
-	got := decodeHeader(in)
-	if got != want {
-		t.Error("got:", got, "want:", want)
-	}
-}
-
-// Ensure that a string of plain text tokens do not get mangled
-func TestPlainTokens(t *testing.T) {
-	in := "Testing One two 3 4"
-	want := in
-	got := decodeHeader(in)
-	if got != want {
-		t.Error("got:", got, "want:", want)
-	}
-}
-
-// Test control character detection & abort
-func TestCharsetControlDetect(t *testing.T) {
-	in := "=?US\nASCII?Q?Keith_Moore?="
-	want := in
-	got := decodeHeader(in)
-	if got != want {
-		t.Error("got:", got, "want:", want)
-	}
-}
-
-// Test control character detection & abort
-func TestEncodingControlDetect(t *testing.T) {
-	in := "=?US-ASCII?\r?Keith_Moore?="
-	want := in
-	got := decodeHeader(in)
-	if got != want {
-		t.Error("got:", got, "want:", want)
-	}
-}
-
-// Test mangled termination
-func TestInvalidTermination(t *testing.T) {
-	in := "=?US-ASCII?Q?Keith_Moore?!"
-	want := in
-	got := decodeHeader(in)
-	if got != want {
-		t.Error("got:", got, "want:", want)
-	}
-}
-
-// Try decoding a simple ASCII quoted-printable encoded word
-func TestAsciiQ(t *testing.T) {
-	in := "=?US-ASCII?Q?Keith_Moore?="
-	want := "Keith Moore"
-	got := decodeHeader(in)
-	if got != want {
-		t.Error("got:", got, "want:", want)
-	}
-}
-
-// Try decoding a simple ASCII quoted-printable encoded word
-func TestAsciiB64(t *testing.T) {
-	in := "=?US-ASCII?B?SGVsbG8gV29ybGQ=?="
-	want := "Hello World"
-	got := decodeHeader(in)
-	if got != want {
-		t.Error("got:", got, "want:", want)
-	}
-}
-
-// Try decoding an embedded ASCII quoted-printable encoded word
-func TestEmbeddedAsciiQ(t *testing.T) {
-	var testTable = []struct {
-		in, want string
-	}{
-		// Abutting a MIME header comment is legal
-		{"(=?US-ASCII?Q?Keith_Moore?=)", "(Keith Moore)"},
-		// The entire header does not need to be encoded
-		{"(Keith =?US-ASCII?Q?Moore?=)", "(Keith Moore)"},
-	}
-
-	for _, tt := range testTable {
-		got := decodeHeader(tt.in)
-		if got != tt.want {
-			t.Errorf("DecodeHeader(%q) == %q, want: %q", tt.in, got, tt.want)
-		}
-	}
-}
-
-// Spacing rules from RFC 2047
-func TestSpacing(t *testing.T) {
-	var testTable = []struct {
-		in, want string
-	}{
-		{"(=?ISO-8859-1?Q?a?=)", "(a)"},
-		{"(=?ISO-8859-1?Q?a?= b)", "(a b)"},
-		{"(=?ISO-8859-1?Q?a?= =?ISO-8859-1?Q?b?=)", "(ab)"},
-		{"(=?ISO-8859-1?Q?a?=  =?ISO-8859-1?Q?b?=)", "(ab)"},
-		{"(=?ISO-8859-1?Q?a?=\r\n  =?ISO-8859-1?Q?b?=)", "(ab)"},
-		{"(=?ISO-8859-1?Q?a_b?=)", "(a b)"},
-		{"(=?ISO-8859-1?Q?a?= =?ISO-8859-2?Q?_b?=)", "(a b)"},
-	}
-
-	for _, tt := range testTable {
-		got := decodeHeader(tt.in)
-		if got != tt.want {
-			t.Errorf("DecodeHeader(%q) == %q, want: %q", tt.in, got, tt.want)
-		}
-	}
-}
-
-// Test some different character sets
-func TestCharsets(t *testing.T) {
-	var testTable = []struct {
-		in, want string
-	}{
-		{"=?utf-8?q?abcABC_=24_=c2=a2_=e2=82=ac?=", "abcABC $ \u00a2 \u20ac"},
-		{"=?iso-8859-1?q?#=a3_c=a9_r=ae_u=b5?=", "#\u00a3 c\u00a9 r\u00ae u\u00b5"},
-		{"=?big5?q?=a1=5d_=a1=61_=a1=71?=", "\uff08 \uff5b \u3008"},
-	}
-
-	for _, tt := range testTable {
-		got := decodeHeader(tt.in)
-		if got != tt.want {
-			t.Errorf("DecodeHeader(%q) == %q, want: %q", tt.in, got, tt.want)
-		}
-	}
-}
-
 // Test re-encoding to base64
 func TestDecodeToUTF8Base64Header(t *testing.T) {
 	var testTable = []struct {
diff --git a/internal/coding/headerext_test.go b/internal/coding/headerext_test.go
new file mode 100644
index 0000000..46725bb
--- /dev/null
+++ b/internal/coding/headerext_test.go
@@ -0,0 +1,137 @@
+package coding
+
+import (
+	"testing"
+)
+
+func TestPlainPassthrough(t *testing.T) {
+	var ttable = []string{
+		"Test",
+		"Testing One two 3 4",
+	}
+
+	for _, in := range ttable {
+		t.Run(in, func(t *testing.T) {
+			got := DecodeExtHeader(in)
+			if got != in {
+				t.Errorf("DecodeHeader(%q) == %q, want: %q", in, got, in)
+			}
+		})
+	}
+}
+
+func TestFailurePassthrough(t *testing.T) {
+	var ttable = []struct {
+		label, in string
+	}{
+		{
+			label: "Newline detection & abort",
+			in:    "=?US\nASCII?Q?Keith_Moore?=",
+		},
+		{
+			label: "Carriage return detection & abort",
+			in:    "=?US-ASCII?\r?Keith_Moore?=",
+		},
+		{
+			label: "Invalid termination",
+			in:    "=?US-ASCII?Q?Keith_Moore?!",
+		},
+	}
+
+	for _, tt := range ttable {
+		t.Run(tt.in, func(t *testing.T) {
+			got := DecodeExtHeader(tt.in)
+			if got != tt.in {
+				t.Errorf("DecodeHeader(%q) == %q, want: %q", tt.in, got, tt.in)
+			}
+		})
+	}
+}
+
+func TestAsciiB64(t *testing.T) {
+	var ttable = []struct {
+		in, want string
+	}{
+		// Simple ASCII quoted-printable encoded word
+		{"=?US-ASCII?B?SGVsbG8gV29ybGQ=?=", "Hello World"},
+		// Abutting a MIME header comment is legal
+		{"(=?US-ASCII?B?SGVsbG8gV29ybGQ=?=)", "(Hello World)"},
+		// The entire header does not need to be encoded
+		{"(Prefix =?US-ASCII?B?SGVsbG8gV29ybGQ=?=)", "(Prefix Hello World)"},
+	}
+
+	for _, tt := range ttable {
+		t.Run(tt.in, func(t *testing.T) {
+			got := DecodeExtHeader(tt.in)
+			if got != tt.want {
+				t.Errorf("DecodeHeader(%q) == %q, want: %q", tt.in, got, tt.want)
+			}
+		})
+	}
+}
+
+func TestAsciiQ(t *testing.T) {
+	var ttable = []struct {
+		in, want string
+	}{
+		// Simple ASCII QP encoded word
+		{"=?US-ASCII?Q?Keith_Moore?=", "Keith Moore"},
+		// Abutting a MIME header comment is legal
+		{"(=?US-ASCII?Q?Keith_Moore?=)", "(Keith Moore)"},
+		// The entire header does not need to be encoded
+		{"(Keith =?US-ASCII?Q?Moore?=)", "(Keith Moore)"},
+	}
+
+	for _, tt := range ttable {
+		t.Run(tt.in, func(t *testing.T) {
+			got := DecodeExtHeader(tt.in)
+			if got != tt.want {
+				t.Errorf("DecodeHeader(%q) == %q, want: %q", tt.in, got, tt.want)
+			}
+		})
+	}
+}
+
+// Spacing rules from RFC 2047
+func TestSpacing(t *testing.T) {
+	var ttable = []struct {
+		in, want string
+	}{
+		{"(=?ISO-8859-1?Q?a?=)", "(a)"},
+		{"(=?ISO-8859-1?Q?a?= b)", "(a b)"},
+		{"(=?ISO-8859-1?Q?a?= =?ISO-8859-1?Q?b?=)", "(ab)"},
+		{"(=?ISO-8859-1?Q?a?=  =?ISO-8859-1?Q?b?=)", "(ab)"},
+		{"(=?ISO-8859-1?Q?a?=\r\n  =?ISO-8859-1?Q?b?=)", "(ab)"},
+		{"(=?ISO-8859-1?Q?a_b?=)", "(a b)"},
+		{"(=?ISO-8859-1?Q?a?= =?ISO-8859-2?Q?_b?=)", "(a b)"},
+	}
+
+	for _, tt := range ttable {
+		t.Run(tt.in, func(t *testing.T) {
+			got := DecodeExtHeader(tt.in)
+			if got != tt.want {
+				t.Errorf("DecodeHeader(%q) == %q, want: %q", tt.in, got, tt.want)
+			}
+		})
+	}
+}
+
+// Test some different character sets
+func TestCharsets(t *testing.T) {
+	var ttable = []struct {
+		in, want string
+	}{
+		{"=?utf-8?q?abcABC_=24_=c2=a2_=e2=82=ac?=", "abcABC $ \u00a2 \u20ac"},
+		{"=?iso-8859-1?q?#=a3_c=a9_r=ae_u=b5?=", "#\u00a3 c\u00a9 r\u00ae u\u00b5"},
+		{"=?big5?q?=a1=5d_=a1=61_=a1=71?=", "\uff08 \uff5b \u3008"},
+	}
+
+	for _, tt := range ttable {
+		t.Run(tt.in, func(t *testing.T) {
+			got := DecodeExtHeader(tt.in)
+			if got != tt.want {
+				t.Errorf("DecodeHeader(%q) == %q, want: %q", tt.in, got, tt.want)
+			}
+		})
+	}
+}