2019-03-16 02:36:06 +01:00
|
|
|
package lib
|
|
|
|
|
|
|
|
import (
|
2019-03-31 17:29:57 +02:00
|
|
|
"io"
|
2019-03-31 17:10:10 +02:00
|
|
|
"time"
|
|
|
|
|
2019-03-16 02:36:06 +01:00
|
|
|
"github.com/emersion/go-imap"
|
|
|
|
|
2019-07-08 04:43:56 +02:00
|
|
|
"git.sr.ht/~sircmpwn/aerc/models"
|
2019-05-18 02:57:10 +02:00
|
|
|
"git.sr.ht/~sircmpwn/aerc/worker/types"
|
2019-03-16 02:36:06 +01:00
|
|
|
)
|
|
|
|
|
2019-04-28 15:26:38 +02:00
|
|
|
// Accesses to fields must be guarded by MessageStore.Lock/Unlock
|
2019-03-16 02:36:06 +01:00
|
|
|
type MessageStore struct {
|
2019-03-30 15:40:55 +01:00
|
|
|
Deleted map[uint32]interface{}
|
2019-07-08 04:43:56 +02:00
|
|
|
DirInfo models.DirectoryInfo
|
|
|
|
Messages map[uint32]*models.MessageInfo
|
2019-03-16 02:36:06 +01:00
|
|
|
// Ordered list of known UIDs
|
|
|
|
Uids []uint32
|
2019-03-30 03:35:53 +01:00
|
|
|
|
2019-06-11 07:05:55 +02:00
|
|
|
selected int
|
2019-03-31 17:29:57 +02:00
|
|
|
bodyCallbacks map[uint32][]func(io.Reader)
|
2019-03-30 03:35:53 +01:00
|
|
|
headerCallbacks map[uint32][]func(*types.MessageInfo)
|
|
|
|
|
2019-06-27 02:50:27 +02:00
|
|
|
// Search/filter results
|
|
|
|
results []uint32
|
|
|
|
resultIndex int
|
|
|
|
|
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
|
2019-03-16 02:36:06 +01:00
|
|
|
pendingBodies map[uint32]interface{}
|
|
|
|
pendingHeaders map[uint32]interface{}
|
|
|
|
worker *types.Worker
|
|
|
|
}
|
|
|
|
|
|
|
|
func NewMessageStore(worker *types.Worker,
|
2019-07-08 04:43:56 +02:00
|
|
|
dirInfo *models.DirectoryInfo) *MessageStore {
|
2019-03-16 02:36:06 +01:00
|
|
|
|
|
|
|
return &MessageStore{
|
2019-03-30 15:40:55 +01:00
|
|
|
Deleted: make(map[uint32]interface{}),
|
2019-03-16 02:36:06 +01:00
|
|
|
DirInfo: *dirInfo,
|
|
|
|
|
2019-06-11 07:05:55 +02:00
|
|
|
selected: 0,
|
2019-03-31 17:29:57 +02:00
|
|
|
bodyCallbacks: make(map[uint32][]func(io.Reader)),
|
2019-03-30 03:35:53 +01:00
|
|
|
headerCallbacks: make(map[uint32][]func(*types.MessageInfo)),
|
|
|
|
|
2019-03-16 02:36:06 +01:00
|
|
|
pendingBodies: make(map[uint32]interface{}),
|
|
|
|
pendingHeaders: make(map[uint32]interface{}),
|
|
|
|
worker: worker,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-03-30 03:35:53 +01:00
|
|
|
func (store *MessageStore) FetchHeaders(uids []uint32,
|
|
|
|
cb func(*types.MessageInfo)) {
|
|
|
|
|
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.
|
2019-07-08 04:43:57 +02:00
|
|
|
var toFetch []uint32
|
2019-03-16 02:36:06 +01:00
|
|
|
for _, uid := range uids {
|
|
|
|
if _, ok := store.pendingHeaders[uid]; !ok {
|
2019-07-08 04:43:57 +02:00
|
|
|
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 cb != nil {
|
|
|
|
if list, ok := store.headerCallbacks[uid]; ok {
|
|
|
|
store.headerCallbacks[uid] = append(list, cb)
|
|
|
|
} else {
|
|
|
|
store.headerCallbacks[uid] = []func(*types.MessageInfo){cb}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2019-07-08 04:43:57 +02:00
|
|
|
if len(toFetch) > 0 {
|
2019-03-30 03:35:53 +01:00
|
|
|
store.worker.PostAction(&types.FetchMessageHeaders{Uids: toFetch}, nil)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-03-31 18:17:57 +02:00
|
|
|
func (store *MessageStore) FetchFull(uids []uint32, cb func(io.Reader)) {
|
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.
|
2019-07-08 04:43:57 +02:00
|
|
|
var toFetch []uint32
|
2019-03-30 03:35:53 +01:00
|
|
|
for _, uid := range uids {
|
|
|
|
if _, ok := store.pendingBodies[uid]; !ok {
|
2019-07-08 04:43:57 +02:00
|
|
|
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 {
|
2019-03-31 17:29:57 +02:00
|
|
|
store.bodyCallbacks[uid] = []func(io.Reader){cb}
|
2019-03-30 03:35:53 +01:00
|
|
|
}
|
|
|
|
}
|
2019-03-16 02:36:06 +01:00
|
|
|
}
|
|
|
|
}
|
2019-07-08 04:43:57 +02:00
|
|
|
if len(toFetch) > 0 {
|
2019-07-09 00:32:31 +02:00
|
|
|
store.worker.PostAction(&types.FetchFullMessages{
|
|
|
|
Uids: toFetch,
|
|
|
|
}, func(msg types.WorkerMessage) {
|
|
|
|
switch msg.(type) {
|
|
|
|
case *types.Error:
|
|
|
|
for _, uid := range toFetch {
|
|
|
|
if _, ok := store.bodyCallbacks[uid]; ok {
|
|
|
|
delete(store.bodyCallbacks, uid)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
2019-03-30 03:35:53 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-03-31 18:14:37 +02:00
|
|
|
func (store *MessageStore) FetchBodyPart(
|
2019-05-20 22:42:44 +02:00
|
|
|
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,
|
|
|
|
}, func(resp types.WorkerMessage) {
|
|
|
|
msg, ok := resp.(*types.MessageBodyPart)
|
|
|
|
if !ok {
|
|
|
|
return
|
|
|
|
}
|
2019-07-08 04:43:56 +02:00
|
|
|
cb(msg.Part.Reader)
|
2019-03-31 18:14:37 +02:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2019-07-08 04:43:56 +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
|
|
|
}
|
2019-06-09 20:55:04 +02:00
|
|
|
to.Flags = from.Flags
|
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
|
|
|
|
switch msg := msg.(type) {
|
|
|
|
case *types.DirectoryInfo:
|
2019-07-08 04:43:56 +02:00
|
|
|
store.DirInfo = *msg.Info
|
2019-06-28 15:15:55 +02:00
|
|
|
store.worker.PostAction(&types.FetchDirectoryContents{}, nil)
|
2019-03-16 02:36:06 +01:00
|
|
|
update = true
|
|
|
|
case *types.DirectoryContents:
|
2019-07-08 04:43:56 +02:00
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
store.Messages = newMap
|
|
|
|
store.Uids = msg.Uids
|
|
|
|
update = true
|
|
|
|
case *types.MessageInfo:
|
2019-07-08 04:43:56 +02:00
|
|
|
if existing, ok := store.Messages[msg.Info.Uid]; ok && existing != nil {
|
|
|
|
merge(existing, msg.Info)
|
2019-03-30 03:35:53 +01:00
|
|
|
} else {
|
2019-07-08 04:43:56 +02:00
|
|
|
store.Messages[msg.Info.Uid] = msg.Info
|
2019-03-30 03:35:53 +01:00
|
|
|
}
|
2019-07-08 04:43:56 +02:00
|
|
|
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 {
|
2019-03-30 03:35:53 +01:00
|
|
|
for _, cb := range cbs {
|
|
|
|
cb(msg)
|
|
|
|
}
|
|
|
|
}
|
2019-03-16 02:36:06 +01:00
|
|
|
}
|
|
|
|
update = true
|
2019-03-31 18:35:51 +02:00
|
|
|
case *types.FullMessage:
|
2019-07-08 04:43:56 +02:00
|
|
|
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 {
|
2019-07-08 04:43:56 +02:00
|
|
|
cb(msg.Content.Reader)
|
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:
|
|
|
|
toDelete := make(map[uint32]interface{})
|
|
|
|
for _, uid := range msg.Uids {
|
|
|
|
toDelete[uid] = nil
|
|
|
|
delete(store.Messages, uid)
|
2019-03-30 15:40:55 +01:00
|
|
|
if _, ok := store.Deleted[uid]; ok {
|
|
|
|
delete(store.Deleted, uid)
|
|
|
|
}
|
2019-03-21 04:23:38 +01:00
|
|
|
}
|
|
|
|
uids := make([]uint32, len(store.Uids)-len(msg.Uids))
|
|
|
|
j := 0
|
2019-05-16 21:28:33 +02:00
|
|
|
for _, uid := range store.Uids {
|
|
|
|
if _, deleted := toDelete[uid]; !deleted && j < len(uids) {
|
|
|
|
uids[j] = uid
|
2019-03-21 04:23:38 +01:00
|
|
|
j += 1
|
|
|
|
}
|
|
|
|
}
|
|
|
|
store.Uids = uids
|
|
|
|
update = true
|
2019-03-16 02:36:06 +01:00
|
|
|
}
|
2019-04-28 15:26:38 +02:00
|
|
|
|
2019-03-30 15:40:55 +01:00
|
|
|
if update {
|
|
|
|
store.update()
|
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
|
|
|
|
2019-03-30 15:40:55 +01:00
|
|
|
func (store *MessageStore) update() {
|
|
|
|
if store.onUpdate != nil {
|
|
|
|
store.onUpdate(store)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-05-14 22:34:42 +02:00
|
|
|
func (store *MessageStore) Delete(uids []uint32,
|
|
|
|
cb func(msg types.WorkerMessage)) {
|
2019-04-28 15:26:38 +02:00
|
|
|
|
2019-03-21 04:23:38 +01:00
|
|
|
for _, uid := range uids {
|
2019-03-30 15:40:55 +01:00
|
|
|
store.Deleted[uid] = nil
|
2019-03-21 04:23:38 +01:00
|
|
|
}
|
2019-04-28 15:26:38 +02:00
|
|
|
|
2019-07-08 04:43:57 +02:00
|
|
|
store.worker.PostAction(&types.DeleteMessages{Uids: uids}, cb)
|
2019-03-30 15:40:55 +01:00
|
|
|
store.update()
|
2019-03-21 04:23:38 +01:00
|
|
|
}
|
2019-05-14 22:34:42 +02:00
|
|
|
|
2019-06-08 19:41:56 +02:00
|
|
|
func (store *MessageStore) Copy(uids []uint32, dest string, createDest bool,
|
2019-05-14 22:34:42 +02:00
|
|
|
cb func(msg types.WorkerMessage)) {
|
2019-06-08 19:41:56 +02:00
|
|
|
|
|
|
|
if createDest {
|
|
|
|
store.worker.PostAction(&types.CreateDirectory{
|
|
|
|
Directory: dest,
|
|
|
|
}, cb)
|
|
|
|
}
|
|
|
|
|
2019-05-14 22:34:42 +02:00
|
|
|
store.worker.PostAction(&types.CopyMessages{
|
|
|
|
Destination: dest,
|
2019-07-08 04:43:57 +02:00
|
|
|
Uids: uids,
|
2019-05-14 22:34:42 +02:00
|
|
|
}, cb)
|
|
|
|
}
|
2019-05-14 22:55:50 +02:00
|
|
|
|
2019-06-08 19:41:56 +02:00
|
|
|
func (store *MessageStore) Move(uids []uint32, dest string, createDest bool,
|
2019-05-14 22:55:50 +02:00
|
|
|
cb func(msg types.WorkerMessage)) {
|
|
|
|
|
|
|
|
for _, uid := range uids {
|
|
|
|
store.Deleted[uid] = nil
|
|
|
|
}
|
|
|
|
|
2019-06-08 19:41:56 +02:00
|
|
|
if createDest {
|
|
|
|
store.worker.PostAction(&types.CreateDirectory{
|
|
|
|
Directory: dest,
|
|
|
|
}, cb)
|
|
|
|
}
|
|
|
|
|
2019-05-14 22:55:50 +02:00
|
|
|
store.worker.PostAction(&types.CopyMessages{
|
|
|
|
Destination: dest,
|
2019-07-08 04:43:57 +02:00
|
|
|
Uids: uids,
|
2019-05-14 22:55:50 +02:00
|
|
|
}, func(msg types.WorkerMessage) {
|
|
|
|
switch msg.(type) {
|
|
|
|
case *types.Error:
|
|
|
|
cb(msg)
|
|
|
|
case *types.Done:
|
2019-07-08 04:43:57 +02:00
|
|
|
store.worker.PostAction(&types.DeleteMessages{Uids: uids}, cb)
|
2019-05-14 22:55:50 +02:00
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
store.update()
|
|
|
|
}
|
2019-06-09 20:55:34 +02:00
|
|
|
|
|
|
|
func (store *MessageStore) Read(uids []uint32, read bool,
|
|
|
|
cb func(msg types.WorkerMessage)) {
|
|
|
|
|
|
|
|
store.worker.PostAction(&types.ReadMessages{
|
|
|
|
Read: read,
|
2019-07-08 04:43:57 +02:00
|
|
|
Uids: uids,
|
2019-06-09 20:55:34 +02:00
|
|
|
}, cb)
|
|
|
|
}
|
2019-06-11 07:05:55 +02:00
|
|
|
|
2019-07-08 04:43:56 +02:00
|
|
|
func (store *MessageStore) Selected() *models.MessageInfo {
|
2019-06-11 07:05:55 +02:00
|
|
|
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) {
|
|
|
|
store.selected = index
|
|
|
|
for ; store.selected < 0; store.selected = len(store.Uids) + store.selected {
|
|
|
|
/* This space deliberately left blank */
|
|
|
|
}
|
|
|
|
if store.selected > len(store.Uids) {
|
|
|
|
store.selected = len(store.Uids)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (store *MessageStore) nextPrev(delta int) {
|
|
|
|
if len(store.Uids) == 0 {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
store.selected += delta
|
|
|
|
if store.selected < 0 {
|
|
|
|
store.selected = 0
|
|
|
|
}
|
|
|
|
if store.selected >= len(store.Uids) {
|
|
|
|
store.selected = len(store.Uids) - 1
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (store *MessageStore) Next() {
|
|
|
|
store.nextPrev(1)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (store *MessageStore) Prev() {
|
|
|
|
store.nextPrev(-1)
|
|
|
|
}
|
2019-06-27 02:50:27 +02:00
|
|
|
|
|
|
|
func (store *MessageStore) Search(c *imap.SearchCriteria, cb func([]uint32)) {
|
|
|
|
store.worker.PostAction(&types.SearchDirectory{
|
|
|
|
Criteria: c,
|
|
|
|
}, func(msg types.WorkerMessage) {
|
|
|
|
switch msg := msg.(type) {
|
|
|
|
case *types.SearchResults:
|
|
|
|
cb(msg.Uids)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func (store *MessageStore) ApplySearch(results []uint32) {
|
|
|
|
store.results = results
|
|
|
|
store.resultIndex = -1
|
|
|
|
store.NextResult()
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
for i, uid := range store.Uids {
|
|
|
|
if store.results[len(store.results)-store.resultIndex-1] == uid {
|
|
|
|
store.Select(len(store.Uids) - i - 1)
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
store.update()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (store *MessageStore) NextResult() {
|
|
|
|
store.nextPrevResult(1)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (store *MessageStore) PrevResult() {
|
|
|
|
store.nextPrevResult(-1)
|
|
|
|
}
|