fix(GODT-3003): Ensure IMAP State is reset after vault corruption

After we detect that the user has suffered the GODT-3003 bug due the
vault corruption not ensuring that a previous sync state would be
erased, we patch the gluon db directly and then reset the sync state.

After the account is added, the sync is automatically triggered and the
account state fixes itself.
This commit is contained in:
Leander Beernaert 2023-10-06 15:00:36 +01:00
parent e7423a9519
commit 951c7c27fb
13 changed files with 493 additions and 12 deletions

View File

@ -304,6 +304,7 @@ ApplyStageInput,BuildStageInput,BuildStageOutput,DownloadStageInput,DownloadStag
StateProvider,Regulator,UpdateApplier,MessageBuilder,APIClient,Reporter,DownloadRateModifier \
> tmp
mv tmp internal/services/syncservice/mocks_test.go
mockgen --package mocks github.com/ProtonMail/gluon/connector IMAPStateWrite > internal/services/imapservice/mocks/mocks.go
lint: gofiles lint-golang lint-license lint-dependencies lint-changelog lint-bug-report

2
go.mod
View File

@ -5,7 +5,7 @@ go 1.20
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.20230829112217-5d5c25c504b5
github.com/ProtonMail/gluon v0.17.1-0.20231009084701-3af0474b0b3c
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
github.com/ProtonMail/go-proton-api v0.4.1-0.20230831064234-0e3a549b3f36
github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton

4
go.sum
View File

@ -23,8 +23,8 @@ github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
github.com/ProtonMail/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk=
github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g=
github.com/ProtonMail/gluon v0.17.1-0.20230829112217-5d5c25c504b5 h1:C/8P5NHAKi2yCKez+OZ5rSR8SsL7k8si4pK4SE2QtV8=
github.com/ProtonMail/gluon v0.17.1-0.20230829112217-5d5c25c504b5/go.mod h1:Og5/Dz1MiGpCJn51XujZwxiLG7WzvvjE5PRpZBQmAHo=
github.com/ProtonMail/gluon v0.17.1-0.20231009084701-3af0474b0b3c h1:gUDu4pOswgbou0QczfreNiXQFrmvVlpSh8Q+vft/JvI=
github.com/ProtonMail/gluon v0.17.1-0.20231009084701-3af0474b0b3c/go.mod h1:Og5/Dz1MiGpCJn51XujZwxiLG7WzvvjE5PRpZBQmAHo=
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:D+aZah+k14Gn6kmL7eKxoo/4Dr/lK3ChBcwce2+SQP4=
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=

View File

@ -585,7 +585,7 @@ func TestBridge_MissingGluonStore(t *testing.T) {
require.NoError(t, os.RemoveAll(gluonDir))
// Bridge starts but can't find the gluon store dir; there should be no error.
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
withBridgeWaitForServers(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
// ...
})
})

View File

@ -37,6 +37,7 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice"
"github.com/bradenaw/juniper/iterator"
"github.com/bradenaw/juniper/stream"
"github.com/bradenaw/juniper/xslices"
@ -579,6 +580,67 @@ func TestBridge_MessageCreateDuringSync(t *testing.T) {
}, server.WithTLS(false))
}
func TestBridge_CorruptedVaultClearsPreviousIMAPSyncState(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
userID, addrID, err := s.CreateUser("imap", password)
require.NoError(t, err)
labelID, err := s.CreateLabel(userID, "folder", "", proton.LabelTypeFolder)
require.NoError(t, err)
withClient(ctx, t, s, "imap", password, func(ctx context.Context, c *proton.Client) {
createNumMessages(ctx, t, c, addrID, labelID, 100)
})
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
defer done()
var err error
userID, err = bridge.LoginFull(context.Background(), "imap", password, nil, nil)
require.NoError(t, err)
// Wait for sync to finish
require.Equal(t, userID, (<-syncCh).UserID)
})
settingsPath, err := locator.ProvideSettingsPath()
require.NoError(t, err)
syncConfigPath, err := locator.ProvideIMAPSyncConfigPath()
require.NoError(t, err)
syncStatePath := imapservice.GetSyncConfigPath(syncConfigPath, userID)
// Check sync state is complete
{
state, err := imapservice.NewSyncState(syncStatePath)
require.NoError(t, err)
syncStatus, err := state.GetSyncStatus(context.Background())
require.NoError(t, err)
require.True(t, syncStatus.IsComplete())
}
// corrupt the vault
require.NoError(t, os.WriteFile(filepath.Join(settingsPath, "vault.enc"), []byte("Trash!"), 0o600))
// Bridge starts but can't find the gluon database dir; there should be no error.
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
_, err := bridge.LoginFull(context.Background(), "imap", password, nil, nil)
require.NoError(t, err)
})
// Check sync state is reset.
{
state, err := imapservice.NewSyncState(syncStatePath)
require.NoError(t, err)
syncStatus, err := state.GetSyncStatus(context.Background())
require.NoError(t, err)
require.False(t, syncStatus.IsComplete())
}
})
}
func withClient(ctx context.Context, t *testing.T, s *server.Server, username string, password []byte, fn func(context.Context, *proton.Client)) { //nolint:unparam
m := proton.New(
proton.WithHostURL(s.GetHostURL()),

View File

@ -63,6 +63,7 @@ type Connector struct {
log *logrus.Entry
sharedCache *SharedCache
syncState *SyncState
}
func NewConnector(
@ -75,6 +76,7 @@ func NewConnector(
panicHandler async.PanicHandler,
telemetry Telemetry,
showAllMail bool,
syncState *SyncState,
) *Connector {
userID := identityState.UserID()
@ -106,6 +108,7 @@ func NewConnector(
}),
sharedCache: NewSharedCached(),
syncState: syncState,
}
}
@ -114,9 +117,35 @@ func (s *Connector) StateClose() {
s.updateCh.CloseAndDiscardQueued()
}
func (s *Connector) Init(_ context.Context, cache connector.IMAPState) error {
func (s *Connector) Init(ctx context.Context, cache connector.IMAPState) error {
s.sharedCache.Set(cache)
return nil
return cache.Write(ctx, func(ctx context.Context, write connector.IMAPStateWrite) error {
rd := s.labels.Read()
defer rd.Close()
mboxes, err := write.GetMailboxesWithoutAttrib(ctx)
if err != nil {
return err
}
// Attempt to fix bug when a vault got corrupted, but the sync state did not get reset leading to
// all labels being written to the root level. If we detect this happened, reset the sync state.
{
applied, err := fixGODT3003Labels(ctx, s.log, mboxes, rd, write)
if err != nil {
return err
}
if applied {
s.log.Debug("Patched folders/labels after GODT-3003 incident, resetting sync state.")
if err := s.syncState.ClearSyncStatus(ctx); err != nil {
return err
}
}
}
return nil
})
}
func (s *Connector) Authorize(ctx context.Context, username string, password []byte) bool {
@ -745,3 +774,41 @@ func (s *Connector) createDraft(ctx context.Context, literal []byte, addrKR *cry
func (s *Connector) publishUpdate(_ context.Context, update imap.Update) {
s.updateCh.Enqueue(update)
}
func fixGODT3003Labels(
ctx context.Context,
log *logrus.Entry,
mboxes []imap.MailboxNoAttrib,
rd labelsRead,
write connector.IMAPStateWrite,
) (bool, error) {
var applied bool
for _, mbox := range mboxes {
lbl, ok := rd.GetLabel(string(mbox.ID))
if !ok {
continue
}
if lbl.Type == proton.LabelTypeFolder {
if mbox.Name[0] != folderPrefix {
log.WithField("labelID", mbox.ID.ShortID()).Debug("Found folder without prefix, patching")
if err := write.PatchMailboxHierarchyWithoutTransforms(ctx, mbox.ID, xslices.Insert(mbox.Name, 0, folderPrefix)); err != nil {
return false, fmt.Errorf("failed to update mailbox name: %w", err)
}
applied = true
}
} else if lbl.Type == proton.LabelTypeLabel {
if mbox.Name[0] != labelPrefix {
log.WithField("labelID", mbox.ID.ShortID()).Debug("Found label without prefix, patching")
if err := write.PatchMailboxHierarchyWithoutTransforms(ctx, mbox.ID, xslices.Insert(mbox.Name, 0, labelPrefix)); err != nil {
return false, fmt.Errorf("failed to update mailbox name: %w", err)
}
applied = true
}
}
}
return applied, nil
}

View File

@ -0,0 +1,205 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package imapservice
import (
"context"
"testing"
"github.com/ProtonMail/gluon/imap"
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice/mocks"
"github.com/golang/mock/gomock"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/require"
)
func TestFixGODT3003Labels(t *testing.T) {
mockCtrl := gomock.NewController(t)
log := logrus.WithField("test", "test")
sharedLabels := newRWLabels()
wr := sharedLabels.Write()
wr.SetLabel("foo", proton.Label{
ID: "foo",
ParentID: "bar",
Name: "Foo",
Path: []string{"bar", "Foo"},
Color: "",
Type: proton.LabelTypeFolder,
})
wr.SetLabel("0", proton.Label{
ID: "0",
ParentID: "",
Name: "Inbox",
Path: []string{"Inbox"},
Color: "",
Type: proton.LabelTypeSystem,
})
wr.SetLabel("bar", proton.Label{
ID: "bar",
ParentID: "",
Name: "boo",
Path: []string{"bar"},
Color: "",
Type: proton.LabelTypeFolder,
})
wr.SetLabel("my_label", proton.Label{
ID: "my_label",
ParentID: "",
Name: "MyLabel",
Path: []string{"MyLabel"},
Color: "",
Type: proton.LabelTypeLabel,
})
wr.SetLabel("my_label2", proton.Label{
ID: "my_label2",
ParentID: "",
Name: "MyLabel2",
Path: []string{labelPrefix, "MyLabel2"},
Color: "",
Type: proton.LabelTypeLabel,
})
wr.Close()
mboxs := []imap.MailboxNoAttrib{
{
ID: "0",
Name: []string{"Inbox"},
},
{
ID: "bar",
Name: []string{"bar"},
},
{
ID: "foo",
Name: []string{"bar", "Foo"},
},
{
ID: "my_label",
Name: []string{"MyLabel"},
},
{
ID: "my_label2",
Name: []string{labelPrefix, "MyLabel2"},
},
}
rd := sharedLabels.Read()
defer rd.Close()
imapState := mocks.NewMockIMAPStateWrite(mockCtrl)
imapState.EXPECT().PatchMailboxHierarchyWithoutTransforms(gomock.Any(), gomock.Eq(imap.MailboxID("bar")), gomock.Eq([]string{folderPrefix, "bar"}))
imapState.EXPECT().PatchMailboxHierarchyWithoutTransforms(gomock.Any(), gomock.Eq(imap.MailboxID("foo")), gomock.Eq([]string{folderPrefix, "bar", "Foo"}))
imapState.EXPECT().PatchMailboxHierarchyWithoutTransforms(gomock.Any(), gomock.Eq(imap.MailboxID("my_label")), gomock.Eq([]string{labelPrefix, "MyLabel"}))
applied, err := fixGODT3003Labels(context.Background(), log, mboxs, rd, imapState)
require.NoError(t, err)
require.True(t, applied)
}
func TestFixGODT3003Labels_Noop(t *testing.T) {
mockCtrl := gomock.NewController(t)
log := logrus.WithField("test", "test")
sharedLabels := newRWLabels()
wr := sharedLabels.Write()
wr.SetLabel("foo", proton.Label{
ID: "foo",
ParentID: "bar",
Name: "Foo",
Path: []string{folderPrefix, "bar", "Foo"},
Color: "",
Type: proton.LabelTypeFolder,
})
wr.SetLabel("0", proton.Label{
ID: "0",
ParentID: "",
Name: "Inbox",
Path: []string{"Inbox"},
Color: "",
Type: proton.LabelTypeSystem,
})
wr.SetLabel("bar", proton.Label{
ID: "bar",
ParentID: "",
Name: "bar",
Path: []string{folderPrefix, "bar"},
Color: "",
Type: proton.LabelTypeFolder,
})
wr.SetLabel("my_label", proton.Label{
ID: "my_label",
ParentID: "",
Name: "MyLabel",
Path: []string{labelPrefix, "MyLabel"},
Color: "",
Type: proton.LabelTypeLabel,
})
wr.SetLabel("my_label2", proton.Label{
ID: "my_label2",
ParentID: "",
Name: "MyLabel2",
Path: []string{labelPrefix, "MyLabel2"},
Color: "",
Type: proton.LabelTypeLabel,
})
wr.Close()
mboxs := []imap.MailboxNoAttrib{
{
ID: "0",
Name: []string{"Inbox"},
},
{
ID: "bar",
Name: []string{folderPrefix, "bar"},
},
{
ID: "foo",
Name: []string{folderPrefix, "bar", "Foo"},
},
{
ID: "my_label",
Name: []string{labelPrefix, "MyLabel"},
},
{
ID: "my_label2",
Name: []string{labelPrefix, "MyLabel2"},
},
}
rd := sharedLabels.Read()
defer rd.Close()
imapState := mocks.NewMockIMAPStateWrite(mockCtrl)
applied, err := fixGODT3003Labels(context.Background(), log, mboxs, rd, imapState)
require.NoError(t, err)
require.False(t, applied)
}

View File

@ -0,0 +1,138 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/ProtonMail/gluon/connector (interfaces: IMAPStateWrite)
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
imap "github.com/ProtonMail/gluon/imap"
gomock "github.com/golang/mock/gomock"
)
// MockIMAPStateWrite is a mock of IMAPStateWrite interface.
type MockIMAPStateWrite struct {
ctrl *gomock.Controller
recorder *MockIMAPStateWriteMockRecorder
}
// MockIMAPStateWriteMockRecorder is the mock recorder for MockIMAPStateWrite.
type MockIMAPStateWriteMockRecorder struct {
mock *MockIMAPStateWrite
}
// NewMockIMAPStateWrite creates a new mock instance.
func NewMockIMAPStateWrite(ctrl *gomock.Controller) *MockIMAPStateWrite {
mock := &MockIMAPStateWrite{ctrl: ctrl}
mock.recorder = &MockIMAPStateWriteMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockIMAPStateWrite) EXPECT() *MockIMAPStateWriteMockRecorder {
return m.recorder
}
// CreateMailbox mocks base method.
func (m *MockIMAPStateWrite) CreateMailbox(arg0 context.Context, arg1 imap.Mailbox) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateMailbox", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// CreateMailbox indicates an expected call of CreateMailbox.
func (mr *MockIMAPStateWriteMockRecorder) CreateMailbox(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateMailbox", reflect.TypeOf((*MockIMAPStateWrite)(nil).CreateMailbox), arg0, arg1)
}
// GetMailboxCount mocks base method.
func (m *MockIMAPStateWrite) GetMailboxCount(arg0 context.Context) (int, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetMailboxCount", arg0)
ret0, _ := ret[0].(int)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetMailboxCount indicates an expected call of GetMailboxCount.
func (mr *MockIMAPStateWriteMockRecorder) GetMailboxCount(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMailboxCount", reflect.TypeOf((*MockIMAPStateWrite)(nil).GetMailboxCount), arg0)
}
// GetMailboxesWithoutAttrib mocks base method.
func (m *MockIMAPStateWrite) GetMailboxesWithoutAttrib(arg0 context.Context) ([]imap.MailboxNoAttrib, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetMailboxesWithoutAttrib", arg0)
ret0, _ := ret[0].([]imap.MailboxNoAttrib)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetMailboxesWithoutAttrib indicates an expected call of GetMailboxesWithoutAttrib.
func (mr *MockIMAPStateWriteMockRecorder) GetMailboxesWithoutAttrib(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMailboxesWithoutAttrib", reflect.TypeOf((*MockIMAPStateWrite)(nil).GetMailboxesWithoutAttrib), arg0)
}
// GetSettings mocks base method.
func (m *MockIMAPStateWrite) GetSettings(arg0 context.Context) (string, bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetSettings", arg0)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(bool)
ret2, _ := ret[2].(error)
return ret0, ret1, ret2
}
// GetSettings indicates an expected call of GetSettings.
func (mr *MockIMAPStateWriteMockRecorder) GetSettings(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSettings", reflect.TypeOf((*MockIMAPStateWrite)(nil).GetSettings), arg0)
}
// PatchMailboxHierarchyWithoutTransforms mocks base method.
func (m *MockIMAPStateWrite) PatchMailboxHierarchyWithoutTransforms(arg0 context.Context, arg1 imap.MailboxID, arg2 []string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "PatchMailboxHierarchyWithoutTransforms", arg0, arg1, arg2)
ret0, _ := ret[0].(error)
return ret0
}
// PatchMailboxHierarchyWithoutTransforms indicates an expected call of PatchMailboxHierarchyWithoutTransforms.
func (mr *MockIMAPStateWriteMockRecorder) PatchMailboxHierarchyWithoutTransforms(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PatchMailboxHierarchyWithoutTransforms", reflect.TypeOf((*MockIMAPStateWrite)(nil).PatchMailboxHierarchyWithoutTransforms), arg0, arg1, arg2)
}
// StoreSettings mocks base method.
func (m *MockIMAPStateWrite) StoreSettings(arg0 context.Context, arg1 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "StoreSettings", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// StoreSettings indicates an expected call of StoreSettings.
func (mr *MockIMAPStateWriteMockRecorder) StoreSettings(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StoreSettings", reflect.TypeOf((*MockIMAPStateWrite)(nil).StoreSettings), arg0, arg1)
}
// UpdateMessageFlags mocks base method.
func (m *MockIMAPStateWrite) UpdateMessageFlags(arg0 context.Context, arg1 imap.MessageID, arg2 imap.FlagSet) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateMessageFlags", arg0, arg1, arg2)
ret0, _ := ret[0].(error)
return ret0
}
// UpdateMessageFlags indicates an expected call of UpdateMessageFlags.
func (mr *MockIMAPStateWriteMockRecorder) UpdateMessageFlags(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateMessageFlags", reflect.TypeOf((*MockIMAPStateWrite)(nil).UpdateMessageFlags), arg0, arg1, arg2)
}

View File

@ -158,7 +158,7 @@ func NewService(
syncUpdateApplier: syncUpdateApplier,
syncMessageBuilder: syncMessageBuilder,
syncReporter: syncReporter,
syncConfigPath: getSyncConfigPath(syncConfigDir, identityState.User.ID),
syncConfigPath: GetSyncConfigPath(syncConfigDir, identityState.User.ID),
}
}
@ -498,6 +498,7 @@ func (s *Service) buildConnectors() (map[string]*Connector, error) {
s.panicHandler,
s.telemetry,
s.showAllMail,
s.syncStateProvider,
)
return connectors, nil
@ -514,6 +515,7 @@ func (s *Service) buildConnectors() (map[string]*Connector, error) {
s.panicHandler,
s.telemetry,
s.showAllMail,
s.syncStateProvider,
)
}
@ -644,6 +646,6 @@ type setAddressModeReq struct {
type getSyncFailedMessagesReq struct{}
func getSyncConfigPath(path string, userID string) string {
func GetSyncConfigPath(path string, userID string) string {
return filepath.Join(path, fmt.Sprintf("sync-%v", userID))
}

View File

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

View File

@ -220,7 +220,7 @@ func (s *SyncState) loadUnsafe() error {
}
func DeleteSyncState(configDir, userID string) error {
path := getSyncConfigPath(configDir, userID)
path := GetSyncConfigPath(configDir, userID)
if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) {
return err
@ -234,7 +234,7 @@ func MigrateVaultSettings(
hasLabels, hasMessages bool,
failedMessageIDs []string,
) (bool, error) {
filePath := getSyncConfigPath(configDir, userID)
filePath := GetSyncConfigPath(configDir, userID)
_, err := os.ReadFile(filePath) //nolint:gosec
if err == nil {

View File

@ -29,7 +29,7 @@ import (
func TestMigrateSyncSettings_AlreadyExists(t *testing.T) {
tmpDir := t.TempDir()
testFile := getSyncConfigPath(tmpDir, "test")
testFile := GetSyncConfigPath(tmpDir, "test")
expected, err := generateTestState(testFile)
require.NoError(t, err)
@ -53,7 +53,7 @@ func TestMigrateSyncSettings_DoesNotExist(t *testing.T) {
require.NoError(t, err)
require.True(t, migrated)
state, err := NewSyncState(getSyncConfigPath(tmpDir, "test"))
state, err := NewSyncState(GetSyncConfigPath(tmpDir, "test"))
require.NoError(t, err)
status, err := state.GetSyncStatus(context.Background())
require.NoError(t, err)

View File

@ -390,6 +390,11 @@ func (sm *Service) handleAddIMAPUserImpl(ctx context.Context,
} else {
log.Info("Creating new IMAP user")
// GODT-3003: Ensure previous IMAP sync state is cleared if we run into code path after vault corruption.
if err := syncStateProvider.ClearSyncStatus(ctx); err != nil {
return fmt.Errorf("failed to reset sync status: %w", err)
}
gluonID, err := sm.imapServer.AddUser(ctx, connector, idProvider.GluonKey())
if err != nil {
return fmt.Errorf("failed to add IMAP user: %w", err)