From b0c0e425aeb1a0f84bab509198c6ba8477d433f4 Mon Sep 17 00:00:00 2001 From: Lukasz Janyst Date: Tue, 10 May 2022 18:08:19 +0200 Subject: [PATCH] peroxide: Bring the user online only when either an IMAP or an SMTP connection is requested Issue #13 --- pkg/imap/backend.go | 12 ++++--- pkg/smtp/backend.go | 6 ++++ pkg/store/factory.go | 3 +- pkg/store/store.go | 3 +- pkg/users/types.go | 2 +- pkg/users/user.go | 80 ++++++++++++++++++++++++++++++++++---------- pkg/users/users.go | 64 ++--------------------------------- 7 files changed, 84 insertions(+), 86 deletions(-) diff --git a/pkg/imap/backend.go b/pkg/imap/backend.go index ec7fd20..c4d9716 100644 --- a/pkg/imap/backend.go +++ b/pkg/imap/backend.go @@ -95,7 +95,7 @@ func newIMAPBackend( } } -func (ib *imapBackend) getUser(address string) (*imapUser, error) { +func (ib *imapBackend) getUser(address, username, password string) (*imapUser, error) { ib.usersLocker.Lock() defer ib.usersLocker.Unlock() @@ -104,11 +104,11 @@ func (ib *imapBackend) getUser(address string) (*imapUser, error) { if ok { return imapUser, nil } - return ib.createUser(address) + return ib.createUser(address, username, password) } // createUser require that address MUST be in lowercase. -func (ib *imapBackend) createUser(address string) (*imapUser, error) { +func (ib *imapBackend) createUser(address, username, password string) (*imapUser, error) { log.WithField("address", address).Debug("Creating new IMAP user") user, err := ib.usersMgr.GetUser(address) @@ -116,6 +116,10 @@ func (ib *imapBackend) createUser(address string) (*imapUser, error) { return nil, err } + if err := user.BringOnline(username, password); err != nil { + return nil, err + } + // Make sure you return the same user for all valid addresses when in combined mode. if user.IsCombinedAddressMode() { address = strings.ToLower(user.GetPrimaryAddress()) @@ -153,7 +157,7 @@ func (ib *imapBackend) deleteUser(address string) { // Login authenticates a user. func (ib *imapBackend) Login(_ *imap.ConnInfo, username, password string) (goIMAPBackend.User, error) { - imapUser, err := ib.getUser(username) + imapUser, err := ib.getUser(username, username, password) if err != nil { log.WithError(err).Warn("Cannot get user") return nil, err diff --git a/pkg/smtp/backend.go b/pkg/smtp/backend.go index 81b01d1..e3b38c2 100644 --- a/pkg/smtp/backend.go +++ b/pkg/smtp/backend.go @@ -61,6 +61,11 @@ func (sb *smtpBackend) Login(_ *goSMTPBackend.ConnectionState, username, passwor log.Warn("Cannot get user: ", err) return nil, err } + + if err := user.BringOnline(username, password); err != nil { + return nil, err + } + if err := user.CheckBridgeLogin(password); err != nil { log.WithError(err).Error("Could not check bridge password") // Apple Mail sometimes generates a lot of requests very quickly. It's good practice @@ -68,6 +73,7 @@ func (sb *smtpBackend) Login(_ *goSMTPBackend.ConnectionState, username, passwor time.Sleep(10 * time.Second) return nil, err } + // Client can log in only using address so we can properly close all SMTP connections. addressID, err := user.GetAddressID(username) if err != nil { diff --git a/pkg/store/factory.go b/pkg/store/factory.go index 7fbfcf3..9b85f0f 100644 --- a/pkg/store/factory.go +++ b/pkg/store/factory.go @@ -52,7 +52,7 @@ func NewStoreFactory( } // New creates new store for given user. -func (f *StoreFactory) New(user BridgeUser) (*Store, error) { +func (f *StoreFactory) New(user BridgeUser, connected bool) (*Store, error) { return New( user, f.listener, @@ -60,6 +60,7 @@ func (f *StoreFactory) New(user BridgeUser) (*Store, error) { f.builder, getUserStorePath(f.settings.Get(settings.CacheDir), user.ID()), f.events, + connected, ) } diff --git a/pkg/store/store.go b/pkg/store/store.go index 0b4ce38..66f564d 100644 --- a/pkg/store/store.go +++ b/pkg/store/store.go @@ -150,6 +150,7 @@ func New( // nolint[funlen] builder *message.Builder, path string, currentEvents *Events, + connected bool, ) (store *Store, err error) { if user == nil || listener == nil || currentEvents == nil { return nil, fmt.Errorf("missing parameters - user: %v, listener: %v, currentEvents: %v", user, listener, currentEvents) @@ -201,7 +202,7 @@ func New( // nolint[funlen] return } - if user.IsConnected() { + if connected { store.eventLoop = newEventLoop(currentEvents, store, user, listener) go func() { store.eventLoop.start() diff --git a/pkg/users/types.go b/pkg/users/types.go index 4cbc4ab..1681719 100644 --- a/pkg/users/types.go +++ b/pkg/users/types.go @@ -39,6 +39,6 @@ type CredentialsStorer interface { } type StoreMaker interface { - New(user store.BridgeUser) (*store.Store, error) + New(user store.BridgeUser, connected bool) (*store.Store, error) Remove(userID string) error } diff --git a/pkg/users/user.go b/pkg/users/user.go index 32ca7a7..22f29a8 100644 --- a/pkg/users/user.go +++ b/pkg/users/user.go @@ -22,6 +22,7 @@ import ( "runtime" "strings" "sync" + "time" "github.com/ljanyst/peroxide/pkg/events" "github.com/ljanyst/peroxide/pkg/listener" @@ -37,10 +38,11 @@ var ErrLoggedOutUser = errors.New("account is logged out, use the app to login a // User is a struct on top of API client and credentials store. type User struct { - log *logrus.Entry - listener listener.Listener - client pmapi.Client - credStorer CredentialsStorer + log *logrus.Entry + listener listener.Listener + client pmapi.Client + clientManager pmapi.Manager + credStorer CredentialsStorer storeFactory StoreMaker store *store.Store @@ -60,24 +62,26 @@ func newUser( eventListener listener.Listener, credStorer CredentialsStorer, storeFactory StoreMaker, -) (*User, *credentials.Credentials, error) { + clientManager pmapi.Manager, +) (*User, error) { log := log.WithField("user", userID) log.Debug("Creating or loading user") creds, err := credStorer.Get(userID) if err != nil { - return nil, nil, errors.Wrap(err, "failed to load user credentials") + return nil, errors.Wrap(err, "failed to load user credentials") } return &User{ - log: log, - listener: eventListener, - credStorer: credStorer, - storeFactory: storeFactory, - userID: userID, - creds: creds, - }, creds, nil + log: log, + listener: eventListener, + clientManager: clientManager, + credStorer: credStorer, + storeFactory: storeFactory, + userID: userID, + creds: creds, + }, nil } // connect connects a user. This includes @@ -85,7 +89,7 @@ func newUser( // - loading its credentials from the credentials store // - loading and unlocking its PGP keys // - loading its store. -func (u *User) connect(client pmapi.Client, creds *credentials.Credentials) error { +func (u *User) connect(client pmapi.Client) error { u.log.Info("Connecting user") // Connected users have an API client. @@ -93,9 +97,6 @@ func (u *User) connect(client pmapi.Client, creds *credentials.Credentials) erro u.client.AddAuthRefreshHandler(u.handleAuthRefresh) - // Save the latest credentials for the user. - u.creds = creds - // Connected users have unlocked keys. if err := u.unlockIfNecessary(); err != nil { return err @@ -135,7 +136,7 @@ func (u *User) loadStore() error { u.store = nil } - store, err := u.storeFactory.New(u) + store, err := u.storeFactory.New(u, u.creds.IsConnected()) if err != nil { return errors.Wrap(err, "failed to create store") } @@ -382,6 +383,49 @@ func (u *User) CheckBridgeLogin(password string) error { return u.creds.CheckPassword(password) } +func (u *User) BringOnline(username, password string) error { + u.lock.Lock() + defer u.lock.Unlock() + + if u.client != nil { + return nil + } + + if !u.creds.IsConnected() { + return u.connect(u.clientManager.NewClient("", "", "", time.Time{})) + } + + uid, ref, err := u.creds.SplitAPIToken() + if err != nil { + return errors.Wrap(err, "could not get user's refresh token") + } + + ctx := pmapi.ContextWithoutRetry(context.Background()) + client, auth, err := u.clientManager.NewClientWithRefresh(ctx, uid, ref) + if err != nil { + connectErr := u.connect(u.clientManager.NewClient(uid, "", ref, time.Time{})) + + switch errors.Cause(err) { + case pmapi.ErrNoConnection, pmapi.ErrUpgradeApplication: + return connectErr + } + + if pmapi.IsFailedAuth(connectErr) { + if logoutErr := u.Logout(); logoutErr != nil { + logrus.WithError(logoutErr).Warn("Could not logout user") + } + } + return errors.Wrap(err, "could not refresh token") + } + + // Update the user's credentials with the latest auth used to connect this user. + if u.creds, err = u.credStorer.UpdateToken(u.creds.UserID, auth.UID, auth.RefreshToken); err != nil { + return errors.Wrap(err, "could not create get user's refresh token") + } + + return u.connect(client) +} + // UpdateUser updates user details from API and saves to the credentials. func (u *User) UpdateUser(ctx context.Context) error { u.lock.Lock() diff --git a/pkg/users/users.go b/pkg/users/users.go index bdf8f89..48c4de6 100644 --- a/pkg/users/users.go +++ b/pkg/users/users.go @@ -22,12 +22,10 @@ import ( "context" "strings" "sync" - "time" "github.com/hashicorp/go-multierror" "github.com/ljanyst/peroxide/pkg/listener" "github.com/ljanyst/peroxide/pkg/pmapi" - "github.com/ljanyst/peroxide/pkg/users/credentials" "github.com/pkg/errors" logrus "github.com/sirupsen/logrus" ) @@ -96,65 +94,18 @@ func (u *Users) loadUsersFromCredentialsStore() error { for _, userID := range userIDs { l := log.WithField("user", userID) - user, creds, err := newUser(userID, u.events, u.credStorer, u.storeFactory) + user, err := newUser(userID, u.events, u.credStorer, u.storeFactory, u.clientManager) if err != nil { l.WithError(err).Warn("Could not create user, skipping") continue } u.users = append(u.users, user) - - if creds.IsConnected() { - // If there is no connection, we don't want to retry. Load should - // happen fast enough to not block GUI. When connection is back up, - // watchEvents and unlockIfNecessary will finish user init later. - if err := u.loadConnectedUser(pmapi.ContextWithoutRetry(context.Background()), user, creds); err != nil { - l.WithError(err).Warn("Could not load connected user") - } - } else { - l.Warn("User is disconnected and must be connected manually") - if err := user.connect(u.clientManager.NewClient("", "", "", time.Time{}), creds); err != nil { - l.WithError(err).Warn("Could not load disconnected user") - } - } } return err } -func (u *Users) loadConnectedUser(ctx context.Context, user *User, creds *credentials.Credentials) error { - uid, ref, err := creds.SplitAPIToken() - if err != nil { - return errors.Wrap(err, "could not get user's refresh token") - } - - client, auth, err := u.clientManager.NewClientWithRefresh(ctx, uid, ref) - if err != nil { - // When client cannot be refreshed right away due to no connection, - // we create client which will refresh automatically when possible. - connectErr := user.connect(u.clientManager.NewClient(uid, "", ref, time.Time{}), creds) - - switch errors.Cause(err) { - case pmapi.ErrNoConnection, pmapi.ErrUpgradeApplication: - return connectErr - } - - if pmapi.IsFailedAuth(connectErr) { - if logoutErr := user.Logout(); logoutErr != nil { - logrus.WithError(logoutErr).Warn("Could not logout user") - } - } - return errors.Wrap(err, "could not refresh token") - } - - // Update the user's credentials with the latest auth used to connect this user. - if creds, err = u.credStorer.UpdateToken(creds.UserID, auth.UID, auth.RefreshToken); err != nil { - return errors.Wrap(err, "could not create get user's refresh token") - } - - return user.connect(client, creds) -} - func (u *Users) closeAllConnections() { for _, user := range u.users { user.CloseAllConnections() @@ -191,16 +142,11 @@ func (u *Users) FinishLogin(client pmapi.Client, auth *pmapi.Auth, password []by } // Update the password in case the user changed it. - creds, err := u.credStorer.UpdatePassword(apiUser.ID, passphrase) + _, err := u.credStorer.UpdatePassword(apiUser.ID, passphrase) if err != nil { return nil, errors.Wrap(err, "failed to update password of user in credentials store") } - // will go and unlock cache if not already done - if err := user.connect(client, creds); err != nil { - return nil, errors.Wrap(err, "failed to reconnect existing user") - } - return user, nil } @@ -220,15 +166,11 @@ func (u *Users) addNewUser(client pmapi.Client, apiUser *pmapi.User, auth *pmapi return errors.Wrap(err, "failed to add user credentials to credentials store") } - user, creds, err := newUser(apiUser.ID, u.events, u.credStorer, u.storeFactory) + user, err := newUser(apiUser.ID, u.events, u.credStorer, u.storeFactory, u.clientManager) if err != nil { return errors.Wrap(err, "failed to create new user") } - if err := user.connect(client, creds); err != nil { - return errors.Wrap(err, "failed to connect new user") - } - u.users = append(u.users, user) return nil