aerc/worker/imap/cache.go
Tim Culverhouse 9fdc7acf5b cache: fetch flags from UI
When cached headers are fetched, an action is posted back to the Worker
to immediately fetch the flags for the message from the server (we can't
know the flags state, therefore it's not cached). When scrolling, a lag
occurs when loading cached headers because the n+1 message has to wait
for the flag request to return before the cached headers are retrieved.

Collect the message UIDs in the UI that need flags, and fetch them based
off a debounce timer in a single request. Post the action from the UI to
eliminate an (ugly) go routine in the worker.

Signed-off-by: Tim Culverhouse <tim@timculverhouse.com>
Acked-by: Robin Jarry <robin@jarry.cc>
2022-09-20 21:10:35 +02:00

170 lines
4.4 KiB
Go

package imap
import (
"bufio"
"bytes"
"encoding/gob"
"fmt"
"os"
"path"
"time"
"git.sr.ht/~rjarry/aerc/logging"
"git.sr.ht/~rjarry/aerc/models"
"git.sr.ht/~rjarry/aerc/worker/types"
"github.com/emersion/go-message"
"github.com/emersion/go-message/mail"
"github.com/emersion/go-message/textproto"
"github.com/mitchellh/go-homedir"
"github.com/syndtr/goleveldb/leveldb"
)
type CachedHeader struct {
BodyStructure models.BodyStructure
Envelope models.Envelope
InternalDate time.Time
Uid uint32
Header []byte
Created time.Time
}
// initCacheDb opens (or creates) the database for the cache. One database is
// created per account
func (w *IMAPWorker) initCacheDb(acct string) {
cd, err := cacheDir()
if err != nil {
w.cache = nil
logging.Errorf("unable to find cache directory: %v", err)
return
}
p := path.Join(cd, acct)
db, err := leveldb.OpenFile(p, nil)
if err != nil {
w.cache = nil
logging.Errorf("failed opening cache db: %v", err)
return
}
w.cache = db
logging.Infof("cache db opened: %s", p)
if w.config.cacheMaxAge.Hours() > 0 {
go w.cleanCache()
}
}
func (w *IMAPWorker) cacheHeader(mi *models.MessageInfo) {
uv := fmt.Sprintf("%d", w.selected.UidValidity)
uid := fmt.Sprintf("%d", mi.Uid)
logging.Debugf("caching header for message %s.%s", uv, uid)
hdr := bytes.NewBuffer(nil)
err := textproto.WriteHeader(hdr, mi.RFC822Headers.Header.Header)
if err != nil {
logging.Errorf("cannot write header %s.%s: %v", uv, uid, err)
return
}
h := &CachedHeader{
BodyStructure: *mi.BodyStructure,
Envelope: *mi.Envelope,
InternalDate: mi.InternalDate,
Uid: mi.Uid,
Header: hdr.Bytes(),
Created: time.Now(),
}
data := bytes.NewBuffer(nil)
enc := gob.NewEncoder(data)
err = enc.Encode(h)
if err != nil {
logging.Errorf("cannot encode message %s.%s: %v", uv, uid, err)
return
}
err = w.cache.Put([]byte("header."+uv+"."+uid), data.Bytes(), nil)
if err != nil {
logging.Errorf("cannot write header for message %s.%s: %v", uv, uid, err)
return
}
}
func (w *IMAPWorker) getCachedHeaders(msg *types.FetchMessageHeaders) []uint32 {
logging.Debugf("Retrieving headers from cache: %v", msg.Uids)
var need []uint32
uv := fmt.Sprintf("%d", w.selected.UidValidity)
for _, uid := range msg.Uids {
u := fmt.Sprintf("%d", uid)
data, err := w.cache.Get([]byte("header."+uv+"."+u), nil)
if err != nil {
need = append(need, uid)
continue
}
ch := &CachedHeader{}
dec := gob.NewDecoder(bytes.NewReader(data))
err = dec.Decode(ch)
if err != nil {
logging.Errorf("cannot decode cached header %s.%s: %v", uv, u, err)
need = append(need, uid)
continue
}
hr := bytes.NewReader(ch.Header)
textprotoHeader, err := textproto.ReadHeader(bufio.NewReader(hr))
if err != nil {
logging.Errorf("cannot read cached header %s.%s: %v", uv, u, err)
need = append(need, uid)
continue
}
hdr := &mail.Header{Header: message.Header{Header: textprotoHeader}}
mi := &models.MessageInfo{
BodyStructure: &ch.BodyStructure,
Envelope: &ch.Envelope,
Flags: []models.Flag{models.SeenFlag}, // Always return a SEEN flag
Uid: ch.Uid,
RFC822Headers: hdr,
}
logging.Debugf("located cached header %s.%s", uv, u)
w.worker.PostMessage(&types.MessageInfo{
Message: types.RespondTo(msg),
Info: mi,
NeedsFlags: true,
}, nil)
}
return need
}
func cacheDir() (string, error) {
dir, err := os.UserCacheDir()
if err != nil {
dir, err = homedir.Expand("~/.cache")
if err != nil {
return "", err
}
}
return path.Join(dir, "aerc"), nil
}
// cleanCache removes stale entries from the selected mailbox cachedb
func (w *IMAPWorker) cleanCache() {
start := time.Now()
var scanned, removed int
iter := w.cache.NewIterator(nil, nil)
for iter.Next() {
data := iter.Value()
ch := &CachedHeader{}
dec := gob.NewDecoder(bytes.NewReader(data))
err := dec.Decode(ch)
if err != nil {
logging.Errorf("cannot clean database %d: %v", w.selected.UidValidity, err)
continue
}
exp := ch.Created.Add(w.config.cacheMaxAge)
if exp.Before(time.Now()) {
err = w.cache.Delete(iter.Key(), nil)
if err != nil {
logging.Errorf("cannot clean database %d: %v", w.selected.UidValidity, err)
continue
}
removed++
}
scanned++
}
iter.Release()
elapsed := time.Since(start)
logging.Infof("cleaned cache, removed %d of %d entries in %s", removed, scanned, elapsed)
}