670 lines
19 KiB
Go
670 lines
19 KiB
Go
// Copyright (c) 2024 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 bridge
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"runtime"
|
|
|
|
"github.com/ProtonMail/gluon/async"
|
|
"github.com/ProtonMail/gluon/imap"
|
|
"github.com/ProtonMail/gluon/reporter"
|
|
"github.com/ProtonMail/go-proton-api"
|
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
|
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
|
|
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice"
|
|
"github.com/ProtonMail/proton-bridge/v3/internal/try"
|
|
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
|
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
|
"github.com/go-resty/resty/v2"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
type UserState int
|
|
|
|
const (
|
|
SignedOut UserState = iota
|
|
Locked
|
|
Connected
|
|
)
|
|
|
|
var ErrFailedToUnlock = errors.New("failed to unlock user keys")
|
|
|
|
type UserInfo struct {
|
|
// UserID is the user's API ID.
|
|
UserID string
|
|
|
|
// Username is the user's API username.
|
|
Username string
|
|
|
|
// Signed Out is true if the user is signed out (no AuthUID, user will need to provide credentials to log in again)
|
|
State UserState
|
|
|
|
// Addresses holds the user's email addresses. The first address is the primary address.
|
|
Addresses []string
|
|
|
|
// AddressMode is the user's address mode.
|
|
AddressMode vault.AddressMode
|
|
|
|
// BridgePass is the user's bridge password.
|
|
BridgePass []byte
|
|
|
|
// UsedSpace is the amount of space used by the user.
|
|
UsedSpace uint64
|
|
|
|
// MaxSpace is the total amount of space available to the user.
|
|
MaxSpace uint64
|
|
}
|
|
|
|
// GetUserIDs returns the IDs of all known users (authorized or not).
|
|
func (bridge *Bridge) GetUserIDs() []string {
|
|
return bridge.vault.GetUserIDs()
|
|
}
|
|
|
|
// HasUser returns true iff the given user is known (authorized or not).
|
|
func (bridge *Bridge) HasUser(userID string) bool {
|
|
return bridge.vault.HasUser(userID)
|
|
}
|
|
|
|
// GetUserInfo returns info about the given user.
|
|
func (bridge *Bridge) GetUserInfo(userID string) (UserInfo, error) {
|
|
return safe.RLockRetErr(func() (UserInfo, error) {
|
|
if user, ok := bridge.users[userID]; ok {
|
|
return getConnUserInfo(user), nil
|
|
}
|
|
|
|
var info UserInfo
|
|
|
|
if err := bridge.vault.GetUser(userID, func(user *vault.User) {
|
|
state := Locked
|
|
if len(user.AuthUID()) == 0 {
|
|
state = SignedOut
|
|
}
|
|
info = getUserInfo(user.UserID(), user.Username(), user.PrimaryEmail(), state, user.AddressMode())
|
|
}); err != nil {
|
|
return UserInfo{}, fmt.Errorf("failed to get user info: %w", err)
|
|
}
|
|
|
|
return info, nil
|
|
}, bridge.usersLock)
|
|
}
|
|
|
|
// QueryUserInfo queries the user info by username or address.
|
|
func (bridge *Bridge) QueryUserInfo(query string) (UserInfo, error) {
|
|
return safe.RLockRetErr(func() (UserInfo, error) {
|
|
for _, user := range bridge.users {
|
|
if user.Match(query) {
|
|
return getConnUserInfo(user), nil
|
|
}
|
|
}
|
|
|
|
return UserInfo{}, ErrNoSuchUser
|
|
}, bridge.usersLock)
|
|
}
|
|
|
|
// LoginAuth begins the login process. It returns an authorized client that might need 2FA.
|
|
func (bridge *Bridge) LoginAuth(ctx context.Context, username string, password []byte) (*proton.Client, proton.Auth, error) {
|
|
logrus.WithField("username", logging.Sensitive(username)).Info("Authorizing user for login")
|
|
|
|
if username == "crash@bandicoot" {
|
|
panic("Your wish is my command.. I crash!")
|
|
}
|
|
|
|
client, auth, err := bridge.api.NewClientWithLogin(ctx, username, password)
|
|
if err != nil {
|
|
return nil, proton.Auth{}, fmt.Errorf("failed to create new API client: %w", err)
|
|
}
|
|
|
|
if ok := safe.RLockRet(func() bool { return mapHas(bridge.users, auth.UserID) }, bridge.usersLock); ok {
|
|
logrus.WithField("userID", auth.UserID).Warn("User already logged in")
|
|
|
|
if err := client.AuthDelete(ctx); err != nil {
|
|
logrus.WithError(err).Warn("Failed to delete auth")
|
|
}
|
|
|
|
return nil, proton.Auth{}, ErrUserAlreadyLoggedIn
|
|
}
|
|
|
|
return client, auth, nil
|
|
}
|
|
|
|
// LoginUser finishes the user login process using the client and auth received from LoginAuth.
|
|
func (bridge *Bridge) LoginUser(
|
|
ctx context.Context,
|
|
client *proton.Client,
|
|
auth proton.Auth,
|
|
keyPass []byte,
|
|
) (string, error) {
|
|
logrus.WithField("userID", auth.UserID).Info("Logging in authorized user")
|
|
|
|
userID, err := try.CatchVal(
|
|
func() (string, error) {
|
|
return bridge.loginUser(ctx, client, auth.UID, auth.RefreshToken, keyPass)
|
|
},
|
|
)
|
|
|
|
if err != nil {
|
|
// Failure to unlock will allow retries, so we do not delete auth.
|
|
if !errors.Is(err, ErrFailedToUnlock) {
|
|
if deleteErr := client.AuthDelete(ctx); deleteErr != nil {
|
|
logrus.WithError(deleteErr).Error("Failed to delete auth")
|
|
}
|
|
}
|
|
return "", fmt.Errorf("failed to login user: %w", err)
|
|
}
|
|
|
|
bridge.publish(events.UserLoggedIn{
|
|
UserID: userID,
|
|
})
|
|
|
|
return userID, nil
|
|
}
|
|
|
|
// LoginFull authorizes a new bridge user with the given username and password.
|
|
// If necessary, a TOTP and mailbox password are requested via the callbacks.
|
|
// This is equivalent to doing LoginAuth and LoginUser separately.
|
|
func (bridge *Bridge) LoginFull(
|
|
ctx context.Context,
|
|
username string,
|
|
password []byte,
|
|
getTOTP func() (string, error),
|
|
getKeyPass func() ([]byte, error),
|
|
) (string, error) {
|
|
logrus.WithField("username", logging.Sensitive(username)).Info("Performing full user login")
|
|
|
|
client, auth, err := bridge.LoginAuth(ctx, username, password)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to begin login process: %w", err)
|
|
}
|
|
|
|
if auth.TwoFA.Enabled&proton.HasTOTP != 0 {
|
|
logrus.WithField("userID", auth.UserID).Info("Requesting TOTP")
|
|
|
|
totp, err := getTOTP()
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to get TOTP: %w", err)
|
|
}
|
|
|
|
if err := client.Auth2FA(ctx, proton.Auth2FAReq{TwoFactorCode: totp}); err != nil {
|
|
return "", fmt.Errorf("failed to authorize 2FA: %w", err)
|
|
}
|
|
}
|
|
|
|
var keyPass []byte
|
|
|
|
if auth.PasswordMode == proton.TwoPasswordMode {
|
|
logrus.WithField("userID", auth.UserID).Info("Requesting mailbox password")
|
|
|
|
userKeyPass, err := getKeyPass()
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to get key password: %w", err)
|
|
}
|
|
|
|
keyPass = userKeyPass
|
|
} else {
|
|
keyPass = password
|
|
}
|
|
|
|
userID, err := bridge.LoginUser(ctx, client, auth, keyPass)
|
|
if err != nil {
|
|
if deleteErr := client.AuthDelete(ctx); deleteErr != nil {
|
|
logrus.WithError(err).Error("Failed to delete auth")
|
|
}
|
|
|
|
return "", err
|
|
}
|
|
|
|
return userID, nil
|
|
}
|
|
|
|
// LogoutUser logs out the given user.
|
|
func (bridge *Bridge) LogoutUser(ctx context.Context, userID string) error {
|
|
logrus.WithField("userID", userID).Info("Logging out user")
|
|
|
|
return safe.LockRet(func() error {
|
|
user, ok := bridge.users[userID]
|
|
if !ok {
|
|
return ErrNoSuchUser
|
|
}
|
|
|
|
bridge.logoutUser(ctx, user, true, false, false)
|
|
|
|
bridge.publish(events.UserLoggedOut{
|
|
UserID: userID,
|
|
})
|
|
|
|
return nil
|
|
}, bridge.usersLock)
|
|
}
|
|
|
|
// DeleteUser deletes the given user.
|
|
func (bridge *Bridge) DeleteUser(ctx context.Context, userID string) error {
|
|
logrus.WithField("userID", userID).Info("Deleting user")
|
|
|
|
syncConfigDir, err := bridge.locator.ProvideIMAPSyncConfigPath()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get sync config path")
|
|
}
|
|
|
|
return safe.LockRet(func() error {
|
|
if !bridge.vault.HasUser(userID) {
|
|
return ErrNoSuchUser
|
|
}
|
|
|
|
if user, ok := bridge.users[userID]; ok {
|
|
bridge.logoutUser(ctx, user, true, true, !bridge.GetTelemetryDisabled())
|
|
}
|
|
|
|
if err := imapservice.DeleteSyncState(syncConfigDir, userID); err != nil {
|
|
return fmt.Errorf("failed to delete use sync config")
|
|
}
|
|
|
|
if err := bridge.vault.DeleteUser(userID); err != nil {
|
|
logrus.WithError(err).Error("Failed to delete vault user")
|
|
}
|
|
|
|
bridge.publish(events.UserDeleted{
|
|
UserID: userID,
|
|
})
|
|
|
|
return nil
|
|
}, bridge.usersLock)
|
|
}
|
|
|
|
// SetAddressMode sets the address mode for the given user.
|
|
func (bridge *Bridge) SetAddressMode(ctx context.Context, userID string, mode vault.AddressMode) error {
|
|
logrus.WithField("userID", userID).WithField("mode", mode).Info("Setting address mode")
|
|
|
|
return safe.RLockRet(func() error {
|
|
user, ok := bridge.users[userID]
|
|
if !ok {
|
|
return ErrNoSuchUser
|
|
}
|
|
|
|
if user.GetAddressMode() == mode {
|
|
return fmt.Errorf("address mode is already %q", mode)
|
|
}
|
|
|
|
if err := user.SetAddressMode(ctx, mode); err != nil {
|
|
return fmt.Errorf("failed to set address mode: %w", err)
|
|
}
|
|
|
|
bridge.publish(events.AddressModeChanged{
|
|
UserID: userID,
|
|
AddressMode: mode,
|
|
})
|
|
|
|
var splitMode = false
|
|
for _, user := range bridge.users {
|
|
if user.GetAddressMode() == vault.SplitMode {
|
|
splitMode = true
|
|
break
|
|
}
|
|
}
|
|
bridge.heartbeat.SetSplitMode(splitMode)
|
|
|
|
return nil
|
|
}, bridge.usersLock)
|
|
}
|
|
|
|
// SendBadEventUserFeedback passes the feedback to the given user.
|
|
func (bridge *Bridge) SendBadEventUserFeedback(_ context.Context, userID string, doResync bool) error {
|
|
logrus.WithField("userID", userID).WithField("doResync", doResync).Info("Passing bad event feedback to user")
|
|
|
|
return safe.RLockRet(func() error {
|
|
ctx := context.Background()
|
|
|
|
user, ok := bridge.users[userID]
|
|
if !ok {
|
|
if rerr := bridge.reporter.ReportMessageWithContext(
|
|
"Failed to handle event: feedback failed: no such user",
|
|
reporter.Context{"user_id": userID},
|
|
); rerr != nil {
|
|
logrus.WithError(rerr).Error("Failed to report feedback failure")
|
|
}
|
|
|
|
return ErrNoSuchUser
|
|
}
|
|
|
|
if doResync {
|
|
if rerr := bridge.reporter.ReportMessageWithContext(
|
|
"Failed to handle event: feedback resync",
|
|
reporter.Context{"user_id": userID},
|
|
); rerr != nil {
|
|
logrus.WithError(rerr).Error("Failed to report feedback failure")
|
|
}
|
|
|
|
return user.BadEventFeedbackResync(ctx)
|
|
}
|
|
|
|
if rerr := bridge.reporter.ReportMessageWithContext(
|
|
"Failed to handle event: feedback logout",
|
|
reporter.Context{"user_id": userID},
|
|
); rerr != nil {
|
|
logrus.WithError(rerr).Error("Failed to report feedback failure")
|
|
}
|
|
|
|
bridge.logoutUser(ctx, user, true, false, false)
|
|
|
|
bridge.publish(events.UserLoggedOut{
|
|
UserID: userID,
|
|
})
|
|
|
|
return nil
|
|
}, bridge.usersLock)
|
|
}
|
|
|
|
func (bridge *Bridge) loginUser(ctx context.Context, client *proton.Client, authUID, authRef string, keyPass []byte) (string, error) {
|
|
apiUser, err := client.GetUser(ctx)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to get API user: %w", err)
|
|
}
|
|
|
|
salts, err := client.GetSalts(ctx)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to get key salts: %w", err)
|
|
}
|
|
|
|
saltedKeyPass, err := salts.SaltForKey(keyPass, apiUser.Keys.Primary().ID)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to salt key password: %w", err)
|
|
}
|
|
|
|
if userKR, err := apiUser.Keys.Unlock(saltedKeyPass, nil); err != nil {
|
|
return "", fmt.Errorf("%w: %w", ErrFailedToUnlock, err)
|
|
} else if userKR.CountDecryptionEntities() == 0 {
|
|
return "", ErrFailedToUnlock
|
|
}
|
|
|
|
if err := bridge.addUser(ctx, client, apiUser, authUID, authRef, saltedKeyPass, true); err != nil {
|
|
return "", fmt.Errorf("failed to add bridge user: %w", err)
|
|
}
|
|
|
|
return apiUser.ID, nil
|
|
}
|
|
|
|
// loadUsers tries to load each user in the vault that isn't already loaded.
|
|
func (bridge *Bridge) loadUsers(ctx context.Context) error {
|
|
logrus.WithField("count", len(bridge.vault.GetUserIDs())).Info("Loading users")
|
|
defer logrus.Info("Finished loading users")
|
|
|
|
return bridge.vault.ForUser(runtime.NumCPU(), func(user *vault.User) error {
|
|
log := logrus.WithField("userID", user.UserID())
|
|
|
|
if user.AuthUID() == "" {
|
|
log.Info("User is not connected (skipping)")
|
|
return nil
|
|
}
|
|
|
|
if safe.RLockRet(func() bool { return mapHas(bridge.users, user.UserID()) }, bridge.usersLock) {
|
|
log.Info("User is already loaded (skipping)")
|
|
return nil
|
|
}
|
|
|
|
log.WithField("mode", user.AddressMode()).Info("Loading connected user")
|
|
|
|
bridge.publish(events.UserLoading{
|
|
UserID: user.UserID(),
|
|
})
|
|
|
|
if err := bridge.loadUser(ctx, user); err != nil {
|
|
log.WithError(err).Error("Failed to load connected user")
|
|
|
|
bridge.publish(events.UserLoadFail{
|
|
UserID: user.UserID(),
|
|
Error: err,
|
|
})
|
|
} else {
|
|
log.Info("Successfully loaded connected user")
|
|
|
|
bridge.publish(events.UserLoadSuccess{
|
|
UserID: user.UserID(),
|
|
})
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// loadUser loads an existing user from the vault.
|
|
func (bridge *Bridge) loadUser(ctx context.Context, user *vault.User) error {
|
|
client, auth, err := bridge.api.NewClientWithRefresh(ctx, user.AuthUID(), user.AuthRef())
|
|
if err != nil {
|
|
if apiErr := new(proton.APIError); errors.As(err, &apiErr) && (apiErr.Code == proton.AuthRefreshTokenInvalid) {
|
|
// The session cannot be refreshed, we sign out the user by clearing his auth secrets.
|
|
if err := user.Clear(); err != nil {
|
|
logrus.WithError(err).Warn("Failed to clear user secrets")
|
|
}
|
|
}
|
|
|
|
return fmt.Errorf("failed to create API client: %w", err)
|
|
}
|
|
|
|
if err := user.SetAuth(auth.UID, auth.RefreshToken); err != nil {
|
|
return fmt.Errorf("failed to set auth: %w", err)
|
|
}
|
|
|
|
apiUser, err := client.GetUser(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get user: %w", err)
|
|
}
|
|
|
|
if err := bridge.addUser(ctx, client, apiUser, auth.UID, auth.RefreshToken, user.KeyPass(), false); err != nil {
|
|
return fmt.Errorf("failed to add user: %w", err)
|
|
}
|
|
|
|
if user.PrimaryEmail() != apiUser.Email {
|
|
if err := user.SetPrimaryEmail(apiUser.Email); err != nil {
|
|
return fmt.Errorf("failed to modify user primary email: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// addUser adds a new user with an already salted mailbox password.
|
|
func (bridge *Bridge) addUser(
|
|
ctx context.Context,
|
|
client *proton.Client,
|
|
apiUser proton.User,
|
|
authUID, authRef string,
|
|
saltedKeyPass []byte,
|
|
isLogin bool,
|
|
) error {
|
|
vaultUser, isNew, err := bridge.newVaultUser(apiUser, authUID, authRef, saltedKeyPass)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to add vault user: %w", err)
|
|
}
|
|
|
|
if err := bridge.addUserWithVault(ctx, client, apiUser, vaultUser, isNew); err != nil {
|
|
if _, ok := err.(*resty.ResponseError); ok || isLogin {
|
|
logrus.WithError(err).Error("Failed to add user, clearing its secrets from vault")
|
|
|
|
if err := vaultUser.Clear(); err != nil {
|
|
logrus.WithError(err).Error("Failed to clear user secrets")
|
|
}
|
|
} else {
|
|
logrus.WithError(err).Error("Failed to add user")
|
|
}
|
|
|
|
if err := vaultUser.Close(); err != nil {
|
|
logrus.WithError(err).Error("Failed to close vault user")
|
|
}
|
|
|
|
if isNew {
|
|
logrus.Warn("Deleting newly added vault user")
|
|
|
|
if err := bridge.vault.DeleteUser(apiUser.ID); err != nil {
|
|
logrus.WithError(err).Error("Failed to delete vault user")
|
|
}
|
|
}
|
|
|
|
return fmt.Errorf("failed to add user with vault: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// addUserWithVault adds a new user to bridge with the given vault.
|
|
func (bridge *Bridge) addUserWithVault(
|
|
ctx context.Context,
|
|
client *proton.Client,
|
|
apiUser proton.User,
|
|
vault *vault.User,
|
|
isNew bool,
|
|
) error {
|
|
statsPath, err := bridge.locator.ProvideStatsPath()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get Statistics directory: %w", err)
|
|
}
|
|
|
|
syncSettingsPath, err := bridge.locator.ProvideIMAPSyncConfigPath()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get IMAP sync config path: %w", err)
|
|
}
|
|
|
|
user, err := user.New(
|
|
ctx,
|
|
vault,
|
|
client,
|
|
bridge.reporter,
|
|
apiUser,
|
|
bridge.panicHandler,
|
|
bridge.vault.GetShowAllMail(),
|
|
bridge.vault.GetMaxSyncMemory(),
|
|
statsPath,
|
|
bridge,
|
|
bridge.serverManager,
|
|
bridge.serverManager,
|
|
&bridgeEventSubscription{b: bridge},
|
|
bridge.syncService,
|
|
syncSettingsPath,
|
|
isNew,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create user: %w", err)
|
|
}
|
|
|
|
// Handle events coming from the user before forwarding them to the bridge.
|
|
// For example, if the user's addresses change, we need to update them in gluon.
|
|
bridge.tasks.Once(func(ctx context.Context) {
|
|
async.RangeContext(ctx, user.GetEventCh(), func(event events.Event) {
|
|
logrus.WithFields(logrus.Fields{
|
|
"userID": apiUser.ID,
|
|
"event": event,
|
|
}).Debug("Received user event")
|
|
|
|
bridge.handleUserEvent(ctx, user, event)
|
|
bridge.publish(event)
|
|
})
|
|
})
|
|
|
|
// Gluon will set the IMAP ID in the context, if known, before making requests on behalf of this user.
|
|
// As such, if we find this ID in the context, we should use it to update our user agent.
|
|
client.AddPreRequestHook(func(_ *resty.Client, r *resty.Request) error {
|
|
if imapID, ok := imap.GetIMAPIDFromContext(r.Context()); ok {
|
|
bridge.setUserAgent(imapID.Name, imapID.Version)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
// Finally, save the user in the bridge.
|
|
safe.Lock(func() {
|
|
bridge.users[apiUser.ID] = user
|
|
bridge.heartbeat.SetNbAccount(len(bridge.users))
|
|
}, bridge.usersLock)
|
|
|
|
// As we need at least one user to send heartbeat, try to send it.
|
|
bridge.heartbeat.start()
|
|
|
|
return nil
|
|
}
|
|
|
|
// newVaultUser creates a new vault user from the given auth information.
|
|
// If one already exists in the vault, its data will be updated.
|
|
func (bridge *Bridge) newVaultUser(
|
|
apiUser proton.User,
|
|
authUID, authRef string,
|
|
saltedKeyPass []byte,
|
|
) (*vault.User, bool, error) {
|
|
return bridge.vault.GetOrAddUser(apiUser.ID, apiUser.Name, apiUser.Email, authUID, authRef, saltedKeyPass)
|
|
}
|
|
|
|
// logout logs out the given user, optionally logging them out from the API too.
|
|
func (bridge *Bridge) logoutUser(ctx context.Context, user *user.User, withAPI, withData, withTelemetry bool) {
|
|
defer delete(bridge.users, user.ID())
|
|
|
|
// if this is actually a remove account
|
|
if withData && withAPI {
|
|
user.SendConfigStatusAbort(ctx, withTelemetry)
|
|
}
|
|
|
|
logrus.WithFields(logrus.Fields{
|
|
"userID": user.ID(),
|
|
"withAPI": withAPI,
|
|
"withData": withData,
|
|
}).Debug("Logging out user")
|
|
|
|
if err := user.Logout(ctx, withAPI); err != nil {
|
|
logrus.WithError(err).Error("Failed to logout user")
|
|
}
|
|
|
|
bridge.heartbeat.SetNbAccount(len(bridge.users))
|
|
|
|
user.Close()
|
|
}
|
|
|
|
// getUserInfo returns information about a disconnected user.
|
|
func getUserInfo(userID, username, primaryEmail string, state UserState, addressMode vault.AddressMode) UserInfo {
|
|
var addresses []string
|
|
if len(primaryEmail) > 0 {
|
|
addresses = []string{primaryEmail}
|
|
}
|
|
|
|
return UserInfo{
|
|
State: state,
|
|
UserID: userID,
|
|
Username: username,
|
|
Addresses: addresses,
|
|
AddressMode: addressMode,
|
|
}
|
|
}
|
|
|
|
// getConnUserInfo returns information about a connected user.
|
|
func getConnUserInfo(user *user.User) UserInfo {
|
|
return UserInfo{
|
|
State: Connected,
|
|
UserID: user.ID(),
|
|
Username: user.Name(),
|
|
Addresses: user.Emails(),
|
|
AddressMode: user.GetAddressMode(),
|
|
BridgePass: user.BridgePass(),
|
|
UsedSpace: user.UsedSpace(),
|
|
MaxSpace: user.MaxSpace(),
|
|
}
|
|
}
|
|
|
|
func mapHas[Key comparable, Val any](m map[Key]Val, key Key) bool {
|
|
_, ok := m[key]
|
|
return ok
|
|
}
|