aboutsummaryrefslogblamecommitdiff
path: root/store.go
blob: 153cb6f8badba2543cb5c0a6103de61adaa97ea8 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
            












                                                                         
                                                                


























































                                                                                                                          
                                                                                                   





                                                                
                                                                                                     












                                                         
                                            














                                                                            
                                                                                            





                                                              
                                                                                                






                                                          
                                                                                              








                                                                 
                                                                                          



                                  
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)
}