// Copyright (c) 2021 Proton Technologies AG // // This file is part of ProtonMail Bridge. // // ProtonMail 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. // // ProtonMail 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 ProtonMail Bridge. If not, see . package users import ( "context" "runtime" "strings" "sync" "github.com/ProtonMail/proton-bridge/internal/events" "github.com/ProtonMail/proton-bridge/internal/store" "github.com/ProtonMail/proton-bridge/internal/users/credentials" "github.com/ProtonMail/proton-bridge/pkg/listener" "github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/pkg/errors" "github.com/sirupsen/logrus" ) // ErrLoggedOutUser is sent to IMAP and SMTP if user exists, password is OK but user is logged out from the app. var ErrLoggedOutUser = errors.New("account is logged out, use the app to login again") // User is a struct on top of API client and credentials store. type User struct { log *logrus.Entry panicHandler PanicHandler listener listener.Listener client pmapi.Client credStorer CredentialsStorer storeFactory StoreMaker store *store.Store userID string creds *credentials.Credentials lock sync.RWMutex } // newUser creates a new user. // The user is initially disconnected and must be connected by calling connect(). func newUser( panicHandler PanicHandler, userID string, eventListener listener.Listener, credStorer CredentialsStorer, storeFactory StoreMaker, ) (*User, *credentials.Credentials, 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 &User{ log: log, panicHandler: panicHandler, listener: eventListener, credStorer: credStorer, storeFactory: storeFactory, userID: userID, creds: creds, }, creds, nil } // connect connects a user. This includes // - providing it with an authorised API client // - 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 { u.log.Info("Connecting user") // Connected users have an API client. u.client = client 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 } // Connected users have a store. if err := u.loadStore(); err != nil { //nolint[revive] easier to read return err } return nil } func (u *User) loadStore() error { // Logged-out user keeps store running to access offline data. // Therefore it is necessary to close it before re-init. if u.store != nil { if err := u.store.Close(); err != nil { log.WithError(err).Error("Not able to close store") } u.store = nil } store, err := u.storeFactory.New(u) if err != nil { return errors.Wrap(err, "failed to create store") } u.store = store return nil } func (u *User) handleAuthRefresh(auth *pmapi.AuthRefresh) { u.log.Debug("User received auth refresh update") if auth == nil { if err := u.logout(); err != nil { log.WithError(err). WithField("userID", u.userID). Error("User logout failed while watching API auths") } return } creds, err := u.credStorer.UpdateToken(u.userID, auth.UID, auth.RefreshToken) if err != nil { u.log.WithError(err).Error("Failed to update refresh token in credentials store") return } u.creds = creds } // clearStore removes the database. func (u *User) clearStore() error { u.log.Trace("Clearing user store") if u.store != nil { if err := u.store.Remove(); err != nil { return errors.Wrap(err, "failed to remove store") } } else { u.log.Warn("Store is not initialized: cleaning up store files manually") if err := u.storeFactory.Remove(u.userID); err != nil { return errors.Wrap(err, "failed to remove store manually") } } return nil } // closeStore just closes the store without deleting it. func (u *User) closeStore() error { u.log.Trace("Closing user store") if u.store != nil { if err := u.store.Close(); err != nil { return errors.Wrap(err, "failed to close store") } } return nil } // ID returns the user's userID. func (u *User) ID() string { return u.userID } // Username returns the user's username as found in the user's credentials. func (u *User) Username() string { u.lock.RLock() defer u.lock.RUnlock() return u.creds.Name } // IsConnected returns whether user is logged in. func (u *User) IsConnected() bool { u.lock.RLock() defer u.lock.RUnlock() return u.creds.IsConnected() } func (u *User) GetClient() pmapi.Client { if err := u.unlockIfNecessary(); err != nil { u.log.WithError(err).Error("Failed to unlock user") } return u.client } // unlockIfNecessary will not trigger keyring unlocking if it was already successfully unlocked. func (u *User) unlockIfNecessary() error { if !u.creds.IsConnected() { return nil } if u.client.IsUnlocked() { return nil } // unlockIfNecessary is called with every access to underlying pmapi // client. Unlock should only finish unlocking when connection is back up. // That means it should try it fast enough and not retry if connection // is still down. err := u.client.Unlock(pmapi.ContextWithoutRetry(context.Background()), u.creds.MailboxPassword) if err == nil { return nil } switch errors.Cause(err) { case pmapi.ErrNoConnection, pmapi.ErrUpgradeApplication: u.log.WithError(err).Warn("Could not unlock user") return nil } if logoutErr := u.logout(); logoutErr != nil { u.log.WithError(logoutErr).Warn("Could not logout user") } return errors.Wrap(err, "failed to unlock user") } // IsCombinedAddressMode returns whether user is set in combined or split mode. // Combined mode is the default mode and is what users typically need. // Split mode is mostly for outlook as it cannot handle sending e-mails from an // address other than the primary one. func (u *User) IsCombinedAddressMode() bool { if u.store != nil { return u.store.IsCombinedMode() } return u.creds.IsCombinedAddressMode } // GetPrimaryAddress returns the user's original address (which is // not necessarily the same as the primary address, because a primary address // might be an alias and be in position one). func (u *User) GetPrimaryAddress() string { u.lock.RLock() defer u.lock.RUnlock() return u.creds.EmailList()[0] } // GetStoreAddresses returns all addresses used by the store (so in combined mode, // that's just the original address, but in split mode, that's all active addresses). func (u *User) GetStoreAddresses() []string { u.lock.RLock() defer u.lock.RUnlock() if u.IsCombinedAddressMode() { return u.creds.EmailList()[:1] } return u.creds.EmailList() } // GetAddresses returns list of all addresses. func (u *User) GetAddresses() []string { u.lock.RLock() defer u.lock.RUnlock() return u.creds.EmailList() } // GetAddressID returns the API ID of the given address. func (u *User) GetAddressID(address string) (id string, err error) { u.lock.RLock() defer u.lock.RUnlock() if u.store != nil { address = strings.ToLower(address) return u.store.GetAddressID(address) } addresses := u.client.Addresses() pmapiAddress := addresses.ByEmail(address) if pmapiAddress != nil { return pmapiAddress.ID, nil } return "", errors.New("address not found") } // GetBridgePassword returns bridge password. This is not a password of the PM // account, but generated password for local purposes to not use a PM account // in the clients (such as Thunderbird). func (u *User) GetBridgePassword() string { u.lock.RLock() defer u.lock.RUnlock() return u.creds.BridgePassword } // CheckBridgeLogin checks whether the user is logged in and the bridge // IMAP/SMTP password is correct. func (u *User) CheckBridgeLogin(password string) error { if isApplicationOutdated { u.listener.Emit(events.UpgradeApplicationEvent, "") return pmapi.ErrUpgradeApplication } u.lock.RLock() defer u.lock.RUnlock() if !u.creds.IsConnected() { u.listener.Emit(events.LogoutEvent, u.userID) return ErrLoggedOutUser } return u.creds.CheckPassword(password) } // UpdateUser updates user details from API and saves to the credentials. func (u *User) UpdateUser(ctx context.Context) error { u.lock.Lock() defer u.lock.Unlock() _, err := u.client.UpdateUser(ctx) if err != nil { return err } if err := u.client.ReloadKeys(ctx, u.creds.MailboxPassword); err != nil { return errors.Wrap(err, "failed to reload keys") } creds, err := u.credStorer.UpdateEmails(u.userID, u.client.Addresses().ActiveEmails()) if err != nil { return err } u.creds = creds return nil } // SwitchAddressMode changes mode from combined to split and vice versa. The mode to switch to is determined by the // state of the user's credentials in the credentials store. See `IsCombinedAddressMode` for more details. func (u *User) SwitchAddressMode() error { u.log.Trace("Switching user address mode") u.lock.Lock() defer u.lock.Unlock() u.CloseAllConnections() if u.store == nil { return errors.New("store is not initialised") } newAddressModeState := !u.IsCombinedAddressMode() if err := u.store.UseCombinedMode(newAddressModeState); err != nil { return errors.Wrap(err, "could not switch store address mode") } if u.creds.IsCombinedAddressMode == newAddressModeState { return nil } creds, err := u.credStorer.SwitchAddressMode(u.userID) if err != nil { return errors.Wrap(err, "could not switch credentials store address mode") } u.creds = creds return nil } // logout is the same as Logout, but for internal purposes (logged out from // the server) which emits LogoutEvent to notify other parts of the app. func (u *User) logout() error { u.lock.Lock() wasConnected := u.creds.IsConnected() u.lock.Unlock() err := u.Logout() if wasConnected { u.listener.Emit(events.LogoutEvent, u.userID) u.listener.Emit(events.UserRefreshEvent, u.userID) } return err } // Logout logs out the user from pmapi, the credentials store, the mail store, and tries to remove as much // sensitive data as possible. func (u *User) Logout() error { u.lock.Lock() defer u.lock.Unlock() u.log.Debug("Logging out user") if !u.creds.IsConnected() { return nil } if err := u.client.AuthDelete(context.Background()); err != nil { u.log.WithError(err).Warn("Failed to delete auth") } creds, err := u.credStorer.Logout(u.userID) if err != nil { u.log.WithError(err).Warn("Could not log user out from credentials store") if err := u.credStorer.Delete(u.userID); err != nil { u.log.WithError(err).Error("Could not delete user from credentials store") } } else { u.creds = creds } // Do not close whole store, just event loop. Some information might be needed offline (e.g. addressID) u.closeEventLoop() u.CloseAllConnections() runtime.GC() return nil } func (u *User) closeEventLoop() { if u.store == nil { return } u.store.CloseEventLoop() } // CloseAllConnections calls CloseConnection for all users addresses. func (u *User) CloseAllConnections() { for _, address := range u.creds.EmailList() { u.CloseConnection(address) } if u.store != nil { u.store.SetChangeNotifier(nil) } } // CloseConnection emits closeConnection event on `address` which should close all active connection. func (u *User) CloseConnection(address string) { u.listener.Emit(events.CloseConnectionEvent, address) } func (u *User) GetStore() *store.Store { return u.store }