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)
go func() { done <- c.c.UidFetch(set, items, msgCh) }()
var out []Header
var (
out []Header
ferr error
)
for m := range msgCh {
r := m.GetBody(section)
if r == nil {
@@ -111,17 +114,26 @@ func (c *Client) fetchHeadersByUIDSet(folder string, set *imap.SeqSet) ([]Header
}
raw, err := io.ReadAll(r)
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)
if err != nil {
return nil, err
ferr = err
break
}
h.HasAttachments = hasAttachment(m.BodyStructure)
out = append(out, h)
}
if err := <-done; err != nil {
return nil, err
// Drain any remaining messages so the UidFetch goroutine can finish and
// 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 })
return out, nil
@@ -144,6 +156,7 @@ func (c *Client) fetchFullByUID(folder string, uid uint32) (Message, error) {
var (
msg Message
found bool
ferr error
)
for m := range msgCh {
r := m.GetBody(section)
@@ -152,16 +165,24 @@ func (c *Client) fetchFullByUID(folder string, uid uint32) (Message, error) {
}
raw, err := io.ReadAll(r)
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)
if err != nil {
return Message{}, err
ferr = err
break
}
msg, found = parsed, true
}
if err := <-done; err != nil {
return Message{}, err
// Drain so the UidFetch goroutine can finish even after an early break.
for range msgCh {
}
if err := <-done; err != nil && ferr == nil {
ferr = err
}
if ferr != nil {
return Message{}, ferr
}
if !found {
return Message{}, fmt.Errorf("uid %d not found in %s", uid, folder)