Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 52 additions & 9 deletions encode.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,24 @@ func (p *Part) Encode(writer io.Writer) error {
p.Header = make(textproto.MIMEHeader)
}
if p.ContentReader != nil {
// read some data in order to check whether the content is empty
p.Content = make([]byte, readChunkSize)
n, err := p.ContentReader.Read(p.Content)
if err != nil && err != io.EOF {
return err
// When a forced CTE is set to something other than base64, we need the full content
// in memory to use the standard encode path (not the streaming base64 path).
if forced := p.resolveForcedCTE(); forced != teRaw && forced != teBase64 {
all, err := io.ReadAll(p.ContentReader)
if err != nil {
return err
}
p.Content = all
p.ContentReader = nil
} else {
// read some data in order to check whether the content is empty
p.Content = make([]byte, readChunkSize)
n, err := p.ContentReader.Read(p.Content)
if err != nil && err != io.EOF {
return err
}
p.Content = p.Content[:n]
}
p.Content = p.Content[:n]
}
cte := teRaw
if p.parser == nil || !p.parser.rawContent {
Expand Down Expand Up @@ -108,7 +119,23 @@ func (p *Part) setupMIMEHeaders() transferEncoding {
p.Header.Del(hnContentEncoding)

cte := te7Bit
if len(p.Content) > 0 {
if f := p.resolveForcedCTE(); f != teRaw {
// An explicit override applies regardless of content length, so the header is always
// emitted (including 7bit), even when the body is empty.
cte = f
switch f {
case te7Bit:
p.Header.Set(hnContentEncoding, cte7Bit)
case te8Bit:
p.Header.Set(hnContentEncoding, cte8Bit)
case teBase64:
p.Header.Set(hnContentEncoding, cteBase64)
case teQuoted:
p.Header.Set(hnContentEncoding, cteQuotedPrintable)
}
} else if len(p.Content) > 0 {
// No override: select automatically. RFC 2045: 7bit is assumed if the CTE header is not
// present, so for auto-detected 7bit (and for empty content) no header is emitted.
if strings.Index(strings.ToLower(p.ContentType), "message/") == 0 {
// RFC 1341: `message` types must have no encoding other than "7bit", "8bit", or
// "binary". The message header fields are always US-ASCII in any case, and data within
Expand All @@ -124,8 +151,6 @@ func (p *Part) setupMIMEHeaders() transferEncoding {
}
}
}

// RFC 2045: 7bit is assumed if CTE header not present.
switch cte {
case te8Bit:
p.Header.Set(hnContentEncoding, cte8Bit)
Expand Down Expand Up @@ -298,6 +323,24 @@ func (p *Part) encodeContentFromReader(b *bufio.Writer) error {
return nil
}

// resolveForcedCTE maps the ContentTransferEncoding field to an internal transferEncoding
// value. Returns teRaw (sentinel for "not set / unrecognised") when the field is empty or
// contains an unrecognised value.
func (p *Part) resolveForcedCTE() transferEncoding {
switch strings.ToLower(p.ContentTransferEncoding) {
case cte7Bit:
return te7Bit
case cte8Bit:
return te8Bit
case cteQuotedPrintable:
return teQuoted
case cteBase64:
return teBase64
default:
return teRaw // sentinel: no override
}
}

// selectTransferEncoding scans content for non-ASCII characters and selects 'b' or 'q' encoding.
func (p *Part) selectTransferEncoding(content []byte, quoteLineBreaks bool) transferEncoding {
if len(content) == 0 {
Expand Down
277 changes: 277 additions & 0 deletions encode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,283 @@ func TestParseRawContentTextOptionFalse(t *testing.T) {
test.DiffGolden(t, b.Bytes(), "testdata", "encode", "parser-raw-content-text-option-false.raw.golden")
}

// TestEncodePartForcedCTE7Bit verifies that a non-text part with 7bit-safe content and forced "7bit" CTE
// is not base64-encoded and the header reads Content-Transfer-Encoding: 7bit.
func TestEncodePartForcedCTE7Bit(t *testing.T) {
p := enmime.NewPart("application/pgp-encrypted")
p.ContentTransferEncoding = "7bit"
p.Content = []byte("Version: 1")

b := &bytes.Buffer{}
err := p.Encode(b)
if err != nil {
t.Fatal(err)
}

assert.Equal(t, "7bit", p.Header.Get("Content-Transfer-Encoding"))
assert.Contains(t, b.String(), "Version: 1")
assert.NotContains(t, b.String(), "base64")
}

// TestEncodePartForcedCTEQuotedPrintable verifies QP encoding is applied when forced.
func TestEncodePartForcedCTEQuotedPrintable(t *testing.T) {
p := enmime.NewPart("application/pgp-signature")
p.ContentTransferEncoding = "quoted-printable"
p.Content = []byte("-----BEGIN PGP SIGNATURE-----\r\nVersion: GnuPG\r\n\r\n=AAAA\r\n-----END PGP SIGNATURE-----")

b := &bytes.Buffer{}
err := p.Encode(b)
if err != nil {
t.Fatal(err)
}

assert.Equal(t, "quoted-printable", p.Header.Get("Content-Transfer-Encoding"))
assert.Contains(t, b.String(), "-----BEGIN PGP")
}

// TestEncodePartForcedCTEBase64 verifies base64 is applied when explicitly forced.
func TestEncodePartForcedCTEBase64(t *testing.T) {
p := enmime.NewPart("application/zip")
p.ContentTransferEncoding = "base64"
p.Content = []byte("ZIPZIPZIP")

b := &bytes.Buffer{}
err := p.Encode(b)
if err != nil {
t.Fatal(err)
}

assert.Equal(t, "base64", p.Header.Get("Content-Transfer-Encoding"))
}

// TestEncodePartForcedCTE8Bit verifies 8bit header is set and content is written verbatim.
func TestEncodePartForcedCTE8Bit(t *testing.T) {
p := enmime.NewPart("application/octet-stream")
p.ContentTransferEncoding = "8bit"
p.Content = []byte("some binary data")

b := &bytes.Buffer{}
err := p.Encode(b)
if err != nil {
t.Fatal(err)
}

assert.Equal(t, "8bit", p.Header.Get("Content-Transfer-Encoding"))
assert.Contains(t, b.String(), "some binary data")
}

// TestEncodePartForcedCTEEmpty verifies empty ContentTransferEncoding preserves default behavior (base64 for non-text).
func TestEncodePartForcedCTEEmpty(t *testing.T) {
p := enmime.NewPart("application/octet-stream")
p.ContentTransferEncoding = ""
p.Content = []byte("data")

b := &bytes.Buffer{}
err := p.Encode(b)
if err != nil {
t.Fatal(err)
}

assert.Equal(t, "base64", p.Header.Get("Content-Transfer-Encoding"))
}

// TestEncodePartForcedCTEUnrecognised verifies unrecognised values fall back to automatic detection.
func TestEncodePartForcedCTEUnrecognised(t *testing.T) {
p := enmime.NewPart("application/octet-stream")
p.ContentTransferEncoding = "unknown-encoding"
p.Content = []byte("data")

b := &bytes.Buffer{}
err := p.Encode(b)
if err != nil {
t.Fatal(err)
}

// Non-text part should default to base64.
assert.Equal(t, "base64", p.Header.Get("Content-Transfer-Encoding"))
}

// TestEncodePartForcedCTECaseInsensitive verifies case-insensitive matching.
func TestEncodePartForcedCTECaseInsensitive(t *testing.T) {
for _, val := range []string{"7Bit", "7BIT", "7bit"} {
p := enmime.NewPart("application/pgp-encrypted")
p.ContentTransferEncoding = val
p.Content = []byte("Version: 1")

b := &bytes.Buffer{}
err := p.Encode(b)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, "7bit", p.Header.Get("Content-Transfer-Encoding"),
"expected 7bit for input %q", val)
}

for _, val := range []string{"BASE64", "Base64", "base64"} {
p := enmime.NewPart("text/plain")
p.ContentTransferEncoding = val
p.Content = []byte("hello")

b := &bytes.Buffer{}
err := p.Encode(b)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, "base64", p.Header.Get("Content-Transfer-Encoding"),
"expected base64 for input %q", val)
}
}

// TestEncodePartForcedCTETextPartBase64 verifies that forcing base64 on a text part
// overrides the auto-detection that would normally pick 7bit or QP.
func TestEncodePartForcedCTETextPartBase64(t *testing.T) {
p := enmime.NewPart("text/plain")
p.ContentTransferEncoding = "base64"
p.Content = []byte("Hello, this is plain ASCII text that would normally be 7bit.")

b := &bytes.Buffer{}
err := p.Encode(b)
if err != nil {
t.Fatal(err)
}

assert.Equal(t, "base64", p.Header.Get("Content-Transfer-Encoding"))
// Should not contain the plaintext directly
assert.NotContains(t, b.String(), "Hello, this is plain ASCII")
}

// TestEncodePartForcedCTEWithContentReader verifies that ContentReader + forced 7bit CTE
// writes content verbatim instead of base64-encoding it.
func TestEncodePartForcedCTEWithContentReader(t *testing.T) {
content := []byte("Version: 1")
p := enmime.NewPart("application/pgp-encrypted")
p.ContentTransferEncoding = "7bit"
p.ContentReader = bytes.NewReader(content)

b := &bytes.Buffer{}
err := p.Encode(b)
if err != nil {
t.Fatal(err)
}

assert.Equal(t, "7bit", p.Header.Get("Content-Transfer-Encoding"))
assert.Contains(t, b.String(), "Version: 1")
assert.Nil(t, p.ContentReader, "ContentReader should have been consumed")
}

// TestEncodePartForcedCTEEmptyContent verifies that a forced Content-Transfer-Encoding is still
// applied to the header when the body is empty -- either nil/zero-length Content or a ContentReader
// that yields zero bytes. Previously the override was gated on len(Content) > 0 and was silently
// dropped, contradicting the documented "forces" contract.
func TestEncodePartForcedCTEEmptyContent(t *testing.T) {
for _, tc := range []struct {
name string
cte string
part func() *enmime.Part
}{
{
name: "7bit nil content",
cte: "7bit",
part: func() *enmime.Part {
p := enmime.NewPart("application/pgp-encrypted")
p.ContentTransferEncoding = "7bit"
return p
},
},
{
name: "base64 nil content",
cte: "base64",
part: func() *enmime.Part {
p := enmime.NewPart("application/octet-stream")
p.ContentTransferEncoding = "base64"
return p
},
},
{
name: "7bit zero-byte reader",
cte: "7bit",
part: func() *enmime.Part {
p := enmime.NewPart("application/pgp-encrypted")
p.ContentTransferEncoding = "7bit"
p.ContentReader = bytes.NewReader(nil)
return p
},
},
{
name: "base64 zero-byte reader",
cte: "base64",
part: func() *enmime.Part {
p := enmime.NewPart("application/octet-stream")
p.ContentTransferEncoding = "base64"
p.ContentReader = bytes.NewReader(nil)
return p
},
},
} {
t.Run(tc.name, func(t *testing.T) {
p := tc.part()
b := &bytes.Buffer{}
if err := p.Encode(b); err != nil {
t.Fatal(err)
}
assert.Equal(t, tc.cte, p.Header.Get("Content-Transfer-Encoding"))
})
}
}

// TestEncodePartAutoCTEEmptyContent verifies that, when no CTE override is set, an empty body still
// results in no Content-Transfer-Encoding header (RFC 2045: 7bit is assumed when the header is
// absent). This guards against the forced-override refactor accidentally emitting a header for
// empty auto-detected parts.
func TestEncodePartAutoCTEEmptyContent(t *testing.T) {
p := enmime.NewPart("text/plain")
// No ContentTransferEncoding set; Content is empty.
b := &bytes.Buffer{}
if err := p.Encode(b); err != nil {
t.Fatal(err)
}
assert.Empty(t, p.Header.Get("Content-Transfer-Encoding"))
}

// TestEncodePartForcedCTEPrecedenceOverEncoderOption verifies that ContentTransferEncoding on the part
// takes precedence over the encoder-level ForceQuotedPrintableCte option. Without the override, the
// non-ASCII text content combined with ForceQuotedPrintableCte(true) would be QP-encoded; the forced
// "base64" must win instead.
func TestEncodePartForcedCTEPrecedenceOverEncoderOption(t *testing.T) {
content := bytes.Repeat([]byte{byte(0x10)}, 10) // non-ASCII; ForceQuotedPrintableCte would pick QP
p := enmime.NewPart("text/plain").WithEncoder(
enmime.NewEncoder(enmime.ForceQuotedPrintableCte(true)),
)
p.ContentTransferEncoding = "base64"
p.Content = content

b := &bytes.Buffer{}
err := p.Encode(b)
if err != nil {
t.Fatal(err)
}

// Part-level override should win over the encoder option.
assert.Equal(t, "base64", p.Header.Get("Content-Transfer-Encoding"))
// The QP soft form should not appear, since the override forced base64.
assert.NotContains(t, b.String(), "=10=10")
}

// TestEncodePartWithContentTransferEncodingMethod verifies the WithContentTransferEncoding builder method.
func TestEncodePartWithContentTransferEncodingMethod(t *testing.T) {
p := enmime.NewPart("application/pgp-encrypted").
WithContentTransferEncoding("7bit")
p.Content = []byte("Version: 1")

b := &bytes.Buffer{}
if err := p.Encode(b); err != nil {
t.Fatal(err)
}

assert.Equal(t, "7bit", p.Header.Get("Content-Transfer-Encoding"))
assert.Contains(t, b.String(), "Version: 1")
}

// 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")
Expand Down
Loading
Loading