forked from Silverfish/proton-bridge
feat(GODT-2803): Gluon IMAP State access
Update to latest Gluon to allow access to the database for bridge. The cache is placed in a `SharedCache` type to ensure that we respect calls to `Connector.Close`. In theory, this should never trigger an error, but this way we can catch it if it happens. https://github.com/ProtonMail/gluon/pull/391
This commit is contained in:
@ -60,6 +60,8 @@ type Connector struct {
|
||||
labels sharedLabels
|
||||
updateCh *async.QueuedChannel[imap.Update]
|
||||
log *logrus.Entry
|
||||
|
||||
sharedCache *SharedCache
|
||||
}
|
||||
|
||||
func NewConnector(
|
||||
@ -101,6 +103,8 @@ func NewConnector(
|
||||
"addr-id": addrID,
|
||||
"user-id": userID,
|
||||
}),
|
||||
|
||||
sharedCache: NewSharedCached(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -109,6 +113,11 @@ func (s *Connector) StateClose() {
|
||||
s.updateCh.CloseAndDiscardQueued()
|
||||
}
|
||||
|
||||
func (s *Connector) Init(_ context.Context, cache connector.IMAPState) error {
|
||||
s.sharedCache.Set(cache)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Connector) Authorize(ctx context.Context, username string, password []byte) bool {
|
||||
addrID, err := s.identityState.CheckAuth(username, password, s.telemetry)
|
||||
if err != nil {
|
||||
@ -124,7 +133,7 @@ func (s *Connector) Authorize(ctx context.Context, username string, password []b
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *Connector) CreateMailbox(ctx context.Context, name []string) (imap.Mailbox, error) {
|
||||
func (s *Connector) CreateMailbox(ctx context.Context, _ connector.IMAPStateWrite, name []string) (imap.Mailbox, error) {
|
||||
if len(name) < 2 {
|
||||
return imap.Mailbox{}, fmt.Errorf("invalid mailbox name %q: %w", name, connector.ErrOperationNotAllowed)
|
||||
}
|
||||
@ -177,7 +186,7 @@ func (s *Connector) GetMailboxVisibility(_ context.Context, mboxID imap.MailboxI
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Connector) UpdateMailboxName(ctx context.Context, mboxID imap.MailboxID, name []string) error {
|
||||
func (s *Connector) UpdateMailboxName(ctx context.Context, _ connector.IMAPStateWrite, mboxID imap.MailboxID, name []string) error {
|
||||
if len(name) < 2 {
|
||||
return fmt.Errorf("invalid mailbox name %q: %w", name, connector.ErrOperationNotAllowed)
|
||||
}
|
||||
@ -194,7 +203,7 @@ func (s *Connector) UpdateMailboxName(ctx context.Context, mboxID imap.MailboxID
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Connector) DeleteMailbox(ctx context.Context, mboxID imap.MailboxID) error {
|
||||
func (s *Connector) DeleteMailbox(ctx context.Context, _ connector.IMAPStateWrite, mboxID imap.MailboxID) error {
|
||||
if err := s.client.DeleteLabel(ctx, string(mboxID)); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -207,7 +216,7 @@ func (s *Connector) DeleteMailbox(ctx context.Context, mboxID imap.MailboxID) er
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Connector) CreateMessage(ctx context.Context, mailboxID imap.MailboxID, literal []byte, flags imap.FlagSet, _ time.Time) (imap.Message, []byte, error) {
|
||||
func (s *Connector) CreateMessage(ctx context.Context, _ connector.IMAPStateWrite, mailboxID imap.MailboxID, literal []byte, flags imap.FlagSet, _ time.Time) (imap.Message, []byte, error) {
|
||||
if mailboxID == proton.AllMailLabel {
|
||||
return imap.Message{}, nil, connector.ErrOperationNotAllowed
|
||||
}
|
||||
@ -305,7 +314,7 @@ func (s *Connector) CreateMessage(ctx context.Context, mailboxID imap.MailboxID,
|
||||
return msg, literal, err
|
||||
}
|
||||
|
||||
func (s *Connector) AddMessagesToMailbox(ctx context.Context, messageIDs []imap.MessageID, mboxID imap.MailboxID) error {
|
||||
func (s *Connector) AddMessagesToMailbox(ctx context.Context, _ connector.IMAPStateWrite, messageIDs []imap.MessageID, mboxID imap.MailboxID) error {
|
||||
if isAllMailOrScheduled(mboxID) {
|
||||
return connector.ErrOperationNotAllowed
|
||||
}
|
||||
@ -313,7 +322,7 @@ func (s *Connector) AddMessagesToMailbox(ctx context.Context, messageIDs []imap.
|
||||
return s.client.LabelMessages(ctx, usertypes.MapTo[imap.MessageID, string](messageIDs), string(mboxID))
|
||||
}
|
||||
|
||||
func (s *Connector) RemoveMessagesFromMailbox(ctx context.Context, messageIDs []imap.MessageID, mboxID imap.MailboxID) error {
|
||||
func (s *Connector) RemoveMessagesFromMailbox(ctx context.Context, _ connector.IMAPStateWrite, messageIDs []imap.MessageID, mboxID imap.MailboxID) error {
|
||||
if isAllMailOrScheduled(mboxID) {
|
||||
return connector.ErrOperationNotAllowed
|
||||
}
|
||||
@ -332,7 +341,7 @@ func (s *Connector) RemoveMessagesFromMailbox(ctx context.Context, messageIDs []
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Connector) MoveMessages(ctx context.Context, messageIDs []imap.MessageID, mboxFromID, mboxToID imap.MailboxID) (bool, error) {
|
||||
func (s *Connector) MoveMessages(ctx context.Context, _ connector.IMAPStateWrite, messageIDs []imap.MessageID, mboxFromID, mboxToID imap.MailboxID) (bool, error) {
|
||||
if (mboxFromID == proton.InboxLabel && mboxToID == proton.SentLabel) ||
|
||||
(mboxFromID == proton.SentLabel && mboxToID == proton.InboxLabel) ||
|
||||
isAllMailOrScheduled(mboxFromID) ||
|
||||
@ -370,7 +379,7 @@ func (s *Connector) MoveMessages(ctx context.Context, messageIDs []imap.MessageI
|
||||
return shouldExpungeOldLocation, nil
|
||||
}
|
||||
|
||||
func (s *Connector) MarkMessagesSeen(ctx context.Context, messageIDs []imap.MessageID, seen bool) error {
|
||||
func (s *Connector) MarkMessagesSeen(ctx context.Context, _ connector.IMAPStateWrite, messageIDs []imap.MessageID, seen bool) error {
|
||||
if seen {
|
||||
return s.client.MarkMessagesRead(ctx, usertypes.MapTo[imap.MessageID, string](messageIDs)...)
|
||||
}
|
||||
@ -378,7 +387,7 @@ func (s *Connector) MarkMessagesSeen(ctx context.Context, messageIDs []imap.Mess
|
||||
return s.client.MarkMessagesUnread(ctx, usertypes.MapTo[imap.MessageID, string](messageIDs)...)
|
||||
}
|
||||
|
||||
func (s *Connector) MarkMessagesFlagged(ctx context.Context, messageIDs []imap.MessageID, flagged bool) error {
|
||||
func (s *Connector) MarkMessagesFlagged(ctx context.Context, _ connector.IMAPStateWrite, messageIDs []imap.MessageID, flagged bool) error {
|
||||
if flagged {
|
||||
return s.client.LabelMessages(ctx, usertypes.MapTo[imap.MessageID, string](messageIDs), proton.StarredLabel)
|
||||
}
|
||||
@ -392,6 +401,7 @@ func (s *Connector) GetUpdates() <-chan imap.Update {
|
||||
|
||||
func (s *Connector) Close(_ context.Context) error {
|
||||
// Nothing to do
|
||||
s.sharedCache.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
87
internal/services/imapservice/shared_cache.go
Normal file
87
internal/services/imapservice/shared_cache.go
Normal file
@ -0,0 +1,87 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package imapservice
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"github.com/ProtonMail/gluon/connector"
|
||||
)
|
||||
|
||||
type CacheAccessor interface {
|
||||
connector.IMAPState
|
||||
Close()
|
||||
}
|
||||
|
||||
// SharedCache is meant to protect access to the database and guarantee it's always valid. There may be some corner
|
||||
// cases where the Gluon connector can get closed while we are processing events in parallel. If for some reason
|
||||
// Gluon closes the database, the instance is invalidated and any attempts to access this state will return
|
||||
// `ErrCacheNotAvailable`.
|
||||
type SharedCache struct {
|
||||
cache connector.IMAPState
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
func NewSharedCached() *SharedCache {
|
||||
return &SharedCache{}
|
||||
}
|
||||
|
||||
var ErrCacheNotAvailable = errors.New("cache no longer available")
|
||||
|
||||
func (s *SharedCache) Set(cache connector.IMAPState) {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
s.cache = cache
|
||||
}
|
||||
|
||||
func (s *SharedCache) Close() {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
s.cache = nil
|
||||
}
|
||||
|
||||
func (s *SharedCache) Acquire() (CacheAccessor, error) {
|
||||
s.lock.RLock()
|
||||
|
||||
if s.cache == nil {
|
||||
s.lock.RUnlock()
|
||||
return nil, ErrCacheNotAvailable
|
||||
}
|
||||
|
||||
return &cacheAccessor{sharedCache: s}, nil
|
||||
}
|
||||
|
||||
type cacheAccessor struct {
|
||||
sharedCache *SharedCache
|
||||
}
|
||||
|
||||
func (c cacheAccessor) Read(ctx context.Context, f func(context.Context, connector.IMAPStateRead) error) error {
|
||||
return c.sharedCache.cache.Read(ctx, f)
|
||||
}
|
||||
|
||||
func (c cacheAccessor) Write(ctx context.Context, f func(context.Context, connector.IMAPStateWrite) error) error {
|
||||
return c.sharedCache.cache.Write(ctx, f)
|
||||
}
|
||||
|
||||
func (c cacheAccessor) Close() {
|
||||
c.sharedCache.lock.RUnlock()
|
||||
}
|
||||
Reference in New Issue
Block a user