A webmail client. Forked from https://git.sr.ht/~migadu/alps
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

142 lines
3.4 KiB

  1. package alps
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "reflect"
  6. "sync"
  7. imapmetadata "github.com/emersion/go-imap-metadata"
  8. imapclient "github.com/emersion/go-imap/client"
  9. "github.com/labstack/echo/v4"
  10. )
  11. // ErrNoStoreEntry is returned by Store.Get when the entry doesn't exist.
  12. var ErrNoStoreEntry = fmt.Errorf("alps: no such entry in store")
  13. // Store allows storing per-user persistent data.
  14. //
  15. // Store shouldn't be used from inside Session.DoIMAP.
  16. type Store interface {
  17. Get(key string, out interface{}) error
  18. Put(key string, v interface{}) error
  19. }
  20. var warnedTransientStore = false
  21. func newStore(session *Session, logger echo.Logger) (Store, error) {
  22. s, err := newIMAPStore(session)
  23. if err == nil {
  24. return s, nil
  25. } else if err != errIMAPMetadataUnsupported {
  26. return nil, err
  27. }
  28. if !warnedTransientStore {
  29. logger.Print("Upstream IMAP server doesn't support the METADATA extension, using transient store instead")
  30. warnedTransientStore = true
  31. }
  32. return newMemoryStore(), nil
  33. }
  34. type memoryStore struct {
  35. locker sync.RWMutex
  36. entries map[string]interface{}
  37. }
  38. func newMemoryStore() *memoryStore {
  39. return &memoryStore{entries: make(map[string]interface{})}
  40. }
  41. func (s *memoryStore) Get(key string, out interface{}) error {
  42. s.locker.RLock()
  43. defer s.locker.RUnlock()
  44. v, ok := s.entries[key]
  45. if !ok {
  46. return ErrNoStoreEntry
  47. }
  48. reflect.ValueOf(out).Elem().Set(reflect.ValueOf(v).Elem())
  49. return nil
  50. }
  51. func (s *memoryStore) Put(key string, v interface{}) error {
  52. s.locker.Lock()
  53. s.entries[key] = v
  54. s.locker.Unlock()
  55. return nil
  56. }
  57. type imapStore struct {
  58. session *Session
  59. cache *memoryStore
  60. }
  61. var errIMAPMetadataUnsupported = fmt.Errorf("alps: IMAP server doesn't support METADATA extension")
  62. func newIMAPStore(session *Session) (*imapStore, error) {
  63. err := session.DoIMAP(func(c *imapclient.Client) error {
  64. mc := imapmetadata.NewClient(c)
  65. ok, err := mc.SupportMetadata()
  66. if err != nil {
  67. return fmt.Errorf("alps: failed to check for IMAP METADATA support: %v", err)
  68. }
  69. if !ok {
  70. return errIMAPMetadataUnsupported
  71. }
  72. return nil
  73. })
  74. if err != nil {
  75. return nil, err
  76. }
  77. return &imapStore{session, newMemoryStore()}, nil
  78. }
  79. func (s *imapStore) key(key string) string {
  80. return "/private/vendor/alps/" + key
  81. }
  82. func (s *imapStore) Get(key string, out interface{}) error {
  83. if err := s.cache.Get(key, out); err != ErrNoStoreEntry {
  84. return err
  85. }
  86. var entries map[string]string
  87. err := s.session.DoIMAP(func(c *imapclient.Client) error {
  88. mc := imapmetadata.NewClient(c)
  89. var err error
  90. entries, err = mc.GetMetadata("", []string{s.key(key)}, nil)
  91. return err
  92. })
  93. if err != nil {
  94. return fmt.Errorf("alps: failed to fetch IMAP store entry %q: %v", key, err)
  95. }
  96. v, ok := entries[s.key(key)]
  97. if !ok {
  98. return ErrNoStoreEntry
  99. }
  100. if err := json.Unmarshal([]byte(v), out); err != nil {
  101. return fmt.Errorf("alps: failed to unmarshal IMAP store entry %q: %v", key, err)
  102. }
  103. return s.cache.Put(key, out)
  104. }
  105. func (s *imapStore) Put(key string, v interface{}) error {
  106. b, err := json.Marshal(v)
  107. if err != nil {
  108. return fmt.Errorf("alps: failed to marshal IMAP store entry %q: %v", key, err)
  109. }
  110. entries := map[string]string{
  111. s.key(key): string(b),
  112. }
  113. err = s.session.DoIMAP(func(c *imapclient.Client) error {
  114. mc := imapmetadata.NewClient(c)
  115. return mc.SetMetadata("", entries)
  116. })
  117. if err != nil {
  118. return fmt.Errorf("alps: failed to put IMAP store entry %q: %v", key, err)
  119. }
  120. return s.cache.Put(key, v)
  121. }