message: when parsing a message, don't treat absent header and empty header value the same

We now use "*string" for such header fields, for Content-* fields, as used in
the imapserver when responding to FETCH commands. We'll now return NIL for an
absent header, and "" (empty string) if the header value is empty.
This commit is contained in:
Mechiel Lukkien
2025-04-16 13:04:55 +02:00
parent 3fe765dce9
commit 07533252b3
12 changed files with 96 additions and 59 deletions

View File

@ -65,14 +65,14 @@ type Part struct {
MediaType string // From Content-Type, upper case. E.g. "TEXT". Can be empty because content-type may be absent. In this case, the part may be treated as TEXT/PLAIN.
MediaSubType string // From Content-Type, upper case. E.g. "PLAIN".
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.
ContentDisposition string
ContentMD5 string
ContentLanguage string
ContentLocation string
Envelope *Envelope // Email message headers. Not for non-message parts.
ContentID *string `json:",omitempty"`
ContentDescription *string `json:",omitempty"`
ContentTransferEncoding *string `json:",omitempty"` // In upper case.
ContentDisposition *string `json:",omitempty"`
ContentMD5 *string `json:",omitempty"`
ContentLanguage *string `json:",omitempty"`
ContentLocation *string `json:",omitempty"`
Envelope *Envelope `json:",omitempty"` // Email message headers. Not for non-message parts.
Parts []Part // Parts if this is a multipart.
@ -362,13 +362,18 @@ 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")
p.ContentID = p.headerGet("Content-Id")
p.ContentDescription = p.headerGet("Content-Description")
cte := p.headerGet("Content-Transfer-Encoding")
if cte != nil {
s := strings.ToUpper(*cte)
cte = &s
}
p.ContentTransferEncoding = cte
p.ContentDisposition = p.headerGet("Content-Disposition")
p.ContentMD5 = p.headerGet("Content-Md5")
p.ContentLanguage = p.headerGet("Content-Language")
p.ContentLocation = p.headerGet("Content-Location")
if parent == nil {
p.Envelope, err = parseEnvelope(log, mail.Header(p.header))
@ -424,6 +429,15 @@ func (p *Part) Header() (textproto.MIMEHeader, error) {
return h, err
}
func (p *Part) headerGet(k string) *string {
l := p.header.Values(k)
if len(l) == 0 {
return nil
}
s := l[0]
return &s
}
// HeaderReader returns a reader for the header section of this part, including ending bare CRLF.
func (p *Part) HeaderReader() io.Reader {
return io.NewSectionReader(p.r, p.HeaderOffset, p.BodyOffset-p.HeaderOffset)
@ -657,17 +671,10 @@ var ErrParamEncoding = errors.New("bad header parameter encoding")
// and a filename may still be returned.
func (p *Part) DispositionFilename() (disposition string, filename string, err error) {
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
if cd != "" {
disp, params, err = mime.ParseMediaType(cd)
if cd != nil && *cd != "" {
disp, params, err = mime.ParseMediaType(*cd)
}
if err != nil {
return "", "", fmt.Errorf("%w: parsing disposition header: %v", ErrParamEncoding, err)
@ -786,9 +793,13 @@ func (tr *textReader) Read(buf []byte) (int, error) {
return o, nil
}
func newDecoder(cte string, r io.Reader) io.Reader {
func newDecoder(cte *string, r io.Reader) io.Reader {
var s string
if cte != nil {
s = *cte
}
// ../rfc/2045:775
switch cte {
switch s {
case "BASE64":
return base64.NewDecoder(base64.StdEncoding, r)
case "QUOTED-PRINTABLE":