mime/quotedprintable: accept LWSP-char after =

SP and HTAB are allowed after a = before the following CRLF.

RFC 2045 section 6.7 describes the ABNF for the quoted-printable
encoding:

    qp-line := *(qp-segment transport-padding CRLF)
               qp-part transport-padding
    qp-segment := qp-section *(SPACE / TAB) "="
    transport-padding := *LWSP-char
                          ; Composers MUST NOT generate
                          ; non-zero length transport
                          ; padding, but receivers MUST
                          ; be able to handle padding
                          ; added by message transports.

RFC 822 defines LWSP-char as:

    LWSP-char   =  SPACE / HTAB

Dovecot's imaptest contains such a message in
src/tests/fetch-binary-mime-qp.mbox.

Fixes #70952
This commit is contained in:
Simon Ser 2024-12-21 14:22:33 +01:00
parent 669d87a935
commit e6e6eee8eb
2 changed files with 11 additions and 6 deletions

View file

@ -66,6 +66,7 @@ var (
crlf = []byte("\r\n") crlf = []byte("\r\n")
lf = []byte("\n") lf = []byte("\n")
softSuffix = []byte("=") softSuffix = []byte("=")
lwspChar = " \t"
) )
// Read reads and decodes quoted-printable data from the underlying reader. // Read reads and decodes quoted-printable data from the underlying reader.
@ -92,7 +93,7 @@ func (r *Reader) Read(p []byte) (n int, err error) {
wholeLine := r.line wholeLine := r.line
r.line = bytes.TrimRightFunc(wholeLine, isQPDiscardWhitespace) r.line = bytes.TrimRightFunc(wholeLine, isQPDiscardWhitespace)
if bytes.HasSuffix(r.line, softSuffix) { if bytes.HasSuffix(r.line, softSuffix) {
rightStripped := wholeLine[len(r.line):] rightStripped := bytes.TrimLeft(wholeLine[len(r.line):], lwspChar)
r.line = r.line[:len(r.line)-1] r.line = r.line[:len(r.line)-1]
if !bytes.HasPrefix(rightStripped, lf) && !bytes.HasPrefix(rightStripped, crlf) && if !bytes.HasPrefix(rightStripped, lf) && !bytes.HasPrefix(rightStripped, crlf) &&
!(len(rightStripped) == 0 && len(r.line) > 0 && r.rerr == io.EOF) { !(len(rightStripped) == 0 && len(r.line) > 0 && r.rerr == io.EOF) {

View file

@ -66,6 +66,10 @@ func TestReader(t *testing.T) {
want: "Now's the time for all folk to come to the aid of their country."}, want: "Now's the time for all folk to come to the aid of their country."},
{in: "accept UTF-8 right quotation mark: ", {in: "accept UTF-8 right quotation mark: ",
want: "accept UTF-8 right quotation mark: "}, want: "accept UTF-8 right quotation mark: "},
// Transport padding
{in: "foo= \r\nbar", want: "foobar"},
{in: "foo=\t \r\nbar", want: "foobar"},
} }
for _, tt := range tests { for _, tt := range tests {
var buf strings.Builder var buf strings.Builder
@ -199,13 +203,13 @@ func TestExhaustive(t *testing.T) {
} }
slices.Sort(outcomes) slices.Sort(outcomes)
got := strings.Join(outcomes, "\n") got := strings.Join(outcomes, "\n")
want := `OK: 28934 want := `OK: 30638
invalid bytes after =: 3949 invalid bytes after =: 2243
quotedprintable: invalid hex byte 0x0d: 2048 quotedprintable: invalid hex byte 0x0d: 2050
unexpected EOF: 194` unexpected EOF: 194`
if testing.Short() { if testing.Short() {
want = `OK: 896 want = `OK: 935
invalid bytes after =: 100 invalid bytes after =: 61
quotedprintable: invalid hex byte 0x0d: 26 quotedprintable: invalid hex byte 0x0d: 26
unexpected EOF: 3` unexpected EOF: 3`
} }