Repository: jhillyerd/enmime
Pull request #318 (2024-01-03 13:53:25)

fix: don't encode header value with the RawContent parser option
Hi, with this PR, I want to keep my header values like the original with the raw content parser option set to true.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/64853260/badge)](https://coveralls.io/builds/64853260)

coverage: 86.224% (+0.01%) from 86.213%
when pulling **a50524209ba29c82f1303e15914e4f990297eae9 on jerjako:fix/raw-content-headers-value**
into **ed0394eda018ea5b8f6e81f9e71e7ab68c5b7a91 on jhillyerd:main**.


Patch:
diff --git a/encode.go b/encode.go
index 6a23662..7241a6f 100644
--- a/encode.go
+++ b/encode.go
@@ -170,15 +170,19 @@ func (p *Part) encodeHeader(b *bufio.Writer) error {
 	for k := range p.Header {
 		keys = append(keys, k)
 	}
+	rawContent := p.parser != nil && p.parser.rawContent
+
 	sort.Strings(keys)
 	for _, k := range keys {
 		for _, v := range p.Header[k] {
 			encv := v
-			switch selectTransferEncoding([]byte(v), true) {
-			case teBase64:
-				encv = mime.BEncoding.Encode(utf8, v)
-			case teQuoted:
-				encv = mime.QEncoding.Encode(utf8, v)
+			if !rawContent {
+				switch selectTransferEncoding([]byte(v), true) {
+				case teBase64:
+					encv = mime.BEncoding.Encode(utf8, v)
+				case teQuoted:
+					encv = mime.QEncoding.Encode(utf8, v)
+				}
 			}
 			// _ used to prevent early wrapping
 			wb := stringutil.Wrap(76, k, ":_", encv, "\r\n")
diff --git a/internal/textproto/reader.go b/internal/textproto/reader.go
index 010396d..a0ae11e 100644
--- a/internal/textproto/reader.go
+++ b/internal/textproto/reader.go
@@ -11,6 +11,7 @@ import (
 	"fmt"
 	"io"
 	"math"
+	"net/textproto"
 	"strconv"
 	"strings"
 	"sync"
@@ -198,20 +199,20 @@ func (r *Reader) readCodeLine(expectCode int) (code int, continued bool, message
 
 func parseCodeLine(line string, expectCode int) (code int, continued bool, message string, err error) {
 	if len(line) < 4 || line[3] != ' ' && line[3] != '-' {
-		err = ProtocolError("short response: " + line)
+		err = textproto.ProtocolError("short response: " + line)
 		return
 	}
 	continued = line[3] == '-'
 	code, err = strconv.Atoi(line[0:3])
 	if err != nil || code < 100 {
-		err = ProtocolError("invalid response code: " + line)
+		err = textproto.ProtocolError("invalid response code: " + line)
 		return
 	}
 	message = line[4:]
 	if 1 <= expectCode && expectCode < 10 && code/100 != expectCode ||
 		10 <= expectCode && expectCode < 100 && code/10 != expectCode ||
 		100 <= expectCode && expectCode < 1000 && code != expectCode {
-		err = &Error{code, message}
+		err = &textproto.Error{Code: code, Msg: message}
 	}
 	return
 }
@@ -236,7 +237,7 @@ func parseCodeLine(line string, expectCode int) (code int, continued bool, messa
 func (r *Reader) ReadCodeLine(expectCode int) (code int, message string, err error) {
 	code, continued, message, err := r.readCodeLine(expectCode)
 	if err == nil && continued {
-		err = ProtocolError("unexpected multi-line response: " + message)
+		err = textproto.ProtocolError("unexpected multi-line response: " + message)
 	}
 	return
 }
@@ -288,7 +289,7 @@ func (r *Reader) ReadResponse(expectCode int) (code int, message string, err err
 	}
 	if err != nil && multi && message != "" {
 		// replace one line error message with all lines (full message)
-		err = &Error{code, message}
+		err = &textproto.Error{Code: code, Msg: message}
 	}
 	return
 }
@@ -506,7 +507,7 @@ func readMIMEHeader(r *Reader, lim int64) (MIMEHeader, error) {
 		if err != nil {
 			return m, err
 		}
-		return m, ProtocolError("malformed MIME header initial line: " + string(line))
+		return m, textproto.ProtocolError("malformed MIME header initial line: " + string(line))
 	}
 
 	for {
@@ -518,15 +519,15 @@ func readMIMEHeader(r *Reader, lim int64) (MIMEHeader, error) {
 		// Key ends at first colon.
 		k, v, ok := bytes.Cut(kv, colon)
 		if !ok {
-			return m, ProtocolError("malformed MIME header line: " + string(kv))
+			return m, textproto.ProtocolError("malformed MIME header line: " + string(kv))
 		}
 		key, ok := canonicalMIMEHeaderKey(k)
 		if !ok {
-			return m, ProtocolError("malformed MIME header line: " + string(kv))
+			return m, textproto.ProtocolError("malformed MIME header line: " + string(kv))
 		}
 		for _, c := range v {
 			if !validHeaderValueByte(c) {
-				return m, ProtocolError("malformed MIME header line: " + string(kv))
+				return m, textproto.ProtocolError("malformed MIME header line: " + string(kv))
 			}
 		}
 
@@ -578,7 +579,7 @@ func noValidation(_ []byte) error { return nil }
 // contain a colon.
 func mustHaveFieldNameColon(line []byte) error {
 	if bytes.IndexByte(line, ':') < 0 {
-		return ProtocolError(fmt.Sprintf("malformed MIME header: missing colon: %q", line))
+		return textproto.ProtocolError(fmt.Sprintf("malformed MIME header: missing colon: %q", line))
 	}
 	return nil
 }
diff --git a/internal/textproto/reader_email.go b/internal/textproto/reader_email.go
index 0cc8e36..0b49509 100644
--- a/internal/textproto/reader_email.go
+++ b/internal/textproto/reader_email.go
@@ -4,6 +4,7 @@ import (
 	"bytes"
 	"errors"
 	"math"
+	"net/textproto"
 )
 
 // ReadEmailMIMEHeader reads a MIME-style header from r.
@@ -32,7 +33,7 @@ func readEmailMIMEHeader(r *Reader, lim int64) (MIMEHeader, error) {
 		if err != nil {
 			return m, err
 		}
-		return m, ProtocolError("malformed MIME header initial line: " + string(line))
+		return m, textproto.ProtocolError("malformed MIME header initial line: " + string(line))
 	}
 
 	for {
@@ -44,11 +45,11 @@ func readEmailMIMEHeader(r *Reader, lim int64) (MIMEHeader, error) {
 		// Key ends at first colon.
 		k, v, ok := bytes.Cut(kv, colon)
 		if !ok {
-			return m, ProtocolError("malformed MIME header line: " + string(kv))
+			return m, textproto.ProtocolError("malformed MIME header line: " + string(kv))
 		}
 		key, ok := canonicalEmailMIMEHeaderKey(k)
 		if !ok {
-			return m, ProtocolError("malformed MIME header line: " + string(kv))
+			return m, textproto.ProtocolError("malformed MIME header line: " + string(kv))
 		}
 		// for _, c := range v {
 		// 	if !validHeaderValueByte(c) {
diff --git a/internal/textproto/textproto.go b/internal/textproto/textproto.go
index 70038d5..0d92bf2 100644
--- a/internal/textproto/textproto.go
+++ b/internal/textproto/textproto.go
@@ -26,29 +26,10 @@ package textproto
 
 import (
 	"bufio"
-	"fmt"
 	"io"
 	"net"
 )
 
-// An Error represents a numeric error response from a server.
-type Error struct {
-	Code int
-	Msg  string
-}
-
-func (e *Error) Error() string {
-	return fmt.Sprintf("%03d %s", e.Code, e.Msg)
-}
-
-// A ProtocolError describes a protocol violation such
-// as an invalid response or a hung-up connection.
-type ProtocolError string
-
-func (p ProtocolError) Error() string {
-	return string(p)
-}
-
 // A Conn represents a textual network protocol connection.
 // It consists of a Reader and Writer to manage I/O
 // and a Pipeline to sequence concurrent requests on the connection.


Test patch:
diff --git a/encode_test.go b/encode_test.go
index 3e89d74..3a33ba7 100644
--- a/encode_test.go
+++ b/encode_test.go
@@ -425,3 +425,19 @@ func TestParseRawContentTextOptionFalse(t *testing.T) {
 	}
 	test.DiffGolden(t, b.Bytes(), "testdata", "encode", "parser-raw-content-text-option-false.raw.golden")
 }
+
+// TestRawContentUTF8Headers verifies plain-text headers are unmodified with the rawContent parser option.
+func TestRawContentUTF8Headers(t *testing.T) {
+	r := test.OpenTestData("encode", "utf8-to.raw.golden")
+	p := enmime.NewParser(enmime.RawContent(true))
+	e, err := p.ReadEnvelope(r)
+	if err != nil {
+		t.Fatal("Failed to parse MIME:", err)
+	}
+	b := &bytes.Buffer{}
+	if err := e.Root.Encode(b); err != nil {
+		t.Fatal(err)
+	}
+
+	test.DiffGolden(t, b.Bytes(), "testdata", "encode", "utf8-to.raw.golden")
+}
diff --git a/inspect_test.go b/inspect_test.go
index 58f22ad..e65a83b 100644
--- a/inspect_test.go
+++ b/inspect_test.go
@@ -4,12 +4,12 @@ import (
 	"bytes"
 	"io"
 	"net/mail"
+	"net/textproto"
 	"strings"
 	"testing"
 
 	"github.com/jhillyerd/enmime"
 	"github.com/jhillyerd/enmime/internal/test"
-	"github.com/jhillyerd/enmime/internal/textproto"
 )
 
 func TestDecodeRFC2047(t *testing.T) {
diff --git a/internal/textproto/reader_test.go b/internal/textproto/reader_test.go
index 2c7d2aa..f6c728d 100644
--- a/internal/textproto/reader_test.go
+++ b/internal/textproto/reader_test.go
@@ -9,6 +9,7 @@ import (
 	"bytes"
 	"io"
 	"net"
+	"net/textproto"
 	"reflect"
 	"strings"
 	"sync"
@@ -69,7 +70,7 @@ func TestReadCodeLine(t *testing.T) {
 	if code != 345 || msg != "no way" || err == nil {
 		t.Fatalf("Line 3: %d, %s, %v", code, msg, err)
 	}
-	if e, ok := err.(*Error); !ok || e.Code != code || e.Msg != msg {
+	if e, ok := err.(*textproto.Error); !ok || e.Code != code || e.Msg != msg {
 		t.Fatalf("Line 3: wrong error %v\n", err)
 	}
 	code, msg, err = r.ReadCodeLine(1)
diff --git a/testdata/encode/utf8-to.raw.golden b/testdata/encode/utf8-to.raw.golden
new file mode 100644
index 0000000..dc26e83
--- /dev/null
+++ b/testdata/encode/utf8-to.raw.golden
@@ -0,0 +1,7 @@
+Content-Type: text/plain; charset=utf-8
+From: Weiß foo@bar.com
+Mime-Version: 1.0
+Subject: test utf8 to
+To: Jøhn Døe <bar@baz.com>
+
+This is a test mailing