aerc/worker/imap/fetch.go
Robin Jarry 5dec1f09b1 imap: fix error when server returns a message without body section
When opening unread emails from certain people (I won't name any names,
sorry), an annoying error message is displayed on the status line:

 could not get section &imap.BodySectionName{BodyPartName:
 imap.BodyPartName{Specifier:"", Path:[]int(nil), Fields:[]string(nil),
 NotFields:false}, Peek:false, Partial:[]int(nil), value}

This does not occur for already read messages. This issue is similar to
the one that was fixed in commit 8ed95b0d2a ("imap: avoid crash when
replying to unread message").

This happens because the flags are updated in the callback that receives
the message itself. It causes the flag update to arrive in the same
channel/request. Ignore the messages that have an empty body (i.e. only
containing flag updates). This is inherently racy but there seems no way
to get rid of these extra messages.

Signed-off-by: Robin Jarry <robin@jarry.cc>
Acked-by: Koni Marti <koni.marti@gmail.com>
2022-07-23 21:51:47 +02:00

250 lines
6.2 KiB
Go

package imap
import (
"bufio"
"fmt"
"github.com/emersion/go-imap"
"github.com/emersion/go-message"
_ "github.com/emersion/go-message/charset"
"github.com/emersion/go-message/mail"
"github.com/emersion/go-message/textproto"
"git.sr.ht/~rjarry/aerc/logging"
"git.sr.ht/~rjarry/aerc/models"
"git.sr.ht/~rjarry/aerc/worker/types"
)
func (imapw *IMAPWorker) handleFetchMessageHeaders(
msg *types.FetchMessageHeaders) {
toFetch := msg.Uids
if imapw.config.cacheEnabled && imapw.cache != nil {
toFetch = imapw.getCachedHeaders(msg)
}
if len(toFetch) == 0 {
imapw.worker.PostMessage(&types.Done{Message: types.RespondTo(msg)},
nil)
return
}
imapw.worker.Logger.Printf("Fetching message headers")
section := &imap.BodySectionName{
BodyPartName: imap.BodyPartName{
Specifier: imap.HeaderSpecifier,
},
Peek: true,
}
items := []imap.FetchItem{
imap.FetchBodyStructure,
imap.FetchEnvelope,
imap.FetchInternalDate,
imap.FetchFlags,
imap.FetchUid,
section.FetchItem(),
}
imapw.handleFetchMessages(msg, toFetch, items,
func(_msg *imap.Message) error {
reader := _msg.GetBody(section)
textprotoHeader, err := textproto.ReadHeader(bufio.NewReader(reader))
if err != nil {
imapw.worker.Logger.Printf(
"message %v: could not read header: %v", _msg.Uid, err)
imapw.worker.PostMessage(&types.Error{
Message: types.RespondTo(msg),
Error: err,
}, nil)
return nil
}
header := &mail.Header{Header: message.Header{Header: textprotoHeader}}
info := &models.MessageInfo{
BodyStructure: translateBodyStructure(_msg.BodyStructure),
Envelope: translateEnvelope(_msg.Envelope),
Flags: translateImapFlags(_msg.Flags),
InternalDate: _msg.InternalDate,
RFC822Headers: header,
Uid: _msg.Uid,
}
imapw.worker.PostMessage(&types.MessageInfo{
Message: types.RespondTo(msg),
Info: info,
}, nil)
if imapw.config.cacheEnabled && imapw.cache != nil {
imapw.cacheHeader(info)
}
return nil
})
}
func (imapw *IMAPWorker) handleFetchMessageBodyPart(
msg *types.FetchMessageBodyPart) {
imapw.worker.Logger.Printf("Fetching message part")
var partHeaderSection imap.BodySectionName
partHeaderSection.Peek = true
if len(msg.Part) > 0 {
partHeaderSection.Specifier = imap.MIMESpecifier
} else {
partHeaderSection.Specifier = imap.HeaderSpecifier
}
partHeaderSection.Path = msg.Part
var partBodySection imap.BodySectionName
if len(msg.Part) > 0 {
partBodySection.Specifier = imap.EntireSpecifier
} else {
partBodySection.Specifier = imap.TextSpecifier
}
partBodySection.Path = msg.Part
items := []imap.FetchItem{
imap.FetchEnvelope,
imap.FetchUid,
imap.FetchBodyStructure,
imap.FetchFlags,
partHeaderSection.FetchItem(),
partBodySection.FetchItem(),
}
imapw.handleFetchMessages(msg, []uint32{msg.Uid}, items,
func(_msg *imap.Message) error {
if len(_msg.Body) == 0 {
// ignore duplicate messages with only flag updates
return nil
}
body := _msg.GetBody(&partHeaderSection)
if body == nil {
return fmt.Errorf("failed to find part: %v", partHeaderSection)
}
h, err := textproto.ReadHeader(bufio.NewReader(body))
if err != nil {
return fmt.Errorf("failed to read part header: %v", err)
}
part, err := message.New(message.Header{Header: h},
_msg.GetBody(&partBodySection))
if err != nil {
return fmt.Errorf("failed to create message reader: %v", err)
}
imapw.worker.PostMessage(&types.MessageBodyPart{
Message: types.RespondTo(msg),
Part: &models.MessageBodyPart{
Reader: part.Body,
Uid: _msg.Uid,
},
}, nil)
// Update flags (to mark message as read)
imapw.worker.PostMessage(&types.MessageInfo{
Message: types.RespondTo(msg),
Info: &models.MessageInfo{
Flags: translateImapFlags(_msg.Flags),
Uid: _msg.Uid,
},
}, nil)
return nil
})
}
func (imapw *IMAPWorker) handleFetchFullMessages(
msg *types.FetchFullMessages) {
imapw.worker.Logger.Printf("Fetching full messages")
section := &imap.BodySectionName{}
items := []imap.FetchItem{
imap.FetchEnvelope,
imap.FetchFlags,
imap.FetchUid,
section.FetchItem(),
}
imapw.handleFetchMessages(msg, msg.Uids, items,
func(_msg *imap.Message) error {
if len(_msg.Body) == 0 {
// ignore duplicate messages with only flag updates
return nil
}
r := _msg.GetBody(section)
if r == nil {
return fmt.Errorf("could not get section %#v", section)
}
imapw.worker.PostMessage(&types.FullMessage{
Message: types.RespondTo(msg),
Content: &models.FullMessage{
Reader: bufio.NewReader(r),
Uid: _msg.Uid,
},
}, nil)
// Update flags (to mark message as read)
imapw.worker.PostMessage(&types.MessageInfo{
Message: types.RespondTo(msg),
Info: &models.MessageInfo{
Flags: translateImapFlags(_msg.Flags),
Uid: _msg.Uid,
},
}, nil)
return nil
})
}
func (imapw *IMAPWorker) handleFetchMessageFlags(msg *types.FetchMessageFlags) {
items := []imap.FetchItem{
imap.FetchFlags,
imap.FetchUid,
}
imapw.handleFetchMessages(msg, msg.Uids, items,
func(_msg *imap.Message) error {
imapw.worker.PostMessage(&types.MessageInfo{
Message: types.RespondTo(msg),
Info: &models.MessageInfo{
Flags: translateImapFlags(_msg.Flags),
Uid: _msg.Uid,
},
}, nil)
return nil
})
}
func (imapw *IMAPWorker) handleFetchMessages(
msg types.WorkerMessage, uids []uint32, items []imap.FetchItem,
procFunc func(*imap.Message) error) {
messages := make(chan *imap.Message)
done := make(chan error)
go func() {
defer logging.PanicHandler()
var reterr error
for _msg := range messages {
imapw.seqMap.Put(_msg.SeqNum, _msg.Uid)
err := procFunc(_msg)
if err != nil {
if reterr == nil {
reterr = err
}
// drain the channel upon error
for range messages {
}
}
}
done <- reterr
}()
emitErr := func(err error) {
imapw.worker.PostMessage(&types.Error{
Message: types.RespondTo(msg),
Error: err,
}, nil)
}
set := toSeqSet(uids)
if err := imapw.client.UidFetch(set, items, messages); err != nil {
emitErr(err)
return
}
if err := <-done; err != nil {
emitErr(err)
return
}
imapw.worker.PostMessage(
&types.Done{Message: types.RespondTo(msg)}, nil)
}