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:
+30
-9
@@ -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)
|
||||
|
||||
@@ -95,7 +95,9 @@ func ParseMessage(uid uint32, raw []byte) (Message, error) {
|
||||
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) {
|
||||
m, err := ParseMessage(uid, raw)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user