feat(GODT-2289): UIDValidity as Timestamp

Update UIDValidity to be timestamp with the number of seconds since
the 1st of February 2023. This avoids the problem where we lose the
last UIDValidity value due to the vault being missing/corrupted/deleted.
This commit is contained in:
Leander Beernaert
2023-02-01 14:04:45 +01:00
parent 79c2523585
commit 45ec6b6e74
12 changed files with 28 additions and 42 deletions

View File

@ -23,6 +23,7 @@ import (
"runtime"
"github.com/Masterminds/semver/v3"
"github.com/ProtonMail/gluon/imap"
"github.com/ProtonMail/go-autostart"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
@ -110,6 +111,7 @@ func withBridge( //nolint:funlen
// Crash and report stuff
crashHandler,
reporter,
imap.DefaultEpochUIDValidityGenerator(),
// The logging stuff.
c.String(flagLogIMAP) == "client" || c.String(flagLogIMAP) == "all",

View File

@ -30,6 +30,7 @@ import (
"github.com/Masterminds/semver/v3"
"github.com/ProtonMail/gluon"
imapEvents "github.com/ProtonMail/gluon/events"
"github.com/ProtonMail/gluon/imap"
"github.com/ProtonMail/gluon/reporter"
"github.com/ProtonMail/gluon/watcher"
"github.com/ProtonMail/go-proton-api"
@ -122,6 +123,8 @@ type Bridge struct {
// goUpdate triggers a check/install of updates.
goUpdate func()
uidValidityGenerator imap.UIDValidityGenerator
}
// New creates a new bridge.
@ -140,6 +143,7 @@ func New( //nolint:funlen
proxyCtl ProxyController, // the DoH controller
crashHandler async.PanicHandler,
reporter reporter.Reporter,
uidValidityGenerator imap.UIDValidityGenerator,
logIMAPClient, logIMAPServer bool, // whether to log IMAP client/server activity
logSMTP bool, // whether to log SMTP activity
@ -169,6 +173,7 @@ func New( //nolint:funlen
api,
identifier,
proxyCtl,
uidValidityGenerator,
logIMAPClient, logIMAPServer, logSMTP,
)
if err != nil {
@ -214,6 +219,7 @@ func newBridge(
api *proton.Manager,
identifier Identifier,
proxyCtl ProxyController,
uidValidityGenerator imap.UIDValidityGenerator,
logIMAPClient, logIMAPServer, logSMTP bool,
) (*Bridge, error) {
@ -252,6 +258,7 @@ func newBridge(
logIMAPServer,
imapEventCh,
tasks,
uidValidityGenerator,
)
if err != nil {
return nil, fmt.Errorf("failed to create IMAP server: %w", err)
@ -298,6 +305,8 @@ func newBridge(
lastVersion: lastVersion,
tasks: tasks,
uidValidityGenerator: uidValidityGenerator,
}
bridge.smtpServer = newSMTPServer(bridge, tlsConfig, logSMTP)

View File

@ -29,6 +29,7 @@ import (
"time"
"github.com/Masterminds/semver/v3"
"github.com/ProtonMail/gluon/imap"
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/go-proton-api/server"
"github.com/ProtonMail/go-proton-api/server/backend"
@ -594,6 +595,9 @@ func withMocks(t *testing.T, tests func(*bridge.Mocks)) {
tests(mocks)
}
// Needs to be global to survive bridge shutdown/startup in unit tests as they happen to fast.
var testUIDValidityGenerator = imap.DefaultEpochUIDValidityGenerator()
// withBridge creates a new bridge which points to the given API URL and uses the given keychain, and closes it when done.
func withBridgeNoMocks(
ctx context.Context,
@ -639,6 +643,7 @@ func withBridgeNoMocks(
mocks.ProxyCtl,
mocks.CrashHandler,
mocks.Reporter,
testUIDValidityGenerator,
// The logging stuff.
os.Getenv("BRIDGE_LOG_IMAP_CLIENT") == "1",

View File

@ -28,6 +28,7 @@ import (
"github.com/Masterminds/semver/v3"
"github.com/ProtonMail/gluon"
imapEvents "github.com/ProtonMail/gluon/events"
"github.com/ProtonMail/gluon/imap"
"github.com/ProtonMail/gluon/reporter"
"github.com/ProtonMail/gluon/store"
"github.com/ProtonMail/proton-bridge/v3/internal/async"
@ -254,6 +255,7 @@ func newIMAPServer(
logClient, logServer bool,
eventCh chan<- imapEvents.Event,
tasks *async.Group,
uidValidityGenerator imap.UIDValidityGenerator,
) (*gluon.Server, error) {
gluonCacheDir = ApplyGluonCachePathSuffix(gluonCacheDir)
gluonConfigDir = ApplyGluonConfigPathSuffix(gluonConfigDir)
@ -297,6 +299,7 @@ func newIMAPServer(
gluon.WithLogger(imapClientLog, imapServerLog),
getGluonVersionInfo(version),
gluon.WithReporter(reporter),
gluon.WithUIDValidityGenerator(uidValidityGenerator),
)
if err != nil {
return nil, err

View File

@ -57,6 +57,7 @@ func TestBridge_Refresh(t *testing.T) {
require.Equal(t, userID, (<-syncCh).UserID)
})
var uidValidities = make(map[string]uint32, len(names))
// If we then connect an IMAP client, it should see all the labels with UID validity of 1.
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
mocks.Reporter.EXPECT().ReportMessageWithContext(gomock.Any(), gomock.Any()).AnyTimes()
@ -73,7 +74,7 @@ func TestBridge_Refresh(t *testing.T) {
for _, name := range names {
status, err := client.Select("Folders/"+name, false)
require.NoError(t, err)
require.Equal(t, uint32(1000), status.UidValidity)
uidValidities[name] = status.UidValidity
}
})
@ -106,7 +107,7 @@ func TestBridge_Refresh(t *testing.T) {
for _, name := range names {
status, err := client.Select("Folders/"+name, false)
require.NoError(t, err)
require.Equal(t, uint32(1001), status.UidValidity)
require.Greater(t, status.UidValidity, uidValidities[name])
}
})
})

View File

@ -163,6 +163,7 @@ func (bridge *Bridge) SetGluonDir(ctx context.Context, newGluonDir string) error
bridge.logIMAPServer,
bridge.imapEventCh,
bridge.tasks,
bridge.uidValidityGenerator,
)
if err != nil {
panic(fmt.Errorf("failed to create new IMAP server: %w", err))

View File

@ -477,16 +477,6 @@ func (conn *imapConnector) GetUpdates() <-chan imap.Update {
}, conn.updateChLock)
}
// GetUIDValidity returns the default UID validity for this user.
func (conn *imapConnector) GetUIDValidity() imap.UID {
return conn.vault.GetUIDValidity(conn.addrID)
}
// SetUIDValidity sets the default UID validity for this user.
func (conn *imapConnector) SetUIDValidity(validity imap.UID) error {
return conn.vault.SetUIDValidity(conn.addrID, validity)
}
// IsMailboxVisible returns whether this mailbox should be visible over IMAP.
func (conn *imapConnector) IsMailboxVisible(_ context.Context, mailboxID imap.MailboxID) bool {
return atomic.LoadUint32(&conn.showAllMail) != 0 || mailboxID != proton.AllMailLabel

View File

@ -17,8 +17,6 @@
package vault
import "github.com/ProtonMail/gluon/imap"
// UserData holds information about a single bridge user.
// The user may or may not be logged in.
type UserData struct {
@ -28,7 +26,6 @@ type UserData struct {
GluonKey []byte
GluonIDs map[string]string
UIDValidity map[string]imap.UID
BridgePass []byte // raw token represented as byte slice (needs to be encoded)
AddressMode AddressMode
@ -79,7 +76,6 @@ func newDefaultUser(userID, username, primaryEmail, authUID, authRef string, key
GluonKey: newRandomToken(32),
GluonIDs: make(map[string]string),
UIDValidity: make(map[string]imap.UID),
BridgePass: newRandomToken(16),
AddressMode: CombinedMode,

View File

@ -20,7 +20,6 @@ package vault
import (
"fmt"
"github.com/ProtonMail/gluon/imap"
"github.com/bradenaw/juniper/xslices"
"golang.org/x/exp/slices"
)
@ -81,24 +80,6 @@ func (user *User) RemoveGluonID(addrID, gluonID string) error {
return err
}
func (user *User) GetUIDValidity(addrID string) imap.UID {
if validity, ok := user.vault.getUser(user.userID).UIDValidity[addrID]; ok {
return validity
}
if err := user.SetUIDValidity(addrID, 1000); err != nil {
panic(err)
}
return user.GetUIDValidity(addrID)
}
func (user *User) SetUIDValidity(addrID string, validity imap.UID) error {
return user.vault.modUser(user.userID, func(data *UserData) {
data.UIDValidity[addrID] = validity
})
}
// AddressMode returns the user's address mode.
func (user *User) AddressMode() AddressMode {
return user.vault.getUser(user.userID).AddressMode
@ -208,10 +189,6 @@ func (user *User) ClearSyncStatus() error {
data.SyncStatus = SyncStatus{}
data.EventID = ""
for addrID := range data.UIDValidity {
data.UIDValidity[addrID]++
}
})
}