From a00af3a39866ec17da116f0fcc3a79877ac7901d Mon Sep 17 00:00:00 2001 From: Atanas Janeshliev Date: Tue, 10 Jun 2025 23:18:26 +0200 Subject: [PATCH] feat(BRIDGE-383): Internal mailbox conflict resolution extended; Minor alterations to mailbox conflict pre-checker --- go.mod | 2 +- go.sum | 4 +- internal/bridge/mocks/gluon_mocks.go | 15 ++ internal/sentry/reporter.go | 25 ++- internal/services/imapservice/conflicts.go | 144 ++++++++++++--- .../services/imapservice/conflicts_test.go | 172 ++++++++++++++---- internal/services/imapservice/connector.go | 18 ++ internal/services/imapservice/labelchecker.go | 6 +- .../services/imapservice/server_manager.go | 7 + internal/services/imapservice/service.go | 2 + .../imapservice/service_address_events.go | 1 + .../imapservice/service_label_events.go | 4 +- .../imapservice/sync_update_applier.go | 27 ++- internal/services/imapsmtpserver/service.go | 4 + internal/unleash/service.go | 15 +- tests/ctx_reporter_test.go | 5 + 16 files changed, 357 insertions(+), 94 deletions(-) diff --git a/go.mod b/go.mod index 2a0d15aa..b033c2a4 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ toolchain go1.24.2 require ( github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557 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-proton-api v0.4.1-0.20250417134000-e624a080f7ba github.com/ProtonMail/gopenpgp/v2 v2.8.2-proton diff --git a/go.sum b/go.sum index 3563e3e8..086abd54 100644 --- a/go.sum +++ b/go.sum @@ -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-20211005172633-e235017c1baf h1:yc9daCCYUefEs69zUkSzubzjBbL+cmOXgnmt9Fyd9ug= 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.20250604083016-c6e17f8461b1/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8= +github.com/ProtonMail/gluon v0.17.1-0.20250611120816-05167d499f8d h1:45W7G+X0w7nzLzeB0eiFkGho5DTK1jNmmNbt3IhN524= +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 v1.1.4-proton h1:KIo9uNlk3vzlwI7o5VjhiEjI4Ld1TDixOMnoNZyfpFE= github.com/ProtonMail/go-crypto v1.1.4-proton/go.mod h1:zNoyBJW3p/yVWiHNZgfTF9VsjwqYof5YY0M9kt2QaX0= diff --git a/internal/bridge/mocks/gluon_mocks.go b/internal/bridge/mocks/gluon_mocks.go index 758c0efc..b7cbe233 100644 --- a/internal/bridge/mocks/gluon_mocks.go +++ b/internal/bridge/mocks/gluon_mocks.go @@ -88,3 +88,18 @@ func (mr *MockReporterMockRecorder) ReportMessageWithContext(arg0, arg1 interfac mr.mock.ctrl.T.Helper() 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) +} \ No newline at end of file diff --git a/internal/sentry/reporter.go b/internal/sentry/reporter.go index c22e2f1c..09aebde5 100644 --- a/internal/sentry/reporter.go +++ b/internal/sentry/reporter.go @@ -157,7 +157,7 @@ func (r *Reporter) ReportExceptionWithContext(i interface{}, context map[string] SkipDuringUnwind() err := fmt.Errorf("recover: %v", i) - return r.scopedReport(context, func() { + return r.scopedReport(context, func(_ *sentry.Scope) { SkipDuringUnwind() if eventID := sentry.CaptureException(err); eventID != nil { 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 { 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() if eventID := sentry.CaptureMessage(msg); eventID != nil { 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. -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() 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)}, ) } - doReport() + doReport(scope) }) if !sentry.Flush(time.Second * 10) { @@ -302,6 +315,10 @@ func (n NullSentryReporter) ReportMessageWithContext(string, reporter.Context) e return nil } +func (n NullSentryReporter) ReportWarningWithContext(string, reporter.Context) error { + return nil +} + func (n NullSentryReporter) ReportExceptionWithContext(any, reporter.Context) error { return nil } diff --git a/internal/services/imapservice/conflicts.go b/internal/services/imapservice/conflicts.go index 9c6eb2cf..89827cfd 100644 --- a/internal/services/imapservice/conflicts.go +++ b/internal/services/imapservice/conflicts.go @@ -42,6 +42,7 @@ type gluonIDProvider interface { type sentryReporter interface { ReportMessageWithContext(string, reporter.Context) error + ReportWarningWithContext(string, reporter.Context) error } type apiClient interface { @@ -50,6 +51,8 @@ type apiClient interface { 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 { gluonLabelNameProvider GluonLabelNameProvider 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) } -type userLabelConflictResolverImpl struct { +type labelConflictResolverImpl struct { mailboxFetch mailboxFetcherFn client apiClient reporter sentryReporter 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 []imap.Update{} }, nil } -func (m *LabelConflictManager) NewUserConflictResolver(connectors []*Connector) UserLabelConflictResolver { +func (m *LabelConflictManager) NewConflictResolver(connectors []*Connector) LabelConflictResolver { if m.featureFlagProvider.GetFlagValue(unleash.LabelConflictResolverDisabled) { - return &nullUserLabelConflictResolverImpl{} + return &nullLabelConflictResolverImpl{} } - return &userLabelConflictResolverImpl{ + return &labelConflictResolverImpl{ mailboxFetch: m.generateMailboxFetcher(connectors), client: m.client, reporter: m.reporter, log: logrus.WithFields(logrus.Fields{ - "pkg": "imapservice/userLabelConflictResolver", + "pkg": "imapservice/labelConflictResolver", "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{ "labelID": label.ID, "labelPath": hashLabelPaths(GetMailboxName(label)), }) + // For system type labels we shouldn't care. var updateFns []func() []imap.Update // 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") // 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 it's not present on the remote we should delete it. And create the new label. if errors.Is(err, proton.ErrNoSuchLabel) { logger.Info("Conflicting label does not exist on remote. Deleting.") fn := func() []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)), } } @@ -240,19 +258,22 @@ func hashLabelPaths(path []string) string { } 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 { - mailboxFetch mailboxFetcherFn - client apiClient - reporter sentryReporter - log *logrus.Entry + mailboxFetch mailboxFetcherFn + mailboxMessageCountFetch mailboxMessageCountFetcherFn + userLabelConflictResolver LabelConflictResolver + allowNonEmptyMailboxDeletion bool + client apiClient + reporter sentryReporter + log *logrus.Entry } 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 } @@ -262,9 +283,12 @@ func (m *LabelConflictManager) NewInternalLabelConflictResolver(connectors []*Co } return &internalLabelConflictResolverImpl{ - mailboxFetch: m.generateMailboxFetcher(connectors), - client: m.client, - reporter: m.reporter, + mailboxFetch: m.generateMailboxFetcher(connectors), + mailboxMessageCountFetch: m.generateMailboxMessageCountFetcher(connectors), + userLabelConflictResolver: m.NewConflictResolver(connectors), + allowNonEmptyMailboxDeletion: m.featureFlagProvider.GetFlagValue(unleash.ItnternalLabelConflictNonEmptyMailboxDeletion), + client: m.client, + reporter: m.reporter, log: logrus.WithFields(logrus.Fields{ "pkg": "imapservice/internalLabelConflictResolver", "numberOfConnectors": len(connectors), @@ -272,17 +296,17 @@ func (m *LabelConflictManager) NewInternalLabelConflictResolver(connectors []*Co } } -func (r *internalLabelConflictResolverImpl) ResolveConflict(ctx context.Context) (func() []imap.Update, error) { - var updateFns []func() []imap.Update +func (r *internalLabelConflictResolverImpl) ResolveConflict(ctx context.Context, apiLabels map[string]proton.Label) (func() []imap.Update, error) { + updateFns := []func() []imap.Update{} for _, prefix := range []string{folderPrefix, labelPrefix} { - label := proton.Label{ + internalLabel := proton.Label{ Path: []string{prefix}, ID: prefix, Name: prefix, } - mbox, err := r.mailboxFetch(ctx, label) + mbox, err := r.mailboxFetch(ctx, internalLabel) if err != nil { if db.IsErrNotFound(err) { continue @@ -290,11 +314,75 @@ func (r *internalLabelConflictResolverImpl) ResolveConflict(ctx context.Context) return nil, err } - if mbox.RemoteID != label.ID { - // If the ID's don't match we should delete these. - fn := func() []imap.Update { return []imap.Update{imap.NewMailboxDeleted(imap.MailboxID(prefix))} } - updateFns = append(updateFns, fn) + // If the ID's match then we don't have a discrepancy. + if mbox.RemoteID == internalLabel.ID { + continue } + + 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 } diff --git a/internal/services/imapservice/conflicts_test.go b/internal/services/imapservice/conflicts_test.go index ad11c29f..3c531e77 100644 --- a/internal/services/imapservice/conflicts_test.go +++ b/internal/services/imapservice/conflicts_test.go @@ -88,6 +88,11 @@ func (m *mockReporter) ReportMessageWithContext(msg string, ctx reporter.Context 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) { ctx := context.Background() label := proton.Label{ @@ -121,7 +126,7 @@ func TestResolveConflict_UnexpectedLabelConflict(t *testing.T) { connector := &imapservice.Connector{} connector.SetAddrIDTest("addr-1") resolver := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, ffProviderFalse{}). - NewUserConflictResolver([]*imapservice.Connector{connector}) + NewConflictResolver([]*imapservice.Connector{connector}) visited := make(map[string]bool) _, err := resolver.ResolveConflict(ctx, label, visited) @@ -152,7 +157,7 @@ func TestResolveDiscrepancy_LabelDoesNotExist(t *testing.T) { connectors := []*imapservice.Connector{connector} manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, ffProviderFalse{}) - resolver := manager.NewUserConflictResolver(connectors) + resolver := manager.NewConflictResolver(connectors) visited := make(map[string]bool) fn, err := resolver.ResolveConflict(ctx, label, visited) @@ -185,7 +190,7 @@ func TestResolveConflict_MailboxFetchError(t *testing.T) { connector := &imapservice.Connector{} connector.SetAddrIDTest("addr-1") resolver := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, ffProviderFalse{}). - NewUserConflictResolver([]*imapservice.Connector{connector}) + NewConflictResolver([]*imapservice.Connector{connector}) visited := make(map[string]bool) _, err := resolver.ResolveConflict(ctx, label, visited) @@ -223,7 +228,7 @@ func TestResolveDiscrepancy_ConflictingLabelDeletedRemotely(t *testing.T) { connectors := []*imapservice.Connector{connector} manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, ffProviderFalse{}) - resolver := manager.NewUserConflictResolver(connectors) + resolver := manager.NewConflictResolver(connectors) visited := make(map[string]bool) fn, err := resolver.ResolveConflict(ctx, label, visited) @@ -266,7 +271,7 @@ func TestResolveDiscrepancy_LabelAlreadyCorrect(t *testing.T) { connectors := []*imapservice.Connector{connector} manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, ffProviderFalse{}) - resolver := manager.NewUserConflictResolver(connectors) + resolver := manager.NewConflictResolver(connectors) visited := make(map[string]bool) fn, err := resolver.ResolveConflict(ctx, label, visited) @@ -295,7 +300,7 @@ func TestResolveConflict_DeepNestedPath(t *testing.T) { connector := &imapservice.Connector{} connector.SetAddrIDTest("addr-1") resolver := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, ffProviderFalse{}). - NewUserConflictResolver([]*imapservice.Connector{connector}) + NewConflictResolver([]*imapservice.Connector{connector}) visited := make(map[string]bool) fn, err := resolver.ResolveConflict(ctx, label, visited) @@ -351,7 +356,7 @@ func TestResolveLabelDiscrepancy_LabelSwap(t *testing.T) { for _, label := range apiLabels { 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) } @@ -360,7 +365,7 @@ func TestResolveLabelDiscrepancy_LabelSwap(t *testing.T) { connectors := []*imapservice.Connector{connector} manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, ffProviderFalse{}) - resolver := manager.NewUserConflictResolver(connectors) + resolver := manager.NewConflictResolver(connectors) visited := make(map[string]bool) fn, err := resolver.ResolveConflict(context.Background(), apiLabels[0], visited) @@ -444,7 +449,7 @@ func TestResolveLabelDiscrepancy_LabelSwapExtended(t *testing.T) { for _, label := range apiLabels { 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) } @@ -453,7 +458,7 @@ func TestResolveLabelDiscrepancy_LabelSwapExtended(t *testing.T) { connectors := []*imapservice.Connector{connector} 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)) require.NoError(t, err) @@ -529,7 +534,7 @@ func TestResolveLabelDiscrepancy_LabelSwapCyclic(t *testing.T) { for _, label := range apiLabels { 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) } @@ -538,7 +543,7 @@ func TestResolveLabelDiscrepancy_LabelSwapCyclic(t *testing.T) { connectors := []*imapservice.Connector{connector} 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)) require.NoError(t, err) @@ -602,17 +607,17 @@ func TestResolveLabelDiscrepancy_LabelSwapCyclicWithDeletedLabel(t *testing.T) { for _, label := range apiLabels { 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) } - 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.SetAddrIDTest("addr-1") connectors := []*imapservice.Connector{connector} 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)) require.NoError(t, err) @@ -665,17 +670,17 @@ func TestResolveLabelDiscrepancy_LabelSwapCyclicWithDeletedLabel_KillSwitchEnabl for _, label := range apiLabels { 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) } - 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.SetAddrIDTest("addr-1") connectors := []*imapservice.Connector{connector} 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)) require.NoError(t, err) @@ -706,7 +711,8 @@ func TestInternalLabelConflictResolver_NoConflicts(t *testing.T) { manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, ffProviderFalse{}) 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) updates := fn() @@ -735,81 +741,159 @@ func TestInternalLabelConflictResolver_CorrectIDs(t *testing.T) { manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, ffProviderFalse{}) 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) updates := fn() 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() mockLabelProvider := new(mockLabelNameProvider) mockClient := new(mockAPIClient) mockIDProvider := new(mockIDProvider) mockReporter := new(mockReporter) + mockCountProvider := new(mockMailboxCountProvider) 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"}). - 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"}). 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.SetAddrIDTest("addr-1") + mockCountProvider.On("GetUserMailboxCountByInternalID", + mock.Anything, + "addr-1", + imap.InternalMailboxID(123)). + Return(0, nil) + + connector.SetMailboxCountProviderTest(mockCountProvider) connectors := []*imapservice.Connector{connector} manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, ffProviderFalse{}) 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) updates := fn() assert.Len(t, updates, 1) - deleted, ok := updates[0].(*imap.MailboxDeleted) + deleted, ok := updates[0].(*imap.MailboxDeletedSilent) 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() mockLabelProvider := new(mockLabelNameProvider) mockClient := new(mockAPIClient) mockIDProvider := new(mockIDProvider) mockReporter := new(mockReporter) + mockCountProvider := new(mockMailboxCountProvider) 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"}). - 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"}). - 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.SetAddrIDTest("addr-1") + mockCountProvider.On("GetUserMailboxCountByInternalID", + mock.Anything, + "addr-1", + imap.InternalMailboxID(123)). + Return(10, nil) + + connector.SetMailboxCountProviderTest(mockCountProvider) connectors := []*imapservice.Connector{connector} manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, ffProviderFalse{}) resolver := manager.NewInternalLabelConflictResolver(connectors) - fn, err := resolver.ResolveConflict(ctx) - assert.NoError(t, err) + // API labels don't contain the conflicting label ID + 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() - assert.Len(t, updates, 2) + assert.Empty(t, updates, 0) +} - updateOne, ok := updates[0].(*imap.MailboxDeleted) - assert.True(t, ok) - assert.Equal(t, imap.MailboxID("Folders"), updateOne.MailboxID) +func TestInternalLabelConflictResolver_ConflictingAPILabelSameName(t *testing.T) { + ctx := context.Background() - updateTwo, ok := updates[1].(*imap.MailboxDeleted) - assert.True(t, ok) - assert.Equal(t, imap.MailboxID("Labels"), updateTwo.MailboxID) + mockLabelProvider := new(mockLabelNameProvider) + mockClient := new(mockAPIClient) + 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) { @@ -832,7 +916,8 @@ func TestInternalLabelConflictResolver_MailboxFetchError(t *testing.T) { manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, ffProviderFalse{}) resolver := manager.NewInternalLabelConflictResolver(connectors) - _, err := resolver.ResolveConflict(ctx) + apiLabels := make(map[string]proton.Label) + _, err := resolver.ResolveConflict(ctx, apiLabels) assert.Error(t, err) 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{}) 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) updates := fn() diff --git a/internal/services/imapservice/connector.go b/internal/services/imapservice/connector.go index 826569c1..dcdd900c 100644 --- a/internal/services/imapservice/connector.go +++ b/internal/services/imapservice/connector.go @@ -45,6 +45,10 @@ import ( "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. type Connector struct { addrID string @@ -67,6 +71,8 @@ type Connector struct { sharedCache *SharedCache syncState *SyncState + + mailboxCountProvider mailboxCountProvider } var errNoSenderAddressMatch = errors.New("no matching sender found in address list") @@ -82,6 +88,7 @@ func NewConnector( reporter reporter.Reporter, showAllMail bool, syncState *SyncState, + mailboxCountProvider mailboxCountProvider, ) *Connector { userID := identityState.UserID() @@ -115,6 +122,8 @@ func NewConnector( sharedCache: NewSharedCached(), syncState: syncState, + + mailboxCountProvider: mailboxCountProvider, } } @@ -909,3 +918,12 @@ func (s *Connector) getSenderProtonAddress(p *parser.Parser) (proton.Address, er func (s *Connector) SetAddrIDTest(addrID string) { 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 +} diff --git a/internal/services/imapservice/labelchecker.go b/internal/services/imapservice/labelchecker.go index 6e187e1a..3d6438cc 100644 --- a/internal/services/imapservice/labelchecker.go +++ b/internal/services/imapservice/labelchecker.go @@ -55,6 +55,7 @@ func (t labelDiscrepancyType) String() string { type labelDiscrepancy struct { labelName string labelPath string + labelPathParsed string labelID string conflictingLabelName string conflictingLabelID string @@ -76,10 +77,12 @@ func newLabelDiscrepancy(label proton.Label, mbox imap.MailboxData, dType labelD if dType == discrepancyUser { discrepancy.labelName = algo.HashBase64SHA256(label.Name) discrepancy.labelPath = algo.HashBase64SHA256(joinStrings(label.Path)) + discrepancy.labelPathParsed = algo.HashBase64SHA256(joinStrings(GetMailboxName(label))) discrepancy.conflictingLabelName = algo.HashBase64SHA256(joinStrings(mbox.BridgeName)) } else { discrepancy.labelName = label.Name discrepancy.labelPath = joinStrings(label.Path) + discrepancy.labelPathParsed = joinStrings(GetMailboxName(label)) discrepancy.conflictingLabelName = joinStrings(mbox.BridgeName) } @@ -93,9 +96,10 @@ func discrepanciesToContext(discrepancies []labelDiscrepancy) reporter.Context { prefix := fmt.Sprintf("discrepancy_%d_", i) ctx[prefix+"type"] = d.Type.String() + ctx[prefix+"label_id"] = d.labelID ctx[prefix+"label_name"] = d.labelName 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_id"] = d.conflictingLabelID } diff --git a/internal/services/imapservice/server_manager.go b/internal/services/imapservice/server_manager.go index fbeea542..8a8aa5d4 100644 --- a/internal/services/imapservice/server_manager.go +++ b/internal/services/imapservice/server_manager.go @@ -40,7 +40,10 @@ type IMAPServerManager interface { GetUserMailboxByName(ctx context.Context, addrID string, mailboxName []string) (imap.MailboxData, error) + GetUserMailboxCountByInternalID(ctx context.Context, addrID string, internalID imap.InternalMailboxID) (int, error) + GetOpenIMAPSessionCount() int + GetRollingIMAPConnectionCount() int } @@ -77,6 +80,10 @@ func (n NullIMAPServerManager) GetUserMailboxByName(_ context.Context, _ string, return imap.MailboxData{}, nil } +func (n NullIMAPServerManager) GetUserMailboxCountByInternalID(_ context.Context, _ string, _ imap.InternalMailboxID) (int, error) { + return 0, nil +} + func (n NullIMAPServerManager) GetOpenIMAPSessionCount() int { return 0 } diff --git a/internal/services/imapservice/service.go b/internal/services/imapservice/service.go index f018f385..a5a0527d 100644 --- a/internal/services/imapservice/service.go +++ b/internal/services/imapservice/service.go @@ -540,6 +540,7 @@ func (s *Service) buildConnectors() (map[string]*Connector, error) { s.reporter, s.showAllMail, s.syncStateProvider, + s.serverManager, ) return connectors, nil @@ -557,6 +558,7 @@ func (s *Service) buildConnectors() (map[string]*Connector, error) { s.reporter, s.showAllMail, s.syncStateProvider, + s.serverManager, ) } diff --git a/internal/services/imapservice/service_address_events.go b/internal/services/imapservice/service_address_events.go index e1eb4e36..f6690b7a 100644 --- a/internal/services/imapservice/service_address_events.go +++ b/internal/services/imapservice/service_address_events.go @@ -157,6 +157,7 @@ func addNewAddressSplitMode(ctx context.Context, s *Service, addrID string) erro s.reporter, s.showAllMail, s.syncStateProvider, + s.serverManager, ) if err := s.serverManager.AddIMAPUser(ctx, connector, connector.addrID, s.gluonIDProvider, s.syncStateProvider); err != nil { diff --git a/internal/services/imapservice/service_label_events.go b/internal/services/imapservice/service_label_events.go index 529755ad..766ec201 100644 --- a/internal/services/imapservice/service_label_events.go +++ b/internal/services/imapservice/service_label_events.go @@ -90,7 +90,7 @@ func onLabelCreated(ctx context.Context, s *Service, event proton.LabelEvent) ([ 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)) if err != nil { return updates, err @@ -150,7 +150,7 @@ func onLabelUpdated(ctx context.Context, s *Service, event proton.LabelEvent) ([ wr.SetLabel(apiLabel.ID, apiLabel, "onLabelUpdatedApiID") // 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)) if err != nil { return updates, err diff --git a/internal/services/imapservice/sync_update_applier.go b/internal/services/imapservice/sync_update_applier.go index 3b5932aa..47add5cf 100644 --- a/internal/services/imapservice/sync_update_applier.go +++ b/internal/services/imapservice/sync_update_applier.go @@ -133,24 +133,31 @@ 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) { var updates []imap.Update - userLabelConflictResolver := labelConflictManager.NewUserConflictResolver(connectors) + userLabelConflictResolver := labelConflictManager.NewConflictResolver(connectors) internalLabelConflictResolver := labelConflictManager.NewInternalLabelConflictResolver(connectors) + conflictUpdateGenerator, err := internalLabelConflictResolver.ResolveConflict(ctx, labels) + if err != nil { + return updates, err + } + + for _, updateCh := range connectors { + conflictUpdates := conflictUpdateGenerator() + updateCh.publishUpdate(ctx, conflictUpdates...) + updates = append(updates, conflictUpdates...) + } + // Create placeholder Folders/Labels mailboxes with the \Noselect attribute. for _, prefix := range []string{folderPrefix, labelPrefix} { - conflictUpdateGenerator, err := internalLabelConflictResolver.ResolveConflict(ctx) - if err != nil { - return updates, err - } - for _, updateCh := range connectors { - conflictUpdates := conflictUpdateGenerator() - updateCh.publishUpdate(ctx, conflictUpdates...) - updates = append(updates, conflictUpdates...) - update := newPlaceHolderMailboxCreatedUpdate(prefix) updateCh.publishUpdate(ctx, 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) } } diff --git a/internal/services/imapsmtpserver/service.go b/internal/services/imapsmtpserver/service.go index c745cc81..d82ddf18 100644 --- a/internal/services/imapsmtpserver/service.go +++ b/internal/services/imapsmtpserver/service.go @@ -208,6 +208,10 @@ func (sm *Service) GetUserMailboxByName(ctx context.Context, addrID string, mail 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 { return sm.imapServer.GetOpenSessionCount() } diff --git a/internal/unleash/service.go b/internal/unleash/service.go index 622ff5dc..974f1a64 100644 --- a/internal/unleash/service.go +++ b/internal/unleash/service.go @@ -37,13 +37,14 @@ var pollJitter = 2 * time.Minute //nolint:gochecknoglobals const filename = "unleash_flags" const ( - EventLoopNotificationDisabled = "InboxBridgeEventLoopNotificationDisabled" - IMAPAuthenticateCommandDisabled = "InboxBridgeImapAuthenticateCommandDisabled" - UserRemovalGluonDataCleanupDisabled = "InboxBridgeUserRemovalGluonDataCleanupDisabled" - UpdateUseNewVersionFileStructureDisabled = "InboxBridgeUpdateWithOsFilterDisabled" - LabelConflictResolverDisabled = "InboxBridgeLabelConflictResolverDisabled" - SMTPSubmissionRequestSentryReportDisabled = "InboxBridgeSmtpSubmissionRequestSentryReportDisabled" - InternalLabelConflictResolverDisabled = "InboxBridgeUnexpectedFoldersLabelsStartupFixupDisabled" + EventLoopNotificationDisabled = "InboxBridgeEventLoopNotificationDisabled" + IMAPAuthenticateCommandDisabled = "InboxBridgeImapAuthenticateCommandDisabled" + UserRemovalGluonDataCleanupDisabled = "InboxBridgeUserRemovalGluonDataCleanupDisabled" + UpdateUseNewVersionFileStructureDisabled = "InboxBridgeUpdateWithOsFilterDisabled" + LabelConflictResolverDisabled = "InboxBridgeLabelConflictResolverDisabled" + SMTPSubmissionRequestSentryReportDisabled = "InboxBridgeSmtpSubmissionRequestSentryReportDisabled" + InternalLabelConflictResolverDisabled = "InboxBridgeUnexpectedFoldersLabelsStartupFixupDisabled" + ItnternalLabelConflictNonEmptyMailboxDeletion = "InboxBridgeUnknownNonEmptyMailboxDeletion" ) type FeatureFlagValueProvider interface { diff --git a/tests/ctx_reporter_test.go b/tests/ctx_reporter_test.go index bf9e58a0..5879baae 100644 --- a/tests/ctx_reporter_test.go +++ b/tests/ctx_reporter_test.go @@ -146,6 +146,11 @@ func (r *reportRecorder) ReportMessageWithContext(message string, context report 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 { if context == nil { context = reporter.Context{}