aboutsummaryrefslogtreecommitdiff
path: root/store.go
blob: 9ef432e9824a6601476cc32556b2ebbe4e5f0b18 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
package koushin

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("koushin: 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("koushin: 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("koushin: 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/koushin/" + 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("koushin: 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("koushin: 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("koushin: 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("koushin: failed to put IMAP store entry %q: %v", key, err)
	}

	return s.cache.Put(key, v)
}