imapserver: return all the extensible fields for bodystructure, notably for content-disposition

The gmail iOS/Android app were showing mime image parts as (garbled) text
instead of rendering them as image. By returning all the optional fields in the
bodystructure fetch attribute, the gmail app renders the image as expected by
the user. So we now add all fields. We didn't before, because we weren't
keeping track of Content-MD5, Content-Language and Content-Location header
fields, since they aren't that useful.

Messages in mailboxes have to be reparsed:
	./mox reparse

Without reparsing, imap responses will claim the extra fields
(content-disposition) are absent for existing messages, instead of not claiming
anything at all, which is what we did before.

Accounts and all/some mailboxes can get their "uid validity" bumped ("./mox
bumpuidvalidity $account [$mailbox]"), which should trigger clients to load all
messages from scratch, but gmail doesn't appear to notice, so it would be
better to remove & add the account in gmail.

For issue #327, also relevant to issue #217.
This commit is contained in:
Mechiel Lukkien
2025-04-05 15:46:17 +02:00
parent 69d2699961
commit 2defbce0bc
11 changed files with 185 additions and 65 deletions

View File

@ -67,7 +67,11 @@ type Part struct {
ContentTypeParams map[string]string // E.g. holds "boundary" for multipart messages. Has lower-case keys, and original case values.
ContentID string
ContentDescription string
ContentTransferEncoding string // In upper case.
ContentTransferEncoding string // In upper case.
ContentDisposition string
ContentMD5 string
ContentLanguage string
ContentLocation string
Envelope *Envelope // Email message headers. Not for non-message parts.
Parts []Part // Parts if this is a multipart.
@ -155,6 +159,10 @@ func fallbackPart(p Part, r io.ReaderAt, size int64) (Part, error) {
ContentID: p.ContentID,
ContentDescription: p.ContentDescription,
ContentTransferEncoding: p.ContentTransferEncoding,
ContentDisposition: p.ContentDisposition,
ContentMD5: p.ContentMD5,
ContentLanguage: p.ContentLanguage,
ContentLocation: p.ContentLocation,
Envelope: p.Envelope,
// We don't keep:
// - BoundaryOffset: irrelevant for top-level message.
@ -357,6 +365,10 @@ func newPart(log mlog.Log, strict bool, r io.ReaderAt, offset int64, parent *Par
p.ContentID = p.header.Get("Content-Id")
p.ContentDescription = p.header.Get("Content-Description")
p.ContentTransferEncoding = strings.ToUpper(p.header.Get("Content-Transfer-Encoding"))
p.ContentDisposition = p.header.Get("Content-Disposition")
p.ContentMD5 = p.header.Get("Content-Md5")
p.ContentLanguage = p.header.Get("Content-Language")
p.ContentLocation = p.header.Get("Content-Location")
if parent == nil {
p.Envelope, err = parseEnvelope(log, mail.Header(p.header))
@ -644,13 +656,16 @@ var ErrParamEncoding = errors.New("bad header parameter encoding")
// If the returned error is an ErrParamEncoding, it can be treated as a diagnostic
// and a filename may still be returned.
func (p *Part) DispositionFilename() (disposition string, filename string, err error) {
h, err := p.Header()
if err != nil {
return "", "", fmt.Errorf("parsing header: %w", err)
cd := p.ContentDisposition
if cd == "" {
h, err := p.Header()
if err != nil {
return "", "", fmt.Errorf("parsing header: %w", err)
}
cd = h.Get("Content-Disposition")
}
var disp string
var params map[string]string
cd := h.Get("Content-Disposition")
if cd != "" {
disp, params, err = mime.ParseMediaType(cd)
}