mirror of
https://github.com/ProtonMail/proton-bridge.git
synced 2025-12-10 04:36:43 +00:00
fix(BRIDGE-362): added label conflict reconciliation logic
This commit is contained in:
211
internal/services/imapservice/conflicts.go
Normal file
211
internal/services/imapservice/conflicts.go
Normal file
@ -0,0 +1,211 @@
|
||||
// Copyright (c) 2025 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"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/gluon/db"
|
||||
"github.com/ProtonMail/gluon/imap"
|
||||
"github.com/ProtonMail/gluon/reporter"
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/unleash"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type GluonLabelNameProvider interface {
|
||||
GetUserMailboxByName(ctx context.Context, addrID string, labelName []string) (imap.MailboxData, error)
|
||||
}
|
||||
|
||||
type gluonIDProvider interface {
|
||||
GetGluonID(addrID string) (string, bool)
|
||||
}
|
||||
|
||||
type sentryReporter interface {
|
||||
ReportMessageWithContext(string, reporter.Context) error
|
||||
}
|
||||
|
||||
type apiClient interface {
|
||||
GetLabel(ctx context.Context, labelID string, labelTypes ...proton.LabelType) (proton.Label, error)
|
||||
}
|
||||
|
||||
type mailboxFetcherFn func(ctx context.Context, label proton.Label) (imap.MailboxData, error)
|
||||
|
||||
type LabelConflictManager struct {
|
||||
gluonLabelNameProvider GluonLabelNameProvider
|
||||
gluonIDProvider gluonIDProvider
|
||||
client apiClient
|
||||
reporter sentryReporter
|
||||
getFeatureFlagValueFn unleash.GetFlagValueFn
|
||||
}
|
||||
|
||||
func NewLabelConflictManager(
|
||||
gluonLabelNameProvider GluonLabelNameProvider,
|
||||
gluonIDProvider gluonIDProvider,
|
||||
client apiClient,
|
||||
reporter sentryReporter,
|
||||
getFeatureFlagValueFn unleash.GetFlagValueFn) *LabelConflictManager {
|
||||
return &LabelConflictManager{
|
||||
gluonLabelNameProvider: gluonLabelNameProvider,
|
||||
gluonIDProvider: gluonIDProvider,
|
||||
client: client,
|
||||
reporter: reporter,
|
||||
getFeatureFlagValueFn: getFeatureFlagValueFn,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *LabelConflictManager) generateMailboxFetcher(connectors []*Connector) mailboxFetcherFn {
|
||||
return func(ctx context.Context, label proton.Label) (imap.MailboxData, error) {
|
||||
for _, updateCh := range connectors {
|
||||
addrID, ok := m.gluonIDProvider.GetGluonID(updateCh.addrID)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
return m.gluonLabelNameProvider.GetUserMailboxByName(ctx, addrID, GetMailboxName(label))
|
||||
}
|
||||
return imap.MailboxData{}, errors.New("no gluon connectors found")
|
||||
}
|
||||
}
|
||||
|
||||
type LabelConflictResolver interface {
|
||||
ResolveConflict(ctx context.Context, label proton.Label, visited map[string]bool) (func() []imap.Update, error)
|
||||
}
|
||||
type labelConflictResolverImpl struct {
|
||||
mailboxFetch mailboxFetcherFn
|
||||
client apiClient
|
||||
reporter sentryReporter
|
||||
}
|
||||
|
||||
type nullLabelConflictResolverImpl struct {
|
||||
}
|
||||
|
||||
func (r *nullLabelConflictResolverImpl) ResolveConflict(_ context.Context, _ proton.Label, _ map[string]bool) (func() []imap.Update, error) {
|
||||
return func() []imap.Update {
|
||||
return []imap.Update{}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *LabelConflictManager) NewConflictResolver(connectors []*Connector) LabelConflictResolver {
|
||||
if m.getFeatureFlagValueFn(unleash.LabelConflictResolverDisabled) {
|
||||
return &nullLabelConflictResolverImpl{}
|
||||
}
|
||||
|
||||
return &labelConflictResolverImpl{
|
||||
mailboxFetch: m.generateMailboxFetcher(connectors),
|
||||
client: m.client,
|
||||
reporter: m.reporter,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *labelConflictResolverImpl) ResolveConflict(ctx context.Context, label proton.Label, visited map[string]bool) (func() []imap.Update, error) {
|
||||
var updateFns []func() []imap.Update
|
||||
|
||||
// There's a cycle, such as in a label swap operation, we'll need to temporarily rename the label.
|
||||
// The change will be overwritten by one of the previous recursive calls.
|
||||
if visited[label.ID] {
|
||||
fn := func() []imap.Update {
|
||||
return []imap.Update{newMailboxUpdatedOrCreated(imap.MailboxID(label.ID), getMailboxNameWithTempPrefix(label))}
|
||||
}
|
||||
updateFns = append(updateFns, fn)
|
||||
return combineIMAPUpdateFns(updateFns), nil
|
||||
}
|
||||
visited[label.ID] = true
|
||||
|
||||
// Fetch the gluon mailbox data and verify whether there are conflicts with the name.
|
||||
mailboxData, err := r.mailboxFetch(ctx, label)
|
||||
if err != nil {
|
||||
// Name is free, create the mailbox.
|
||||
if db.IsErrNotFound(err) {
|
||||
fn := func() []imap.Update {
|
||||
return []imap.Update{newMailboxUpdatedOrCreated(imap.MailboxID(label.ID), GetMailboxName(label))}
|
||||
}
|
||||
updateFns = append(updateFns, fn)
|
||||
return combineIMAPUpdateFns(updateFns), nil
|
||||
}
|
||||
return combineIMAPUpdateFns(updateFns), err
|
||||
}
|
||||
|
||||
// Verify whether the label name corresponds to the same label ID. If true terminate, we don't need to update.
|
||||
if mailboxData.RemoteID == label.ID {
|
||||
return combineIMAPUpdateFns(updateFns), nil
|
||||
}
|
||||
|
||||
// If the label name belongs to some other label ID. Fetch it's state from the remote.
|
||||
conflictingLabel, err := r.client.GetLabel(ctx, mailboxData.RemoteID, proton.LabelTypeFolder, proton.LabelTypeLabel)
|
||||
if err != nil {
|
||||
// If it's not present on the remote we should delete it. And create the new label.
|
||||
if errors.Is(err, proton.ErrNoSuchLabel) {
|
||||
fn := func() []imap.Update {
|
||||
return []imap.Update{
|
||||
imap.NewMailboxDeleted(imap.MailboxID(mailboxData.RemoteID)),
|
||||
newMailboxUpdatedOrCreated(imap.MailboxID(label.ID), GetMailboxName(label)),
|
||||
}
|
||||
}
|
||||
updateFns = append(updateFns, fn)
|
||||
return combineIMAPUpdateFns(updateFns), nil
|
||||
}
|
||||
return combineIMAPUpdateFns(updateFns), err
|
||||
}
|
||||
|
||||
// Check if the conflicting label name has changed. If not, then this is a BE inconsistency.
|
||||
if compareLabelNames(GetMailboxName(conflictingLabel), mailboxData.BridgeName) {
|
||||
if err := r.reporter.ReportMessageWithContext("Unexpected label conflict", reporter.Context{
|
||||
"labelID": label.ID,
|
||||
"conflictingLabelID": conflictingLabel.ID,
|
||||
}); err != nil {
|
||||
logrus.WithError(err).Error("Failed to report update error")
|
||||
}
|
||||
|
||||
err := fmt.Errorf("unexpected label conflict: the name of label ID %s is already used by label ID %s", label.ID, conflictingLabel.ID)
|
||||
return combineIMAPUpdateFns(updateFns), err
|
||||
}
|
||||
|
||||
// The name of the conflicting label has changed on the remote. We need to verify that the new name does not conflict with anything else.
|
||||
// Thus, a recursive check can be performed.
|
||||
childUpdateFns, err := r.ResolveConflict(ctx, conflictingLabel, visited)
|
||||
if err != nil {
|
||||
return combineIMAPUpdateFns(updateFns), err
|
||||
}
|
||||
updateFns = append(updateFns, childUpdateFns)
|
||||
|
||||
fn := func() []imap.Update {
|
||||
return []imap.Update{newMailboxUpdatedOrCreated(imap.MailboxID(label.ID), GetMailboxName(label))}
|
||||
}
|
||||
updateFns = append(updateFns, fn)
|
||||
|
||||
return combineIMAPUpdateFns(updateFns), nil
|
||||
}
|
||||
|
||||
func combineIMAPUpdateFns(updateFunctions []func() []imap.Update) func() []imap.Update {
|
||||
return func() []imap.Update {
|
||||
var updates []imap.Update
|
||||
for _, fn := range updateFunctions {
|
||||
updates = append(updates, fn()...)
|
||||
}
|
||||
return updates
|
||||
}
|
||||
}
|
||||
|
||||
func compareLabelNames(labelName1, labelName2 []string) bool {
|
||||
name1 := strings.Join(labelName1, "")
|
||||
name2 := strings.Join(labelName2, "")
|
||||
return name1 == name2
|
||||
}
|
||||
682
internal/services/imapservice/conflicts_test.go
Normal file
682
internal/services/imapservice/conflicts_test.go
Normal file
@ -0,0 +1,682 @@
|
||||
// Copyright (c) 2025 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_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/ProtonMail/gluon/db"
|
||||
"github.com/ProtonMail/gluon/imap"
|
||||
"github.com/ProtonMail/gluon/reporter"
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func getFeatureFlagValueMock(_ string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type mockLabelNameProvider struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *mockLabelNameProvider) GetUserMailboxByName(ctx context.Context, addrID string, labelName []string) (imap.MailboxData, error) {
|
||||
args := m.Called(ctx, addrID, labelName)
|
||||
v, ok := args.Get(0).(imap.MailboxData)
|
||||
if !ok {
|
||||
return imap.MailboxData{}, fmt.Errorf("failed to assert type")
|
||||
}
|
||||
return v, args.Error(1)
|
||||
}
|
||||
|
||||
type mockIDProvider struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *mockIDProvider) GetGluonID(addrID string) (string, bool) {
|
||||
args := m.Called(addrID)
|
||||
return args.String(0), args.Bool(1)
|
||||
}
|
||||
|
||||
type mockAPIClient struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *mockAPIClient) GetLabel(ctx context.Context, id string, types ...proton.LabelType) (proton.Label, error) {
|
||||
args := m.Called(ctx, id, types)
|
||||
v, ok := args.Get(0).(proton.Label)
|
||||
if !ok {
|
||||
return proton.Label{}, fmt.Errorf("failed to assert type")
|
||||
}
|
||||
return v, args.Error(1)
|
||||
}
|
||||
|
||||
type mockReporter struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *mockReporter) ReportMessageWithContext(msg string, ctx reporter.Context) error {
|
||||
args := m.Called(msg, ctx)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func TestResolveConflict_UnexpectedLabelConflict(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
label := proton.Label{
|
||||
ID: "label-1",
|
||||
Path: []string{"Work"},
|
||||
Type: proton.LabelTypeLabel,
|
||||
}
|
||||
conflictingLabel := proton.Label{
|
||||
ID: "label-2",
|
||||
Path: []string{"Work"},
|
||||
Type: proton.LabelTypeLabel,
|
||||
}
|
||||
conflictMbox := imap.MailboxData{
|
||||
RemoteID: "label-2",
|
||||
BridgeName: []string{"Labels", "Work"},
|
||||
}
|
||||
|
||||
mockLabelProvider := new(mockLabelNameProvider)
|
||||
mockIDProvider := new(mockIDProvider)
|
||||
mockClient := new(mockAPIClient)
|
||||
mockReporter := new(mockReporter)
|
||||
|
||||
mockLabelProvider.On("GetUserMailboxByName", mock.Anything, "gluon-id", imapservice.GetMailboxName(label)).
|
||||
Return(conflictMbox, nil)
|
||||
mockIDProvider.On("GetGluonID", "addr-1").Return("gluon-id", true)
|
||||
mockClient.On("GetLabel", mock.Anything, "label-2", mock.Anything).
|
||||
Return(conflictingLabel, nil)
|
||||
mockReporter.On("ReportMessageWithContext", "Unexpected label conflict", mock.Anything).
|
||||
Return(nil)
|
||||
|
||||
connector := &imapservice.Connector{}
|
||||
connector.SetAddrIDTest("addr-1")
|
||||
resolver := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, getFeatureFlagValueMock).
|
||||
NewConflictResolver([]*imapservice.Connector{connector})
|
||||
|
||||
visited := make(map[string]bool)
|
||||
_, err := resolver.ResolveConflict(ctx, label, visited)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "unexpected label conflict")
|
||||
}
|
||||
|
||||
func TestResolveDiscrepancy_LabelDoesNotExist(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
label := proton.Label{
|
||||
ID: "label-id-1",
|
||||
Name: "Inbox",
|
||||
Type: proton.LabelTypeLabel,
|
||||
}
|
||||
|
||||
mockLabelProvider := new(mockLabelNameProvider)
|
||||
mockIDProvider := new(mockIDProvider)
|
||||
mockClient := new(mockAPIClient)
|
||||
mockReporter := new(mockReporter)
|
||||
|
||||
mockLabelProvider.On("GetUserMailboxByName", mock.Anything, "gluon-id-1", imapservice.GetMailboxName(label)).
|
||||
Return(imap.MailboxData{}, db.ErrNotFound)
|
||||
|
||||
mockIDProvider.On("GetGluonID", "addr-1").Return("gluon-id-1", true)
|
||||
|
||||
connector := &imapservice.Connector{}
|
||||
connector.SetAddrIDTest("addr-1")
|
||||
connectors := []*imapservice.Connector{connector}
|
||||
|
||||
manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, getFeatureFlagValueMock)
|
||||
resolver := manager.NewConflictResolver(connectors)
|
||||
|
||||
visited := make(map[string]bool)
|
||||
fn, err := resolver.ResolveConflict(ctx, label, visited)
|
||||
|
||||
assert.NoError(t, err)
|
||||
updates := fn()
|
||||
assert.Len(t, updates, 1)
|
||||
muc, ok := updates[0].(*imap.MailboxUpdatedOrCreated)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, imap.MailboxID(label.ID), muc.Mailbox.ID)
|
||||
}
|
||||
|
||||
func TestResolveConflict_MailboxFetchError(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
label := proton.Label{
|
||||
ID: "111",
|
||||
Path: []string{"Work"},
|
||||
Type: proton.LabelTypeLabel,
|
||||
}
|
||||
|
||||
mockLabelProvider := new(mockLabelNameProvider)
|
||||
mockIDProvider := new(mockIDProvider)
|
||||
mockClient := new(mockAPIClient)
|
||||
mockReporter := new(mockReporter)
|
||||
|
||||
mockLabelProvider.On("GetUserMailboxByName", mock.Anything, "gluon-id", imapservice.GetMailboxName(label)).
|
||||
Return(imap.MailboxData{}, errors.New("database connection error"))
|
||||
mockIDProvider.On("GetGluonID", "addr-1").Return("gluon-id", true)
|
||||
|
||||
connector := &imapservice.Connector{}
|
||||
connector.SetAddrIDTest("addr-1")
|
||||
resolver := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, getFeatureFlagValueMock).
|
||||
NewConflictResolver([]*imapservice.Connector{connector})
|
||||
|
||||
visited := make(map[string]bool)
|
||||
_, err := resolver.ResolveConflict(ctx, label, visited)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "database connection error")
|
||||
}
|
||||
|
||||
func TestResolveDiscrepancy_ConflictingLabelDeletedRemotely(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
label := proton.Label{
|
||||
ID: "label-new",
|
||||
Path: []string{"Work"},
|
||||
Type: proton.LabelTypeLabel,
|
||||
}
|
||||
conflictMbox := imap.MailboxData{
|
||||
RemoteID: "label-old",
|
||||
BridgeName: []string{"Labels", "Work"},
|
||||
}
|
||||
|
||||
mockLabelProvider := new(mockLabelNameProvider)
|
||||
mockIDProvider := new(mockIDProvider)
|
||||
mockClient := new(mockAPIClient)
|
||||
mockReporter := new(mockReporter)
|
||||
|
||||
mockLabelProvider.On("GetUserMailboxByName", mock.Anything, "gluon-id-1", imapservice.GetMailboxName(label)).
|
||||
Return(conflictMbox, nil)
|
||||
|
||||
mockIDProvider.On("GetGluonID", "addr-1").Return("gluon-id-1", true)
|
||||
|
||||
mockClient.On("GetLabel", mock.Anything, "label-old", mock.Anything).
|
||||
Return(proton.Label{}, proton.ErrNoSuchLabel)
|
||||
|
||||
connector := &imapservice.Connector{}
|
||||
connector.SetAddrIDTest("addr-1")
|
||||
connectors := []*imapservice.Connector{connector}
|
||||
|
||||
manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, getFeatureFlagValueMock)
|
||||
resolver := manager.NewConflictResolver(connectors)
|
||||
|
||||
visited := make(map[string]bool)
|
||||
fn, err := resolver.ResolveConflict(ctx, label, visited)
|
||||
|
||||
assert.NoError(t, err)
|
||||
updates := fn()
|
||||
assert.Len(t, updates, 2)
|
||||
deleted, ok := updates[0].(*imap.MailboxDeleted)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, imap.MailboxID("label-old"), deleted.MailboxID)
|
||||
|
||||
updated, ok := updates[1].(*imap.MailboxUpdatedOrCreated)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "Work", updated.Mailbox.Name[len(updated.Mailbox.Name)-1])
|
||||
}
|
||||
|
||||
func TestResolveDiscrepancy_LabelAlreadyCorrect(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
label := proton.Label{
|
||||
ID: "label-id-1",
|
||||
Name: "Personal",
|
||||
Type: proton.LabelTypeLabel,
|
||||
}
|
||||
mbox := imap.MailboxData{
|
||||
RemoteID: "label-id-1",
|
||||
BridgeName: []string{"Labels", "Personal"},
|
||||
}
|
||||
|
||||
mockLabelProvider := new(mockLabelNameProvider)
|
||||
mockIDProvider := new(mockIDProvider)
|
||||
mockClient := new(mockAPIClient)
|
||||
mockReporter := new(mockReporter)
|
||||
|
||||
mockLabelProvider.On("GetUserMailboxByName", mock.Anything, "gluon-id-1", imapservice.GetMailboxName(label)).
|
||||
Return(mbox, nil)
|
||||
mockIDProvider.On("GetGluonID", "addr-1").Return("gluon-id-1", true)
|
||||
|
||||
connector := &imapservice.Connector{}
|
||||
connector.SetAddrIDTest("addr-1")
|
||||
connectors := []*imapservice.Connector{connector}
|
||||
|
||||
manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, getFeatureFlagValueMock)
|
||||
resolver := manager.NewConflictResolver(connectors)
|
||||
|
||||
visited := make(map[string]bool)
|
||||
fn, err := resolver.ResolveConflict(ctx, label, visited)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, fn(), 0)
|
||||
}
|
||||
|
||||
func TestResolveConflict_DeepNestedPath(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
label := proton.Label{
|
||||
ID: "111",
|
||||
Path: []string{"Level1", "Level2", "Level3", "DeepFolder"},
|
||||
Type: proton.LabelTypeFolder,
|
||||
}
|
||||
|
||||
mockLabelProvider := new(mockLabelNameProvider)
|
||||
mockIDProvider := new(mockIDProvider)
|
||||
mockClient := new(mockAPIClient)
|
||||
mockReporter := new(mockReporter)
|
||||
|
||||
mockLabelProvider.On("GetUserMailboxByName", mock.Anything, "gluon-id", imapservice.GetMailboxName(label)).
|
||||
Return(imap.MailboxData{}, db.ErrNotFound)
|
||||
mockIDProvider.On("GetGluonID", "addr-1").Return("gluon-id", true)
|
||||
|
||||
connector := &imapservice.Connector{}
|
||||
connector.SetAddrIDTest("addr-1")
|
||||
resolver := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, getFeatureFlagValueMock).
|
||||
NewConflictResolver([]*imapservice.Connector{connector})
|
||||
|
||||
visited := make(map[string]bool)
|
||||
fn, err := resolver.ResolveConflict(ctx, label, visited)
|
||||
|
||||
assert.NoError(t, err)
|
||||
updates := fn()
|
||||
assert.Len(t, updates, 1)
|
||||
|
||||
updated, ok := updates[0].(*imap.MailboxUpdatedOrCreated)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, imap.MailboxID("111"), updated.Mailbox.ID)
|
||||
expectedName := imapservice.GetMailboxName(label)
|
||||
assert.Equal(t, expectedName, updated.Mailbox.Name)
|
||||
}
|
||||
|
||||
func TestResolveLabelDiscrepancy_LabelSwap(t *testing.T) {
|
||||
apiLabels := []proton.Label{
|
||||
{
|
||||
ID: "111",
|
||||
Path: []string{"X"},
|
||||
Type: proton.LabelTypeLabel,
|
||||
},
|
||||
{
|
||||
ID: "222",
|
||||
Path: []string{"Y"},
|
||||
Type: proton.LabelTypeLabel,
|
||||
},
|
||||
}
|
||||
|
||||
gluonLabels := []imap.MailboxData{
|
||||
{
|
||||
RemoteID: "111",
|
||||
BridgeName: []string{"Labels", "Y"},
|
||||
},
|
||||
{
|
||||
RemoteID: "222",
|
||||
BridgeName: []string{"Labels", "X"},
|
||||
},
|
||||
}
|
||||
|
||||
mockLabelProvider := new(mockLabelNameProvider)
|
||||
mockClient := new(mockAPIClient)
|
||||
mockIDProvider := new(mockIDProvider)
|
||||
mockReporter := new(mockReporter)
|
||||
|
||||
mockIDProvider.On("GetGluonID", "addr-1").Return("gluon-id-1", true)
|
||||
|
||||
for _, mbox := range gluonLabels {
|
||||
mockLabelProvider.
|
||||
On("GetUserMailboxByName", mock.Anything, "gluon-id-1", mbox.BridgeName).
|
||||
Return(mbox, nil)
|
||||
}
|
||||
|
||||
for _, label := range apiLabels {
|
||||
mockClient.
|
||||
On("GetLabel", mock.Anything, label.ID, []proton.LabelType{proton.LabelTypeFolder, proton.LabelTypeLabel}).
|
||||
Return(label, nil)
|
||||
}
|
||||
|
||||
connector := &imapservice.Connector{}
|
||||
connector.SetAddrIDTest("addr-1")
|
||||
connectors := []*imapservice.Connector{connector}
|
||||
|
||||
manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, getFeatureFlagValueMock)
|
||||
resolver := manager.NewConflictResolver(connectors)
|
||||
|
||||
visited := make(map[string]bool)
|
||||
fn, err := resolver.ResolveConflict(context.Background(), apiLabels[0], visited)
|
||||
require.NoError(t, err)
|
||||
|
||||
updates := fn()
|
||||
assert.NotEmpty(t, updates)
|
||||
assert.Equal(t, 3, len(updates)) // We expect three calls to be made for a swap operation.
|
||||
|
||||
updateOne, ok := updates[0].(*imap.MailboxUpdatedOrCreated)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, imap.MailboxID(apiLabels[0].ID), updateOne.Mailbox.ID)
|
||||
assert.Equal(t, "tmp_X", updateOne.Mailbox.Name[len(updateOne.Mailbox.Name)-1])
|
||||
|
||||
updateTwo, ok := updates[1].(*imap.MailboxUpdatedOrCreated)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, imap.MailboxID(apiLabels[1].ID), updateTwo.Mailbox.ID)
|
||||
assert.Equal(t, "Y", updateTwo.Mailbox.Name[len(updateTwo.Mailbox.Name)-1])
|
||||
|
||||
updateThree, ok := updates[2].(*imap.MailboxUpdatedOrCreated)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, imap.MailboxID(apiLabels[0].ID), updateThree.Mailbox.ID)
|
||||
assert.Equal(t, "X", updateThree.Mailbox.Name[len(updateThree.Mailbox.Name)-1])
|
||||
}
|
||||
|
||||
func TestResolveLabelDiscrepancy_LabelSwapExtended(t *testing.T) {
|
||||
apiLabels := []proton.Label{
|
||||
{
|
||||
ID: "111",
|
||||
Path: []string{"X"},
|
||||
Type: proton.LabelTypeLabel,
|
||||
},
|
||||
{
|
||||
ID: "222",
|
||||
Path: []string{"Y"},
|
||||
Type: proton.LabelTypeLabel,
|
||||
},
|
||||
{
|
||||
ID: "333",
|
||||
Path: []string{"Z"},
|
||||
Type: proton.LabelTypeLabel,
|
||||
},
|
||||
{
|
||||
ID: "444",
|
||||
Path: []string{"D"},
|
||||
Type: proton.LabelTypeLabel,
|
||||
},
|
||||
}
|
||||
|
||||
gluonLabels := []imap.MailboxData{
|
||||
{
|
||||
RemoteID: "111",
|
||||
BridgeName: []string{"Labels", "D"},
|
||||
},
|
||||
{
|
||||
RemoteID: "222",
|
||||
BridgeName: []string{"Labels", "Z"},
|
||||
},
|
||||
{
|
||||
RemoteID: "333",
|
||||
BridgeName: []string{"Labels", "Y"},
|
||||
},
|
||||
{
|
||||
RemoteID: "444",
|
||||
BridgeName: []string{"Labels", "X"},
|
||||
},
|
||||
}
|
||||
|
||||
mockLabelProvider := new(mockLabelNameProvider)
|
||||
mockClient := new(mockAPIClient)
|
||||
mockIDProvider := new(mockIDProvider)
|
||||
mockReporter := new(mockReporter)
|
||||
|
||||
mockIDProvider.On("GetGluonID", "addr-1").Return("gluon-id-1", true)
|
||||
|
||||
for _, mbox := range gluonLabels {
|
||||
mockLabelProvider.
|
||||
On("GetUserMailboxByName", mock.Anything, "gluon-id-1", mbox.BridgeName).
|
||||
Return(mbox, nil)
|
||||
}
|
||||
|
||||
for _, label := range apiLabels {
|
||||
mockClient.
|
||||
On("GetLabel", mock.Anything, label.ID, []proton.LabelType{proton.LabelTypeFolder, proton.LabelTypeLabel}).
|
||||
Return(label, nil)
|
||||
}
|
||||
|
||||
connector := &imapservice.Connector{}
|
||||
connector.SetAddrIDTest("addr-1")
|
||||
connectors := []*imapservice.Connector{connector}
|
||||
|
||||
manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, getFeatureFlagValueMock)
|
||||
resolver := manager.NewConflictResolver(connectors)
|
||||
|
||||
fn, err := resolver.ResolveConflict(context.Background(), apiLabels[0], make(map[string]bool))
|
||||
require.NoError(t, err)
|
||||
|
||||
updates := fn()
|
||||
assert.NotEmpty(t, updates)
|
||||
// Three calls yet again for a swap operation.
|
||||
assert.Equal(t, 3, len(updates))
|
||||
updateOne, ok := updates[0].(*imap.MailboxUpdatedOrCreated)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, imap.MailboxID(apiLabels[0].ID), updateOne.Mailbox.ID)
|
||||
assert.Equal(t, "tmp_X", updateOne.Mailbox.Name[len(updateOne.Mailbox.Name)-1])
|
||||
|
||||
updateTwo, ok := updates[1].(*imap.MailboxUpdatedOrCreated)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, imap.MailboxID(apiLabels[3].ID), updateTwo.Mailbox.ID)
|
||||
assert.Equal(t, "D", updateTwo.Mailbox.Name[len(updateTwo.Mailbox.Name)-1])
|
||||
|
||||
updateThree, ok := updates[2].(*imap.MailboxUpdatedOrCreated)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, imap.MailboxID(apiLabels[0].ID), updateThree.Mailbox.ID)
|
||||
assert.Equal(t, "X", updateThree.Mailbox.Name[len(updateThree.Mailbox.Name)-1])
|
||||
|
||||
// Fix the secondary swap.
|
||||
fn, err = resolver.ResolveConflict(context.Background(), apiLabels[1], make(map[string]bool))
|
||||
require.NoError(t, err)
|
||||
|
||||
updates = fn()
|
||||
assert.Equal(t, 3, len(updates))
|
||||
updateOne, ok = updates[0].(*imap.MailboxUpdatedOrCreated)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, imap.MailboxID(apiLabels[1].ID), updateOne.Mailbox.ID)
|
||||
assert.Equal(t, "tmp_Y", updateOne.Mailbox.Name[len(updateOne.Mailbox.Name)-1])
|
||||
|
||||
updateTwo, ok = updates[1].(*imap.MailboxUpdatedOrCreated)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, imap.MailboxID(apiLabels[2].ID), updateTwo.Mailbox.ID)
|
||||
assert.Equal(t, "Z", updateTwo.Mailbox.Name[len(updateTwo.Mailbox.Name)-1])
|
||||
|
||||
updateThree, ok = updates[2].(*imap.MailboxUpdatedOrCreated)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, imap.MailboxID(apiLabels[1].ID), updateThree.Mailbox.ID)
|
||||
assert.Equal(t, "Y", updateThree.Mailbox.Name[len(updateThree.Mailbox.Name)-1])
|
||||
}
|
||||
|
||||
func TestResolveLabelDiscrepancy_LabelSwapCyclic(t *testing.T) {
|
||||
apiLabels := []proton.Label{
|
||||
{ID: "111", Path: []string{"A"}, Type: proton.LabelTypeLabel},
|
||||
{ID: "222", Path: []string{"B"}, Type: proton.LabelTypeLabel},
|
||||
{ID: "333", Path: []string{"C"}, Type: proton.LabelTypeLabel},
|
||||
{ID: "444", Path: []string{"D"}, Type: proton.LabelTypeLabel},
|
||||
}
|
||||
|
||||
gluonLabels := []imap.MailboxData{
|
||||
{RemoteID: "111", BridgeName: []string{"Labels", "D"}}, // A <- D
|
||||
{RemoteID: "222", BridgeName: []string{"Labels", "A"}}, // B <- A
|
||||
{RemoteID: "333", BridgeName: []string{"Labels", "B"}}, // C <- B
|
||||
{RemoteID: "444", BridgeName: []string{"Labels", "C"}}, // D <- C
|
||||
}
|
||||
|
||||
mockLabelProvider := new(mockLabelNameProvider)
|
||||
mockClient := new(mockAPIClient)
|
||||
mockIDProvider := new(mockIDProvider)
|
||||
mockReporter := new(mockReporter)
|
||||
|
||||
mockIDProvider.On("GetGluonID", "addr-1").Return("gluon-id-1", true)
|
||||
|
||||
for _, mbox := range gluonLabels {
|
||||
mockLabelProvider.
|
||||
On("GetUserMailboxByName", mock.Anything, "gluon-id-1", mbox.BridgeName).
|
||||
Return(mbox, nil)
|
||||
}
|
||||
|
||||
for _, label := range apiLabels {
|
||||
mockClient.
|
||||
On("GetLabel", mock.Anything, label.ID, []proton.LabelType{proton.LabelTypeFolder, proton.LabelTypeLabel}).
|
||||
Return(label, nil)
|
||||
}
|
||||
|
||||
connector := &imapservice.Connector{}
|
||||
connector.SetAddrIDTest("addr-1")
|
||||
connectors := []*imapservice.Connector{connector}
|
||||
|
||||
manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, getFeatureFlagValueMock)
|
||||
resolver := manager.NewConflictResolver(connectors)
|
||||
|
||||
fn, err := resolver.ResolveConflict(context.Background(), apiLabels[0], make(map[string]bool))
|
||||
require.NoError(t, err)
|
||||
|
||||
updates := fn()
|
||||
assert.NotEmpty(t, updates)
|
||||
assert.Equal(t, 5, len(updates))
|
||||
|
||||
updateOne, ok := updates[0].(*imap.MailboxUpdatedOrCreated)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, imap.MailboxID(apiLabels[0].ID), updateOne.Mailbox.ID)
|
||||
assert.Equal(t, "tmp_A", updateOne.Mailbox.Name[len(updateOne.Mailbox.Name)-1])
|
||||
|
||||
updateTwo, ok := updates[1].(*imap.MailboxUpdatedOrCreated)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, imap.MailboxID(apiLabels[3].ID), updateTwo.Mailbox.ID)
|
||||
assert.Equal(t, "D", updateTwo.Mailbox.Name[len(updateTwo.Mailbox.Name)-1])
|
||||
|
||||
updateThree, ok := updates[2].(*imap.MailboxUpdatedOrCreated)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, imap.MailboxID(apiLabels[2].ID), updateThree.Mailbox.ID)
|
||||
assert.Equal(t, "C", updateThree.Mailbox.Name[len(updateThree.Mailbox.Name)-1])
|
||||
|
||||
updateFour, ok := updates[3].(*imap.MailboxUpdatedOrCreated)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, imap.MailboxID(apiLabels[1].ID), updateFour.Mailbox.ID)
|
||||
assert.Equal(t, "B", updateFour.Mailbox.Name[len(updateFour.Mailbox.Name)-1])
|
||||
|
||||
updateFive, ok := updates[4].(*imap.MailboxUpdatedOrCreated)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, imap.MailboxID(apiLabels[0].ID), updateFive.Mailbox.ID)
|
||||
assert.Equal(t, "A", updateFive.Mailbox.Name[len(updateFive.Mailbox.Name)-1])
|
||||
}
|
||||
|
||||
func TestResolveLabelDiscrepancy_LabelSwapCyclicWithDeletedLabel(t *testing.T) {
|
||||
apiLabels := []proton.Label{
|
||||
{ID: "111", Path: []string{"A"}, Type: proton.LabelTypeLabel},
|
||||
{ID: "333", Path: []string{"C"}, Type: proton.LabelTypeLabel},
|
||||
{ID: "444", Path: []string{"D"}, Type: proton.LabelTypeLabel},
|
||||
}
|
||||
|
||||
gluonLabels := []imap.MailboxData{
|
||||
{RemoteID: "111", BridgeName: []string{"Labels", "D"}},
|
||||
{RemoteID: "222", BridgeName: []string{"Labels", "A"}},
|
||||
{RemoteID: "333", BridgeName: []string{"Labels", "B"}},
|
||||
{RemoteID: "444", BridgeName: []string{"Labels", "C"}},
|
||||
}
|
||||
|
||||
mockLabelProvider := new(mockLabelNameProvider)
|
||||
mockClient := new(mockAPIClient)
|
||||
mockIDProvider := new(mockIDProvider)
|
||||
mockReporter := new(mockReporter)
|
||||
|
||||
mockIDProvider.On("GetGluonID", "addr-1").Return("gluon-id-1", true)
|
||||
|
||||
for _, mbox := range gluonLabels {
|
||||
mockLabelProvider.
|
||||
On("GetUserMailboxByName", mock.Anything, "gluon-id-1", mbox.BridgeName).
|
||||
Return(mbox, nil)
|
||||
}
|
||||
|
||||
for _, label := range apiLabels {
|
||||
mockClient.
|
||||
On("GetLabel", mock.Anything, label.ID, []proton.LabelType{proton.LabelTypeFolder, proton.LabelTypeLabel}).
|
||||
Return(label, nil)
|
||||
}
|
||||
mockClient.On("GetLabel", mock.Anything, "222", []proton.LabelType{proton.LabelTypeFolder, proton.LabelTypeLabel}).Return(proton.Label{}, proton.ErrNoSuchLabel)
|
||||
|
||||
connector := &imapservice.Connector{}
|
||||
connector.SetAddrIDTest("addr-1")
|
||||
connectors := []*imapservice.Connector{connector}
|
||||
|
||||
manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, getFeatureFlagValueMock)
|
||||
resolver := manager.NewConflictResolver(connectors)
|
||||
|
||||
fn, err := resolver.ResolveConflict(context.Background(), apiLabels[2], make(map[string]bool))
|
||||
require.NoError(t, err)
|
||||
|
||||
updates := fn()
|
||||
assert.NotEmpty(t, updates)
|
||||
assert.Equal(t, 3, len(updates))
|
||||
|
||||
updateOne, ok := updates[0].(*imap.MailboxDeleted)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, imap.MailboxID("222"), updateOne.MailboxID)
|
||||
|
||||
updateTwo, ok := updates[1].(*imap.MailboxUpdatedOrCreated)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, imap.MailboxID(apiLabels[0].ID), updateTwo.Mailbox.ID)
|
||||
assert.Equal(t, "A", updateTwo.Mailbox.Name[len(updateTwo.Mailbox.Name)-1])
|
||||
|
||||
updateThree, ok := updates[2].(*imap.MailboxUpdatedOrCreated)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, imap.MailboxID(apiLabels[2].ID), updateThree.Mailbox.ID)
|
||||
assert.Equal(t, "D", updateThree.Mailbox.Name[len(updateThree.Mailbox.Name)-1])
|
||||
}
|
||||
|
||||
func TestResolveLabelDiscrepancy_LabelSwapCyclicWithDeletedLabel_KillSwitchEnabled(t *testing.T) {
|
||||
apiLabels := []proton.Label{
|
||||
{ID: "111", Path: []string{"A"}, Type: proton.LabelTypeLabel},
|
||||
{ID: "333", Path: []string{"C"}, Type: proton.LabelTypeLabel},
|
||||
{ID: "444", Path: []string{"D"}, Type: proton.LabelTypeLabel},
|
||||
}
|
||||
|
||||
gluonLabels := []imap.MailboxData{
|
||||
{RemoteID: "111", BridgeName: []string{"Labels", "D"}},
|
||||
{RemoteID: "222", BridgeName: []string{"Labels", "A"}},
|
||||
{RemoteID: "333", BridgeName: []string{"Labels", "B"}},
|
||||
{RemoteID: "444", BridgeName: []string{"Labels", "C"}},
|
||||
}
|
||||
|
||||
mockLabelProvider := new(mockLabelNameProvider)
|
||||
mockClient := new(mockAPIClient)
|
||||
mockIDProvider := new(mockIDProvider)
|
||||
mockReporter := new(mockReporter)
|
||||
|
||||
mockIDProvider.On("GetGluonID", "addr-1").Return("gluon-id-1", true)
|
||||
|
||||
for _, mbox := range gluonLabels {
|
||||
mockLabelProvider.
|
||||
On("GetUserMailboxByName", mock.Anything, "gluon-id-1", mbox.BridgeName).
|
||||
Return(mbox, nil)
|
||||
}
|
||||
|
||||
for _, label := range apiLabels {
|
||||
mockClient.
|
||||
On("GetLabel", mock.Anything, label.ID, []proton.LabelType{proton.LabelTypeFolder, proton.LabelTypeLabel}).
|
||||
Return(label, nil)
|
||||
}
|
||||
mockClient.On("GetLabel", mock.Anything, "222", []proton.LabelType{proton.LabelTypeFolder, proton.LabelTypeLabel}).Return(proton.Label{}, proton.ErrNoSuchLabel)
|
||||
|
||||
connector := &imapservice.Connector{}
|
||||
connector.SetAddrIDTest("addr-1")
|
||||
connectors := []*imapservice.Connector{connector}
|
||||
|
||||
getFeatureFlagFn := func(_ string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, getFeatureFlagFn)
|
||||
resolver := manager.NewConflictResolver(connectors)
|
||||
|
||||
fn, err := resolver.ResolveConflict(context.Background(), apiLabels[2], make(map[string]bool))
|
||||
require.NoError(t, err)
|
||||
|
||||
updates := fn()
|
||||
assert.Empty(t, updates)
|
||||
}
|
||||
@ -800,8 +800,10 @@ func (s *Connector) createDraftWithParser(ctx context.Context, parser *parser.Pa
|
||||
return draft, nil
|
||||
}
|
||||
|
||||
func (s *Connector) publishUpdate(_ context.Context, update imap.Update) {
|
||||
s.updateCh.Enqueue(update)
|
||||
func (s *Connector) publishUpdate(_ context.Context, updates ...imap.Update) {
|
||||
for _, update := range updates {
|
||||
s.updateCh.Enqueue(update)
|
||||
}
|
||||
}
|
||||
|
||||
func fixGODT3003Labels(
|
||||
@ -903,3 +905,7 @@ func (s *Connector) getSenderProtonAddress(p *parser.Parser) (proton.Address, er
|
||||
|
||||
return addressList[index], nil
|
||||
}
|
||||
|
||||
func (s *Connector) SetAddrIDTest(addrID string) {
|
||||
s.addrID = addrID
|
||||
}
|
||||
|
||||
@ -102,6 +102,16 @@ func newMailboxCreatedUpdate(labelID imap.MailboxID, labelName []string) *imap.M
|
||||
})
|
||||
}
|
||||
|
||||
func newMailboxUpdatedOrCreated(labelID imap.MailboxID, labelName []string) *imap.MailboxUpdatedOrCreated {
|
||||
return imap.NewMailboxUpdatedOrCreated(imap.Mailbox{
|
||||
ID: labelID,
|
||||
Name: labelName,
|
||||
Flags: defaultMailboxFlags(),
|
||||
PermanentFlags: defaultMailboxPermanentFlags(),
|
||||
Attributes: imap.NewFlagSet(),
|
||||
})
|
||||
}
|
||||
|
||||
func GetMailboxName(label proton.Label) []string {
|
||||
var name []string
|
||||
|
||||
@ -122,3 +132,12 @@ func GetMailboxName(label proton.Label) []string {
|
||||
|
||||
return name
|
||||
}
|
||||
|
||||
func nameWithTempPrefix(path []string) []string {
|
||||
path[len(path)-1] = "tmp_" + path[len(path)-1]
|
||||
return path
|
||||
}
|
||||
|
||||
func getMailboxNameWithTempPrefix(label proton.Label) []string {
|
||||
return nameWithTempPrefix(GetMailboxName(label))
|
||||
}
|
||||
|
||||
@ -21,6 +21,7 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/ProtonMail/gluon/connector"
|
||||
"github.com/ProtonMail/gluon/imap"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/syncservice"
|
||||
)
|
||||
|
||||
@ -37,6 +38,8 @@ type IMAPServerManager interface {
|
||||
|
||||
LogRemoteLabelIDs(ctx context.Context, provider GluonIDProvider, addrID ...string) error
|
||||
|
||||
GetUserMailboxByName(ctx context.Context, addrID string, mailboxName []string) (imap.MailboxData, error)
|
||||
|
||||
GetOpenIMAPSessionCount() int
|
||||
GetRollingIMAPConnectionCount() int
|
||||
}
|
||||
@ -70,6 +73,10 @@ func (n NullIMAPServerManager) LogRemoteLabelIDs(
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n NullIMAPServerManager) GetUserMailboxByName(_ context.Context, _ string, _ []string) (imap.MailboxData, error) {
|
||||
return imap.MailboxData{}, nil
|
||||
}
|
||||
|
||||
func (n NullIMAPServerManager) GetOpenIMAPSessionCount() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
@ -36,6 +36,7 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/syncservice"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/userevents"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/useridentity"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/unleash"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/usertypes"
|
||||
"github.com/ProtonMail/proton-bridge/v3/pkg/cpc"
|
||||
"github.com/sirupsen/logrus"
|
||||
@ -91,7 +92,8 @@ type Service struct {
|
||||
lastHandledEventID string
|
||||
isSyncing atomic.Bool
|
||||
|
||||
observabilitySender observability.Sender
|
||||
observabilitySender observability.Sender
|
||||
labelConflictManager *LabelConflictManager
|
||||
}
|
||||
|
||||
func NewService(
|
||||
@ -112,6 +114,7 @@ func NewService(
|
||||
maxSyncMemory uint64,
|
||||
showAllMail bool,
|
||||
observabilitySender observability.Sender,
|
||||
getFeatureFlagValueFn unleash.GetFlagValueFn,
|
||||
) *Service {
|
||||
subscriberName := fmt.Sprintf("imap-%v", identityState.User.ID)
|
||||
|
||||
@ -121,7 +124,8 @@ func NewService(
|
||||
})
|
||||
rwIdentity := newRWIdentity(identityState, bridgePassProvider, keyPassProvider)
|
||||
|
||||
syncUpdateApplier := NewSyncUpdateApplier()
|
||||
labelConflictManager := NewLabelConflictManager(serverManager, gluonIDProvider, client, reporter, getFeatureFlagValueFn)
|
||||
syncUpdateApplier := NewSyncUpdateApplier(labelConflictManager)
|
||||
syncMessageBuilder := NewSyncMessageBuilder(rwIdentity)
|
||||
syncReporter := newSyncReporter(identityState.User.ID, eventPublisher, time.Second)
|
||||
|
||||
@ -156,7 +160,8 @@ func NewService(
|
||||
syncReporter: syncReporter,
|
||||
syncConfigPath: GetSyncConfigPath(syncConfigDir, identityState.User.ID),
|
||||
|
||||
observabilitySender: observabilitySender,
|
||||
observabilitySender: observabilitySender,
|
||||
labelConflictManager: labelConflictManager,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -165,7 +165,7 @@ func addNewAddressSplitMode(ctx context.Context, s *Service, addrID string) erro
|
||||
|
||||
s.connectors[connector.addrID] = connector
|
||||
|
||||
updates, err := syncLabels(ctx, s.labels.GetLabelMap(), []*Connector{connector})
|
||||
updates, err := syncLabels(ctx, s.labels.GetLabelMap(), []*Connector{connector}, s.labelConflictManager)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create labels updates for new address: %w", err)
|
||||
}
|
||||
|
||||
@ -42,7 +42,10 @@ func (s *Service) HandleLabelEvents(ctx context.Context, events []proton.LabelEv
|
||||
continue
|
||||
}
|
||||
|
||||
updates := onLabelCreated(ctx, s, event)
|
||||
updates, err := onLabelCreated(ctx, s, event)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to handle create label event: %w", err)
|
||||
}
|
||||
|
||||
if err := waitOnIMAPUpdates(ctx, updates); err != nil {
|
||||
return err
|
||||
@ -74,8 +77,8 @@ func (s *Service) HandleLabelEvents(ctx context.Context, events []proton.LabelEv
|
||||
return nil
|
||||
}
|
||||
|
||||
func onLabelCreated(ctx context.Context, s *Service, event proton.LabelEvent) []imap.Update {
|
||||
updates := make([]imap.Update, 0, len(s.connectors))
|
||||
func onLabelCreated(ctx context.Context, s *Service, event proton.LabelEvent) ([]imap.Update, error) {
|
||||
updates := []imap.Update{}
|
||||
|
||||
s.log.WithFields(logrus.Fields{
|
||||
"labelID": event.ID,
|
||||
@ -87,7 +90,17 @@ func onLabelCreated(ctx context.Context, s *Service, event proton.LabelEvent) []
|
||||
|
||||
wr.SetLabel(event.Label.ID, event.Label, "onLabelCreated")
|
||||
|
||||
labelConflictResolver := s.labelConflictManager.NewConflictResolver(maps.Values(s.connectors))
|
||||
conflictUpdatesGenerator, err := labelConflictResolver.ResolveConflict(ctx, event.Label, make(map[string]bool))
|
||||
if err != nil {
|
||||
return updates, err
|
||||
}
|
||||
|
||||
for _, updateCh := range maps.Values(s.connectors) {
|
||||
conflictUpdates := conflictUpdatesGenerator()
|
||||
updateCh.publishUpdate(ctx, conflictUpdates...)
|
||||
updates = append(updates, conflictUpdates...)
|
||||
|
||||
update := newMailboxCreatedUpdate(imap.MailboxID(event.ID), GetMailboxName(event.Label))
|
||||
updateCh.publishUpdate(ctx, update)
|
||||
updates = append(updates, update)
|
||||
@ -99,7 +112,7 @@ func onLabelCreated(ctx context.Context, s *Service, event proton.LabelEvent) []
|
||||
Name: event.Label.Name,
|
||||
})
|
||||
|
||||
return updates
|
||||
return updates, nil
|
||||
}
|
||||
|
||||
func onLabelUpdated(ctx context.Context, s *Service, event proton.LabelEvent) ([]imap.Update, error) {
|
||||
@ -136,8 +149,19 @@ func onLabelUpdated(ctx context.Context, s *Service, event proton.LabelEvent) ([
|
||||
// Update the label in the map.
|
||||
wr.SetLabel(apiLabel.ID, apiLabel, "onLabelUpdatedApiID")
|
||||
|
||||
// Resolve potential conflicts
|
||||
labelConflictResolver := s.labelConflictManager.NewConflictResolver(maps.Values(s.connectors))
|
||||
conflictUpdatesGenerator, err := labelConflictResolver.ResolveConflict(ctx, event.Label, make(map[string]bool))
|
||||
if err != nil {
|
||||
return updates, err
|
||||
}
|
||||
|
||||
// Notify the IMAP clients.
|
||||
for _, updateCh := range maps.Values(s.connectors) {
|
||||
conflictUpdates := conflictUpdatesGenerator()
|
||||
updateCh.publishUpdate(ctx, conflictUpdates...)
|
||||
updates = append(updates, conflictUpdates...)
|
||||
|
||||
update := imap.NewMailboxUpdated(
|
||||
imap.MailboxID(apiLabel.ID),
|
||||
GetMailboxName(apiLabel),
|
||||
|
||||
@ -31,8 +31,9 @@ import (
|
||||
)
|
||||
|
||||
type SyncUpdateApplier struct {
|
||||
requestCh chan updateRequest
|
||||
replyCh chan updateReply
|
||||
requestCh chan updateRequest
|
||||
replyCh chan updateReply
|
||||
labelConflictManager *LabelConflictManager
|
||||
}
|
||||
|
||||
type updateReply struct {
|
||||
@ -42,10 +43,11 @@ type updateReply struct {
|
||||
|
||||
type updateRequest = func(ctx context.Context, mode usertypes.AddressMode, connectors map[string]*Connector) ([]imap.Update, error)
|
||||
|
||||
func NewSyncUpdateApplier() *SyncUpdateApplier {
|
||||
func NewSyncUpdateApplier(labelConflictManager *LabelConflictManager) *SyncUpdateApplier {
|
||||
return &SyncUpdateApplier{
|
||||
requestCh: make(chan updateRequest),
|
||||
replyCh: make(chan updateReply),
|
||||
requestCh: make(chan updateRequest),
|
||||
replyCh: make(chan updateReply),
|
||||
labelConflictManager: labelConflictManager,
|
||||
}
|
||||
}
|
||||
|
||||
@ -113,7 +115,7 @@ func (s *SyncUpdateApplier) ApplySyncUpdates(ctx context.Context, updates []sync
|
||||
|
||||
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))
|
||||
return syncLabels(ctx, labels, maps.Values(connectors), s.labelConflictManager)
|
||||
}
|
||||
|
||||
updates, err := s.sendRequest(ctx, request)
|
||||
@ -128,9 +130,11 @@ func (s *SyncUpdateApplier) SyncLabels(ctx context.Context, labels map[string]pr
|
||||
}
|
||||
|
||||
// nolint:exhaustive
|
||||
func syncLabels(ctx context.Context, labels map[string]proton.Label, connectors []*Connector) ([]imap.Update, error) {
|
||||
func syncLabels(ctx context.Context, labels map[string]proton.Label, connectors []*Connector, labelConflictManager *LabelConflictManager) ([]imap.Update, error) {
|
||||
var updates []imap.Update
|
||||
|
||||
labelConflictResolver := labelConflictManager.NewConflictResolver(connectors)
|
||||
|
||||
// Create placeholder Folders/Labels mailboxes with the \Noselect attribute.
|
||||
for _, prefix := range []string{folderPrefix, labelPrefix} {
|
||||
for _, updateCh := range connectors {
|
||||
@ -155,7 +159,16 @@ func syncLabels(ctx context.Context, labels map[string]proton.Label, connectors
|
||||
}
|
||||
|
||||
case proton.LabelTypeFolder, proton.LabelTypeLabel:
|
||||
conflictUpdatesGenerator, err := labelConflictResolver.ResolveConflict(ctx, label, make(map[string]bool))
|
||||
if err != nil {
|
||||
return updates, err
|
||||
}
|
||||
|
||||
for _, updateCh := range connectors {
|
||||
conflictUpdates := conflictUpdatesGenerator()
|
||||
updateCh.publishUpdate(ctx, conflictUpdates...)
|
||||
updates = append(updates, conflictUpdates...)
|
||||
|
||||
update := newMailboxCreatedUpdate(imap.MailboxID(labelID), GetMailboxName(label))
|
||||
updateCh.publishUpdate(ctx, update)
|
||||
updates = append(updates, update)
|
||||
|
||||
@ -204,6 +204,10 @@ func (sm *Service) RemoveSMTPAccount(ctx context.Context, service *bridgesmtp.Se
|
||||
return err
|
||||
}
|
||||
|
||||
func (sm *Service) GetUserMailboxByName(ctx context.Context, addrID string, mailboxName []string) (imap.MailboxData, error) {
|
||||
return sm.imapServer.GetUserMailboxByName(ctx, addrID, mailboxName)
|
||||
}
|
||||
|
||||
func (sm *Service) GetOpenIMAPSessionCount() int {
|
||||
return sm.imapServer.GetOpenSessionCount()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user