feat(BRIDGE-383): Internal mailbox conflict resolution extended; Minor alterations to mailbox conflict pre-checker

This commit is contained in:
Atanas Janeshliev
2025-06-10 23:18:26 +02:00
parent 50ab740b92
commit a00af3a398
16 changed files with 357 additions and 94 deletions

2
go.mod
View File

@ -7,7 +7,7 @@ toolchain go1.24.2
require ( require (
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557 github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557
github.com/Masterminds/semver/v3 v3.2.0 github.com/Masterminds/semver/v3 v3.2.0
github.com/ProtonMail/gluon v0.17.1-0.20250604083016-c6e17f8461b1 github.com/ProtonMail/gluon v0.17.1-0.20250611120816-05167d499f8d
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
github.com/ProtonMail/go-proton-api v0.4.1-0.20250417134000-e624a080f7ba github.com/ProtonMail/go-proton-api v0.4.1-0.20250417134000-e624a080f7ba
github.com/ProtonMail/gopenpgp/v2 v2.8.2-proton github.com/ProtonMail/gopenpgp/v2 v2.8.2-proton

4
go.sum
View File

@ -36,8 +36,8 @@ github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAE
github.com/ProtonMail/bcrypt v0.0.0-20210511135022-227b4adcab57/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I= github.com/ProtonMail/bcrypt v0.0.0-20210511135022-227b4adcab57/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I=
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs69zUkSzubzjBbL+cmOXgnmt9Fyd9ug= github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs69zUkSzubzjBbL+cmOXgnmt9Fyd9ug=
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo= github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
github.com/ProtonMail/gluon v0.17.1-0.20250604083016-c6e17f8461b1 h1:FvkPBZF/M5GpZTy+hzhaheyi+Z5XWeZOL5GKVKqj85Y= github.com/ProtonMail/gluon v0.17.1-0.20250611120816-05167d499f8d h1:45W7G+X0w7nzLzeB0eiFkGho5DTK1jNmmNbt3IhN524=
github.com/ProtonMail/gluon v0.17.1-0.20250604083016-c6e17f8461b1/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8= github.com/ProtonMail/gluon v0.17.1-0.20250611120816-05167d499f8d/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8=
github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE= github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
github.com/ProtonMail/go-crypto v1.1.4-proton h1:KIo9uNlk3vzlwI7o5VjhiEjI4Ld1TDixOMnoNZyfpFE= github.com/ProtonMail/go-crypto v1.1.4-proton h1:KIo9uNlk3vzlwI7o5VjhiEjI4Ld1TDixOMnoNZyfpFE=
github.com/ProtonMail/go-crypto v1.1.4-proton/go.mod h1:zNoyBJW3p/yVWiHNZgfTF9VsjwqYof5YY0M9kt2QaX0= github.com/ProtonMail/go-crypto v1.1.4-proton/go.mod h1:zNoyBJW3p/yVWiHNZgfTF9VsjwqYof5YY0M9kt2QaX0=

View File

@ -88,3 +88,18 @@ func (mr *MockReporterMockRecorder) ReportMessageWithContext(arg0, arg1 interfac
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReportMessageWithContext", reflect.TypeOf((*MockReporter)(nil).ReportMessageWithContext), arg0, arg1) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReportMessageWithContext", reflect.TypeOf((*MockReporter)(nil).ReportMessageWithContext), arg0, arg1)
} }
// ReportWarningWithContext mocks base method.
func (m *MockReporter) ReportWarningWithContext(arg0 string, arg1 map[string]interface{}) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReportWarningWithContext", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// ReportWarningWithContext indicates an expected call of ReportWarningWithContext.
func (mr *MockReporterMockRecorder) ReportWarningWithContext(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReportWarningWithContext", reflect.TypeOf((*MockReporter)(nil).ReportMessageWithContext), arg0, arg1)
}

View File

@ -157,7 +157,7 @@ func (r *Reporter) ReportExceptionWithContext(i interface{}, context map[string]
SkipDuringUnwind() SkipDuringUnwind()
err := fmt.Errorf("recover: %v", i) err := fmt.Errorf("recover: %v", i)
return r.scopedReport(context, func() { return r.scopedReport(context, func(_ *sentry.Scope) {
SkipDuringUnwind() SkipDuringUnwind()
if eventID := sentry.CaptureException(err); eventID != nil { if eventID := sentry.CaptureException(err); eventID != nil {
logrus.WithError(err). logrus.WithError(err).
@ -169,7 +169,20 @@ func (r *Reporter) ReportExceptionWithContext(i interface{}, context map[string]
func (r *Reporter) ReportMessageWithContext(msg string, context map[string]interface{}) error { func (r *Reporter) ReportMessageWithContext(msg string, context map[string]interface{}) error {
SkipDuringUnwind() SkipDuringUnwind()
return r.scopedReport(context, func() { return r.scopedReport(context, func(_ *sentry.Scope) {
SkipDuringUnwind()
if eventID := sentry.CaptureMessage(msg); eventID != nil {
logrus.WithField("message", msg).
WithField("reportID", *eventID).
Warn("Captured message")
}
})
}
func (r *Reporter) ReportWarningWithContext(msg string, context map[string]interface{}) error {
SkipDuringUnwind()
return r.scopedReport(context, func(scope *sentry.Scope) {
scope.SetLevel(sentry.LevelWarning)
SkipDuringUnwind() SkipDuringUnwind()
if eventID := sentry.CaptureMessage(msg); eventID != nil { if eventID := sentry.CaptureMessage(msg); eventID != nil {
logrus.WithField("message", msg). logrus.WithField("message", msg).
@ -180,7 +193,7 @@ func (r *Reporter) ReportMessageWithContext(msg string, context map[string]inter
} }
// Report reports a sentry crash with stacktrace from all goroutines. // Report reports a sentry crash with stacktrace from all goroutines.
func (r *Reporter) scopedReport(context map[string]interface{}, doReport func()) error { func (r *Reporter) scopedReport(context map[string]interface{}, doReport func(scope *sentry.Scope)) error {
SkipDuringUnwind() SkipDuringUnwind()
if os.Getenv("PROTONMAIL_ENV") == "dev" { if os.Getenv("PROTONMAIL_ENV") == "dev" {
@ -206,7 +219,7 @@ func (r *Reporter) scopedReport(context map[string]interface{}, doReport func())
map[string]sentry.Context{"bridge": contextToString(context)}, map[string]sentry.Context{"bridge": contextToString(context)},
) )
} }
doReport() doReport(scope)
}) })
if !sentry.Flush(time.Second * 10) { if !sentry.Flush(time.Second * 10) {
@ -302,6 +315,10 @@ func (n NullSentryReporter) ReportMessageWithContext(string, reporter.Context) e
return nil return nil
} }
func (n NullSentryReporter) ReportWarningWithContext(string, reporter.Context) error {
return nil
}
func (n NullSentryReporter) ReportExceptionWithContext(any, reporter.Context) error { func (n NullSentryReporter) ReportExceptionWithContext(any, reporter.Context) error {
return nil return nil
} }

View File

@ -42,6 +42,7 @@ type gluonIDProvider interface {
type sentryReporter interface { type sentryReporter interface {
ReportMessageWithContext(string, reporter.Context) error ReportMessageWithContext(string, reporter.Context) error
ReportWarningWithContext(string, reporter.Context) error
} }
type apiClient interface { type apiClient interface {
@ -50,6 +51,8 @@ type apiClient interface {
type mailboxFetcherFn func(ctx context.Context, label proton.Label) (imap.MailboxData, error) type mailboxFetcherFn func(ctx context.Context, label proton.Label) (imap.MailboxData, error)
type mailboxMessageCountFetcherFn func(ctx context.Context, internalMailboxID imap.InternalMailboxID) (int, error)
type LabelConflictManager struct { type LabelConflictManager struct {
gluonLabelNameProvider GluonLabelNameProvider gluonLabelNameProvider GluonLabelNameProvider
gluonIDProvider gluonIDProvider gluonIDProvider gluonIDProvider
@ -86,47 +89,62 @@ func (m *LabelConflictManager) generateMailboxFetcher(connectors []*Connector) m
} }
} }
type UserLabelConflictResolver interface { func (m *LabelConflictManager) generateMailboxMessageCountFetcher(connectors []*Connector) mailboxMessageCountFetcherFn {
return func(ctx context.Context, id imap.InternalMailboxID) (int, error) {
var countSum int
var errs []error
for _, conn := range connectors {
count, err := conn.GetMailboxMessageCount(ctx, id)
countSum += count
errs = append(errs, err)
}
return countSum, errors.Join(errs...)
}
}
type LabelConflictResolver interface {
ResolveConflict(ctx context.Context, label proton.Label, visited map[string]bool) (func() []imap.Update, error) ResolveConflict(ctx context.Context, label proton.Label, visited map[string]bool) (func() []imap.Update, error)
} }
type userLabelConflictResolverImpl struct { type labelConflictResolverImpl struct {
mailboxFetch mailboxFetcherFn mailboxFetch mailboxFetcherFn
client apiClient client apiClient
reporter sentryReporter reporter sentryReporter
log *logrus.Entry log *logrus.Entry
} }
type nullUserLabelConflictResolverImpl struct { type nullLabelConflictResolverImpl struct {
} }
func (r *nullUserLabelConflictResolverImpl) ResolveConflict(_ context.Context, _ proton.Label, _ map[string]bool) (func() []imap.Update, error) { func (r *nullLabelConflictResolverImpl) ResolveConflict(_ context.Context, _ proton.Label, _ map[string]bool) (func() []imap.Update, error) {
return func() []imap.Update { return func() []imap.Update {
return []imap.Update{} return []imap.Update{}
}, nil }, nil
} }
func (m *LabelConflictManager) NewUserConflictResolver(connectors []*Connector) UserLabelConflictResolver { func (m *LabelConflictManager) NewConflictResolver(connectors []*Connector) LabelConflictResolver {
if m.featureFlagProvider.GetFlagValue(unleash.LabelConflictResolverDisabled) { if m.featureFlagProvider.GetFlagValue(unleash.LabelConflictResolverDisabled) {
return &nullUserLabelConflictResolverImpl{} return &nullLabelConflictResolverImpl{}
} }
return &userLabelConflictResolverImpl{ return &labelConflictResolverImpl{
mailboxFetch: m.generateMailboxFetcher(connectors), mailboxFetch: m.generateMailboxFetcher(connectors),
client: m.client, client: m.client,
reporter: m.reporter, reporter: m.reporter,
log: logrus.WithFields(logrus.Fields{ log: logrus.WithFields(logrus.Fields{
"pkg": "imapservice/userLabelConflictResolver", "pkg": "imapservice/labelConflictResolver",
"numberOfConnectors": len(connectors), "numberOfConnectors": len(connectors),
}), }),
} }
} }
func (r *userLabelConflictResolverImpl) ResolveConflict(ctx context.Context, label proton.Label, visited map[string]bool) (func() []imap.Update, error) { func (r *labelConflictResolverImpl) ResolveConflict(ctx context.Context, label proton.Label, visited map[string]bool) (func() []imap.Update, error) {
logger := r.log.WithFields(logrus.Fields{ logger := r.log.WithFields(logrus.Fields{
"labelID": label.ID, "labelID": label.ID,
"labelPath": hashLabelPaths(GetMailboxName(label)), "labelPath": hashLabelPaths(GetMailboxName(label)),
}) })
// For system type labels we shouldn't care.
var updateFns []func() []imap.Update var updateFns []func() []imap.Update
// There's a cycle, such as in a label swap operation, we'll need to temporarily rename the label. // There's a cycle, such as in a label swap operation, we'll need to temporarily rename the label.
@ -170,14 +188,14 @@ func (r *userLabelConflictResolverImpl) ResolveConflict(ctx context.Context, lab
logger.Info("Label conflict found") logger.Info("Label conflict found")
// If the label name belongs to some other label ID. Fetch it's state from the remote. // 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) conflictingLabel, err := r.client.GetLabel(ctx, mailboxData.RemoteID, proton.LabelTypeFolder, proton.LabelTypeLabel, proton.LabelTypeSystem)
if err != nil { if err != nil {
// If it's not present on the remote we should delete it. And create the new label. // If it's not present on the remote we should delete it. And create the new label.
if errors.Is(err, proton.ErrNoSuchLabel) { if errors.Is(err, proton.ErrNoSuchLabel) {
logger.Info("Conflicting label does not exist on remote. Deleting.") logger.Info("Conflicting label does not exist on remote. Deleting.")
fn := func() []imap.Update { fn := func() []imap.Update {
return []imap.Update{ return []imap.Update{
imap.NewMailboxDeleted(imap.MailboxID(mailboxData.RemoteID)), imap.NewMailboxDeleted(imap.MailboxID(mailboxData.RemoteID)), // Should this be with remote ID
newMailboxUpdatedOrCreated(imap.MailboxID(label.ID), GetMailboxName(label)), newMailboxUpdatedOrCreated(imap.MailboxID(label.ID), GetMailboxName(label)),
} }
} }
@ -240,11 +258,14 @@ func hashLabelPaths(path []string) string {
} }
type InternalLabelConflictResolver interface { type InternalLabelConflictResolver interface {
ResolveConflict(ctx context.Context) (func() []imap.Update, error) ResolveConflict(ctx context.Context, apiLabels map[string]proton.Label) (func() []imap.Update, error)
} }
type internalLabelConflictResolverImpl struct { type internalLabelConflictResolverImpl struct {
mailboxFetch mailboxFetcherFn mailboxFetch mailboxFetcherFn
mailboxMessageCountFetch mailboxMessageCountFetcherFn
userLabelConflictResolver LabelConflictResolver
allowNonEmptyMailboxDeletion bool
client apiClient client apiClient
reporter sentryReporter reporter sentryReporter
log *logrus.Entry log *logrus.Entry
@ -252,7 +273,7 @@ type internalLabelConflictResolverImpl struct {
type nullInternalLabelConflictResolver struct{} type nullInternalLabelConflictResolver struct{}
func (r *nullInternalLabelConflictResolver) ResolveConflict(_ context.Context) (func() []imap.Update, error) { func (r *nullInternalLabelConflictResolver) ResolveConflict(_ context.Context, _ map[string]proton.Label) (func() []imap.Update, error) {
return func() []imap.Update { return []imap.Update{} }, nil return func() []imap.Update { return []imap.Update{} }, nil
} }
@ -263,6 +284,9 @@ func (m *LabelConflictManager) NewInternalLabelConflictResolver(connectors []*Co
return &internalLabelConflictResolverImpl{ return &internalLabelConflictResolverImpl{
mailboxFetch: m.generateMailboxFetcher(connectors), mailboxFetch: m.generateMailboxFetcher(connectors),
mailboxMessageCountFetch: m.generateMailboxMessageCountFetcher(connectors),
userLabelConflictResolver: m.NewConflictResolver(connectors),
allowNonEmptyMailboxDeletion: m.featureFlagProvider.GetFlagValue(unleash.ItnternalLabelConflictNonEmptyMailboxDeletion),
client: m.client, client: m.client,
reporter: m.reporter, reporter: m.reporter,
log: logrus.WithFields(logrus.Fields{ log: logrus.WithFields(logrus.Fields{
@ -272,17 +296,17 @@ func (m *LabelConflictManager) NewInternalLabelConflictResolver(connectors []*Co
} }
} }
func (r *internalLabelConflictResolverImpl) ResolveConflict(ctx context.Context) (func() []imap.Update, error) { func (r *internalLabelConflictResolverImpl) ResolveConflict(ctx context.Context, apiLabels map[string]proton.Label) (func() []imap.Update, error) {
var updateFns []func() []imap.Update updateFns := []func() []imap.Update{}
for _, prefix := range []string{folderPrefix, labelPrefix} { for _, prefix := range []string{folderPrefix, labelPrefix} {
label := proton.Label{ internalLabel := proton.Label{
Path: []string{prefix}, Path: []string{prefix},
ID: prefix, ID: prefix,
Name: prefix, Name: prefix,
} }
mbox, err := r.mailboxFetch(ctx, label) mbox, err := r.mailboxFetch(ctx, internalLabel)
if err != nil { if err != nil {
if db.IsErrNotFound(err) { if db.IsErrNotFound(err) {
continue continue
@ -290,11 +314,75 @@ func (r *internalLabelConflictResolverImpl) ResolveConflict(ctx context.Context)
return nil, err return nil, err
} }
if mbox.RemoteID != label.ID { // If the ID's match then we don't have a discrepancy.
// If the ID's don't match we should delete these. if mbox.RemoteID == internalLabel.ID {
fn := func() []imap.Update { return []imap.Update{imap.NewMailboxDeleted(imap.MailboxID(prefix))} } continue
updateFns = append(updateFns, fn)
} }
logFields := logrus.Fields{
"internalLabelID": internalLabel.ID,
"internalLabelName": internalLabel.Name,
"conflictingLabelID": mbox.RemoteID,
"conflictingLabelName": strings.Join(mbox.BridgeName, "/"),
}
reporterContext := reporter.Context(logFields)
logger := r.log.WithFields(logFields)
logger.Info("Encountered conflict, resolving.")
// There is a discrepancy, let's see if it comes from API.
apiLabel, ok := apiLabels[mbox.RemoteID]
if !ok {
// Label does not come from API, we should delete it.
// Due diligence, check if there are any messages associated with the mailbox.
msgCount, _ := r.mailboxMessageCountFetch(ctx, mbox.InternalID)
if msgCount != 0 {
logger.WithField("conflictingLabelMessageCount", msgCount).Info("Non-API conflicting label has associated messages")
reporterContext["conflictingLabelMessageCount"] = msgCount
if rerr := r.reporter.ReportWarningWithContext("Internal mailbox name conflict. Conflicting non-API label has messages.",
reporterContext); rerr != nil {
logger.WithError(rerr).Error("Failed to send report to sentry")
}
if !r.allowNonEmptyMailboxDeletion {
return combineIMAPUpdateFns(updateFns), fmt.Errorf("internal mailbox conflicting non-api label has associated messages")
}
}
fn := func() []imap.Update {
return []imap.Update{imap.NewMailboxDeletedSilent(imap.MailboxID(mbox.RemoteID))}
}
updateFns = append(updateFns, fn)
continue
}
reporterContext["conflictingLabelType"] = apiLabel.Type
// Label is indeed from API let's see if it's name has changed.
if compareLabelNames(GetMailboxName(apiLabel), internalLabel.Path) {
logger.Error("Conflict, same-name mailbox is returned by API")
if err := r.reporter.ReportMessageWithContext("Internal mailbox name conflict. Same-name mailbox is returned by API", reporterContext); err != nil {
logger.WithError(err).Error("Could not send report to sentry")
}
return combineIMAPUpdateFns(updateFns), fmt.Errorf("API label %s conflicts with internal label %s",
GetMailboxName(apiLabel),
strings.Join(mbox.BridgeName, "/"),
)
}
// If it's name has changed then we ought to rename it while still taking care of potential conflicts.
labelRenameUpdates, err := r.userLabelConflictResolver.ResolveConflict(ctx, apiLabel, make(map[string]bool))
if err != nil {
reporterContext["err"] = err.Error()
if rerr := r.reporter.ReportMessageWithContext("Failed to resolve internal mailbox conflict", reporterContext); rerr != nil {
logger.WithError(rerr).Error("Could not send report to sentry")
}
return combineIMAPUpdateFns(updateFns),
fmt.Errorf("failed to resolve user label conflict for '%s': %w", apiLabel.Name, err)
}
updateFns = append(updateFns, labelRenameUpdates)
} }
return combineIMAPUpdateFns(updateFns), nil return combineIMAPUpdateFns(updateFns), nil
} }

View File

@ -88,6 +88,11 @@ func (m *mockReporter) ReportMessageWithContext(msg string, ctx reporter.Context
return args.Error(0) return args.Error(0)
} }
func (m *mockReporter) ReportWarningWithContext(msg string, ctx reporter.Context) error {
args := m.Called(msg, ctx)
return args.Error(0)
}
func TestResolveConflict_UnexpectedLabelConflict(t *testing.T) { func TestResolveConflict_UnexpectedLabelConflict(t *testing.T) {
ctx := context.Background() ctx := context.Background()
label := proton.Label{ label := proton.Label{
@ -121,7 +126,7 @@ func TestResolveConflict_UnexpectedLabelConflict(t *testing.T) {
connector := &imapservice.Connector{} connector := &imapservice.Connector{}
connector.SetAddrIDTest("addr-1") connector.SetAddrIDTest("addr-1")
resolver := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, ffProviderFalse{}). resolver := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, ffProviderFalse{}).
NewUserConflictResolver([]*imapservice.Connector{connector}) NewConflictResolver([]*imapservice.Connector{connector})
visited := make(map[string]bool) visited := make(map[string]bool)
_, err := resolver.ResolveConflict(ctx, label, visited) _, err := resolver.ResolveConflict(ctx, label, visited)
@ -152,7 +157,7 @@ func TestResolveDiscrepancy_LabelDoesNotExist(t *testing.T) {
connectors := []*imapservice.Connector{connector} connectors := []*imapservice.Connector{connector}
manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, ffProviderFalse{}) manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, ffProviderFalse{})
resolver := manager.NewUserConflictResolver(connectors) resolver := manager.NewConflictResolver(connectors)
visited := make(map[string]bool) visited := make(map[string]bool)
fn, err := resolver.ResolveConflict(ctx, label, visited) fn, err := resolver.ResolveConflict(ctx, label, visited)
@ -185,7 +190,7 @@ func TestResolveConflict_MailboxFetchError(t *testing.T) {
connector := &imapservice.Connector{} connector := &imapservice.Connector{}
connector.SetAddrIDTest("addr-1") connector.SetAddrIDTest("addr-1")
resolver := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, ffProviderFalse{}). resolver := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, ffProviderFalse{}).
NewUserConflictResolver([]*imapservice.Connector{connector}) NewConflictResolver([]*imapservice.Connector{connector})
visited := make(map[string]bool) visited := make(map[string]bool)
_, err := resolver.ResolveConflict(ctx, label, visited) _, err := resolver.ResolveConflict(ctx, label, visited)
@ -223,7 +228,7 @@ func TestResolveDiscrepancy_ConflictingLabelDeletedRemotely(t *testing.T) {
connectors := []*imapservice.Connector{connector} connectors := []*imapservice.Connector{connector}
manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, ffProviderFalse{}) manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, ffProviderFalse{})
resolver := manager.NewUserConflictResolver(connectors) resolver := manager.NewConflictResolver(connectors)
visited := make(map[string]bool) visited := make(map[string]bool)
fn, err := resolver.ResolveConflict(ctx, label, visited) fn, err := resolver.ResolveConflict(ctx, label, visited)
@ -266,7 +271,7 @@ func TestResolveDiscrepancy_LabelAlreadyCorrect(t *testing.T) {
connectors := []*imapservice.Connector{connector} connectors := []*imapservice.Connector{connector}
manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, ffProviderFalse{}) manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, ffProviderFalse{})
resolver := manager.NewUserConflictResolver(connectors) resolver := manager.NewConflictResolver(connectors)
visited := make(map[string]bool) visited := make(map[string]bool)
fn, err := resolver.ResolveConflict(ctx, label, visited) fn, err := resolver.ResolveConflict(ctx, label, visited)
@ -295,7 +300,7 @@ func TestResolveConflict_DeepNestedPath(t *testing.T) {
connector := &imapservice.Connector{} connector := &imapservice.Connector{}
connector.SetAddrIDTest("addr-1") connector.SetAddrIDTest("addr-1")
resolver := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, ffProviderFalse{}). resolver := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, ffProviderFalse{}).
NewUserConflictResolver([]*imapservice.Connector{connector}) NewConflictResolver([]*imapservice.Connector{connector})
visited := make(map[string]bool) visited := make(map[string]bool)
fn, err := resolver.ResolveConflict(ctx, label, visited) fn, err := resolver.ResolveConflict(ctx, label, visited)
@ -351,7 +356,7 @@ func TestResolveLabelDiscrepancy_LabelSwap(t *testing.T) {
for _, label := range apiLabels { for _, label := range apiLabels {
mockClient. mockClient.
On("GetLabel", mock.Anything, label.ID, []proton.LabelType{proton.LabelTypeFolder, proton.LabelTypeLabel}). On("GetLabel", mock.Anything, label.ID, []proton.LabelType{proton.LabelTypeFolder, proton.LabelTypeLabel, proton.LabelTypeSystem}).
Return(label, nil) Return(label, nil)
} }
@ -360,7 +365,7 @@ func TestResolveLabelDiscrepancy_LabelSwap(t *testing.T) {
connectors := []*imapservice.Connector{connector} connectors := []*imapservice.Connector{connector}
manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, ffProviderFalse{}) manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, ffProviderFalse{})
resolver := manager.NewUserConflictResolver(connectors) resolver := manager.NewConflictResolver(connectors)
visited := make(map[string]bool) visited := make(map[string]bool)
fn, err := resolver.ResolveConflict(context.Background(), apiLabels[0], visited) fn, err := resolver.ResolveConflict(context.Background(), apiLabels[0], visited)
@ -444,7 +449,7 @@ func TestResolveLabelDiscrepancy_LabelSwapExtended(t *testing.T) {
for _, label := range apiLabels { for _, label := range apiLabels {
mockClient. mockClient.
On("GetLabel", mock.Anything, label.ID, []proton.LabelType{proton.LabelTypeFolder, proton.LabelTypeLabel}). On("GetLabel", mock.Anything, label.ID, []proton.LabelType{proton.LabelTypeFolder, proton.LabelTypeLabel, proton.LabelTypeSystem}).
Return(label, nil) Return(label, nil)
} }
@ -453,7 +458,7 @@ func TestResolveLabelDiscrepancy_LabelSwapExtended(t *testing.T) {
connectors := []*imapservice.Connector{connector} connectors := []*imapservice.Connector{connector}
manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, ffProviderFalse{}) manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, ffProviderFalse{})
resolver := manager.NewUserConflictResolver(connectors) resolver := manager.NewConflictResolver(connectors)
fn, err := resolver.ResolveConflict(context.Background(), apiLabels[0], make(map[string]bool)) fn, err := resolver.ResolveConflict(context.Background(), apiLabels[0], make(map[string]bool))
require.NoError(t, err) require.NoError(t, err)
@ -529,7 +534,7 @@ func TestResolveLabelDiscrepancy_LabelSwapCyclic(t *testing.T) {
for _, label := range apiLabels { for _, label := range apiLabels {
mockClient. mockClient.
On("GetLabel", mock.Anything, label.ID, []proton.LabelType{proton.LabelTypeFolder, proton.LabelTypeLabel}). On("GetLabel", mock.Anything, label.ID, []proton.LabelType{proton.LabelTypeFolder, proton.LabelTypeLabel, proton.LabelTypeSystem}).
Return(label, nil) Return(label, nil)
} }
@ -538,7 +543,7 @@ func TestResolveLabelDiscrepancy_LabelSwapCyclic(t *testing.T) {
connectors := []*imapservice.Connector{connector} connectors := []*imapservice.Connector{connector}
manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, ffProviderFalse{}) manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, ffProviderFalse{})
resolver := manager.NewUserConflictResolver(connectors) resolver := manager.NewConflictResolver(connectors)
fn, err := resolver.ResolveConflict(context.Background(), apiLabels[0], make(map[string]bool)) fn, err := resolver.ResolveConflict(context.Background(), apiLabels[0], make(map[string]bool))
require.NoError(t, err) require.NoError(t, err)
@ -602,17 +607,17 @@ func TestResolveLabelDiscrepancy_LabelSwapCyclicWithDeletedLabel(t *testing.T) {
for _, label := range apiLabels { for _, label := range apiLabels {
mockClient. mockClient.
On("GetLabel", mock.Anything, label.ID, []proton.LabelType{proton.LabelTypeFolder, proton.LabelTypeLabel}). On("GetLabel", mock.Anything, label.ID, []proton.LabelType{proton.LabelTypeFolder, proton.LabelTypeLabel, proton.LabelTypeSystem}).
Return(label, nil) Return(label, nil)
} }
mockClient.On("GetLabel", mock.Anything, "222", []proton.LabelType{proton.LabelTypeFolder, proton.LabelTypeLabel}).Return(proton.Label{}, proton.ErrNoSuchLabel) mockClient.On("GetLabel", mock.Anything, "222", []proton.LabelType{proton.LabelTypeFolder, proton.LabelTypeLabel, proton.LabelTypeSystem}).Return(proton.Label{}, proton.ErrNoSuchLabel)
connector := &imapservice.Connector{} connector := &imapservice.Connector{}
connector.SetAddrIDTest("addr-1") connector.SetAddrIDTest("addr-1")
connectors := []*imapservice.Connector{connector} connectors := []*imapservice.Connector{connector}
manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, ffProviderFalse{}) manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, ffProviderFalse{})
resolver := manager.NewUserConflictResolver(connectors) resolver := manager.NewConflictResolver(connectors)
fn, err := resolver.ResolveConflict(context.Background(), apiLabels[2], make(map[string]bool)) fn, err := resolver.ResolveConflict(context.Background(), apiLabels[2], make(map[string]bool))
require.NoError(t, err) require.NoError(t, err)
@ -665,17 +670,17 @@ func TestResolveLabelDiscrepancy_LabelSwapCyclicWithDeletedLabel_KillSwitchEnabl
for _, label := range apiLabels { for _, label := range apiLabels {
mockClient. mockClient.
On("GetLabel", mock.Anything, label.ID, []proton.LabelType{proton.LabelTypeFolder, proton.LabelTypeLabel}). On("GetLabel", mock.Anything, label.ID, []proton.LabelType{proton.LabelTypeFolder, proton.LabelTypeLabel, proton.LabelTypeSystem}).
Return(label, nil) Return(label, nil)
} }
mockClient.On("GetLabel", mock.Anything, "222", []proton.LabelType{proton.LabelTypeFolder, proton.LabelTypeLabel}).Return(proton.Label{}, proton.ErrNoSuchLabel) mockClient.On("GetLabel", mock.Anything, "222", []proton.LabelType{proton.LabelTypeFolder, proton.LabelTypeLabel, proton.LabelTypeSystem}).Return(proton.Label{}, proton.ErrNoSuchLabel)
connector := &imapservice.Connector{} connector := &imapservice.Connector{}
connector.SetAddrIDTest("addr-1") connector.SetAddrIDTest("addr-1")
connectors := []*imapservice.Connector{connector} connectors := []*imapservice.Connector{connector}
manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, ffProviderTrue{}) manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, ffProviderTrue{})
resolver := manager.NewUserConflictResolver(connectors) resolver := manager.NewConflictResolver(connectors)
fn, err := resolver.ResolveConflict(context.Background(), apiLabels[2], make(map[string]bool)) fn, err := resolver.ResolveConflict(context.Background(), apiLabels[2], make(map[string]bool))
require.NoError(t, err) require.NoError(t, err)
@ -706,7 +711,8 @@ func TestInternalLabelConflictResolver_NoConflicts(t *testing.T) {
manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, ffProviderFalse{}) manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, ffProviderFalse{})
resolver := manager.NewInternalLabelConflictResolver(connectors) resolver := manager.NewInternalLabelConflictResolver(connectors)
fn, err := resolver.ResolveConflict(ctx) apiLabels := make(map[string]proton.Label)
fn, err := resolver.ResolveConflict(ctx, apiLabels)
assert.NoError(t, err) assert.NoError(t, err)
updates := fn() updates := fn()
@ -735,81 +741,159 @@ func TestInternalLabelConflictResolver_CorrectIDs(t *testing.T) {
manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, ffProviderFalse{}) manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, ffProviderFalse{})
resolver := manager.NewInternalLabelConflictResolver(connectors) resolver := manager.NewInternalLabelConflictResolver(connectors)
fn, err := resolver.ResolveConflict(ctx) apiLabels := make(map[string]proton.Label)
fn, err := resolver.ResolveConflict(ctx, apiLabels)
assert.NoError(t, err) assert.NoError(t, err)
updates := fn() updates := fn()
assert.Empty(t, updates) assert.Empty(t, updates)
} }
func TestInternalLabelConflictResolver_ConflictingFoldersID(t *testing.T) { type mockMailboxCountProvider struct {
mock.Mock
}
func (m *mockMailboxCountProvider) GetUserMailboxCountByInternalID(ctx context.Context, addrID string, internalID imap.InternalMailboxID) (int, error) {
args := m.Called(ctx, addrID, internalID)
return args.Int(0), args.Error(1)
}
func TestInternalLabelConflictResolver_ConflictingNonAPILabel_ZeroCount(t *testing.T) {
ctx := context.Background() ctx := context.Background()
mockLabelProvider := new(mockLabelNameProvider) mockLabelProvider := new(mockLabelNameProvider)
mockClient := new(mockAPIClient) mockClient := new(mockAPIClient)
mockIDProvider := new(mockIDProvider) mockIDProvider := new(mockIDProvider)
mockReporter := new(mockReporter) mockReporter := new(mockReporter)
mockCountProvider := new(mockMailboxCountProvider)
mockIDProvider.On("GetGluonID", "addr-1").Return("gluon-id-1", true) mockIDProvider.On("GetGluonID", "addr-1").Return("gluon-id-1", true)
// Mock mailbox fetch to return conflicting mailbox
mockLabelProvider.On("GetUserMailboxByName", mock.Anything, "gluon-id-1", []string{"Folders"}). mockLabelProvider.On("GetUserMailboxByName", mock.Anything, "gluon-id-1", []string{"Folders"}).
Return(imap.MailboxData{RemoteID: "wrong-id", BridgeName: []string{"Folders"}}, nil) Return(imap.MailboxData{RemoteID: "wrong-id", BridgeName: []string{"Folders"}, InternalID: imap.InternalMailboxID(123)}, nil)
mockLabelProvider.On("GetUserMailboxByName", mock.Anything, "gluon-id-1", []string{"Labels"}). mockLabelProvider.On("GetUserMailboxByName", mock.Anything, "gluon-id-1", []string{"Labels"}).
Return(imap.MailboxData{}, db.ErrNotFound) Return(imap.MailboxData{}, db.ErrNotFound)
// Mock message count fetch to return 0 messages.
mockLabelProvider.On("GetMailboxMessageCount", mock.Anything, "gluon-id-1", imap.InternalMailboxID(123)).
Return(0, nil)
connector := &imapservice.Connector{} connector := &imapservice.Connector{}
connector.SetAddrIDTest("addr-1") connector.SetAddrIDTest("addr-1")
mockCountProvider.On("GetUserMailboxCountByInternalID",
mock.Anything,
"addr-1",
imap.InternalMailboxID(123)).
Return(0, nil)
connector.SetMailboxCountProviderTest(mockCountProvider)
connectors := []*imapservice.Connector{connector} connectors := []*imapservice.Connector{connector}
manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, ffProviderFalse{}) manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, ffProviderFalse{})
resolver := manager.NewInternalLabelConflictResolver(connectors) resolver := manager.NewInternalLabelConflictResolver(connectors)
fn, err := resolver.ResolveConflict(ctx) // API labels don't contain the conflicting label ID
apiLabels := make(map[string]proton.Label)
fn, err := resolver.ResolveConflict(ctx, apiLabels)
assert.NoError(t, err) assert.NoError(t, err)
updates := fn() updates := fn()
assert.Len(t, updates, 1) assert.Len(t, updates, 1)
deleted, ok := updates[0].(*imap.MailboxDeleted) deleted, ok := updates[0].(*imap.MailboxDeletedSilent)
assert.True(t, ok) assert.True(t, ok)
assert.Equal(t, imap.MailboxID("Folders"), deleted.MailboxID) assert.Equal(t, imap.MailboxID("wrong-id"), deleted.MailboxID)
} }
func TestInternalLabelConflictResolver_BothConflicting(t *testing.T) { func TestInternalLabelConflictResolver_ConflictingNonAPILabel_PositiveCount(t *testing.T) {
ctx := context.Background() ctx := context.Background()
mockLabelProvider := new(mockLabelNameProvider) mockLabelProvider := new(mockLabelNameProvider)
mockClient := new(mockAPIClient) mockClient := new(mockAPIClient)
mockIDProvider := new(mockIDProvider) mockIDProvider := new(mockIDProvider)
mockReporter := new(mockReporter) mockReporter := new(mockReporter)
mockCountProvider := new(mockMailboxCountProvider)
mockIDProvider.On("GetGluonID", "addr-1").Return("gluon-id-1", true) mockIDProvider.On("GetGluonID", "addr-1").Return("gluon-id-1", true)
mockReporter.On("ReportWarningWithContext", mock.Anything, mock.Anything).
Return(nil)
// Mock mailbox fetch to return conflicting mailbox
mockLabelProvider.On("GetUserMailboxByName", mock.Anything, "gluon-id-1", []string{"Folders"}). mockLabelProvider.On("GetUserMailboxByName", mock.Anything, "gluon-id-1", []string{"Folders"}).
Return(imap.MailboxData{RemoteID: "wrong-folders-id", BridgeName: []string{"Folders"}}, nil) Return(imap.MailboxData{RemoteID: "wrong-id", BridgeName: []string{"Folders"}, InternalID: imap.InternalMailboxID(123)}, nil)
mockLabelProvider.On("GetUserMailboxByName", mock.Anything, "gluon-id-1", []string{"Labels"}). mockLabelProvider.On("GetUserMailboxByName", mock.Anything, "gluon-id-1", []string{"Labels"}).
Return(imap.MailboxData{RemoteID: "wrong-labels-id", BridgeName: []string{"Labels"}}, nil) Return(imap.MailboxData{}, db.ErrNotFound)
// Mock message count fetch to return 0 messages.
mockLabelProvider.On("GetMailboxMessageCount", mock.Anything, "gluon-id-1", imap.InternalMailboxID(123)).
Return(0, nil)
connector := &imapservice.Connector{} connector := &imapservice.Connector{}
connector.SetAddrIDTest("addr-1") connector.SetAddrIDTest("addr-1")
mockCountProvider.On("GetUserMailboxCountByInternalID",
mock.Anything,
"addr-1",
imap.InternalMailboxID(123)).
Return(10, nil)
connector.SetMailboxCountProviderTest(mockCountProvider)
connectors := []*imapservice.Connector{connector} connectors := []*imapservice.Connector{connector}
manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, ffProviderFalse{}) manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, ffProviderFalse{})
resolver := manager.NewInternalLabelConflictResolver(connectors) resolver := manager.NewInternalLabelConflictResolver(connectors)
fn, err := resolver.ResolveConflict(ctx) // API labels don't contain the conflicting label ID
assert.NoError(t, err) apiLabels := make(map[string]proton.Label)
fn, err := resolver.ResolveConflict(ctx, apiLabels)
assert.EqualError(t, err, "internal mailbox conflicting non-api label has associated messages")
updates := fn() updates := fn()
assert.Len(t, updates, 2) assert.Empty(t, updates, 0)
}
updateOne, ok := updates[0].(*imap.MailboxDeleted) func TestInternalLabelConflictResolver_ConflictingAPILabelSameName(t *testing.T) {
assert.True(t, ok) ctx := context.Background()
assert.Equal(t, imap.MailboxID("Folders"), updateOne.MailboxID)
updateTwo, ok := updates[1].(*imap.MailboxDeleted) mockLabelProvider := new(mockLabelNameProvider)
assert.True(t, ok) mockClient := new(mockAPIClient)
assert.Equal(t, imap.MailboxID("Labels"), updateTwo.MailboxID) mockIDProvider := new(mockIDProvider)
mockReporter := new(mockReporter)
mockCountProvider := new(mockMailboxCountProvider)
mockIDProvider.On("GetGluonID", "addr-1").Return("gluon-id-1", true)
mockLabelProvider.On("GetUserMailboxByName", mock.Anything, "gluon-id-1", []string{"Folders"}).
Return(imap.MailboxData{RemoteID: "api-label-id", BridgeName: []string{"Folders"}}, nil)
mockLabelProvider.On("GetUserMailboxByName", mock.Anything, "gluon-id-1", []string{"Labels"}).
Return(imap.MailboxData{}, db.ErrNotFound)
mockReporter.On("ReportMessageWithContext", "Internal mailbox name conflict. Same-name mailbox is returned by API", mock.Anything).
Return(nil)
connector := &imapservice.Connector{}
connector.SetAddrIDTest("addr-1")
connector.SetMailboxCountProviderTest(mockCountProvider)
connectors := []*imapservice.Connector{connector}
manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, ffProviderFalse{})
resolver := manager.NewInternalLabelConflictResolver(connectors)
// API user label with empty path.
apiLabels := map[string]proton.Label{
"api-label-id": {
ID: "api-label-id",
Name: "Folders",
Path: []string{""},
Type: proton.LabelTypeFolder,
},
}
_, err := resolver.ResolveConflict(ctx, apiLabels)
assert.Error(t, err)
assert.Contains(t, err.Error(), "API label")
assert.Contains(t, err.Error(), "conflicts with internal label")
} }
func TestInternalLabelConflictResolver_MailboxFetchError(t *testing.T) { func TestInternalLabelConflictResolver_MailboxFetchError(t *testing.T) {
@ -832,7 +916,8 @@ func TestInternalLabelConflictResolver_MailboxFetchError(t *testing.T) {
manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, ffProviderFalse{}) manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, ffProviderFalse{})
resolver := manager.NewInternalLabelConflictResolver(connectors) resolver := manager.NewInternalLabelConflictResolver(connectors)
_, err := resolver.ResolveConflict(ctx) apiLabels := make(map[string]proton.Label)
_, err := resolver.ResolveConflict(ctx, apiLabels)
assert.Error(t, err) assert.Error(t, err)
assert.Contains(t, err.Error(), "database connection error") assert.Contains(t, err.Error(), "database connection error")
} }
@ -859,7 +944,16 @@ func TestNewInternalLabelConflictResolver_KillSwitchEnabled(t *testing.T) {
manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, ffProviderTrue{}) manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, ffProviderTrue{})
resolver := manager.NewInternalLabelConflictResolver(connectors) resolver := manager.NewInternalLabelConflictResolver(connectors)
fn, err := resolver.ResolveConflict(ctx) apiLabels := map[string]proton.Label{
"some-api-label": {
ID: "some-api-label",
Name: "SomeLabel",
Path: []string{"SomeLabel"},
Type: proton.LabelTypeLabel,
},
}
fn, err := resolver.ResolveConflict(ctx, apiLabels)
assert.NoError(t, err) assert.NoError(t, err)
updates := fn() updates := fn()

View File

@ -45,6 +45,10 @@ import (
"golang.org/x/exp/slices" "golang.org/x/exp/slices"
) )
type mailboxCountProvider interface {
GetUserMailboxCountByInternalID(ctx context.Context, addrID string, internalID imap.InternalMailboxID) (int, error)
}
// Connector contains all IMAP state required to satisfy sync and or imap queries. // Connector contains all IMAP state required to satisfy sync and or imap queries.
type Connector struct { type Connector struct {
addrID string addrID string
@ -67,6 +71,8 @@ type Connector struct {
sharedCache *SharedCache sharedCache *SharedCache
syncState *SyncState syncState *SyncState
mailboxCountProvider mailboxCountProvider
} }
var errNoSenderAddressMatch = errors.New("no matching sender found in address list") var errNoSenderAddressMatch = errors.New("no matching sender found in address list")
@ -82,6 +88,7 @@ func NewConnector(
reporter reporter.Reporter, reporter reporter.Reporter,
showAllMail bool, showAllMail bool,
syncState *SyncState, syncState *SyncState,
mailboxCountProvider mailboxCountProvider,
) *Connector { ) *Connector {
userID := identityState.UserID() userID := identityState.UserID()
@ -115,6 +122,8 @@ func NewConnector(
sharedCache: NewSharedCached(), sharedCache: NewSharedCached(),
syncState: syncState, syncState: syncState,
mailboxCountProvider: mailboxCountProvider,
} }
} }
@ -909,3 +918,12 @@ func (s *Connector) getSenderProtonAddress(p *parser.Parser) (proton.Address, er
func (s *Connector) SetAddrIDTest(addrID string) { func (s *Connector) SetAddrIDTest(addrID string) {
s.addrID = addrID s.addrID = addrID
} }
func (s *Connector) GetMailboxMessageCount(ctx context.Context, mailboxInternalID imap.InternalMailboxID) (int, error) {
return s.mailboxCountProvider.GetUserMailboxCountByInternalID(ctx, s.addrID, mailboxInternalID)
}
// SetMailboxCountProviderTest - sets the relevant provider. Should only be used for testing.
func (s *Connector) SetMailboxCountProviderTest(provider mailboxCountProvider) {
s.mailboxCountProvider = provider
}

View File

@ -55,6 +55,7 @@ func (t labelDiscrepancyType) String() string {
type labelDiscrepancy struct { type labelDiscrepancy struct {
labelName string labelName string
labelPath string labelPath string
labelPathParsed string
labelID string labelID string
conflictingLabelName string conflictingLabelName string
conflictingLabelID string conflictingLabelID string
@ -76,10 +77,12 @@ func newLabelDiscrepancy(label proton.Label, mbox imap.MailboxData, dType labelD
if dType == discrepancyUser { if dType == discrepancyUser {
discrepancy.labelName = algo.HashBase64SHA256(label.Name) discrepancy.labelName = algo.HashBase64SHA256(label.Name)
discrepancy.labelPath = algo.HashBase64SHA256(joinStrings(label.Path)) discrepancy.labelPath = algo.HashBase64SHA256(joinStrings(label.Path))
discrepancy.labelPathParsed = algo.HashBase64SHA256(joinStrings(GetMailboxName(label)))
discrepancy.conflictingLabelName = algo.HashBase64SHA256(joinStrings(mbox.BridgeName)) discrepancy.conflictingLabelName = algo.HashBase64SHA256(joinStrings(mbox.BridgeName))
} else { } else {
discrepancy.labelName = label.Name discrepancy.labelName = label.Name
discrepancy.labelPath = joinStrings(label.Path) discrepancy.labelPath = joinStrings(label.Path)
discrepancy.labelPathParsed = joinStrings(GetMailboxName(label))
discrepancy.conflictingLabelName = joinStrings(mbox.BridgeName) discrepancy.conflictingLabelName = joinStrings(mbox.BridgeName)
} }
@ -93,9 +96,10 @@ func discrepanciesToContext(discrepancies []labelDiscrepancy) reporter.Context {
prefix := fmt.Sprintf("discrepancy_%d_", i) prefix := fmt.Sprintf("discrepancy_%d_", i)
ctx[prefix+"type"] = d.Type.String() ctx[prefix+"type"] = d.Type.String()
ctx[prefix+"label_id"] = d.labelID
ctx[prefix+"label_name"] = d.labelName ctx[prefix+"label_name"] = d.labelName
ctx[prefix+"label_path"] = d.labelPath ctx[prefix+"label_path"] = d.labelPath
ctx[prefix+"label_id"] = d.labelID ctx[prefix+"label_path_parsed"] = d.labelPathParsed
ctx[prefix+"conflicting_label_name"] = d.conflictingLabelName ctx[prefix+"conflicting_label_name"] = d.conflictingLabelName
ctx[prefix+"conflicting_label_id"] = d.conflictingLabelID ctx[prefix+"conflicting_label_id"] = d.conflictingLabelID
} }

View File

@ -40,7 +40,10 @@ type IMAPServerManager interface {
GetUserMailboxByName(ctx context.Context, addrID string, mailboxName []string) (imap.MailboxData, error) GetUserMailboxByName(ctx context.Context, addrID string, mailboxName []string) (imap.MailboxData, error)
GetUserMailboxCountByInternalID(ctx context.Context, addrID string, internalID imap.InternalMailboxID) (int, error)
GetOpenIMAPSessionCount() int GetOpenIMAPSessionCount() int
GetRollingIMAPConnectionCount() int GetRollingIMAPConnectionCount() int
} }
@ -77,6 +80,10 @@ func (n NullIMAPServerManager) GetUserMailboxByName(_ context.Context, _ string,
return imap.MailboxData{}, nil return imap.MailboxData{}, nil
} }
func (n NullIMAPServerManager) GetUserMailboxCountByInternalID(_ context.Context, _ string, _ imap.InternalMailboxID) (int, error) {
return 0, nil
}
func (n NullIMAPServerManager) GetOpenIMAPSessionCount() int { func (n NullIMAPServerManager) GetOpenIMAPSessionCount() int {
return 0 return 0
} }

View File

@ -540,6 +540,7 @@ func (s *Service) buildConnectors() (map[string]*Connector, error) {
s.reporter, s.reporter,
s.showAllMail, s.showAllMail,
s.syncStateProvider, s.syncStateProvider,
s.serverManager,
) )
return connectors, nil return connectors, nil
@ -557,6 +558,7 @@ func (s *Service) buildConnectors() (map[string]*Connector, error) {
s.reporter, s.reporter,
s.showAllMail, s.showAllMail,
s.syncStateProvider, s.syncStateProvider,
s.serverManager,
) )
} }

View File

@ -157,6 +157,7 @@ func addNewAddressSplitMode(ctx context.Context, s *Service, addrID string) erro
s.reporter, s.reporter,
s.showAllMail, s.showAllMail,
s.syncStateProvider, s.syncStateProvider,
s.serverManager,
) )
if err := s.serverManager.AddIMAPUser(ctx, connector, connector.addrID, s.gluonIDProvider, s.syncStateProvider); err != nil { if err := s.serverManager.AddIMAPUser(ctx, connector, connector.addrID, s.gluonIDProvider, s.syncStateProvider); err != nil {

View File

@ -90,7 +90,7 @@ func onLabelCreated(ctx context.Context, s *Service, event proton.LabelEvent) ([
wr.SetLabel(event.Label.ID, event.Label, "onLabelCreated") wr.SetLabel(event.Label.ID, event.Label, "onLabelCreated")
labelConflictResolver := s.labelConflictManager.NewUserConflictResolver(maps.Values(s.connectors)) labelConflictResolver := s.labelConflictManager.NewConflictResolver(maps.Values(s.connectors))
conflictUpdatesGenerator, err := labelConflictResolver.ResolveConflict(ctx, event.Label, make(map[string]bool)) conflictUpdatesGenerator, err := labelConflictResolver.ResolveConflict(ctx, event.Label, make(map[string]bool))
if err != nil { if err != nil {
return updates, err return updates, err
@ -150,7 +150,7 @@ func onLabelUpdated(ctx context.Context, s *Service, event proton.LabelEvent) ([
wr.SetLabel(apiLabel.ID, apiLabel, "onLabelUpdatedApiID") wr.SetLabel(apiLabel.ID, apiLabel, "onLabelUpdatedApiID")
// Resolve potential conflicts // Resolve potential conflicts
labelConflictResolver := s.labelConflictManager.NewUserConflictResolver(maps.Values(s.connectors)) labelConflictResolver := s.labelConflictManager.NewConflictResolver(maps.Values(s.connectors))
conflictUpdatesGenerator, err := labelConflictResolver.ResolveConflict(ctx, apiLabel, make(map[string]bool)) conflictUpdatesGenerator, err := labelConflictResolver.ResolveConflict(ctx, apiLabel, make(map[string]bool))
if err != nil { if err != nil {
return updates, err return updates, err

View File

@ -133,12 +133,10 @@ func (s *SyncUpdateApplier) SyncLabels(ctx context.Context, labels map[string]pr
func syncLabels(ctx context.Context, labels map[string]proton.Label, connectors []*Connector, labelConflictManager *LabelConflictManager) ([]imap.Update, error) { func syncLabels(ctx context.Context, labels map[string]proton.Label, connectors []*Connector, labelConflictManager *LabelConflictManager) ([]imap.Update, error) {
var updates []imap.Update var updates []imap.Update
userLabelConflictResolver := labelConflictManager.NewUserConflictResolver(connectors) userLabelConflictResolver := labelConflictManager.NewConflictResolver(connectors)
internalLabelConflictResolver := labelConflictManager.NewInternalLabelConflictResolver(connectors) internalLabelConflictResolver := labelConflictManager.NewInternalLabelConflictResolver(connectors)
// Create placeholder Folders/Labels mailboxes with the \Noselect attribute. conflictUpdateGenerator, err := internalLabelConflictResolver.ResolveConflict(ctx, labels)
for _, prefix := range []string{folderPrefix, labelPrefix} {
conflictUpdateGenerator, err := internalLabelConflictResolver.ResolveConflict(ctx)
if err != nil { if err != nil {
return updates, err return updates, err
} }
@ -147,10 +145,19 @@ func syncLabels(ctx context.Context, labels map[string]proton.Label, connectors
conflictUpdates := conflictUpdateGenerator() conflictUpdates := conflictUpdateGenerator()
updateCh.publishUpdate(ctx, conflictUpdates...) updateCh.publishUpdate(ctx, conflictUpdates...)
updates = append(updates, conflictUpdates...) updates = append(updates, conflictUpdates...)
}
// Create placeholder Folders/Labels mailboxes with the \Noselect attribute.
for _, prefix := range []string{folderPrefix, labelPrefix} {
for _, updateCh := range connectors {
update := newPlaceHolderMailboxCreatedUpdate(prefix) update := newPlaceHolderMailboxCreatedUpdate(prefix)
updateCh.publishUpdate(ctx, update) updateCh.publishUpdate(ctx, update)
updates = append(updates, update) updates = append(updates, update)
// Ensure we perform a rename operation as well. The created event won't update the name if the ID exists.
renameUpdate := imap.NewMailboxUpdated(imap.MailboxID(prefix), []string{prefix})
updateCh.publishUpdate(ctx, renameUpdate)
updates = append(updates, renameUpdate)
} }
} }

View File

@ -208,6 +208,10 @@ func (sm *Service) GetUserMailboxByName(ctx context.Context, addrID string, mail
return sm.imapServer.GetUserMailboxByName(ctx, addrID, mailboxName) return sm.imapServer.GetUserMailboxByName(ctx, addrID, mailboxName)
} }
func (sm *Service) GetUserMailboxCountByInternalID(ctx context.Context, addrID string, internalID imap.InternalMailboxID) (int, error) {
return sm.imapServer.GetUserMailboxCountByInternalID(ctx, addrID, internalID)
}
func (sm *Service) GetOpenIMAPSessionCount() int { func (sm *Service) GetOpenIMAPSessionCount() int {
return sm.imapServer.GetOpenSessionCount() return sm.imapServer.GetOpenSessionCount()
} }

View File

@ -44,6 +44,7 @@ const (
LabelConflictResolverDisabled = "InboxBridgeLabelConflictResolverDisabled" LabelConflictResolverDisabled = "InboxBridgeLabelConflictResolverDisabled"
SMTPSubmissionRequestSentryReportDisabled = "InboxBridgeSmtpSubmissionRequestSentryReportDisabled" SMTPSubmissionRequestSentryReportDisabled = "InboxBridgeSmtpSubmissionRequestSentryReportDisabled"
InternalLabelConflictResolverDisabled = "InboxBridgeUnexpectedFoldersLabelsStartupFixupDisabled" InternalLabelConflictResolverDisabled = "InboxBridgeUnexpectedFoldersLabelsStartupFixupDisabled"
ItnternalLabelConflictNonEmptyMailboxDeletion = "InboxBridgeUnknownNonEmptyMailboxDeletion"
) )
type FeatureFlagValueProvider interface { type FeatureFlagValueProvider interface {

View File

@ -146,6 +146,11 @@ func (r *reportRecorder) ReportMessageWithContext(message string, context report
return nil return nil
} }
func (r *reportRecorder) ReportWarningWithContext(message string, context reporter.Context) error {
r.add(false, message, context)
return nil
}
func (r *reportRecorder) ReportExceptionWithContext(data any, context reporter.Context) error { func (r *reportRecorder) ReportExceptionWithContext(data any, context reporter.Context) error {
if context == nil { if context == nil {
context = reporter.Context{} context = reporter.Context{}