aerc/lib/msgstore.go
Koni Marti f6e34e4cf1 imap: fix no-envelope-for-message error
Fix the "no envelope available for this message" error that can occur
when using the same imap mailbox in another mailclient (e.g.
through a webmail interface) at the same time.

Complements the fixes in commit 7fe7fe4 ("ui: fix panic in header
formatter") and commit 074b0a1 ("view,list: fix crash when viewing
incomplete imap messages").

The error is caused when a message attribute update is received by the
message store before the message list had a chance to fetch the proper
header first. In this case, an (incomplete) message info is stored in
the message store and the message list will never fetch the correct
header. To prevent this, add only messages to the store with a non-nil
envelope but make sure that message attribute updates are properly
merged.

Signed-off-by: Koni Marti <koni.marti@gmail.com>
Acked-by: Robin Jarry <robin@jarry.cc>
2022-04-11 12:06:53 +02:00

756 lines
18 KiB
Go

package lib
import (
"io"
"time"
"git.sr.ht/~rjarry/aerc/lib/sort"
"git.sr.ht/~rjarry/aerc/models"
"git.sr.ht/~rjarry/aerc/worker/types"
)
// Accesses to fields must be guarded by MessageStore.Lock/Unlock
type MessageStore struct {
Deleted map[uint32]interface{}
DirInfo models.DirectoryInfo
Messages map[uint32]*models.MessageInfo
Sorting bool
// Ordered list of known UIDs
uids []uint32
Threads []*types.Thread
selected int
bodyCallbacks map[uint32][]func(*types.FullMessage)
headerCallbacks map[uint32][]func(*types.MessageInfo)
//marking
marked map[uint32]struct{}
visualStartUid uint32
visualMarkMode bool
// Search/filter results
results []uint32
resultIndex int
filtered []uint32
filter bool
sortCriteria []*types.SortCriterion
thread bool
buildThreads bool
builder *ThreadBuilder
// Map of uids we've asked the worker to fetch
onUpdate func(store *MessageStore) // TODO: multiple onUpdate handlers
onFilterChange func(store *MessageStore)
onUpdateDirs func()
pendingBodies map[uint32]interface{}
pendingHeaders map[uint32]interface{}
worker *types.Worker
triggerNewEmail func(*models.MessageInfo)
triggerDirectoryChange func()
dirInfoUpdateDebounce *time.Timer
dirInfoUpdateDelay time.Duration
}
func NewMessageStore(worker *types.Worker,
dirInfo *models.DirectoryInfo,
defaultSortCriteria []*types.SortCriterion,
thread bool,
triggerNewEmail func(*models.MessageInfo),
triggerDirectoryChange func()) *MessageStore {
dirInfoUpdateDelay := 5 * time.Second
return &MessageStore{
Deleted: make(map[uint32]interface{}),
DirInfo: *dirInfo,
Messages: make(map[uint32]*models.MessageInfo),
selected: 0,
marked: make(map[uint32]struct{}),
bodyCallbacks: make(map[uint32][]func(*types.FullMessage)),
headerCallbacks: make(map[uint32][]func(*types.MessageInfo)),
thread: thread,
sortCriteria: defaultSortCriteria,
pendingBodies: make(map[uint32]interface{}),
pendingHeaders: make(map[uint32]interface{}),
worker: worker,
triggerNewEmail: triggerNewEmail,
triggerDirectoryChange: triggerDirectoryChange,
dirInfoUpdateDelay: dirInfoUpdateDelay,
dirInfoUpdateDebounce: time.NewTimer(dirInfoUpdateDelay),
}
}
func (store *MessageStore) FetchHeaders(uids []uint32,
cb func(*types.MessageInfo)) {
// TODO: this could be optimized by pre-allocating toFetch and trimming it
// at the end. In practice we expect to get most messages back in one frame.
var toFetch []uint32
for _, uid := range uids {
if _, ok := store.pendingHeaders[uid]; !ok {
toFetch = append(toFetch, uid)
store.pendingHeaders[uid] = nil
if cb != nil {
if list, ok := store.headerCallbacks[uid]; ok {
store.headerCallbacks[uid] = append(list, cb)
} else {
store.headerCallbacks[uid] = []func(*types.MessageInfo){cb}
}
}
}
}
if len(toFetch) > 0 {
store.worker.PostAction(&types.FetchMessageHeaders{Uids: toFetch}, nil)
}
}
func (store *MessageStore) FetchFull(uids []uint32, cb func(*types.FullMessage)) {
// TODO: this could be optimized by pre-allocating toFetch and trimming it
// at the end. In practice we expect to get most messages back in one frame.
var toFetch []uint32
for _, uid := range uids {
if _, ok := store.pendingBodies[uid]; !ok {
toFetch = append(toFetch, uid)
store.pendingBodies[uid] = nil
if cb != nil {
if list, ok := store.bodyCallbacks[uid]; ok {
store.bodyCallbacks[uid] = append(list, cb)
} else {
store.bodyCallbacks[uid] = []func(*types.FullMessage){cb}
}
}
}
}
if len(toFetch) > 0 {
store.worker.PostAction(&types.FetchFullMessages{
Uids: toFetch,
}, func(msg types.WorkerMessage) {
switch msg.(type) {
case *types.Error:
for _, uid := range toFetch {
delete(store.bodyCallbacks, uid)
}
}
})
}
}
func (store *MessageStore) FetchBodyPart(uid uint32, part []int, cb func(io.Reader)) {
store.worker.PostAction(&types.FetchMessageBodyPart{
Uid: uid,
Part: part,
}, func(resp types.WorkerMessage) {
msg, ok := resp.(*types.MessageBodyPart)
if !ok {
return
}
cb(msg.Part.Reader)
})
}
func merge(to *models.MessageInfo, from *models.MessageInfo) {
if from.BodyStructure != nil {
to.BodyStructure = from.BodyStructure
}
if from.Envelope != nil {
to.Envelope = from.Envelope
}
to.Flags = from.Flags
to.Labels = from.Labels
if from.Size != 0 {
to.Size = from.Size
}
var zero time.Time
if from.InternalDate != zero {
to.InternalDate = from.InternalDate
}
}
func (store *MessageStore) Update(msg types.WorkerMessage) {
update := false
directoryChange := false
switch msg := msg.(type) {
case *types.DirectoryInfo:
store.DirInfo = *msg.Info
store.Sort(store.sortCriteria, nil)
update = true
case *types.DirectoryContents:
newMap := make(map[uint32]*models.MessageInfo)
for _, uid := range msg.Uids {
if msg, ok := store.Messages[uid]; ok {
newMap[uid] = msg
} else {
newMap[uid] = nil
directoryChange = true
}
}
store.Messages = newMap
store.uids = msg.Uids
sort.SortBy(store.filtered, store.uids)
update = true
case *types.DirectoryThreaded:
var uids []uint32
newMap := make(map[uint32]*models.MessageInfo)
for i := len(msg.Threads) - 1; i >= 0; i-- {
msg.Threads[i].Walk(func(t *types.Thread, level int, currentErr error) error {
uid := t.Uid
uids = append([]uint32{uid}, uids...)
if msg, ok := store.Messages[uid]; ok {
newMap[uid] = msg
} else {
newMap[uid] = nil
directoryChange = true
}
return nil
})
}
store.Messages = newMap
store.uids = uids
store.Threads = msg.Threads
update = true
case *types.MessageInfo:
if existing, ok := store.Messages[msg.Info.Uid]; ok && existing != nil {
merge(existing, msg.Info)
} else {
if msg.Info.Envelope != nil {
store.Messages[msg.Info.Uid] = msg.Info
}
}
seen := false
recent := false
for _, flag := range msg.Info.Flags {
if flag == models.RecentFlag {
recent = true
} else if flag == models.SeenFlag {
seen = true
}
}
if !seen && recent {
store.triggerNewEmail(msg.Info)
}
if _, ok := store.pendingHeaders[msg.Info.Uid]; msg.Info.Envelope != nil && ok {
delete(store.pendingHeaders, msg.Info.Uid)
if cbs, ok := store.headerCallbacks[msg.Info.Uid]; ok {
for _, cb := range cbs {
cb(msg)
}
}
}
if store.builder != nil {
store.builder.Update(msg.Info)
}
update = true
case *types.FullMessage:
if _, ok := store.pendingBodies[msg.Content.Uid]; ok {
delete(store.pendingBodies, msg.Content.Uid)
if cbs, ok := store.bodyCallbacks[msg.Content.Uid]; ok {
for _, cb := range cbs {
cb(msg)
}
delete(store.bodyCallbacks, msg.Content.Uid)
}
}
case *types.MessagesDeleted:
if len(store.uids) < len(msg.Uids) {
update = true
break
}
toDelete := make(map[uint32]interface{})
for _, uid := range msg.Uids {
toDelete[uid] = nil
delete(store.Messages, uid)
delete(store.Deleted, uid)
delete(store.marked, uid)
}
uids := make([]uint32, len(store.uids)-len(msg.Uids))
j := 0
for _, uid := range store.uids {
if _, deleted := toDelete[uid]; !deleted && j < len(uids) {
uids[j] = uid
j += 1
}
}
store.uids = uids
var newResults []uint32
for _, res := range store.results {
if _, deleted := toDelete[res]; !deleted {
newResults = append(newResults, res)
}
}
store.results = newResults
var newFiltered []uint32
for _, res := range store.filtered {
if _, deleted := toDelete[res]; !deleted {
newFiltered = append(newFiltered, res)
}
}
store.filtered = newFiltered
for _, thread := range store.Threads {
thread.Walk(func(t *types.Thread, _ int, _ error) error {
if _, deleted := toDelete[t.Uid]; deleted {
t.Deleted = true
}
return nil
})
}
update = true
}
if update {
store.update()
}
if directoryChange && store.triggerDirectoryChange != nil {
store.triggerDirectoryChange()
}
}
func (store *MessageStore) OnUpdate(fn func(store *MessageStore)) {
store.onUpdate = fn
}
func (store *MessageStore) OnFilterChange(fn func(store *MessageStore)) {
store.onFilterChange = fn
}
func (store *MessageStore) OnUpdateDirs(fn func()) {
store.onUpdateDirs = fn
}
func (store *MessageStore) update() {
if store.onUpdate != nil {
store.onUpdate(store)
}
if store.onUpdateDirs != nil {
store.onUpdateDirs()
}
if store.BuildThreads() {
store.runThreadBuilder()
}
}
func (store *MessageStore) SetBuildThreads(buildThreads bool) {
// if worker provides threading, don't build our own threads
if store.thread {
return
}
store.buildThreads = buildThreads
if store.BuildThreads() {
store.runThreadBuilder()
}
}
func (store *MessageStore) BuildThreads() bool {
// if worker provides threading, don't build our own threads
if store.thread {
return false
}
return store.buildThreads
}
func (store *MessageStore) runThreadBuilder() {
if store.builder == nil {
store.builder = NewThreadBuilder(store.worker.Logger)
for _, msg := range store.Messages {
store.builder.Update(msg)
}
}
var uids []uint32
if store.filter {
uids = store.filtered
} else {
uids = store.uids
}
store.Threads = store.builder.Threads(uids)
}
func (store *MessageStore) Delete(uids []uint32,
cb func(msg types.WorkerMessage)) {
for _, uid := range uids {
store.Deleted[uid] = nil
}
store.worker.PostAction(&types.DeleteMessages{Uids: uids}, cb)
store.update()
}
func (store *MessageStore) Copy(uids []uint32, dest string, createDest bool,
cb func(msg types.WorkerMessage)) {
if createDest {
store.worker.PostAction(&types.CreateDirectory{
Directory: dest,
Quiet: true,
}, cb)
}
store.worker.PostAction(&types.CopyMessages{
Destination: dest,
Uids: uids,
}, cb)
}
func (store *MessageStore) Move(uids []uint32, dest string, createDest bool,
cb func(msg types.WorkerMessage)) {
for _, uid := range uids {
store.Deleted[uid] = nil
}
if createDest {
store.worker.PostAction(&types.CreateDirectory{
Directory: dest,
Quiet: true,
}, nil) // quiet doesn't return an error, don't want the done cb here
}
store.worker.PostAction(&types.CopyMessages{
Destination: dest,
Uids: uids,
}, func(msg types.WorkerMessage) {
switch msg.(type) {
case *types.Error:
cb(msg)
case *types.Done:
store.worker.PostAction(&types.DeleteMessages{Uids: uids}, cb)
}
})
store.update()
}
func (store *MessageStore) Flag(uids []uint32, flag models.Flag,
enable bool, cb func(msg types.WorkerMessage)) {
store.worker.PostAction(&types.FlagMessages{
Enable: enable,
Flag: flag,
Uids: uids,
}, cb)
}
func (store *MessageStore) Answered(uids []uint32, answered bool,
cb func(msg types.WorkerMessage)) {
store.worker.PostAction(&types.AnsweredMessages{
Answered: answered,
Uids: uids,
}, cb)
}
func (store *MessageStore) Uids() []uint32 {
if store.BuildThreads() && store.builder != nil {
if uids := store.builder.Uids(); len(uids) > 0 {
return uids
}
}
if store.filter {
return store.filtered
}
return store.uids
}
func (store *MessageStore) Selected() *models.MessageInfo {
return store.Messages[store.Uids()[len(store.Uids())-store.selected-1]]
}
func (store *MessageStore) SelectedIndex() int {
return store.selected
}
func (store *MessageStore) Select(index int) {
uids := store.Uids()
store.selected = index
if store.selected < 0 {
store.selected = len(uids) - 1
} else if store.selected > len(uids) {
store.selected = len(uids)
}
store.updateVisual()
}
// Mark sets the marked state on a MessageInfo
func (store *MessageStore) Mark(uid uint32) {
if store.visualMarkMode {
// visual mode has override, bogus input from user
return
}
store.marked[uid] = struct{}{}
}
// Unmark removes the marked state on a MessageInfo
func (store *MessageStore) Unmark(uid uint32) {
if store.visualMarkMode {
// user probably wanted to clear the visual marking
store.ClearVisualMark()
return
}
delete(store.marked, uid)
}
// ToggleMark toggles the marked state on a MessageInfo
func (store *MessageStore) ToggleMark(uid uint32) {
if store.visualMarkMode {
// visual mode has override, bogus input from user
return
}
if store.IsMarked(uid) {
store.Unmark(uid)
} else {
store.Mark(uid)
}
}
// resetMark removes the marking from all messages
func (store *MessageStore) resetMark() {
store.marked = make(map[uint32]struct{})
}
//IsMarked checks whether a MessageInfo has been marked
func (store *MessageStore) IsMarked(uid uint32) bool {
_, marked := store.marked[uid]
return marked
}
//ToggleVisualMark enters or leaves the visual marking mode
func (store *MessageStore) ToggleVisualMark() {
store.visualMarkMode = !store.visualMarkMode
switch store.visualMarkMode {
case true:
// just entered visual mode, reset whatever marking was already done
store.resetMark()
store.visualStartUid = store.Selected().Uid
store.marked[store.visualStartUid] = struct{}{}
case false:
// visual mode ended, nothing to do
return
}
}
//ClearVisualMark leaves the visual marking mode and resets any marking
func (store *MessageStore) ClearVisualMark() {
store.resetMark()
store.visualMarkMode = false
store.visualStartUid = 0
}
// Marked returns the uids of all marked messages
func (store *MessageStore) Marked() []uint32 {
marked := make([]uint32, len(store.marked))
i := 0
for uid := range store.marked {
marked[i] = uid
i++
}
return marked
}
func (store *MessageStore) updateVisual() {
if !store.visualMarkMode {
// nothing to do
return
}
startIdx := store.visualStartIdx()
if startIdx < 0 {
// something deleted the startuid, abort the marking process
store.ClearVisualMark()
return
}
uidLen := len(store.Uids())
// store.selected is the inverted form of the actual array
selectedIdx := uidLen - store.selected - 1
var visUids []uint32
if selectedIdx > startIdx {
visUids = store.Uids()[startIdx : selectedIdx+1]
} else {
visUids = store.Uids()[selectedIdx : startIdx+1]
}
store.resetMark()
for _, uid := range visUids {
store.marked[uid] = struct{}{}
}
missing := make([]uint32, 0)
for _, uid := range visUids {
if msg := store.Messages[uid]; msg == nil {
missing = append(missing, uid)
}
}
store.FetchHeaders(missing, nil)
}
func (store *MessageStore) NextPrev(delta int) {
uids := store.Uids()
if len(uids) == 0 {
return
}
store.selected += delta
if store.selected < 0 {
store.selected = 0
}
if store.selected >= len(uids) {
store.selected = len(uids) - 1
}
store.updateVisual()
nextResultIndex := len(store.results) - store.resultIndex - 2*delta
if nextResultIndex < 0 || nextResultIndex >= len(store.results) {
return
}
nextResultUid := store.results[nextResultIndex]
selectedUid := uids[len(uids)-store.selected-1]
if nextResultUid == selectedUid {
store.resultIndex += delta
}
}
func (store *MessageStore) Next() {
store.NextPrev(1)
}
func (store *MessageStore) Prev() {
store.NextPrev(-1)
}
func (store *MessageStore) Search(args []string, cb func([]uint32)) {
store.worker.PostAction(&types.SearchDirectory{
Argv: args,
}, func(msg types.WorkerMessage) {
switch msg := msg.(type) {
case *types.SearchResults:
allowedUids := store.Uids()
uids := make([]uint32, 0, len(msg.Uids))
for _, uid := range msg.Uids {
for _, uidCheck := range allowedUids {
if uid == uidCheck {
uids = append(uids, uid)
break
}
}
}
sort.SortBy(uids, allowedUids)
cb(uids)
}
})
}
func (store *MessageStore) ApplySearch(results []uint32) {
store.results = results
store.resultIndex = -1
store.NextResult()
}
func (store *MessageStore) ApplyFilter(results []uint32) {
store.results = nil
store.filtered = results
store.filter = true
if store.onFilterChange != nil {
store.onFilterChange(store)
}
store.update()
// any marking is now invalid
// TODO: could save that probably
store.ClearVisualMark()
}
func (store *MessageStore) ApplyClear() {
store.results = nil
store.filtered = nil
store.filter = false
if store.BuildThreads() {
store.runThreadBuilder()
}
if store.onFilterChange != nil {
store.onFilterChange(store)
}
}
func (store *MessageStore) nextPrevResult(delta int) {
if len(store.results) == 0 {
return
}
store.resultIndex += delta
if store.resultIndex >= len(store.results) {
store.resultIndex = 0
}
if store.resultIndex < 0 {
store.resultIndex = len(store.results) - 1
}
uids := store.Uids()
for i, uid := range uids {
if store.results[len(store.results)-store.resultIndex-1] == uid {
store.Select(len(uids) - i - 1)
break
}
}
store.update()
}
func (store *MessageStore) NextResult() {
store.nextPrevResult(1)
}
func (store *MessageStore) PrevResult() {
store.nextPrevResult(-1)
}
func (store *MessageStore) ModifyLabels(uids []uint32, add, remove []string,
cb func(msg types.WorkerMessage)) {
store.worker.PostAction(&types.ModifyLabels{
Uids: uids,
Add: add,
Remove: remove,
}, cb)
}
func (store *MessageStore) Sort(criteria []*types.SortCriterion, cb func()) {
store.Sorting = true
store.sortCriteria = criteria
handle_return := func(msg types.WorkerMessage) {
store.Sorting = false
if cb != nil {
cb()
}
}
if store.thread {
store.worker.PostAction(&types.FetchDirectoryThreaded{
SortCriteria: criteria,
}, handle_return)
} else {
store.worker.PostAction(&types.FetchDirectoryContents{
SortCriteria: criteria,
}, handle_return)
}
}
// returns the index of needle in haystack or -1 if not found
func (store *MessageStore) visualStartIdx() int {
for idx, u := range store.Uids() {
if u == store.visualStartUid {
return idx
}
}
return -1
}