aerc/lib/msgstore.go

778 lines
18 KiB
Go
Raw Normal View History

2019-03-16 02:36:06 +01:00
package lib
import (
"io"
"sync"
2019-03-31 17:10:10 +02:00
"time"
"git.sr.ht/~rjarry/aerc/lib/marker"
"git.sr.ht/~rjarry/aerc/lib/sort"
"git.sr.ht/~rjarry/aerc/logging"
"git.sr.ht/~rjarry/aerc/models"
"git.sr.ht/~rjarry/aerc/worker/types"
2019-03-16 02:36:06 +01:00
)
// Accesses to fields must be guarded by MessageStore.Lock/Unlock
2019-03-16 02:36:06 +01:00
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
2019-03-30 03:35:53 +01:00
selectedUid uint32
bodyCallbacks map[uint32][]func(*types.FullMessage)
2019-03-30 03:35:53 +01:00
// marking
marker marker.Marker
2019-12-18 06:33:59 +01:00
// Search/filter results
results []uint32
resultIndex int
filter []string
sortCriteria []*types.SortCriterion
threadedView bool
buildThreads bool
builder *ThreadBuilder
2019-03-16 02:36:06 +01:00
// Map of uids we've asked the worker to fetch
2019-03-16 02:43:33 +01:00
onUpdate func(store *MessageStore) // TODO: multiple onUpdate handlers
onFilterChange func(store *MessageStore)
onUpdateDirs func()
2019-03-16 02:36:06 +01:00
pendingBodies map[uint32]interface{}
pendingHeaders map[uint32]interface{}
worker *types.Worker
needsFlags []uint32
fetchFlagsDebounce *time.Timer
fetchFlagsDelay time.Duration
triggerNewEmail func(*models.MessageInfo)
triggerDirectoryChange func()
threadBuilderDebounce *time.Timer
threadBuilderDelay time.Duration
2022-08-12 23:15:42 +02:00
threadCallback func()
2022-08-12 23:15:42 +02:00
// threads mutex protects the store.threads and store.threadCallback
threadsMutex sync.Mutex
2019-03-16 02:36:06 +01:00
}
const MagicUid = 0xFFFFFFFF
2019-03-16 02:36:06 +01:00
func NewMessageStore(worker *types.Worker,
dirInfo *models.DirectoryInfo,
defaultSortCriteria []*types.SortCriterion,
thread bool, clientThreads bool, clientThreadsDelay time.Duration,
triggerNewEmail func(*models.MessageInfo),
triggerDirectoryChange func(),
) *MessageStore {
if !dirInfo.Caps.Thread {
clientThreads = true
}
2019-03-16 02:36:06 +01:00
return &MessageStore{
Deleted: make(map[uint32]interface{}),
DirInfo: *dirInfo,
Messages: make(map[uint32]*models.MessageInfo),
2019-03-16 02:36:06 +01:00
selectedUid: MagicUid,
bodyCallbacks: make(map[uint32][]func(*types.FullMessage)),
2019-03-30 03:35:53 +01:00
threadedView: thread,
buildThreads: clientThreads,
filter: []string{"filter"},
sortCriteria: defaultSortCriteria,
2019-03-16 02:36:06 +01:00
pendingBodies: make(map[uint32]interface{}),
pendingHeaders: make(map[uint32]interface{}),
worker: worker,
needsFlags: []uint32{},
fetchFlagsDelay: 50 * time.Millisecond,
triggerNewEmail: triggerNewEmail,
triggerDirectoryChange: triggerDirectoryChange,
threadBuilderDelay: clientThreadsDelay,
2019-03-16 02:36:06 +01:00
}
}
2019-03-30 03:35:53 +01:00
func (store *MessageStore) FetchHeaders(uids []uint32,
cb func(types.WorkerMessage),
) {
2019-03-16 02:36:06 +01:00
// 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
2019-03-16 02:36:06 +01:00
for _, uid := range uids {
if _, ok := store.pendingHeaders[uid]; !ok {
toFetch = append(toFetch, uid)
2019-03-16 02:36:06 +01:00
store.pendingHeaders[uid] = nil
2019-03-30 03:35:53 +01:00
}
}
if len(toFetch) > 0 {
store.worker.PostAction(&types.FetchMessageHeaders{Uids: toFetch}, func(msg types.WorkerMessage) {
if msg, ok := msg.(*types.Error); ok {
for _, uid := range toFetch {
store.postInvalidMessageInfo(uid, msg.Error)
delete(store.pendingHeaders, uid)
}
}
if cb != nil {
cb(msg)
}
})
2019-03-30 03:35:53 +01:00
}
}
func (store *MessageStore) postInvalidMessageInfo(uid uint32, err error) {
logging.Errorf("Unable to fetch header %d: %w", uid, err)
info := &models.MessageInfo{
Envelope: &models.Envelope{},
Flags: []models.Flag{models.SeenFlag},
Uid: uid,
Error: err,
}
store.Update(&types.MessageInfo{Info: info})
}
func (store *MessageStore) FetchFull(uids []uint32, cb func(*types.FullMessage)) {
2019-03-30 03:35:53 +01:00
// 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
2019-03-30 03:35:53 +01:00
for _, uid := range uids {
if _, ok := store.pendingBodies[uid]; !ok {
toFetch = append(toFetch, uid)
2019-03-30 03:35:53 +01:00
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}
2019-03-30 03:35:53 +01:00
}
}
2019-03-16 02:36:06 +01:00
}
}
if len(toFetch) > 0 {
2019-07-09 00:32:31 +02:00
store.worker.PostAction(&types.FetchFullMessages{
Uids: toFetch,
}, func(msg types.WorkerMessage) {
if _, ok := msg.(*types.Error); ok {
2019-07-09 00:32:31 +02:00
for _, uid := range toFetch {
delete(store.pendingBodies, uid)
delete(store.bodyCallbacks, uid)
2019-07-09 00:32:31 +02:00
}
}
})
2019-03-30 03:35:53 +01:00
}
}
func (store *MessageStore) FetchBodyPart(uid uint32, part []int, cb func(io.Reader)) {
2019-03-31 18:14:37 +02:00
store.worker.PostAction(&types.FetchMessageBodyPart{
Uid: uid,
Part: part,
2019-03-31 18:14:37 +02:00
}, func(resp types.WorkerMessage) {
msg, ok := resp.(*types.MessageBodyPart)
if !ok {
return
}
cb(msg.Part.Reader)
2019-03-31 18:14:37 +02:00
})
}
func merge(to *models.MessageInfo, from *models.MessageInfo) {
2019-03-31 17:10:10 +02:00
if from.BodyStructure != nil {
to.BodyStructure = from.BodyStructure
}
2019-03-30 03:35:53 +01:00
if from.Envelope != nil {
to.Envelope = from.Envelope
2019-03-16 02:36:06 +01:00
}
to.Flags = from.Flags
to.Labels = from.Labels
2019-03-31 17:10:10 +02:00
if from.Size != 0 {
to.Size = from.Size
}
var zero time.Time
if from.InternalDate != zero {
to.InternalDate = from.InternalDate
}
2019-03-16 02:36:06 +01:00
}
func (store *MessageStore) Update(msg types.WorkerMessage) {
update := false
updateThreads := false
directoryChange := false
2019-03-16 02:36:06 +01:00
switch msg := msg.(type) {
case *types.DirectoryInfo:
store.DirInfo = *msg.Info
if !msg.SkipSort {
store.Sort(store.sortCriteria, nil)
}
2019-03-16 02:36:06 +01:00
update = true
case *types.DirectoryContents:
newMap := make(map[uint32]*models.MessageInfo)
2019-03-16 02:36:06 +01:00
for _, uid := range msg.Uids {
if msg, ok := store.Messages[uid]; ok {
newMap[uid] = msg
} else {
newMap[uid] = nil
directoryChange = true
2019-03-16 02:36:06 +01:00
}
}
store.Messages = newMap
store.uids = msg.Uids
if store.threadedView {
store.runThreadBuilderNow()
}
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
2019-03-16 02:36:06 +01:00
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
2019-03-30 03:35:53 +01:00
}
if msg.NeedsFlags {
store.needsFlags = append(store.needsFlags, msg.Info.Uid)
store.fetchFlags()
}
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)
2019-03-16 02:36:06 +01:00
}
if store.builder != nil {
store.builder.Update(msg.Info)
}
2019-03-16 02:36:06 +01:00
update = true
updateThreads = true
2019-03-31 18:35:51 +02:00
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 {
2019-03-30 03:35:53 +01:00
for _, cb := range cbs {
cb(msg)
2019-03-30 03:35:53 +01:00
}
2019-07-09 00:32:31 +02:00
delete(store.bodyCallbacks, msg.Content.Uid)
2019-03-30 03:35:53 +01:00
}
}
2019-03-21 04:23:38 +01:00
case *types.MessagesDeleted:
if len(store.uids) < len(msg.Uids) {
update = true
break
}
2019-03-21 04:23:38 +01:00
toDelete := make(map[uint32]interface{})
for _, uid := range msg.Uids {
toDelete[uid] = nil
delete(store.Messages, uid)
delete(store.Deleted, uid)
2019-03-21 04:23:38 +01:00
}
uids := make([]uint32, len(store.uids)-len(msg.Uids))
2019-03-21 04:23:38 +01:00
j := 0
for _, uid := range store.uids {
2019-05-16 21:28:33 +02:00
if _, deleted := toDelete[uid]; !deleted && j < len(uids) {
uids[j] = uid
2019-03-21 04:23:38 +01:00
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
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
})
}
2019-03-21 04:23:38 +01:00
update = true
updateThreads = true
2019-03-16 02:36:06 +01:00
}
if update {
store.update(updateThreads)
2019-03-16 02:36:06 +01:00
}
if directoryChange && store.triggerDirectoryChange != nil {
store.triggerDirectoryChange()
}
2019-03-16 02:36:06 +01:00
}
func (store *MessageStore) OnUpdate(fn func(store *MessageStore)) {
store.onUpdate = fn
}
2019-03-21 04:23:38 +01:00
func (store *MessageStore) OnFilterChange(fn func(store *MessageStore)) {
store.onFilterChange = fn
}
func (store *MessageStore) OnUpdateDirs(fn func()) {
store.onUpdateDirs = fn
}
func (store *MessageStore) update(threads bool) {
if store.onUpdate != nil {
store.onUpdate(store)
}
if store.onUpdateDirs != nil {
store.onUpdateDirs()
}
if store.BuildThreads() && store.ThreadedView() && threads {
store.runThreadBuilder()
}
}
func (store *MessageStore) SetThreadedView(thread bool) {
store.threadedView = thread
if store.buildThreads {
if store.threadedView {
store.runThreadBuilder()
2022-08-12 23:15:42 +02:00
} else if store.threadBuilderDebounce != nil {
store.threadBuilderDebounce.Stop()
}
return
}
store.Sort(store.sortCriteria, nil)
}
func (store *MessageStore) Threads() []*types.Thread {
store.threadsMutex.Lock()
defer store.threadsMutex.Unlock()
return store.threads
}
func (store *MessageStore) ThreadedView() bool {
return store.threadedView
}
func (store *MessageStore) BuildThreads() bool {
return store.buildThreads
}
func (store *MessageStore) runThreadBuilder() {
if store.threadBuilderDebounce != nil {
if store.threadBuilderDebounce.Stop() {
logging.Infof("thread builder debounced")
}
}
store.threadBuilderDebounce = time.AfterFunc(store.threadBuilderDelay, func() {
store.runThreadBuilderNow()
})
}
// runThreadBuilderNow runs the threadbuilder without any debounce logic
func (store *MessageStore) runThreadBuilderNow() {
if store.builder == nil {
store.builder = NewThreadBuilder()
for _, msg := range store.Messages {
store.builder.Update(msg)
}
}
// build new threads
th := store.builder.Threads(store.uids)
// save local threads to the message store variable and
// run callback if defined (callback should reposition cursor)
store.threadsMutex.Lock()
store.threads = th
if store.threadCallback != nil {
store.threadCallback()
}
store.threadsMutex.Unlock()
// invalidate message list
if store.onUpdate != nil {
store.onUpdate(store)
}
}
// SelectedThread returns the thread with the UID from the selected message
func (store *MessageStore) SelectedThread() *types.Thread {
var thread *types.Thread
for _, root := range store.Threads() {
found := false
err := root.Walk(func(t *types.Thread, _ int, _ error) error {
if t.Uid == store.SelectedUid() {
thread = t
found = true
}
return nil
})
if err != nil {
logging.Errorf("SelectedThread failed: %w", err)
}
if found {
break
}
}
return thread
}
2019-05-14 22:34:42 +02:00
func (store *MessageStore) Delete(uids []uint32,
cb func(msg types.WorkerMessage),
) {
2019-03-21 04:23:38 +01:00
for _, uid := range uids {
store.Deleted[uid] = nil
2019-03-21 04:23:38 +01:00
}
store.worker.PostAction(&types.DeleteMessages{Uids: uids},
func(msg types.WorkerMessage) {
if _, ok := msg.(*types.Error); ok {
store.revertDeleted(uids)
}
if _, ok := msg.(*types.Unsupported); ok {
store.revertDeleted(uids)
}
cb(msg)
})
2019-03-21 04:23:38 +01:00
}
2019-05-14 22:34:42 +02:00
func (store *MessageStore) revertDeleted(uids []uint32) {
for _, uid := range uids {
delete(store.Deleted, uid)
}
}
func (store *MessageStore) Copy(uids []uint32, dest string, createDest bool,
cb func(msg types.WorkerMessage),
) {
if createDest {
store.worker.PostAction(&types.CreateDirectory{
Directory: dest,
2019-07-11 06:49:09 +02:00
Quiet: true,
}, cb)
}
2019-05-14 22:34:42 +02:00
store.worker.PostAction(&types.CopyMessages{
Destination: dest,
Uids: uids,
2019-05-14 22:34:42 +02:00
}, cb)
}
2019-05-14 22:55:50 +02:00
func (store *MessageStore) Move(uids []uint32, dest string, createDest bool,
cb func(msg types.WorkerMessage),
) {
2019-05-14 22:55:50 +02:00
for _, uid := range uids {
store.Deleted[uid] = nil
}
if createDest {
store.worker.PostAction(&types.CreateDirectory{
Directory: dest,
2019-07-11 06:49:09 +02:00
Quiet: true,
}, nil) // quiet doesn't return an error, don't want the done cb here
}
store.worker.PostAction(&types.MoveMessages{
2019-05-14 22:55:50 +02:00
Destination: dest,
Uids: uids,
2019-05-14 22:55:50 +02:00
}, func(msg types.WorkerMessage) {
switch msg.(type) {
case *types.Error:
store.revertDeleted(uids)
2019-05-14 22:55:50 +02:00
cb(msg)
case *types.Done:
cb(msg)
2019-05-14 22:55:50 +02:00
}
})
}
2019-06-09 20:55:34 +02:00
func (store *MessageStore) Flag(uids []uint32, flag models.Flag,
enable bool, cb func(msg types.WorkerMessage),
) {
store.worker.PostAction(&types.FlagMessages{
Enable: enable,
2020-07-17 17:50:24 +02:00
Flag: flag,
Uids: uids,
2019-06-09 20:55:34 +02:00
}, cb)
}
2020-05-25 16:59:48 +02:00
func (store *MessageStore) Answered(uids []uint32, answered bool,
cb func(msg types.WorkerMessage),
) {
2020-05-25 16:59:48 +02:00
store.worker.PostAction(&types.AnsweredMessages{
Answered: answered,
Uids: uids,
}, cb)
}
func (store *MessageStore) Uids() []uint32 {
if store.ThreadedView() && store.builder != nil {
if uids := store.builder.Uids(); len(uids) > 0 {
return uids
}
}
return store.uids
}
func (store *MessageStore) Selected() *models.MessageInfo {
return store.Messages[store.selectedUid]
}
func (store *MessageStore) SelectedUid() uint32 {
if store.selectedUid == MagicUid && len(store.Uids()) > 0 {
uids := store.Uids()
store.selectedUid = uids[len(uids)-1]
}
return store.selectedUid
2019-12-18 06:33:59 +01:00
}
func (store *MessageStore) Select(uid uint32) {
2022-08-12 23:15:42 +02:00
store.threadsMutex.Lock()
if store.threadCallback != nil {
store.threadCallback = nil
}
store.threadsMutex.Unlock()
store.selectedUid = uid
if store.marker != nil {
store.marker.UpdateVisualMark()
2019-12-18 06:33:59 +01:00
}
}
func (store *MessageStore) NextPrev(delta int) {
uids := store.Uids()
if len(uids) == 0 {
return
}
uid := store.SelectedUid()
newIdx := store.FindIndexByUid(uid)
if newIdx < 0 {
store.Select(uids[len(uids)-1])
return
}
newIdx -= delta
if newIdx >= len(uids) {
newIdx = len(uids) - 1
} else if newIdx < 0 {
newIdx = 0
}
store.Select(uids[newIdx])
2022-08-12 23:15:42 +02:00
if store.BuildThreads() && store.ThreadedView() {
store.threadsMutex.Lock()
store.threadCallback = func() {
if uids := store.Uids(); len(uids) > newIdx {
store.selectedUid = uids[newIdx]
}
}
store.threadsMutex.Unlock()
}
if store.marker != nil {
store.marker.UpdateVisualMark()
}
nextResultIndex := len(store.results) - store.resultIndex - 2*delta
if nextResultIndex < 0 || nextResultIndex >= len(store.results) {
return
}
nextResultUid := store.results[nextResultIndex]
if nextResultUid == store.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) {
if msg, ok := msg.(*types.SearchResults); ok {
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) SetFilter(args []string) {
store.filter = append(store.filter, args...)
}
func (store *MessageStore) ApplyClear() {
store.filter = []string{"filter"}
store.results = nil
if store.onFilterChange != nil {
store.onFilterChange(store)
}
store.Sort(nil, nil)
}
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
}
store.Select(store.results[len(store.results)-store.resultIndex-1])
store.update(false)
}
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(types.WorkerMessage)) {
store.sortCriteria = criteria
store.Sorting = true
handle_return := func(msg types.WorkerMessage) {
2022-08-12 23:15:42 +02:00
store.Select(store.SelectedUid())
store.Sorting = false
if cb != nil {
cb(msg)
}
}
if store.threadedView && !store.buildThreads {
store.worker.PostAction(&types.FetchDirectoryThreaded{
SortCriteria: criteria,
FilterCriteria: store.filter,
}, handle_return)
} else {
store.worker.PostAction(&types.FetchDirectoryContents{
SortCriteria: criteria,
FilterCriteria: store.filter,
}, handle_return)
}
}
2019-12-18 06:33:59 +01:00
func (store *MessageStore) GetCurrentSortCriteria() []*types.SortCriterion {
return store.sortCriteria
}
func (store *MessageStore) SetMarker(m marker.Marker) {
store.marker = m
}
func (store *MessageStore) Marker() marker.Marker {
if store.marker == nil {
store.marker = marker.New(store)
2019-12-18 06:33:59 +01:00
}
return store.marker
2019-12-18 06:33:59 +01:00
}
// FindIndexByUid returns the index in store.Uids() or -1 if not found
func (store *MessageStore) FindIndexByUid(uid uint32) int {
for idx, u := range store.Uids() {
if u == uid {
return idx
}
}
return -1
}
// Capabilities returns a models.Capabilities struct or nil if not available
func (store *MessageStore) Capabilities() *models.Capabilities {
return store.DirInfo.Caps
}
// SelectedIndex returns the index of the selected message in the uid list or
// -1 if not found
func (store *MessageStore) SelectedIndex() int {
return store.FindIndexByUid(store.selectedUid)
}
func (store *MessageStore) fetchFlags() {
if store.fetchFlagsDebounce != nil {
store.fetchFlagsDebounce.Stop()
}
store.fetchFlagsDebounce = time.AfterFunc(store.fetchFlagsDelay, func() {
store.worker.PostAction(&types.FetchMessageFlags{
Uids: store.needsFlags,
}, nil)
store.needsFlags = []uint32{}
})
}