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)
|
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)
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user