aboutsummaryrefslogblamecommitdiffstats
path: root/worker/imap/cache.go
blob: 3c807c50902c2e39f8404cd458fba94d0a108d23 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11










                      
                                        























                                                                             
                                                                         





                                           
                                                                  


                      
                                               







                                                          
                                                                   


                                                                         
                                                                             













                                                 
                                                                               



                                                                          
                                                                                         




                                                                                
                                                                     












                                                                         
                                                                                           





                                                                                 
                                                                                         












                                                                                                   
                                                                    





                                                        

                                                                      



























                                                                     
                                                                                                   





                                                             
                                                                                                           

                                        
                                 
                 
                         


                                    
                                                                                                 
 
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, found []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,
		}
		found = append(found, uid)
		logging.Debugf("located cached header %s.%s", uv, u)
		w.worker.PostMessage(&types.MessageInfo{
			Message: types.RespondTo(msg),
			Info:    mi,
		}, nil)
	}
	if len(found) > 0 {
		// Post in a separate goroutine to prevent deadlocking
		go w.worker.PostAction(&types.FetchMessageFlags{
			Uids: found,
		}, 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)
}