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:
Leander Beernaert
2023-08-11 09:37:56 +02:00
parent 24331f9715
commit 41c125f65e
8 changed files with 197 additions and 12 deletions

View File

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

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