Skip to content

Commit da268ad

Browse files
committed
always emit header when forced
Signed-off-by: James Hillyerd <james@hillyerd.com>
1 parent f657ca0 commit da268ad

2 files changed

Lines changed: 92 additions & 13 deletions

File tree

encode.go

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -119,13 +119,24 @@ func (p *Part) setupMIMEHeaders() transferEncoding {
119119
p.Header.Del(hnContentEncoding)
120120

121121
cte := te7Bit
122-
forcedCTE := false
123-
if len(p.Content) > 0 {
124-
// Check for explicit override first.
125-
if f := p.resolveForcedCTE(); f != teRaw {
126-
cte = f
127-
forcedCTE = true
128-
} else if strings.Index(strings.ToLower(p.ContentType), "message/") == 0 {
122+
if f := p.resolveForcedCTE(); f != teRaw {
123+
// An explicit override applies regardless of content length, so the header is always
124+
// emitted (including 7bit), even when the body is empty.
125+
cte = f
126+
switch f {
127+
case te7Bit:
128+
p.Header.Set(hnContentEncoding, cte7Bit)
129+
case te8Bit:
130+
p.Header.Set(hnContentEncoding, cte8Bit)
131+
case teBase64:
132+
p.Header.Set(hnContentEncoding, cteBase64)
133+
case teQuoted:
134+
p.Header.Set(hnContentEncoding, cteQuotedPrintable)
135+
}
136+
} else if len(p.Content) > 0 {
137+
// No override: select automatically. RFC 2045: 7bit is assumed if the CTE header is not
138+
// present, so for auto-detected 7bit (and for empty content) no header is emitted.
139+
if strings.Index(strings.ToLower(p.ContentType), "message/") == 0 {
129140
// RFC 1341: `message` types must have no encoding other than "7bit", "8bit", or
130141
// "binary". The message header fields are always US-ASCII in any case, and data within
131142
// the body can still be encoded, in which case the Content-Transfer-Encoding header
@@ -140,13 +151,7 @@ func (p *Part) setupMIMEHeaders() transferEncoding {
140151
}
141152
}
142153
}
143-
144-
// RFC 2045: 7bit is assumed if CTE header not present.
145154
switch cte {
146-
case te7Bit:
147-
if forcedCTE {
148-
p.Header.Set(hnContentEncoding, cte7Bit)
149-
}
150155
case te8Bit:
151156
p.Header.Set(hnContentEncoding, cte8Bit)
152157
case teBase64:

encode_test.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -626,6 +626,80 @@ func TestEncodePartForcedCTEWithContentReader(t *testing.T) {
626626
assert.Nil(t, p.ContentReader, "ContentReader should have been consumed")
627627
}
628628

629+
// TestEncodePartForcedCTEEmptyContent verifies that a forced Content-Transfer-Encoding is still
630+
// applied to the header when the body is empty -- either nil/zero-length Content or a ContentReader
631+
// that yields zero bytes. Previously the override was gated on len(Content) > 0 and was silently
632+
// dropped, contradicting the documented "forces" contract.
633+
func TestEncodePartForcedCTEEmptyContent(t *testing.T) {
634+
for _, tc := range []struct {
635+
name string
636+
cte string
637+
part func() *enmime.Part
638+
}{
639+
{
640+
name: "7bit nil content",
641+
cte: "7bit",
642+
part: func() *enmime.Part {
643+
p := enmime.NewPart("application/pgp-encrypted")
644+
p.ContentTransferEncoding = "7bit"
645+
return p
646+
},
647+
},
648+
{
649+
name: "base64 nil content",
650+
cte: "base64",
651+
part: func() *enmime.Part {
652+
p := enmime.NewPart("application/octet-stream")
653+
p.ContentTransferEncoding = "base64"
654+
return p
655+
},
656+
},
657+
{
658+
name: "7bit zero-byte reader",
659+
cte: "7bit",
660+
part: func() *enmime.Part {
661+
p := enmime.NewPart("application/pgp-encrypted")
662+
p.ContentTransferEncoding = "7bit"
663+
p.ContentReader = bytes.NewReader(nil)
664+
return p
665+
},
666+
},
667+
{
668+
name: "base64 zero-byte reader",
669+
cte: "base64",
670+
part: func() *enmime.Part {
671+
p := enmime.NewPart("application/octet-stream")
672+
p.ContentTransferEncoding = "base64"
673+
p.ContentReader = bytes.NewReader(nil)
674+
return p
675+
},
676+
},
677+
} {
678+
t.Run(tc.name, func(t *testing.T) {
679+
p := tc.part()
680+
b := &bytes.Buffer{}
681+
if err := p.Encode(b); err != nil {
682+
t.Fatal(err)
683+
}
684+
assert.Equal(t, tc.cte, p.Header.Get("Content-Transfer-Encoding"))
685+
})
686+
}
687+
}
688+
689+
// TestEncodePartAutoCTEEmptyContent verifies that, when no CTE override is set, an empty body still
690+
// results in no Content-Transfer-Encoding header (RFC 2045: 7bit is assumed when the header is
691+
// absent). This guards against the forced-override refactor accidentally emitting a header for
692+
// empty auto-detected parts.
693+
func TestEncodePartAutoCTEEmptyContent(t *testing.T) {
694+
p := enmime.NewPart("text/plain")
695+
// No ContentTransferEncoding set; Content is empty.
696+
b := &bytes.Buffer{}
697+
if err := p.Encode(b); err != nil {
698+
t.Fatal(err)
699+
}
700+
assert.Equal(t, "", p.Header.Get("Content-Transfer-Encoding"))
701+
}
702+
629703
// TestEncodePartForcedCTEPrecedenceOverEncoderOption verifies that ContentTransferEncoding on the part
630704
// takes precedence over the encoder-level ForceQuotedPrintableCte option. Without the override, the
631705
// non-ASCII text content combined with ForceQuotedPrintableCte(true) would be QP-encoded; the forced

0 commit comments

Comments
 (0)