peroxide: Bring the user online only when either an IMAP or an SMTP connection is requested

Issue #13
create-reload-action
Lukasz Janyst 4 years ago
parent 2dccb628fb
commit b0c0e425ae
No known key found for this signature in database
GPG Key ID: 32DE641041F17A9A
  1. 12
      pkg/imap/backend.go
  2. 6
      pkg/smtp/backend.go
  3. 3
      pkg/store/factory.go
  4. 3
      pkg/store/store.go
  5. 2
      pkg/users/types.go
  6. 80
      pkg/users/user.go
  7. 64
      pkg/users/users.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

@ -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 {

@ -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,
)
}

@ -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()

@ -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
}

@ -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()

@ -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

Loading…
Cancel
Save