koushin: add Store interface
References: https://todo.sr.ht/~sircmpwn/koushin/5
This commit is contained in:
parent
c0b4998b38
commit
bdf1a8b02b
1
go.mod
1
go.mod
|
@ -6,6 +6,7 @@ require (
|
||||||
github.com/aymerick/douceur v0.2.0
|
github.com/aymerick/douceur v0.2.0
|
||||||
github.com/chris-ramon/douceur v0.2.0
|
github.com/chris-ramon/douceur v0.2.0
|
||||||
github.com/emersion/go-imap v1.0.3
|
github.com/emersion/go-imap v1.0.3
|
||||||
|
github.com/emersion/go-imap-metadata v0.0.0-20200128185110-9d939d2a0915
|
||||||
github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342
|
github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342
|
||||||
github.com/emersion/go-imap-specialuse v0.0.0-20161227184202-ba031ced6a62
|
github.com/emersion/go-imap-specialuse v0.0.0-20161227184202-ba031ced6a62
|
||||||
github.com/emersion/go-message v0.11.1
|
github.com/emersion/go-message v0.11.1
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -12,6 +12,8 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumC
|
||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||||
github.com/emersion/go-imap v1.0.3 h1:5eEee8/DTSIPfliiWqwfvjPGkU8bBtvOy/Wx+eeXzO4=
|
github.com/emersion/go-imap v1.0.3 h1:5eEee8/DTSIPfliiWqwfvjPGkU8bBtvOy/Wx+eeXzO4=
|
||||||
github.com/emersion/go-imap v1.0.3/go.mod h1:yKASt+C3ZiDAiCSssxg9caIckWF/JG7ZQTO7GAmvicU=
|
github.com/emersion/go-imap v1.0.3/go.mod h1:yKASt+C3ZiDAiCSssxg9caIckWF/JG7ZQTO7GAmvicU=
|
||||||
|
github.com/emersion/go-imap-metadata v0.0.0-20200128185110-9d939d2a0915 h1:8xzODjLqrfAJo+CNhX0Fp47vdVN0ZvmGV3CPt/Ex1nU=
|
||||||
|
github.com/emersion/go-imap-metadata v0.0.0-20200128185110-9d939d2a0915/go.mod h1:6mXMzbK9Ts0mrrBibqy56SqZpuFMry5AedTgu6qY5zM=
|
||||||
github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342 h1:5p1t3e1PomYgLWwEwhwEU5kVBwcyAcVrOpexv8AeZx0=
|
github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342 h1:5p1t3e1PomYgLWwEwhwEU5kVBwcyAcVrOpexv8AeZx0=
|
||||||
github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342/go.mod h1:QuMaZcKFDVI0yCrnAbPLfbwllz1wtOrZH8/vZ5yzp4w=
|
github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342/go.mod h1:QuMaZcKFDVI0yCrnAbPLfbwllz1wtOrZH8/vZ5yzp4w=
|
||||||
github.com/emersion/go-imap-specialuse v0.0.0-20161227184202-ba031ced6a62 h1:4ZAfwfc8aDlj26kkEap1UDSwwDnJp9Ie8Uj1MSXAkPk=
|
github.com/emersion/go-imap-specialuse v0.0.0-20161227184202-ba031ced6a62 h1:4ZAfwfc8aDlj26kkEap1UDSwwDnJp9Ie8Uj1MSXAkPk=
|
||||||
|
|
|
@ -59,7 +59,7 @@ func newServer(e *echo.Echo, options *Options) (*Server, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
s.Sessions = newSessionManager(s.dialIMAP, s.dialSMTP)
|
s.Sessions = newSessionManager(s.dialIMAP, s.dialSMTP, e.Logger)
|
||||||
|
|
||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
18
session.go
18
session.go
|
@ -12,6 +12,7 @@ import (
|
||||||
imapclient "github.com/emersion/go-imap/client"
|
imapclient "github.com/emersion/go-imap/client"
|
||||||
"github.com/emersion/go-sasl"
|
"github.com/emersion/go-sasl"
|
||||||
"github.com/emersion/go-smtp"
|
"github.com/emersion/go-smtp"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO: make this configurable
|
// TODO: make this configurable
|
||||||
|
@ -48,6 +49,7 @@ type Session struct {
|
||||||
closed chan struct{}
|
closed chan struct{}
|
||||||
pings chan struct{}
|
pings chan struct{}
|
||||||
timer *time.Timer
|
timer *time.Timer
|
||||||
|
store Store
|
||||||
|
|
||||||
imapLocker sync.Mutex
|
imapLocker sync.Mutex
|
||||||
imapConn *imapclient.Client // protected by locker, can be nil
|
imapConn *imapclient.Client // protected by locker, can be nil
|
||||||
|
@ -122,6 +124,11 @@ func (s *Session) Close() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store returns a store suitable for storing persistent user data.
|
||||||
|
func (s *Session) Store() Store {
|
||||||
|
return s.store
|
||||||
|
}
|
||||||
|
|
||||||
type (
|
type (
|
||||||
// DialIMAPFunc connects to the upstream IMAP server.
|
// DialIMAPFunc connects to the upstream IMAP server.
|
||||||
DialIMAPFunc func() (*imapclient.Client, error)
|
DialIMAPFunc func() (*imapclient.Client, error)
|
||||||
|
@ -134,16 +141,18 @@ type (
|
||||||
type SessionManager struct {
|
type SessionManager struct {
|
||||||
dialIMAP DialIMAPFunc
|
dialIMAP DialIMAPFunc
|
||||||
dialSMTP DialSMTPFunc
|
dialSMTP DialSMTPFunc
|
||||||
|
logger echo.Logger
|
||||||
|
|
||||||
locker sync.Mutex
|
locker sync.Mutex
|
||||||
sessions map[string]*Session // protected by locker
|
sessions map[string]*Session // protected by locker
|
||||||
}
|
}
|
||||||
|
|
||||||
func newSessionManager(dialIMAP DialIMAPFunc, dialSMTP DialSMTPFunc) *SessionManager {
|
func newSessionManager(dialIMAP DialIMAPFunc, dialSMTP DialSMTPFunc, logger echo.Logger) *SessionManager {
|
||||||
return &SessionManager{
|
return &SessionManager{
|
||||||
sessions: make(map[string]*Session),
|
sessions: make(map[string]*Session),
|
||||||
dialIMAP: dialIMAP,
|
dialIMAP: dialIMAP,
|
||||||
dialSMTP: dialSMTP,
|
dialSMTP: dialSMTP,
|
||||||
|
logger: logger,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -185,7 +194,6 @@ func (sm *SessionManager) Put(username, password string) (*Session, error) {
|
||||||
|
|
||||||
var token string
|
var token string
|
||||||
for {
|
for {
|
||||||
var err error
|
|
||||||
token, err = generateToken()
|
token, err = generateToken()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Logout()
|
c.Logout()
|
||||||
|
@ -206,6 +214,12 @@ func (sm *SessionManager) Put(username, password string) (*Session, error) {
|
||||||
password: password,
|
password: password,
|
||||||
token: token,
|
token: token,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s.store, err = newStore(s, sm.logger)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
sm.sessions[token] = s
|
sm.sessions[token] = s
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
|
|
141
store.go
Normal file
141
store.go
Normal file
|
@ -0,0 +1,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)
|
||||||
|
}
|
Loading…
Reference in a new issue