Skip to content

Commit

Permalink
imapserver: for the "bodystructure" fetch response item, add the cont…
Browse files Browse the repository at this point in the history
…ent-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!
  • Loading branch information
mjl- committed Nov 1, 2024
1 parent 598c5ea commit 8fa197b
Show file tree
Hide file tree
Showing 2 changed files with 43 additions and 25 deletions.
66 changes: 41 additions & 25 deletions imapserver/fetch.go
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
}
2 changes: 2 additions & 0 deletions imapserver/fetch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand All @@ -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))
Expand Down

0 comments on commit 8fa197b

Please sign in to comment.