package alps
import (
"encoding/json"
"fmt"
"reflect"
"sync"
imapmetadata "github.com/emersion/go-imap-metadata"
imapclient "github.com/emersion/go-imap/client"
"github.com/labstack/echo/v4"
)
// ErrNoStoreEntry is returned by Store.Get when the entry doesn't exist.
var ErrNoStoreEntry = fmt.Errorf("alps: no such entry in store")
// Store allows storing per-user persistent data.
//
// Store shouldn't be used from inside Session.DoIMAP.
type Store interface {
Get(key string, out interface{}) error
Put(key string, v interface{}) error
}
var warnedTransientStore = false
func newStore(session *Session, logger echo.Logger) (Store, error) {
s, err := newIMAPStore(session)
if err == nil {
return s, nil
} else if err != errIMAPMetadataUnsupported {
return nil, err
}
if !warnedTransientStore {
logger.Print("Upstream IMAP server doesn't support the METADATA extension, using transient store instead")
warnedTransientStore = true
}
return newMemoryStore(), nil
}
type memoryStore struct {
locker sync.RWMutex
entries map[string]interface{}
}
func newMemoryStore() *memoryStore {
return &memoryStore{entries: make(map[string]interface{})}
}
func (s *memoryStore) Get(key string, out interface{}) error {
s.locker.RLock()
defer s.locker.RUnlock()
v, ok := s.entries[key]
if !ok {
return ErrNoStoreEntry
}
reflect.ValueOf(out).Elem().Set(reflect.ValueOf(v).Elem())
return nil
}
func (s *memoryStore) Put(key string, v interface{}) error {
s.locker.Lock()
s.entries[key] = v
s.locker.Unlock()
return nil
}
type imapStore struct {
session *Session
cache *memoryStore
}
var errIMAPMetadataUnsupported = fmt.Errorf("alps: IMAP server doesn't support METADATA extension")
func newIMAPStore(session *Session) (*imapStore, error) {
err := session.DoIMAP(func(c *imapclient.Client) error {
mc := imapmetadata.NewClient(c)
ok, err := mc.SupportMetadata()
if err != nil {
return fmt.Errorf("alps: failed to check for IMAP METADATA support: %v", err)
}
if !ok {
return errIMAPMetadataUnsupported
}
return nil
})
if err != nil {
return nil, err
}
return &imapStore{session, newMemoryStore()}, nil
}
func (s *imapStore) key(key string) string {
return "/private/vendor/alps/" + key
}
func (s *imapStore) Get(key string, out interface{}) error {
if err := s.cache.Get(key, out); err != ErrNoStoreEntry {
return err
}
var entries map[string]string
err := s.session.DoIMAP(func(c *imapclient.Client) error {
mc := imapmetadata.NewClient(c)
var err error
entries, err = mc.GetMetadata("", []string{s.key(key)}, nil)
return err
})
if err != nil {
return fmt.Errorf("alps: failed to fetch IMAP store entry %q: %v", key, err)
}
v, ok := entries[s.key(key)]
if !ok {
return ErrNoStoreEntry
}
if err := json.Unmarshal([]byte(v), out); err != nil {
return fmt.Errorf("alps: failed to unmarshal IMAP store entry %q: %v", key, err)
}
return s.cache.Put(key, out)
}
func (s *imapStore) Put(key string, v interface{}) error {
b, err := json.Marshal(v)
if err != nil {
return fmt.Errorf("alps: failed to marshal IMAP store entry %q: %v", key, err)
}
entries := map[string]string{
s.key(key): string(b),
}
err = s.session.DoIMAP(func(c *imapclient.Client) error {
mc := imapmetadata.NewClient(c)
return mc.SetMetadata("", entries)
})
if err != nil {
return fmt.Errorf("alps: failed to put IMAP store entry %q: %v", key, err)
}
return s.cache.Put(key, v)
}