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>
This commit is contained in:
2026-06-22 07:51:45 +01:00
parent 8379fddbb2
commit 5d2461ad94
2 changed files with 33 additions and 10 deletions
+30 -9
View File
@@ -103,7 +103,10 @@ func (c *Client) fetchHeadersByUIDSet(folder string, set *imap.SeqSet) ([]Header
done := make(chan error, 1) done := make(chan error, 1)
go func() { done <- c.c.UidFetch(set, items, msgCh) }() go func() { done <- c.c.UidFetch(set, items, msgCh) }()
var out []Header var (
out []Header
ferr error
)
for m := range msgCh { for m := range msgCh {
r := m.GetBody(section) r := m.GetBody(section)
if r == nil { if r == nil {
@@ -111,17 +114,26 @@ func (c *Client) fetchHeadersByUIDSet(folder string, set *imap.SeqSet) ([]Header
} }
raw, err := io.ReadAll(r) raw, err := io.ReadAll(r)
if err != nil { if err != nil {
return nil, fmt.Errorf("uid %d: read header: %w", m.Uid, err) ferr = fmt.Errorf("uid %d: read header: %w", m.Uid, err)
break
} }
h, err := ParseHeaderBytes(m.Uid, raw) h, err := ParseHeaderBytes(m.Uid, raw)
if err != nil { if err != nil {
return nil, err ferr = err
break
} }
h.HasAttachments = hasAttachment(m.BodyStructure) h.HasAttachments = hasAttachment(m.BodyStructure)
out = append(out, h) out = append(out, h)
} }
if err := <-done; err != nil { // Drain any remaining messages so the UidFetch goroutine can finish and
return nil, err // never blocks on a full channel after an early break.
for range msgCh {
}
if err := <-done; err != nil && ferr == nil {
ferr = err
}
if ferr != nil {
return nil, ferr
} }
sort.Slice(out, func(i, j int) bool { return out[i].UID > out[j].UID }) sort.Slice(out, func(i, j int) bool { return out[i].UID > out[j].UID })
return out, nil return out, nil
@@ -144,6 +156,7 @@ func (c *Client) fetchFullByUID(folder string, uid uint32) (Message, error) {
var ( var (
msg Message msg Message
found bool found bool
ferr error
) )
for m := range msgCh { for m := range msgCh {
r := m.GetBody(section) r := m.GetBody(section)
@@ -152,16 +165,24 @@ func (c *Client) fetchFullByUID(folder string, uid uint32) (Message, error) {
} }
raw, err := io.ReadAll(r) raw, err := io.ReadAll(r)
if err != nil { if err != nil {
return Message{}, fmt.Errorf("uid %d: read body: %w", m.Uid, err) ferr = fmt.Errorf("uid %d: read body: %w", m.Uid, err)
break
} }
parsed, err := ParseMessage(m.Uid, raw) parsed, err := ParseMessage(m.Uid, raw)
if err != nil { if err != nil {
return Message{}, err ferr = err
break
} }
msg, found = parsed, true msg, found = parsed, true
} }
if err := <-done; err != nil { // Drain so the UidFetch goroutine can finish even after an early break.
return Message{}, err for range msgCh {
}
if err := <-done; err != nil && ferr == nil {
ferr = err
}
if ferr != nil {
return Message{}, ferr
} }
if !found { if !found {
return Message{}, fmt.Errorf("uid %d not found in %s", uid, folder) return Message{}, fmt.Errorf("uid %d not found in %s", uid, folder)
+3 -1
View File
@@ -95,7 +95,9 @@ func ParseMessage(uid uint32, raw []byte) (Message, error) {
return m, nil return m, nil
} }
// ParseHeaderOnly decodes headers and detects attachments without keeping bodies. // 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) { func ParseHeaderOnly(uid uint32, raw []byte) (Header, error) {
m, err := ParseMessage(uid, raw) m, err := ParseMessage(uid, raw)
if err != nil { if err != nil {