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!
This commit is contained in:
Mechiel Lukkien 2024-11-01 11:28:25 +01:00
parent 598c5ea6ac
commit 8fa197b19d
No known key found for this signature in database
2 changed files with 43 additions and 25 deletions

View file

@ -406,7 +406,7 @@ func (cmd *fetchCmd) xprocessAtt(a fetchAtt) []token {
case "BODYSTRUCTURE": case "BODYSTRUCTURE":
_, part := cmd.xensureParsed() _, part := cmd.xensureParsed()
bs := xbodystructure(part) bs := xbodystructure(part, true)
return []token{bare("BODYSTRUCTURE"), bs} return []token{bare("BODYSTRUCTURE"), bs}
case "BODY": case "BODY":
@ -660,7 +660,7 @@ func (cmd *fetchCmd) xbody(a fetchAtt) (string, token) {
if a.section == nil { if a.section == nil {
// Non-extensible form of BODYSTRUCTURE. // Non-extensible form of BODYSTRUCTURE.
return a.field, xbodystructure(part) return a.field, xbodystructure(part, false)
} }
cmd.peekOrSeen(a.peek) cmd.peekOrSeen(a.peek)
@ -865,20 +865,33 @@ func bodyFldEnc(s string) token {
// xbodystructure returns a "body". // xbodystructure returns a "body".
// calls itself for multipart messages and message/{rfc822,global}. // 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" { if p.MediaType == "MULTIPART" {
// Multipart, ../rfc/9051:6355 ../rfc/9051:6411 // Multipart, ../rfc/9051:6355 ../rfc/9051:6411
var bodies concat var bodies concat
for i := range p.Parts { 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 // ../rfc/9051:6355
var r listspace
if p.MediaType == "TEXT" { if p.MediaType == "TEXT" {
// ../rfc/9051:6404 ../rfc/9051:6418 // ../rfc/9051:6404 ../rfc/9051:6418
return listspace{ r = listspace{
dquote("TEXT"), string0(p.MediaSubType), // ../rfc/9051:6739 dquote("TEXT"), string0(p.MediaSubType), // ../rfc/9051:6739
// ../rfc/9051:6376 // ../rfc/9051:6376
bodyFldParams(p.ContentTypeParams), // ../rfc/9051:6401 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") { } else if p.MediaType == "MESSAGE" && (p.MediaSubType == "RFC822" || p.MediaSubType == "GLOBAL") {
// ../rfc/9051:6415 // ../rfc/9051:6415
// note: we don't have to prepare p.Message for reading, because we aren't going to read from it. // 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 dquote("MESSAGE"), dquote(p.MediaSubType), // ../rfc/9051:6732
// ../rfc/9051:6376 // ../rfc/9051:6376
bodyFldParams(p.ContentTypeParams), // ../rfc/9051:6401 bodyFldParams(p.ContentTypeParams), // ../rfc/9051:6401
@ -900,25 +913,28 @@ func xbodystructure(p *message.Part) token {
bodyFldEnc(p.ContentTransferEncoding), bodyFldEnc(p.ContentTransferEncoding),
number(p.EndOffset - p.BodyOffset), number(p.EndOffset - p.BodyOffset),
xenvelope(p.Message), xenvelope(p.Message),
xbodystructure(p.Message), xbodystructure(p.Message, extensible),
number(p.RawLineCount), // todo: or mp.RawLineCount? 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 // 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
switch p.MediaType { return r
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),
}
} }

View file

@ -241,6 +241,7 @@ func TestFetch(t *testing.T) {
imapclient.BodyTypeBasic{MediaType: "IMAGE", MediaSubtype: "JPEG", BodyFields: imapclient.BodyFields{CTE: "BASE64"}}, imapclient.BodyTypeBasic{MediaType: "IMAGE", MediaSubtype: "JPEG", BodyFields: imapclient.BodyFields{CTE: "BASE64"}},
}, },
MediaSubtype: "PARALLEL", 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.BodyTypeText{MediaType: "TEXT", MediaSubtype: "ENRICHED", BodyFields: imapclient.BodyFields{Octets: 145}, Lines: 5},
imapclient.BodyTypeMsg{ imapclient.BodyTypeMsg{
@ -260,6 +261,7 @@ func TestFetch(t *testing.T) {
}, },
}, },
MediaSubtype: "MIXED", MediaSubtype: "MIXED",
Ext: &imapclient.BodyExtensionMpart{Params: [][2]string{{"boundary", "unique-boundary-1"}}},
}, },
} }
tc.client.Append("inbox", nil, &received, []byte(nestedMessage)) tc.client.Append("inbox", nil, &received, []byte(nestedMessage))