From 39c21f80cd319474b62519f95c5bb7e25ecf1346 Mon Sep 17 00:00:00 2001 From: Mechiel Lukkien Date: Mon, 7 Apr 2025 12:15:13 +0200 Subject: [PATCH] imapserver: return proper response for FETCH of "BODY[1.MIME]" where 1 is a message MIME returns the part headers. If 1 is a message, i.e. a message/rfc822 or message/global, for example when top-level is a multipart/mixed, we were returning the MIME headers from the message, not from the part. We also shouldn't be returning a MIME-Version header or the separating newline for MIME. Those are for MIME headers of a message, but the "MIME" fetch body part is always about the part. Found after looking into FETCH BODY handling for issue #327. --- imapserver/fetch.go | 32 ++++++++++++++++++-------------- imapserver/fetch_test.go | 32 +++++++++++++++++--------------- 2 files changed, 35 insertions(+), 29 deletions(-) diff --git a/imapserver/fetch.go b/imapserver/fetch.go index 4171aef..5fe7588 100644 --- a/imapserver/fetch.go +++ b/imapserver/fetch.go @@ -826,35 +826,40 @@ func (cmd *fetchCmd) xpartnumsDeref(nums []uint32, p *message.Part) *message.Par } func (cmd *fetchCmd) xsection(section *sectionSpec, p *message.Part) io.Reader { + // msgtext is not nil, i.e. HEADER* or TEXT (not MIME), for the top-level part (a message). if section.part == nil { return cmd.xsectionMsgtext(section.msgtext, p) } p = cmd.xpartnumsDeref(section.part.part, p) + // If there is no sectionMsgText, then this isn't for HEADER*, TEXT or MIME, i.e. a + // part body, e.g. "BODY[1]". if section.part.text == nil { return p.RawReader() } - // ../rfc/9051:4535 - if p.Message != nil { + // MIME is defined for all parts. Otherwise it's HEADER* or TEXT, which is only + // defined for parts that are messages. ../rfc/9051:4500 ../rfc/9051:4517 + if !section.part.text.mime { + if p.Message == nil { + cmd.xerrorf("part is not a message, cannot request header* or text") + } + err := p.SetMessageReaderAt() cmd.xcheckf(err, "preparing submessage") p = p.Message - } - if !section.part.text.mime { return cmd.xsectionMsgtext(section.part.text.msgtext, p) } - // MIME header, see ../rfc/9051:4534 ../rfc/2045:1645 + // MIME header, see ../rfc/9051:4514 ../rfc/2045:1652 h, err := io.ReadAll(p.HeaderReader()) cmd.xcheckf(err, "reading header") matchesFields := func(line []byte) bool { k := textproto.CanonicalMIMEHeaderKey(string(bytes.TrimRight(bytes.SplitN(line, []byte(":"), 2)[0], " \t"))) - // Only add MIME-Version and additional CRLF for messages, not other parts. ../rfc/2045:1645 ../rfc/2045:1652 - return (p.Envelope != nil && k == "Mime-Version") || strings.HasPrefix(k, "Content-") + return strings.HasPrefix(k, "Content-") } var match bool @@ -868,7 +873,7 @@ func (cmd *fetchCmd) xsection(section *sectionSpec, p *message.Part) io.Reader { h = h[len(line):] match = matchesFields(line) || match && (bytes.HasPrefix(line, []byte(" ")) || bytes.HasPrefix(line, []byte("\t"))) - if match || len(line) == 2 { + if match { hb.Write(line) } } @@ -876,11 +881,10 @@ func (cmd *fetchCmd) xsection(section *sectionSpec, p *message.Part) io.Reader { } func (cmd *fetchCmd) xsectionMsgtext(smt *sectionMsgtext, p *message.Part) io.Reader { - if smt.s == "HEADER" { - return p.HeaderReader() - } - switch smt.s { + case "HEADER": + return p.HeaderReader() + case "HEADER.FIELDS": return cmd.xmodifiedHeader(p, smt.headers, false) @@ -888,8 +892,8 @@ func (cmd *fetchCmd) xsectionMsgtext(smt *sectionMsgtext, p *message.Part) io.Re return cmd.xmodifiedHeader(p, smt.headers, true) case "TEXT": - // It appears imap clients expect to get the body of the message, not a "text body" - // which sounds like it means a text/* part of a message. ../rfc/9051:4517 + // TEXT the body (excluding headers) of a message, either the top-level message, or + // a nested as message/rfc822 or message/global. ../rfc/9051:4517 return p.RawReader() } panic(serverError{fmt.Errorf("missing case")}) diff --git a/imapserver/fetch_test.go b/imapserver/fetch_test.go index ad84c43..fe87436 100644 --- a/imapserver/fetch_test.go +++ b/imapserver/fetch_test.go @@ -78,9 +78,7 @@ func TestFetch(t *testing.T) { headerSplit := strings.SplitN(exampleMsgHeader, "\r\n", 2) dateheader1 := imapclient.FetchBody{RespAttr: "BODY[HEADER.FIELDS (Date)]", Section: "HEADER.FIELDS (Date)", Body: headerSplit[0] + "\r\n\r\n"} nodateheader1 := imapclient.FetchBody{RespAttr: "BODY[HEADER.FIELDS.NOT (Date)]", Section: "HEADER.FIELDS.NOT (Date)", Body: headerSplit[1]} - date1header1 := imapclient.FetchBody{RespAttr: "BODY[1.HEADER.FIELDS (Date)]", Section: "1.HEADER.FIELDS (Date)", Body: headerSplit[0] + "\r\n\r\n"} - nodate1header1 := imapclient.FetchBody{RespAttr: "BODY[1.HEADER.FIELDS.NOT (Date)]", Section: "1.HEADER.FIELDS.NOT (Date)", Body: headerSplit[1]} - mime1 := imapclient.FetchBody{RespAttr: "BODY[1.MIME]", Section: "1.MIME", Body: "MIME-Version: 1.0\r\nContent-Type: TEXT/PLAIN; CHARSET=US-ASCII\r\n\r\n"} + mime1 := imapclient.FetchBody{RespAttr: "BODY[1.MIME]", Section: "1.MIME", Body: "Content-Type: TEXT/PLAIN; CHARSET=US-ASCII\r\n"} flagsSeen := imapclient.FetchFlags{`\Seen`} @@ -212,11 +210,10 @@ func TestFetch(t *testing.T) { tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, dateheader1}}) tc.transactf("ok", "fetch 1 body.peek[header.fields.not (date)]") tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, nodateheader1}}) - // For non-multipart messages, 1 means the whole message. ../rfc/9051:4481 - tc.transactf("ok", "fetch 1 body.peek[1.header.fields (date)]") - tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, date1header1}}) - tc.transactf("ok", "fetch 1 body.peek[1.header.fields.not (date)]") - tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, nodate1header1}}) + // For non-multipart messages, 1 means the whole message, but since it's not of + // type message/{rfc822,global} (a message), you can't get the message headers. + // ../rfc/9051:4481 + tc.transactf("no", "fetch 1 body.peek[1.header]") // MIME, part 1 for non-multipart messages is the message itself. ../rfc/9051:4481 tc.transactf("ok", "fetch 1 body.peek[1.mime]") @@ -407,9 +404,7 @@ Content-Disposition: inline; filename=image.jpg tc.transactf("ok", "fetch 2 body.peek[3]") tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, imapclient.FetchBody{RespAttr: "BODY[3]", Section: "3", Body: part3}}}) - part2mime := tocrlf(`Content-type: text/plain; charset=US-ASCII - -`) + part2mime := "Content-type: text/plain; charset=US-ASCII\r\n" tc.transactf("ok", "fetch 2 body.peek[2.mime]") tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, imapclient.FetchBody{RespAttr: "BODY[2.MIME]", Section: "2.MIME", Body: part2mime}}}) @@ -434,19 +429,26 @@ Content-Transfer-Encoding: Quoted-printable tc.transactf("ok", "fetch 2 body.peek[5.header]") tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, imapclient.FetchBody{RespAttr: "BODY[5.HEADER]", Section: "5.HEADER", Body: part5header}}}) - part5mime := tocrlf(`Content-Type: Text/plain; charset=ISO-8859-1 -Content-Transfer-Encoding: Quoted-printable - + part5mime := tocrlf(`Content-Type: message/rfc822 +Content-MD5: MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY= +Content-Language: en,de +Content-Location: http://localhost `) tc.transactf("ok", "fetch 2 body.peek[5.mime]") tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, imapclient.FetchBody{RespAttr: "BODY[5.MIME]", Section: "5.MIME", Body: part5mime}}}) part5text := " ... Additional text in ISO-8859-1 goes here ...\r\n" + tc.transactf("ok", "fetch 2 body.peek[5.text]") tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, imapclient.FetchBody{RespAttr: "BODY[5.TEXT]", Section: "5.TEXT", Body: part5text}}}) + part5body := " ... Additional text in ISO-8859-1 goes here ...\r\n" tc.transactf("ok", "fetch 2 body.peek[5.1]") - tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, imapclient.FetchBody{RespAttr: "BODY[5.1]", Section: "5.1", Body: part5text}}}) + tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, imapclient.FetchBody{RespAttr: "BODY[5.1]", Section: "5.1", Body: part5body}}}) + + // 5.1 is the part that is the sub message, but not as message/rfc822, but as part, + // so we cannot request a header. + tc.transactf("no", "fetch 2 body.peek[5.1.header]") // In case of EXAMINE instead of SELECT, we should not be seeing any changed \Seen flags for non-peek commands. tc.client.StoreFlagsClear("1", true, `\Seen`)