From 8fa197b19dfea22f3215e1768e8d90001f0ded06 Mon Sep 17 00:00:00 2001 From: Mechiel Lukkien Date: Fri, 1 Nov 2024 11:28:25 +0100 Subject: [PATCH] imapserver: for the "bodystructure" fetch response item, add the content-type parameters for multiparts so clients will get the mime boundary without having to parse the message themselves "bodystructure" is like "body", but bodystructure allows returning more information. we chose not to do that, initially because it was easier to implement, and more recently because we can't easily return the additional content-md5 field for leaf parts (since we don't have it in parsed form). but now we just return the extended form for multiparts, and non-extended form for leaf parts. likely no one would be looking for any content-md5-value for leaf parts anyway. knowing the boundary is much more likely to be useful. for issue #217 by danieleggert, thanks for reporting! --- imapserver/fetch.go | 66 +++++++++++++++++++++++++--------------- imapserver/fetch_test.go | 2 ++ 2 files changed, 43 insertions(+), 25 deletions(-) diff --git a/imapserver/fetch.go b/imapserver/fetch.go index 1cb666a9d0..250b7af0d6 100644 --- a/imapserver/fetch.go +++ b/imapserver/fetch.go @@ -406,7 +406,7 @@ func (cmd *fetchCmd) xprocessAtt(a fetchAtt) []token { case "BODYSTRUCTURE": _, part := cmd.xensureParsed() - bs := xbodystructure(part) + bs := xbodystructure(part, true) return []token{bare("BODYSTRUCTURE"), bs} case "BODY": @@ -660,7 +660,7 @@ func (cmd *fetchCmd) xbody(a fetchAtt) (string, token) { if a.section == nil { // Non-extensible form of BODYSTRUCTURE. - return a.field, xbodystructure(part) + return a.field, xbodystructure(part, false) } cmd.peekOrSeen(a.peek) @@ -865,20 +865,33 @@ func bodyFldEnc(s string) token { // xbodystructure returns a "body". // calls itself for multipart messages and message/{rfc822,global}. -func xbodystructure(p *message.Part) token { +func xbodystructure(p *message.Part, extensible bool) token { if p.MediaType == "MULTIPART" { // Multipart, ../rfc/9051:6355 ../rfc/9051:6411 var bodies concat for i := range p.Parts { - bodies = append(bodies, xbodystructure(&p.Parts[i])) + bodies = append(bodies, xbodystructure(&p.Parts[i], extensible)) } - return listspace{bodies, string0(p.MediaSubType)} + r := listspace{bodies, string0(p.MediaSubType)} + if extensible { + if len(p.ContentTypeParams) == 0 { + r = append(r, nilt) + } else { + params := make(listspace, 0, 2*len(p.ContentTypeParams)) + for k, v := range p.ContentTypeParams { + params = append(params, string0(k), string0(v)) + } + r = append(r, params) + } + } + return r } // ../rfc/9051:6355 + var r listspace if p.MediaType == "TEXT" { // ../rfc/9051:6404 ../rfc/9051:6418 - return listspace{ + r = listspace{ dquote("TEXT"), string0(p.MediaSubType), // ../rfc/9051:6739 // ../rfc/9051:6376 bodyFldParams(p.ContentTypeParams), // ../rfc/9051:6401 @@ -891,7 +904,7 @@ func xbodystructure(p *message.Part) token { } else if p.MediaType == "MESSAGE" && (p.MediaSubType == "RFC822" || p.MediaSubType == "GLOBAL") { // ../rfc/9051:6415 // note: we don't have to prepare p.Message for reading, because we aren't going to read from it. - return listspace{ + r = listspace{ dquote("MESSAGE"), dquote(p.MediaSubType), // ../rfc/9051:6732 // ../rfc/9051:6376 bodyFldParams(p.ContentTypeParams), // ../rfc/9051:6401 @@ -900,25 +913,28 @@ func xbodystructure(p *message.Part) token { bodyFldEnc(p.ContentTransferEncoding), number(p.EndOffset - p.BodyOffset), xenvelope(p.Message), - xbodystructure(p.Message), + xbodystructure(p.Message, extensible), number(p.RawLineCount), // todo: or mp.RawLineCount? } + } else { + var media token + switch p.MediaType { + case "APPLICATION", "AUDIO", "IMAGE", "FONT", "MESSAGE", "MODEL", "VIDEO": + media = dquote(p.MediaType) + default: + media = string0(p.MediaType) + } + // ../rfc/9051:6404 ../rfc/9051:6407 + r = listspace{ + media, string0(p.MediaSubType), // ../rfc/9051:6723 + // ../rfc/9051:6376 + bodyFldParams(p.ContentTypeParams), // ../rfc/9051:6401 + nilOrString(p.ContentID), + nilOrString(p.ContentDescription), + bodyFldEnc(p.ContentTransferEncoding), + number(p.EndOffset - p.BodyOffset), + } } - var media token - switch p.MediaType { - case "APPLICATION", "AUDIO", "IMAGE", "FONT", "MESSAGE", "MODEL", "VIDEO": - media = dquote(p.MediaType) - default: - media = string0(p.MediaType) - } - // ../rfc/9051:6404 ../rfc/9051:6407 - return listspace{ - media, string0(p.MediaSubType), // ../rfc/9051:6723 - // ../rfc/9051:6376 - bodyFldParams(p.ContentTypeParams), // ../rfc/9051:6401 - nilOrString(p.ContentID), - nilOrString(p.ContentDescription), - bodyFldEnc(p.ContentTransferEncoding), - number(p.EndOffset - p.BodyOffset), - } + // todo: if "extensible", we could add the value of the "content-md5" header. we don't have it in our parsed data structure, so we don't add it. likely no one would use it, also not any of the other optional fields. ../rfc/9051:6366 + return r } diff --git a/imapserver/fetch_test.go b/imapserver/fetch_test.go index a034969ccd..8c8d8a74e7 100644 --- a/imapserver/fetch_test.go +++ b/imapserver/fetch_test.go @@ -241,6 +241,7 @@ func TestFetch(t *testing.T) { imapclient.BodyTypeBasic{MediaType: "IMAGE", MediaSubtype: "JPEG", BodyFields: imapclient.BodyFields{CTE: "BASE64"}}, }, MediaSubtype: "PARALLEL", + Ext: &imapclient.BodyExtensionMpart{Params: [][2]string{{"boundary", "unique-boundary-2"}}}, }, imapclient.BodyTypeText{MediaType: "TEXT", MediaSubtype: "ENRICHED", BodyFields: imapclient.BodyFields{Octets: 145}, Lines: 5}, imapclient.BodyTypeMsg{ @@ -260,6 +261,7 @@ func TestFetch(t *testing.T) { }, }, MediaSubtype: "MIXED", + Ext: &imapclient.BodyExtensionMpart{Params: [][2]string{{"boundary", "unique-boundary-1"}}}, }, } tc.client.Append("inbox", nil, &received, []byte(nestedMessage))