Files
emcli/internal/mail/message.go
T
steve 5d2461ad94 fix(mail): drain UidFetch channel on early error; clarify ParseHeaderOnly doc
If header/body parsing errored mid-fetch we returned without draining the
message channel, so the UidFetch goroutine could block on a full channel.
Both fetch paths now break, drain remaining messages, then read the done
error. Verified with the race detector.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 07:51:45 +01:00

121 lines
3.1 KiB
Go

// Package mail provides IMAP reading and RFC822 message parsing.
package mail
import (
"bytes"
"fmt"
"io"
"strings"
"github.com/emersion/go-message/mail"
)
type Header struct {
UID uint32
From string
To string
Subject string
Date string
MessageID string
HasAttachments bool
}
type Attachment struct {
Name string
Size int
MIME string
Content []byte
}
type Message struct {
Header Header
BodyText string
Attachments []Attachment
}
func readHeader(mr *mail.Reader, uid uint32) Header {
h := Header{UID: uid}
hd := mr.Header
h.Subject, _ = hd.Subject()
if addrs, err := hd.AddressList("From"); err == nil && len(addrs) > 0 {
h.From = addrs[0].String()
}
if addrs, err := hd.AddressList("To"); err == nil && len(addrs) > 0 {
h.To = addrs[0].String()
}
if d, err := hd.Date(); err == nil {
h.Date = d.UTC().Format("Mon, 02 Jan 2006 15:04:05 -0700")
}
if msgID, err := hd.MessageID(); err == nil {
h.MessageID = msgID
} else {
h.MessageID = strings.Trim(hd.Get("Message-Id"), "<> ")
}
return h
}
// ParseMessage decodes the full message including attachment contents.
func ParseMessage(uid uint32, raw []byte) (Message, error) {
mr, err := mail.CreateReader(bytes.NewReader(raw))
if err != nil {
return Message{}, err
}
m := Message{Header: readHeader(mr, uid)}
for {
part, err := mr.NextPart()
if err == io.EOF {
break
}
if err != nil {
return Message{}, err
}
switch hdr := part.Header.(type) {
case *mail.InlineHeader:
ct, _, _ := hdr.ContentType()
if strings.HasPrefix(ct, "text/plain") && m.BodyText == "" {
b, err := io.ReadAll(part.Body)
if err != nil {
return Message{}, fmt.Errorf("read message part: %w", err)
}
m.BodyText = string(b)
}
case *mail.AttachmentHeader:
name, _ := hdr.Filename()
ct, _, _ := hdr.ContentType()
b, err := io.ReadAll(part.Body)
if err != nil {
return Message{}, fmt.Errorf("read message part: %w", err)
}
m.Attachments = append(m.Attachments, Attachment{
Name: name, Size: len(b), MIME: ct, Content: b,
})
}
}
m.Header.HasAttachments = len(m.Attachments) > 0
return m, nil
}
// ParseHeaderOnly parses a full RFC822 message (raw bytes) and returns only the
// Header, with HasAttachments set. For IMAP BODY[HEADER]-only bytes (no body),
// use ParseHeaderBytes instead, which avoids downloading the body entirely.
func ParseHeaderOnly(uid uint32, raw []byte) (Header, error) {
m, err := ParseMessage(uid, raw)
if err != nil {
return Header{}, err
}
return m.Header, nil
}
// ParseHeaderBytes parses just a message's RFC822 header block (the bytes from
// an IMAP BODY[HEADER] fetch, with no body) into a Header, reusing the same
// RFC2047-decoding and formatting as ParseMessage. HasAttachments is left false:
// the caller, which holds the BODYSTRUCTURE, sets it. This lets list/search read
// headers without downloading message bodies.
func ParseHeaderBytes(uid uint32, raw []byte) (Header, error) {
mr, err := mail.CreateReader(bytes.NewReader(raw))
if err != nil {
return Header{}, err
}
return readHeader(mr, uid), nil
}