diff --git a/imapserver/fetch.go b/imapserver/fetch.go index 1cb666a9d..250b7af0d 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 a034969cc..8c8d8a74e 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))