Files
proton-bridge/internal/services/imapservice/sync_update_applier.go
Leander Beernaert efbe84964f feat(GODT-2829): Integrate new sync service
Update imap service to use the new sync service.

The new sync state is stored as simple file on disk to avoid contention
with concurrent vault writes.
2023-08-25 15:21:00 +02:00

235 lines
6.1 KiB
Go

// 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"
"fmt"
"github.com/ProtonMail/gluon/imap"
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/proton-bridge/v3/internal/services/syncservice"
"github.com/ProtonMail/proton-bridge/v3/internal/usertypes"
"github.com/bradenaw/juniper/xslices"
"github.com/sirupsen/logrus"
"golang.org/x/exp/maps"
)
type SyncUpdateApplier struct {
requestCh chan updateRequest
replyCh chan updateReply
}
type updateReply struct {
updates []imap.Update
err error
}
type updateRequest = func(ctx context.Context, mode usertypes.AddressMode, connectors map[string]*Connector) ([]imap.Update, error)
func NewSyncUpdateApplier() *SyncUpdateApplier {
return &SyncUpdateApplier{
requestCh: make(chan updateRequest),
replyCh: make(chan updateReply),
}
}
func (s *SyncUpdateApplier) Close() {
close(s.requestCh)
close(s.replyCh)
}
func (s *SyncUpdateApplier) ApplySyncUpdates(ctx context.Context, updates []syncservice.BuildResult) error {
request := func(ctx context.Context, mode usertypes.AddressMode, connectors map[string]*Connector) ([]imap.Update, error) {
if mode == usertypes.AddressModeCombined {
if len(connectors) != 1 {
return nil, fmt.Errorf("unexpected connecto list state")
}
c := maps.Values(connectors)[0]
update := imap.NewMessagesCreated(true, xslices.Map(updates, func(b syncservice.BuildResult) *imap.MessageCreated {
return b.Update
})...)
c.publishUpdate(ctx, update)
return []imap.Update{update}, nil
}
updateMap := make(map[string]*imap.MessagesCreated, len(connectors))
result := make([]imap.Update, 0, len(connectors))
for _, up := range updates {
update, ok := updateMap[up.AddressID]
if !ok {
update = imap.NewMessagesCreated(true)
updateMap[up.AddressID] = update
result = append(result, update)
}
update.Messages = append(update.Messages, up.Update)
}
for addrID, update := range updateMap {
c, ok := connectors[addrID]
if !ok {
logrus.Warnf("Could not find connector for address %v", addrID)
continue
}
c.publishUpdate(ctx, update)
}
return result, nil
}
result, err := s.sendRequest(ctx, request)
if err != nil {
return err
}
if err := waitOnIMAPUpdates(ctx, result); err != nil {
return fmt.Errorf("could not apply updates: %w", err)
}
return nil
}
func (s *SyncUpdateApplier) SyncSystemLabelsOnly(ctx context.Context, labels map[string]proton.Label) error {
request := func(ctx context.Context, _ usertypes.AddressMode, connectors map[string]*Connector) ([]imap.Update, error) {
updates := make([]imap.Update, 0, len(labels)*len(connectors))
for _, label := range labels {
if !WantLabel(label) {
continue
}
for _, c := range connectors {
update := newSystemMailboxCreatedUpdate(imap.MailboxID(label.ID), label.Name)
updates = append(updates, update)
c.publishUpdate(ctx, update)
}
}
return updates, nil
}
updates, err := s.sendRequest(ctx, request)
if err != nil {
return err
}
if err := waitOnIMAPUpdates(ctx, updates); err != nil {
return fmt.Errorf("could not sync system labels: %w", err)
}
return nil
}
func (s *SyncUpdateApplier) SyncLabels(ctx context.Context, labels map[string]proton.Label) error {
request := func(ctx context.Context, _ usertypes.AddressMode, connectors map[string]*Connector) ([]imap.Update, error) {
return syncLabels(ctx, labels, maps.Values(connectors))
}
updates, err := s.sendRequest(ctx, request)
if err != nil {
return err
}
if err := waitOnIMAPUpdates(ctx, updates); err != nil {
return fmt.Errorf("could not sync labels: %w", err)
}
return nil
}
// nolint:exhaustive
func syncLabels(ctx context.Context, labels map[string]proton.Label, connectors []*Connector) ([]imap.Update, error) {
var updates []imap.Update
// Create placeholder Folders/Labels mailboxes with the \Noselect attribute.
for _, prefix := range []string{folderPrefix, labelPrefix} {
for _, updateCh := range connectors {
update := newPlaceHolderMailboxCreatedUpdate(prefix)
updateCh.publishUpdate(ctx, update)
updates = append(updates, update)
}
}
// Sync the user's labels.
for labelID, label := range labels {
if !WantLabel(label) {
continue
}
switch label.Type {
case proton.LabelTypeSystem:
for _, updateCh := range connectors {
update := newSystemMailboxCreatedUpdate(imap.MailboxID(label.ID), label.Name)
updateCh.publishUpdate(ctx, update)
updates = append(updates, update)
}
case proton.LabelTypeFolder, proton.LabelTypeLabel:
for _, updateCh := range connectors {
update := newMailboxCreatedUpdate(imap.MailboxID(labelID), GetMailboxName(label))
updateCh.publishUpdate(ctx, update)
updates = append(updates, update)
}
default:
return nil, fmt.Errorf("unknown label type: %d", label.Type)
}
}
return updates, nil
}
func (s *SyncUpdateApplier) sendRequest(ctx context.Context, request updateRequest) ([]imap.Update, error) {
select {
case <-ctx.Done():
return nil, ctx.Err()
case s.requestCh <- request:
}
select {
case <-ctx.Done():
return nil, ctx.Err()
case reply, ok := <-s.replyCh:
if !ok {
return nil, fmt.Errorf("no reply")
}
if reply.err != nil {
return nil, reply.err
}
return reply.updates, nil
}
}
func (s *SyncUpdateApplier) reply(ctx context.Context, updates []imap.Update, err error) error {
select {
case <-ctx.Done():
return ctx.Err()
case s.replyCh <- updateReply{
updates: updates,
err: err,
}:
return nil
}
}