diff --git a/Changelog.md b/Changelog.md
index 12fb328e..71934666 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -6,6 +6,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
## unreleased
### Changed
+* GODT-386 renamed bridge to general users and keep bridge only for bridge stuff
* GODT-308 better user error message when request is canceled
* GODT-312 validate recipient emails in send before asking for their public keys
diff --git a/Makefile b/Makefile
index 3e43be82..313626af 100644
--- a/Makefile
+++ b/Makefile
@@ -165,9 +165,11 @@ test: gofiles
./internal/frontend/autoconfig/... \
./internal/frontend/cli/... \
./internal/imap/... \
+ ./internal/metrics/... \
./internal/preferences/... \
./internal/smtp/... \
./internal/store/... \
+ ./internal/users/... \
./pkg/...
bench:
@@ -179,7 +181,7 @@ coverage: test
go tool cover -html=/tmp/coverage.out -o=coverage.html
mocks:
- mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/bridge Configer,PreferenceProvider,PanicHandler,ClientManager,CredentialsStorer > internal/bridge/mocks/mocks.go
+ mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/users Configer,PreferenceProvider,PanicHandler,ClientManager,CredentialsStorer > internal/users/mocks/mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/store PanicHandler,ClientManager,BridgeUser > internal/store/mocks/mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/pkg/listener Listener > internal/store/mocks/utils_mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/pkg/pmapi Client > pkg/pmapi/mocks/mocks.go
diff --git a/cmd/Desktop-Bridge/main.go b/cmd/Desktop-Bridge/main.go
index df49d67f..fafa6baa 100644
--- a/cmd/Desktop-Bridge/main.go
+++ b/cmd/Desktop-Bridge/main.go
@@ -47,12 +47,12 @@ import (
"github.com/ProtonMail/proton-bridge/internal/api"
"github.com/ProtonMail/proton-bridge/internal/bridge"
- "github.com/ProtonMail/proton-bridge/internal/bridge/credentials"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/frontend"
"github.com/ProtonMail/proton-bridge/internal/imap"
"github.com/ProtonMail/proton-bridge/internal/preferences"
"github.com/ProtonMail/proton-bridge/internal/smtp"
+ "github.com/ProtonMail/proton-bridge/internal/users/credentials"
"github.com/ProtonMail/proton-bridge/pkg/args"
"github.com/ProtonMail/proton-bridge/pkg/config"
"github.com/ProtonMail/proton-bridge/pkg/constants"
@@ -261,7 +261,7 @@ func run(context *cli.Context) (contextError error) { // nolint[funlen]
eventListener := listener.New()
events.SetupEvents(eventListener)
- credentialsStore, credentialsError := credentials.NewStore()
+ credentialsStore, credentialsError := credentials.NewStore("bridge")
if credentialsError != nil {
log.Error("Could not get credentials store: ", credentialsError)
}
diff --git a/internal/bridge/bridge.go b/internal/bridge/bridge.go
index d137f1b7..2063747c 100644
--- a/internal/bridge/bridge.go
+++ b/internal/bridge/bridge.go
@@ -15,57 +15,24 @@
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see .
-// Package bridge provides core business logic providing API over credentials store and PM API.
+// Package bridge provides core functionality of Bridge app.
package bridge
import (
- "strconv"
- "strings"
- "sync"
- "time"
+ "github.com/ProtonMail/proton-bridge/internal/users"
- "github.com/ProtonMail/proton-bridge/internal/events"
- "github.com/ProtonMail/proton-bridge/internal/metrics"
- "github.com/ProtonMail/proton-bridge/internal/preferences"
- "github.com/ProtonMail/proton-bridge/internal/store"
"github.com/ProtonMail/proton-bridge/pkg/listener"
- "github.com/ProtonMail/proton-bridge/pkg/pmapi"
- imapBackend "github.com/emersion/go-imap/backend"
- "github.com/hashicorp/go-multierror"
- "github.com/pkg/errors"
logrus "github.com/sirupsen/logrus"
)
var (
- log = logrus.WithField("pkg", "bridge") //nolint[gochecknoglobals]
- isApplicationOutdated = false //nolint[gochecknoglobals]
+ log = logrus.WithField("pkg", "bridge") //nolint[gochecknoglobals]
)
-// Bridge is a struct handling users.
type Bridge struct {
- config Configer
- pref PreferenceProvider
- panicHandler PanicHandler
- events listener.Listener
- clientManager ClientManager
- credStorer CredentialsStorer
- storeCache *store.Cache
+ *users.Users
- // users is a list of accounts that have been added to bridge.
- // They are stored sorted in the credentials store in the order
- // that they were added to bridge chronologically.
- // People are used to that and so we preserve that ordering here.
- users []*User
-
- // idleUpdates is a channel which the imap backend listens to and which it uses
- // to send idle updates to the mail client (eg thunderbird).
- // The user stores should send idle updates on this channel.
- idleUpdates chan imapBackend.Update
-
- lock sync.RWMutex
-
- // stopAll can be closed to stop all goroutines from looping (watchBridgeOutdated, watchAPIAuths, heartbeat etc).
- stopAll chan struct{}
+ clientManager users.ClientManager
userAgentClientName string
userAgentClientVersion string
@@ -73,444 +40,19 @@ type Bridge struct {
}
func New(
- config Configer,
- pref PreferenceProvider,
- panicHandler PanicHandler,
+ config users.Configer,
+ pref users.PreferenceProvider,
+ panicHandler users.PanicHandler,
eventListener listener.Listener,
- clientManager ClientManager,
- credStorer CredentialsStorer,
+ clientManager users.ClientManager,
+ credStorer users.CredentialsStorer,
) *Bridge {
- log.Trace("Creating new bridge")
+ u := users.New(config, pref, panicHandler, eventListener, clientManager, credStorer)
+ return &Bridge{
+ Users: u,
- b := &Bridge{
- config: config,
- pref: pref,
- panicHandler: panicHandler,
- events: eventListener,
clientManager: clientManager,
- credStorer: credStorer,
- storeCache: store.NewCache(config.GetIMAPCachePath()),
- idleUpdates: make(chan imapBackend.Update),
- lock: sync.RWMutex{},
- stopAll: make(chan struct{}),
}
-
- // Allow DoH before starting bridge if the user has previously set this setting.
- // This allows us to start even if protonmail is blocked.
- if pref.GetBool(preferences.AllowProxyKey) {
- b.AllowProxy()
- }
-
- go func() {
- defer panicHandler.HandlePanic()
- b.watchBridgeOutdated()
- }()
-
- go func() {
- defer panicHandler.HandlePanic()
- b.watchAPIAuths()
- }()
-
- go b.heartbeat()
-
- if b.credStorer == nil {
- log.Error("Bridge has no credentials store")
- } else if err := b.loadUsersFromCredentialsStore(); err != nil {
- log.WithError(err).Error("Could not load all users from credentials store")
- }
-
- if pref.GetBool(preferences.FirstStartKey) {
- b.SendMetric(metrics.New(metrics.Setup, metrics.FirstStart, metrics.Label(config.GetVersion())))
- }
-
- return b
-}
-
-// heartbeat sends a heartbeat signal once a day.
-func (b *Bridge) heartbeat() {
- ticker := time.NewTicker(1 * time.Minute)
-
- for {
- select {
- case <-ticker.C:
- next, err := strconv.ParseInt(b.pref.Get(preferences.NextHeartbeatKey), 10, 64)
- if err != nil {
- continue
- }
- nextTime := time.Unix(next, 0)
- if time.Now().After(nextTime) {
- b.SendMetric(metrics.New(metrics.Heartbeat, metrics.Daily, metrics.NoLabel))
- nextTime = nextTime.Add(24 * time.Hour)
- b.pref.Set(preferences.NextHeartbeatKey, strconv.FormatInt(nextTime.Unix(), 10))
- }
-
- case <-b.stopAll:
- return
- }
- }
-}
-
-func (b *Bridge) loadUsersFromCredentialsStore() (err error) {
- b.lock.Lock()
- defer b.lock.Unlock()
-
- userIDs, err := b.credStorer.List()
- if err != nil {
- return
- }
-
- for _, userID := range userIDs {
- l := log.WithField("user", userID)
-
- user, newUserErr := newUser(b.panicHandler, userID, b.events, b.credStorer, b.clientManager, b.storeCache, b.config.GetDBDir())
- if newUserErr != nil {
- l.WithField("user", userID).WithError(newUserErr).Warn("Could not load user, skipping")
- continue
- }
-
- b.users = append(b.users, user)
-
- if initUserErr := user.init(b.idleUpdates); initUserErr != nil {
- l.WithField("user", userID).WithError(initUserErr).Warn("Could not initialise user")
- }
- }
-
- return err
-}
-
-func (b *Bridge) watchBridgeOutdated() {
- ch := make(chan string)
-
- b.events.Add(events.UpgradeApplicationEvent, ch)
-
- for {
- select {
- case <-ch:
- isApplicationOutdated = true
- b.closeAllConnections()
-
- case <-b.stopAll:
- return
- }
- }
-}
-
-// watchAPIAuths receives auths from the client manager and sends them to the appropriate user.
-func (b *Bridge) watchAPIAuths() {
- for {
- select {
- case auth := <-b.clientManager.GetAuthUpdateChannel():
- log.Debug("Bridge received auth from ClientManager")
-
- user, ok := b.hasUser(auth.UserID)
- if !ok {
- log.WithField("userID", auth.UserID).Info("User not available for auth update")
- continue
- }
-
- if auth.Auth != nil {
- user.updateAuthToken(auth.Auth)
- } else if err := user.logout(); err != nil {
- log.WithError(err).
- WithField("userID", auth.UserID).
- Error("User logout failed while watching API auths")
- }
-
- case <-b.stopAll:
- return
- }
- }
-}
-
-func (b *Bridge) closeAllConnections() {
- for _, user := range b.users {
- user.closeAllConnections()
- }
-}
-
-// Login authenticates a user.
-// The login flow:
-// * Authenticate user:
-// client, auth, err := bridge.Authenticate(username, password)
-//
-// * In case user `auth.HasTwoFactor()`, ask for it and fully authenticate the user.
-// auth2FA, err := client.Auth2FA(twoFactorCode)
-//
-// * In case user `auth.HasMailboxPassword()`, ask for it, otherwise use `password`
-// and then finish the login procedure.
-// user, err := bridge.FinishLogin(client, auth, mailboxPassword)
-func (b *Bridge) Login(username, password string) (authClient pmapi.Client, auth *pmapi.Auth, err error) {
- b.crashBandicoot(username)
-
- // We need to use anonymous client because we don't yet have userID and so can't save auth tokens yet.
- authClient = b.clientManager.GetAnonymousClient()
-
- authInfo, err := authClient.AuthInfo(username)
- if err != nil {
- log.WithField("username", username).WithError(err).Error("Could not get auth info for user")
- return
- }
-
- if auth, err = authClient.Auth(username, password, authInfo); err != nil {
- log.WithField("username", username).WithError(err).Error("Could not get auth for user")
- return
- }
-
- return
-}
-
-// FinishLogin finishes the login procedure and adds the user into the credentials store.
-// See `Login` for more details of the login flow.
-func (b *Bridge) FinishLogin(authClient pmapi.Client, auth *pmapi.Auth, mbPassword string) (user *User, err error) { //nolint[funlen]
- defer func() {
- if err == pmapi.ErrUpgradeApplication {
- b.events.Emit(events.UpgradeApplicationEvent, "")
- }
- if err != nil {
- log.WithError(err).Debug("Login not finished; removing auth session")
- if delAuthErr := authClient.DeleteAuth(); delAuthErr != nil {
- log.WithError(delAuthErr).Error("Failed to clear login session after unlock")
- }
- }
- // The anonymous client will be removed from list and authentication will not be deleted.
- authClient.Logout()
- }()
-
- apiUser, hashedPassword, err := getAPIUser(authClient, auth, mbPassword)
- if err != nil {
- log.WithError(err).Error("Failed to get API user")
- return
- }
-
- var ok bool
- if user, ok = b.hasUser(apiUser.ID); ok {
- if err = b.connectExistingUser(user, auth, hashedPassword); err != nil {
- log.WithError(err).Error("Failed to connect existing user")
- return
- }
- } else {
- if err = b.addNewUser(apiUser, auth, hashedPassword); err != nil {
- log.WithError(err).Error("Failed to add new user")
- return
- }
- }
-
- b.events.Emit(events.UserRefreshEvent, apiUser.ID)
-
- return b.GetUser(apiUser.ID)
-}
-
-// connectExistingUser connects an existing bridge user to the bridge.
-func (b *Bridge) connectExistingUser(user *User, auth *pmapi.Auth, hashedPassword string) (err error) {
- if user.IsConnected() {
- return errors.New("user is already connected")
- }
-
- // Update the user's password in the cred store in case they changed it.
- if err = b.credStorer.UpdatePassword(user.ID(), hashedPassword); err != nil {
- return errors.Wrap(err, "failed to update password of user in credentials store")
- }
-
- client := b.clientManager.GetClient(user.ID())
-
- if auth, err = client.AuthRefresh(auth.GenToken()); err != nil {
- return errors.Wrap(err, "failed to refresh auth token of new client")
- }
-
- if err = b.credStorer.UpdateToken(user.ID(), auth.GenToken()); err != nil {
- return errors.Wrap(err, "failed to update token of user in credentials store")
- }
-
- if err = user.init(b.idleUpdates); err != nil {
- return errors.Wrap(err, "failed to initialise user")
- }
-
- return
-}
-
-// addNewUser adds a new bridge user to the bridge.
-func (b *Bridge) addNewUser(user *pmapi.User, auth *pmapi.Auth, hashedPassword string) (err error) {
- b.lock.Lock()
- defer b.lock.Unlock()
-
- client := b.clientManager.GetClient(user.ID)
-
- if auth, err = client.AuthRefresh(auth.GenToken()); err != nil {
- return errors.Wrap(err, "failed to refresh token in new client")
- }
-
- if user, err = client.CurrentUser(); err != nil {
- return errors.Wrap(err, "failed to update API user")
- }
-
- activeEmails := client.Addresses().ActiveEmails()
-
- if _, err = b.credStorer.Add(user.ID, user.Name, auth.GenToken(), hashedPassword, activeEmails); err != nil {
- return errors.Wrap(err, "failed to add user to credentials store")
- }
-
- bridgeUser, err := newUser(b.panicHandler, user.ID, b.events, b.credStorer, b.clientManager, b.storeCache, b.config.GetDBDir())
- if err != nil {
- return errors.Wrap(err, "failed to create user")
- }
-
- // The user needs to be part of the users list in order for it to receive an auth during initialisation.
- b.users = append(b.users, bridgeUser)
-
- if err = bridgeUser.init(b.idleUpdates); err != nil {
- b.users = b.users[:len(b.users)-1]
- return errors.Wrap(err, "failed to initialise user")
- }
-
- b.SendMetric(metrics.New(metrics.Setup, metrics.NewUser, metrics.NoLabel))
-
- return err
-}
-
-func getAPIUser(client pmapi.Client, auth *pmapi.Auth, mbPassword string) (user *pmapi.User, hashedPassword string, err error) {
- hashedPassword, err = pmapi.HashMailboxPassword(mbPassword, auth.KeySalt)
- if err != nil {
- log.WithError(err).Error("Could not hash mailbox password")
- return
- }
-
- // We unlock the user's PGP key here to detect if the user's mailbox password is wrong.
- if _, err = client.Unlock(hashedPassword); err != nil {
- log.WithError(err).Error("Wrong mailbox password")
- return
- }
-
- if user, err = client.CurrentUser(); err != nil {
- log.WithError(err).Error("Could not load API user")
- return
- }
-
- return
-}
-
-// GetUsers returns all added users into keychain (even logged out users).
-func (b *Bridge) GetUsers() []*User {
- b.lock.RLock()
- defer b.lock.RUnlock()
-
- return b.users
-}
-
-// GetUser returns a user by `query` which is compared to users' ID, username or any attached e-mail address.
-func (b *Bridge) GetUser(query string) (*User, error) {
- b.crashBandicoot(query)
-
- b.lock.RLock()
- defer b.lock.RUnlock()
-
- for _, user := range b.users {
- if strings.EqualFold(user.ID(), query) || strings.EqualFold(user.Username(), query) {
- return user, nil
- }
- for _, address := range user.GetAddresses() {
- if strings.EqualFold(address, query) {
- return user, nil
- }
- }
- }
-
- return nil, errors.New("user " + query + " not found")
-}
-
-// ClearData closes all connections (to release db files and so on) and clears all data.
-func (b *Bridge) ClearData() error {
- var result *multierror.Error
- for _, user := range b.users {
- if err := user.Logout(); err != nil {
- result = multierror.Append(result, err)
- }
- if err := user.closeStore(); err != nil {
- result = multierror.Append(result, err)
- }
- }
- if err := b.config.ClearData(); err != nil {
- result = multierror.Append(result, err)
- }
- return result.ErrorOrNil()
-}
-
-// DeleteUser deletes user completely; it logs user out from the API, stops any
-// active connection, deletes from credentials store and removes from the Bridge struct.
-func (b *Bridge) DeleteUser(userID string, clearStore bool) error {
- b.lock.Lock()
- defer b.lock.Unlock()
-
- log := log.WithField("user", userID)
-
- for idx, user := range b.users {
- if user.ID() == userID {
- if err := user.Logout(); err != nil {
- log.WithError(err).Error("Cannot logout user")
- // We can try to continue to remove the user.
- // Token will still be valid, but will expire eventually.
- }
-
- if err := user.closeStore(); err != nil {
- log.WithError(err).Error("Failed to close user store")
- }
- if clearStore {
- // Clear cache after closing connections (done in logout).
- if err := user.clearStore(); err != nil {
- log.WithError(err).Error("Failed to clear user")
- }
- }
-
- if err := b.credStorer.Delete(userID); err != nil {
- log.WithError(err).Error("Cannot remove user")
- return err
- }
- b.users = append(b.users[:idx], b.users[idx+1:]...)
- return nil
- }
- }
-
- return errors.New("user " + userID + " not found")
-}
-
-// ReportBug reports a new bug from the user.
-func (b *Bridge) ReportBug(osType, osVersion, description, accountName, address, emailClient string) error {
- c := b.clientManager.GetAnonymousClient()
- defer c.Logout()
-
- title := "[Bridge] Bug"
- if err := c.ReportBugWithEmailClient(
- osType,
- osVersion,
- title,
- description,
- accountName,
- address,
- emailClient,
- ); err != nil {
- log.Error("Reporting bug failed: ", err)
- return err
- }
-
- log.Info("Bug successfully reported")
-
- return nil
-}
-
-// SendMetric sends a metric. We don't want to return any errors, only log them.
-func (b *Bridge) SendMetric(m metrics.Metric) {
- c := b.clientManager.GetAnonymousClient()
- defer c.Logout()
-
- cat, act, lab := m.Get()
- if err := c.SendSimpleMetric(string(cat), string(act), string(lab)); err != nil {
- log.Error("Sending metric failed: ", err)
- }
-
- log.WithFields(logrus.Fields{
- "cat": cat,
- "act": act,
- "lab": lab,
- }).Debug("Metric successfully sent")
}
// GetCurrentClient returns currently connected client (e.g. Thunderbird).
@@ -541,53 +83,26 @@ func (b *Bridge) updateUserAgent() {
b.clientManager.SetUserAgent(b.userAgentClientName, b.userAgentClientVersion, b.userAgentOS)
}
-// GetIMAPUpdatesChannel sets the channel on which idle events should be sent.
-func (b *Bridge) GetIMAPUpdatesChannel() chan imapBackend.Update {
- if b.idleUpdates == nil {
- log.Warn("Bridge updates channel is nil")
+// ReportBug reports a new bug from the user.
+func (b *Bridge) ReportBug(osType, osVersion, description, accountName, address, emailClient string) error {
+ c := b.clientManager.GetAnonymousClient()
+ defer c.Logout()
+
+ title := "[Bridge] Bug"
+ if err := c.ReportBugWithEmailClient(
+ osType,
+ osVersion,
+ title,
+ description,
+ accountName,
+ address,
+ emailClient,
+ ); err != nil {
+ log.Error("Reporting bug failed: ", err)
+ return err
}
- return b.idleUpdates
-}
+ log.Info("Bug successfully reported")
-// AllowProxy instructs bridge to use DoH to access an API proxy if necessary.
-// It also needs to work before bridge is initialised (because we may need to use the proxy at startup).
-func (b *Bridge) AllowProxy() {
- b.clientManager.AllowProxy()
-}
-
-// DisallowProxy instructs bridge to not use DoH to access an API proxy if necessary.
-// It also needs to work before bridge is initialised (because we may need to use the proxy at startup).
-func (b *Bridge) DisallowProxy() {
- b.clientManager.DisallowProxy()
-}
-
-// CheckConnection returns whether there is an internet connection.
-// This should use the connection manager when it is eventually implemented.
-func (b *Bridge) CheckConnection() error {
- return b.clientManager.CheckConnection()
-}
-
-// StopWatchers stops all bridge goroutines.
-func (b *Bridge) StopWatchers() {
- close(b.stopAll)
-}
-
-// hasUser returns whether the bridge currently has a user with ID `id`.
-func (b *Bridge) hasUser(id string) (user *User, ok bool) {
- for _, u := range b.users {
- if u.ID() == id {
- user, ok = u, true
- return
- }
- }
-
- return
-}
-
-// "Easter egg" for testing purposes.
-func (b *Bridge) crashBandicoot(username string) {
- if username == "crash@bandicoot" {
- panic("Your wish is my command… I crash!")
- }
+ return nil
}
diff --git a/internal/imap/bridge.go b/internal/imap/bridge.go
index 63a1088e..0b23ca29 100644
--- a/internal/imap/bridge.go
+++ b/internal/imap/bridge.go
@@ -19,6 +19,7 @@ package imap
import (
"github.com/ProtonMail/proton-bridge/internal/bridge"
+ "github.com/ProtonMail/proton-bridge/internal/users"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
)
@@ -67,10 +68,10 @@ func (b *bridgeWrap) GetUser(query string) (bridgeUser, error) {
}
type bridgeUserWrap struct {
- *bridge.User
+ *users.User
}
-func newBridgeUserWrap(bridgeUser *bridge.User) *bridgeUserWrap {
+func newBridgeUserWrap(bridgeUser *users.User) *bridgeUserWrap {
return &bridgeUserWrap{User: bridgeUser}
}
diff --git a/internal/smtp/bridge.go b/internal/smtp/bridge.go
index 5629bdba..84536fa8 100644
--- a/internal/smtp/bridge.go
+++ b/internal/smtp/bridge.go
@@ -19,6 +19,7 @@ package smtp
import (
"github.com/ProtonMail/proton-bridge/internal/bridge"
+ "github.com/ProtonMail/proton-bridge/internal/users"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
)
@@ -54,10 +55,10 @@ func (b *bridgeWrap) GetUser(query string) (bridgeUser, error) {
}
type bridgeUserWrap struct {
- *bridge.User
+ *users.User
}
-func newBridgeUserWrap(bridgeUser *bridge.User) *bridgeUserWrap {
+func newBridgeUserWrap(bridgeUser *users.User) *bridgeUserWrap {
return &bridgeUserWrap{User: bridgeUser}
}
diff --git a/internal/smtp/sending_info_test.go b/internal/smtp/sending_info_test.go
index 64d81a99..4ef88c5e 100644
--- a/internal/smtp/sending_info_test.go
+++ b/internal/smtp/sending_info_test.go
@@ -22,8 +22,8 @@ import (
"testing"
pmcrypto "github.com/ProtonMail/gopenpgp/crypto"
- "github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/events"
+ "github.com/ProtonMail/proton-bridge/internal/users"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/golang/mock/gomock"
@@ -32,14 +32,14 @@ import (
type mocks struct {
t *testing.T
- eventListener *bridge.MockListener
+ eventListener *users.MockListener
}
func initMocks(t *testing.T) mocks {
mockCtrl := gomock.NewController(t)
return mocks{
t: t,
- eventListener: bridge.NewMockListener(mockCtrl),
+ eventListener: users.NewMockListener(mockCtrl),
}
}
diff --git a/internal/bridge/credentials/credentials.go b/internal/users/credentials/credentials.go
similarity index 97%
rename from internal/bridge/credentials/credentials.go
rename to internal/users/credentials/credentials.go
index b281afad..b269238b 100644
--- a/internal/bridge/credentials/credentials.go
+++ b/internal/users/credentials/credentials.go
@@ -33,7 +33,7 @@ import (
const sep = "\x00"
var (
- log = logrus.WithField("pkg", "bridge") //nolint[gochecknoglobals]
+ log = logrus.WithField("pkg", "credentials") //nolint[gochecknoglobals]
ErrWrongFormat = errors.New("backend/creds: malformed password")
)
diff --git a/internal/bridge/credentials/crypto.go b/internal/users/credentials/crypto.go
similarity index 100%
rename from internal/bridge/credentials/crypto.go
rename to internal/users/credentials/crypto.go
diff --git a/internal/bridge/credentials/store.go b/internal/users/credentials/store.go
similarity index 98%
rename from internal/bridge/credentials/store.go
rename to internal/users/credentials/store.go
index 3bfc795f..02aaba6f 100644
--- a/internal/bridge/credentials/store.go
+++ b/internal/users/credentials/store.go
@@ -36,8 +36,8 @@ type Store struct {
}
// NewStore creates a new encrypted credentials store.
-func NewStore() (*Store, error) {
- secrets, err := keychain.NewAccess("bridge")
+func NewStore(appName string) (*Store, error) {
+ secrets, err := keychain.NewAccess(appName)
return &Store{
secrets: secrets,
}, err
diff --git a/internal/bridge/credentials/store_test.go b/internal/users/credentials/store_test.go
similarity index 100%
rename from internal/bridge/credentials/store_test.go
rename to internal/users/credentials/store_test.go
diff --git a/internal/bridge/mock_listener.go b/internal/users/mock_listener.go
similarity index 98%
rename from internal/bridge/mock_listener.go
rename to internal/users/mock_listener.go
index f1c05e8a..ce5d6145 100644
--- a/internal/bridge/mock_listener.go
+++ b/internal/users/mock_listener.go
@@ -1,8 +1,8 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: ./listener/listener.go
-// Package bridge is a generated GoMock package.
-package bridge
+// Package users is a generated GoMock package.
+package users
import (
reflect "reflect"
diff --git a/internal/bridge/mocks/mocks.go b/internal/users/mocks/mocks.go
similarity index 98%
rename from internal/bridge/mocks/mocks.go
rename to internal/users/mocks/mocks.go
index a24b99b8..624883f6 100644
--- a/internal/bridge/mocks/mocks.go
+++ b/internal/users/mocks/mocks.go
@@ -1,5 +1,5 @@
// Code generated by MockGen. DO NOT EDIT.
-// Source: github.com/ProtonMail/proton-bridge/internal/bridge (interfaces: Configer,PreferenceProvider,PanicHandler,ClientManager,CredentialsStorer)
+// Source: github.com/ProtonMail/proton-bridge/internal/users (interfaces: Configer,PreferenceProvider,PanicHandler,ClientManager,CredentialsStorer)
// Package mocks is a generated GoMock package.
package mocks
@@ -7,7 +7,7 @@ package mocks
import (
reflect "reflect"
- credentials "github.com/ProtonMail/proton-bridge/internal/bridge/credentials"
+ credentials "github.com/ProtonMail/proton-bridge/internal/users/credentials"
pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi"
gomock "github.com/golang/mock/gomock"
)
diff --git a/internal/bridge/types.go b/internal/users/types.go
similarity index 95%
rename from internal/bridge/types.go
rename to internal/users/types.go
index d7533673..0e9a4e7c 100644
--- a/internal/bridge/types.go
+++ b/internal/users/types.go
@@ -15,10 +15,10 @@
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see .
-package bridge
+package users
import (
- "github.com/ProtonMail/proton-bridge/internal/bridge/credentials"
+ "github.com/ProtonMail/proton-bridge/internal/users/credentials"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
)
diff --git a/internal/bridge/user.go b/internal/users/user.go
similarity index 97%
rename from internal/bridge/user.go
rename to internal/users/user.go
index 8f2e17d3..f82ceb8c 100644
--- a/internal/bridge/user.go
+++ b/internal/users/user.go
@@ -15,7 +15,7 @@
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see .
-package bridge
+package users
import (
"fmt"
@@ -24,9 +24,9 @@ import (
"strings"
"sync"
- "github.com/ProtonMail/proton-bridge/internal/bridge/credentials"
"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"
imapBackend "github.com/emersion/go-imap/backend"
@@ -34,8 +34,8 @@ import (
"github.com/sirupsen/logrus"
)
-// ErrLoggedOutUser is sent to IMAP and SMTP if user exists, password is OK but user is logged out from bridge.
-var ErrLoggedOutUser = errors.New("bridge account is logged out, use bridge to login again")
+// 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 {
@@ -61,7 +61,7 @@ type User struct {
wasKeyringUnlocked bool
}
-// newUser creates a new bridge user.
+// newUser creates a new user.
func newUser(
panicHandler PanicHandler,
userID string,
@@ -98,7 +98,7 @@ func (u *User) client() pmapi.Client {
return u.clientManager.GetClient(u.userID)
}
-// init initialises a bridge user. This includes reloading its credentials from the credentials store
+// init initialises a user. This includes reloading its credentials from the credentials store
// (such as when logging out and back in, you need to reload the credentials because the new credentials will
// have the apitoken and password), authorising the user against the api, loading the user store (creating a new one
// if necessary), and setting the imap idle updates channel (used to send imap idle updates to the imap backend if
@@ -119,7 +119,7 @@ func (u *User) init(idleUpdates chan imapBackend.Update) (err error) {
u.creds = creds
// Try to authorise the user if they aren't already authorised.
- // Note: we still allow users to set up bridge if the internet is off.
+ // Note: we still allow users to set up accounts if the internet is off.
if authErr := u.authorizeIfNecessary(false); authErr != nil {
switch errors.Cause(authErr) {
case pmapi.ErrAPINotReachable, pmapi.ErrUpgradeApplication, ErrLoggedOutUser:
@@ -245,7 +245,7 @@ func (u *User) authorizeAndUnlock() (err error) {
}
func (u *User) updateAuthToken(auth *pmapi.Auth) {
- u.log.Debug("User received auth from bridge")
+ u.log.Debug("User received auth")
if err := u.credStorer.UpdateToken(u.userID, auth.GenToken()); err != nil {
u.log.WithError(err).Error("Failed to update refresh token in credentials store")
@@ -495,7 +495,7 @@ func (u *User) SwitchAddressMode() (err error) {
}
// logout is the same as Logout, but for internal purposes (logged out from
-// the server) which emits LogoutEvent to notify other parts of the Bridge.
+// the server) which emits LogoutEvent to notify other parts of the app.
func (u *User) logout() error {
u.lock.Lock()
wasConnected := u.creds.IsConnected()
diff --git a/internal/bridge/user_credentials_test.go b/internal/users/user_credentials_test.go
similarity index 98%
rename from internal/bridge/user_credentials_test.go
rename to internal/users/user_credentials_test.go
index 2da9904e..89415d3a 100644
--- a/internal/bridge/user_credentials_test.go
+++ b/internal/users/user_credentials_test.go
@@ -15,7 +15,7 @@
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see .
-package bridge
+package users
import (
"testing"
@@ -213,7 +213,7 @@ func TestCheckBridgeLoginLoggedOut(t *testing.T) {
err = user.CheckBridgeLogin(testCredentialsDisconnected.BridgePassword)
waitForEvents()
- assert.Equal(t, "bridge account is logged out, use bridge to login again", err.Error())
+ assert.Equal(t, ErrLoggedOutUser, err)
}
func TestCheckBridgeLoginBadPassword(t *testing.T) {
diff --git a/internal/bridge/user_new_test.go b/internal/users/user_new_test.go
similarity index 97%
rename from internal/bridge/user_new_test.go
rename to internal/users/user_new_test.go
index 5606dacc..6a7331b7 100644
--- a/internal/bridge/user_new_test.go
+++ b/internal/users/user_new_test.go
@@ -15,14 +15,14 @@
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see .
-package bridge
+package users
import (
"errors"
"testing"
- "github.com/ProtonMail/proton-bridge/internal/bridge/credentials"
"github.com/ProtonMail/proton-bridge/internal/events"
+ "github.com/ProtonMail/proton-bridge/internal/users/credentials"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
gomock "github.com/golang/mock/gomock"
a "github.com/stretchr/testify/assert"
@@ -38,7 +38,7 @@ func TestNewUserNoCredentialsStore(t *testing.T) {
a.Error(t, err)
}
-func TestNewUserBridgeOutdated(t *testing.T) {
+func TestNewUserAppOutdated(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
diff --git a/internal/bridge/user_test.go b/internal/users/user_test.go
similarity index 99%
rename from internal/bridge/user_test.go
rename to internal/users/user_test.go
index 1c17f529..0367b02e 100644
--- a/internal/bridge/user_test.go
+++ b/internal/users/user_test.go
@@ -15,7 +15,7 @@
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see .
-package bridge
+package users
import (
"testing"
diff --git a/internal/users/users.go b/internal/users/users.go
new file mode 100644
index 00000000..9bab233f
--- /dev/null
+++ b/internal/users/users.go
@@ -0,0 +1,537 @@
+// Copyright (c) 2020 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 provides core business logic providing API over credentials store and PM API.
+package users
+
+import (
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/ProtonMail/proton-bridge/internal/events"
+ "github.com/ProtonMail/proton-bridge/internal/metrics"
+ "github.com/ProtonMail/proton-bridge/internal/preferences"
+ "github.com/ProtonMail/proton-bridge/internal/store"
+ "github.com/ProtonMail/proton-bridge/pkg/listener"
+ "github.com/ProtonMail/proton-bridge/pkg/pmapi"
+ imapBackend "github.com/emersion/go-imap/backend"
+ "github.com/hashicorp/go-multierror"
+ "github.com/pkg/errors"
+ logrus "github.com/sirupsen/logrus"
+)
+
+var (
+ log = logrus.WithField("pkg", "users") //nolint[gochecknoglobals]
+ isApplicationOutdated = false //nolint[gochecknoglobals]
+)
+
+// Users is a struct handling users.
+type Users struct {
+ config Configer
+ pref PreferenceProvider
+ panicHandler PanicHandler
+ events listener.Listener
+ clientManager ClientManager
+ credStorer CredentialsStorer
+ storeCache *store.Cache
+
+ // users is a list of accounts that have been added to the app.
+ // They are stored sorted in the credentials store in the order
+ // that they were added to the app chronologically.
+ // People are used to that and so we preserve that ordering here.
+ users []*User
+
+ // idleUpdates is a channel which the imap backend listens to and which it uses
+ // to send idle updates to the mail client (eg thunderbird).
+ // The user stores should send idle updates on this channel.
+ idleUpdates chan imapBackend.Update
+
+ lock sync.RWMutex
+
+ // stopAll can be closed to stop all goroutines from looping (watchAppOutdated, watchAPIAuths, heartbeat etc).
+ stopAll chan struct{}
+}
+
+func New(
+ config Configer,
+ pref PreferenceProvider,
+ panicHandler PanicHandler,
+ eventListener listener.Listener,
+ clientManager ClientManager,
+ credStorer CredentialsStorer,
+) *Users {
+ log.Trace("Creating new users")
+
+ u := &Users{
+ config: config,
+ pref: pref,
+ panicHandler: panicHandler,
+ events: eventListener,
+ clientManager: clientManager,
+ credStorer: credStorer,
+ storeCache: store.NewCache(config.GetIMAPCachePath()),
+ idleUpdates: make(chan imapBackend.Update),
+ lock: sync.RWMutex{},
+ stopAll: make(chan struct{}),
+ }
+
+ // Allow DoH before starting the app if the user has previously set this setting.
+ // This allows us to start even if protonmail is blocked.
+ if pref.GetBool(preferences.AllowProxyKey) {
+ u.AllowProxy()
+ }
+
+ go func() {
+ defer panicHandler.HandlePanic()
+ u.watchAppOutdated()
+ }()
+
+ go func() {
+ defer panicHandler.HandlePanic()
+ u.watchAPIAuths()
+ }()
+
+ go u.heartbeat()
+
+ if u.credStorer == nil {
+ log.Error("No credentials store is available")
+ } else if err := u.loadUsersFromCredentialsStore(); err != nil {
+ log.WithError(err).Error("Could not load all users from credentials store")
+ }
+
+ if pref.GetBool(preferences.FirstStartKey) {
+ u.SendMetric(metrics.New(metrics.Setup, metrics.FirstStart, metrics.Label(config.GetVersion())))
+ }
+
+ return u
+}
+
+// heartbeat sends a heartbeat signal once a day.
+func (u *Users) heartbeat() {
+ ticker := time.NewTicker(1 * time.Minute)
+
+ for {
+ select {
+ case <-ticker.C:
+ next, err := strconv.ParseInt(u.pref.Get(preferences.NextHeartbeatKey), 10, 64)
+ if err != nil {
+ continue
+ }
+ nextTime := time.Unix(next, 0)
+ if time.Now().After(nextTime) {
+ u.SendMetric(metrics.New(metrics.Heartbeat, metrics.Daily, metrics.NoLabel))
+ nextTime = nextTime.Add(24 * time.Hour)
+ u.pref.Set(preferences.NextHeartbeatKey, strconv.FormatInt(nextTime.Unix(), 10))
+ }
+
+ case <-u.stopAll:
+ return
+ }
+ }
+}
+
+func (u *Users) loadUsersFromCredentialsStore() (err error) {
+ u.lock.Lock()
+ defer u.lock.Unlock()
+
+ userIDs, err := u.credStorer.List()
+ if err != nil {
+ return
+ }
+
+ for _, userID := range userIDs {
+ l := log.WithField("user", userID)
+
+ user, newUserErr := newUser(u.panicHandler, userID, u.events, u.credStorer, u.clientManager, u.storeCache, u.config.GetDBDir())
+ if newUserErr != nil {
+ l.WithField("user", userID).WithError(newUserErr).Warn("Could not load user, skipping")
+ continue
+ }
+
+ u.users = append(u.users, user)
+
+ if initUserErr := user.init(u.idleUpdates); initUserErr != nil {
+ l.WithField("user", userID).WithError(initUserErr).Warn("Could not initialise user")
+ }
+ }
+
+ return err
+}
+
+func (u *Users) watchAppOutdated() {
+ ch := make(chan string)
+
+ u.events.Add(events.UpgradeApplicationEvent, ch)
+
+ for {
+ select {
+ case <-ch:
+ isApplicationOutdated = true
+ u.closeAllConnections()
+
+ case <-u.stopAll:
+ return
+ }
+ }
+}
+
+// watchAPIAuths receives auths from the client manager and sends them to the appropriate user.
+func (u *Users) watchAPIAuths() {
+ for {
+ select {
+ case auth := <-u.clientManager.GetAuthUpdateChannel():
+ log.Debug("Users received auth from ClientManager")
+
+ user, ok := u.hasUser(auth.UserID)
+ if !ok {
+ log.WithField("userID", auth.UserID).Info("User not available for auth update")
+ continue
+ }
+
+ if auth.Auth != nil {
+ user.updateAuthToken(auth.Auth)
+ } else if err := user.logout(); err != nil {
+ log.WithError(err).
+ WithField("userID", auth.UserID).
+ Error("User logout failed while watching API auths")
+ }
+
+ case <-u.stopAll:
+ return
+ }
+ }
+}
+
+func (u *Users) closeAllConnections() {
+ for _, user := range u.users {
+ user.closeAllConnections()
+ }
+}
+
+// Login authenticates a user.
+// The login flow:
+// * Authenticate user:
+// client, auth, err := users.Authenticate(username, password)
+//
+// * In case user `auth.HasTwoFactor()`, ask for it and fully authenticate the user.
+// auth2FA, err := client.Auth2FA(twoFactorCode)
+//
+// * In case user `auth.HasMailboxPassword()`, ask for it, otherwise use `password`
+// and then finish the login procedure.
+// user, err := users.FinishLogin(client, auth, mailboxPassword)
+func (u *Users) Login(username, password string) (authClient pmapi.Client, auth *pmapi.Auth, err error) {
+ u.crashBandicoot(username)
+
+ // We need to use anonymous client because we don't yet have userID and so can't save auth tokens yet.
+ authClient = u.clientManager.GetAnonymousClient()
+
+ authInfo, err := authClient.AuthInfo(username)
+ if err != nil {
+ log.WithField("username", username).WithError(err).Error("Could not get auth info for user")
+ return
+ }
+
+ if auth, err = authClient.Auth(username, password, authInfo); err != nil {
+ log.WithField("username", username).WithError(err).Error("Could not get auth for user")
+ return
+ }
+
+ return
+}
+
+// FinishLogin finishes the login procedure and adds the user into the credentials store.
+// See `Login` for more details of the login flow.
+func (u *Users) FinishLogin(authClient pmapi.Client, auth *pmapi.Auth, mbPassword string) (user *User, err error) { //nolint[funlen]
+ defer func() {
+ if err == pmapi.ErrUpgradeApplication {
+ u.events.Emit(events.UpgradeApplicationEvent, "")
+ }
+ if err != nil {
+ log.WithError(err).Debug("Login not finished; removing auth session")
+ if delAuthErr := authClient.DeleteAuth(); delAuthErr != nil {
+ log.WithError(delAuthErr).Error("Failed to clear login session after unlock")
+ }
+ }
+ // The anonymous client will be removed from list and authentication will not be deleted.
+ authClient.Logout()
+ }()
+
+ apiUser, hashedPassword, err := getAPIUser(authClient, auth, mbPassword)
+ if err != nil {
+ log.WithError(err).Error("Failed to get API user")
+ return
+ }
+
+ var ok bool
+ if user, ok = u.hasUser(apiUser.ID); ok {
+ if err = u.connectExistingUser(user, auth, hashedPassword); err != nil {
+ log.WithError(err).Error("Failed to connect existing user")
+ return
+ }
+ } else {
+ if err = u.addNewUser(apiUser, auth, hashedPassword); err != nil {
+ log.WithError(err).Error("Failed to add new user")
+ return
+ }
+ }
+
+ u.events.Emit(events.UserRefreshEvent, apiUser.ID)
+
+ return u.GetUser(apiUser.ID)
+}
+
+// connectExistingUser connects an existing user.
+func (u *Users) connectExistingUser(user *User, auth *pmapi.Auth, hashedPassword string) (err error) {
+ if user.IsConnected() {
+ return errors.New("user is already connected")
+ }
+
+ // Update the user's password in the cred store in case they changed it.
+ if err = u.credStorer.UpdatePassword(user.ID(), hashedPassword); err != nil {
+ return errors.Wrap(err, "failed to update password of user in credentials store")
+ }
+
+ client := u.clientManager.GetClient(user.ID())
+
+ if auth, err = client.AuthRefresh(auth.GenToken()); err != nil {
+ return errors.Wrap(err, "failed to refresh auth token of new client")
+ }
+
+ if err = u.credStorer.UpdateToken(user.ID(), auth.GenToken()); err != nil {
+ return errors.Wrap(err, "failed to update token of user in credentials store")
+ }
+
+ if err = user.init(u.idleUpdates); err != nil {
+ return errors.Wrap(err, "failed to initialise user")
+ }
+
+ return
+}
+
+// addNewUser adds a new user.
+func (u *Users) addNewUser(apiUser *pmapi.User, auth *pmapi.Auth, hashedPassword string) (err error) {
+ u.lock.Lock()
+ defer u.lock.Unlock()
+
+ client := u.clientManager.GetClient(apiUser.ID)
+
+ if auth, err = client.AuthRefresh(auth.GenToken()); err != nil {
+ return errors.Wrap(err, "failed to refresh token in new client")
+ }
+
+ if apiUser, err = client.CurrentUser(); err != nil {
+ return errors.Wrap(err, "failed to update API user")
+ }
+
+ activeEmails := client.Addresses().ActiveEmails()
+
+ if _, err = u.credStorer.Add(apiUser.ID, apiUser.Name, auth.GenToken(), hashedPassword, activeEmails); err != nil {
+ return errors.Wrap(err, "failed to add user to credentials store")
+ }
+
+ user, err := newUser(u.panicHandler, apiUser.ID, u.events, u.credStorer, u.clientManager, u.storeCache, u.config.GetDBDir())
+ if err != nil {
+ return errors.Wrap(err, "failed to create user")
+ }
+
+ // The user needs to be part of the users list in order for it to receive an auth during initialisation.
+ u.users = append(u.users, user)
+
+ if err = user.init(u.idleUpdates); err != nil {
+ u.users = u.users[:len(u.users)-1]
+ return errors.Wrap(err, "failed to initialise user")
+ }
+
+ u.SendMetric(metrics.New(metrics.Setup, metrics.NewUser, metrics.NoLabel))
+
+ return err
+}
+
+func getAPIUser(client pmapi.Client, auth *pmapi.Auth, mbPassword string) (user *pmapi.User, hashedPassword string, err error) {
+ hashedPassword, err = pmapi.HashMailboxPassword(mbPassword, auth.KeySalt)
+ if err != nil {
+ log.WithError(err).Error("Could not hash mailbox password")
+ return
+ }
+
+ // We unlock the user's PGP key here to detect if the user's mailbox password is wrong.
+ if _, err = client.Unlock(hashedPassword); err != nil {
+ log.WithError(err).Error("Wrong mailbox password")
+ return
+ }
+
+ if user, err = client.CurrentUser(); err != nil {
+ log.WithError(err).Error("Could not load API user")
+ return
+ }
+
+ return
+}
+
+// GetUsers returns all added users into keychain (even logged out users).
+func (u *Users) GetUsers() []*User {
+ u.lock.RLock()
+ defer u.lock.RUnlock()
+
+ return u.users
+}
+
+// GetUser returns a user by `query` which is compared to users' ID, username or any attached e-mail address.
+func (u *Users) GetUser(query string) (*User, error) {
+ u.crashBandicoot(query)
+
+ u.lock.RLock()
+ defer u.lock.RUnlock()
+
+ for _, user := range u.users {
+ if strings.EqualFold(user.ID(), query) || strings.EqualFold(user.Username(), query) {
+ return user, nil
+ }
+ for _, address := range user.GetAddresses() {
+ if strings.EqualFold(address, query) {
+ return user, nil
+ }
+ }
+ }
+
+ return nil, errors.New("user " + query + " not found")
+}
+
+// ClearData closes all connections (to release db files and so on) and clears all data.
+func (u *Users) ClearData() error {
+ var result *multierror.Error
+ for _, user := range u.users {
+ if err := user.Logout(); err != nil {
+ result = multierror.Append(result, err)
+ }
+ if err := user.closeStore(); err != nil {
+ result = multierror.Append(result, err)
+ }
+ }
+ if err := u.config.ClearData(); err != nil {
+ result = multierror.Append(result, err)
+ }
+ return result.ErrorOrNil()
+}
+
+// DeleteUser deletes user completely; it logs user out from the API, stops any
+// active connection, deletes from credentials store and removes from the Bridge struct.
+func (u *Users) DeleteUser(userID string, clearStore bool) error {
+ u.lock.Lock()
+ defer u.lock.Unlock()
+
+ log := log.WithField("user", userID)
+
+ for idx, user := range u.users {
+ if user.ID() == userID {
+ if err := user.Logout(); err != nil {
+ log.WithError(err).Error("Cannot logout user")
+ // We can try to continue to remove the user.
+ // Token will still be valid, but will expire eventually.
+ }
+
+ if err := user.closeStore(); err != nil {
+ log.WithError(err).Error("Failed to close user store")
+ }
+ if clearStore {
+ // Clear cache after closing connections (done in logout).
+ if err := user.clearStore(); err != nil {
+ log.WithError(err).Error("Failed to clear user")
+ }
+ }
+
+ if err := u.credStorer.Delete(userID); err != nil {
+ log.WithError(err).Error("Cannot remove user")
+ return err
+ }
+ u.users = append(u.users[:idx], u.users[idx+1:]...)
+ return nil
+ }
+ }
+
+ return errors.New("user " + userID + " not found")
+}
+
+// SendMetric sends a metric. We don't want to return any errors, only log them.
+func (u *Users) SendMetric(m metrics.Metric) {
+ c := u.clientManager.GetAnonymousClient()
+ defer c.Logout()
+
+ cat, act, lab := m.Get()
+ if err := c.SendSimpleMetric(string(cat), string(act), string(lab)); err != nil {
+ log.Error("Sending metric failed: ", err)
+ }
+
+ log.WithFields(logrus.Fields{
+ "cat": cat,
+ "act": act,
+ "lab": lab,
+ }).Debug("Metric successfully sent")
+}
+
+// GetIMAPUpdatesChannel sets the channel on which idle events should be sent.
+func (u *Users) GetIMAPUpdatesChannel() chan imapBackend.Update {
+ if u.idleUpdates == nil {
+ log.Warn("IMAP updates channel is nil")
+ }
+
+ return u.idleUpdates
+}
+
+// AllowProxy instructs the app to use DoH to access an API proxy if necessary.
+// It also needs to work before the app is initialised (because we may need to use the proxy at startup).
+func (u *Users) AllowProxy() {
+ u.clientManager.AllowProxy()
+}
+
+// DisallowProxy instructs the app to not use DoH to access an API proxy if necessary.
+// It also needs to work before the app is initialised (because we may need to use the proxy at startup).
+func (u *Users) DisallowProxy() {
+ u.clientManager.DisallowProxy()
+}
+
+// CheckConnection returns whether there is an internet connection.
+// This should use the connection manager when it is eventually implemented.
+func (u *Users) CheckConnection() error {
+ return u.clientManager.CheckConnection()
+}
+
+// StopWatchers stops all goroutines.
+func (u *Users) StopWatchers() {
+ close(u.stopAll)
+}
+
+// hasUser returns whether the struct currently has a user with ID `id`.
+func (u *Users) hasUser(id string) (user *User, ok bool) {
+ for _, u := range u.users {
+ if u.ID() == id {
+ user, ok = u, true
+ return
+ }
+ }
+
+ return
+}
+
+// "Easter egg" for testing purposes.
+func (u *Users) crashBandicoot(username string) {
+ if username == "crash@bandicoot" {
+ panic("Your wish is my command… I crash!")
+ }
+}
diff --git a/internal/bridge/bridge_users_test.go b/internal/users/users_actions_test.go
similarity index 77%
rename from internal/bridge/bridge_users_test.go
rename to internal/users/users_actions_test.go
index 783dad26..4bd1a138 100644
--- a/internal/bridge/bridge_users_test.go
+++ b/internal/users/users_actions_test.go
@@ -15,7 +15,7 @@
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see .
-package bridge
+package users
import (
"errors"
@@ -33,7 +33,7 @@ func TestGetNoUser(t *testing.T) {
m.clientManager.EXPECT().GetClient("user").Return(m.pmapiClient).MinTimes(1)
m.clientManager.EXPECT().GetClient("users").Return(m.pmapiClient).MinTimes(1)
- checkBridgeGetUser(t, m, "nouser", -1, "user nouser not found")
+ checkUsersGetUser(t, m, "nouser", -1, "user nouser not found")
}
func TestGetUserByID(t *testing.T) {
@@ -43,8 +43,8 @@ func TestGetUserByID(t *testing.T) {
m.clientManager.EXPECT().GetClient("user").Return(m.pmapiClient).MinTimes(1)
m.clientManager.EXPECT().GetClient("users").Return(m.pmapiClient).MinTimes(1)
- checkBridgeGetUser(t, m, "user", 0, "")
- checkBridgeGetUser(t, m, "users", 1, "")
+ checkUsersGetUser(t, m, "user", 0, "")
+ checkUsersGetUser(t, m, "users", 1, "")
}
func TestGetUserByName(t *testing.T) {
@@ -54,8 +54,8 @@ func TestGetUserByName(t *testing.T) {
m.clientManager.EXPECT().GetClient("user").Return(m.pmapiClient).MinTimes(1)
m.clientManager.EXPECT().GetClient("users").Return(m.pmapiClient).MinTimes(1)
- checkBridgeGetUser(t, m, "username", 0, "")
- checkBridgeGetUser(t, m, "usersname", 1, "")
+ checkUsersGetUser(t, m, "username", 0, "")
+ checkUsersGetUser(t, m, "usersname", 1, "")
}
func TestGetUserByEmail(t *testing.T) {
@@ -65,10 +65,10 @@ func TestGetUserByEmail(t *testing.T) {
m.clientManager.EXPECT().GetClient("user").Return(m.pmapiClient).MinTimes(1)
m.clientManager.EXPECT().GetClient("users").Return(m.pmapiClient).MinTimes(1)
- checkBridgeGetUser(t, m, "user@pm.me", 0, "")
- checkBridgeGetUser(t, m, "users@pm.me", 1, "")
- checkBridgeGetUser(t, m, "anotheruser@pm.me", 1, "")
- checkBridgeGetUser(t, m, "alsouser@pm.me", 1, "")
+ checkUsersGetUser(t, m, "user@pm.me", 0, "")
+ checkUsersGetUser(t, m, "users@pm.me", 1, "")
+ checkUsersGetUser(t, m, "anotheruser@pm.me", 1, "")
+ checkUsersGetUser(t, m, "alsouser@pm.me", 1, "")
}
func TestDeleteUser(t *testing.T) {
@@ -78,8 +78,8 @@ func TestDeleteUser(t *testing.T) {
m.clientManager.EXPECT().GetClient("user").Return(m.pmapiClient).MinTimes(1)
m.clientManager.EXPECT().GetClient("users").Return(m.pmapiClient).MinTimes(1)
- bridge := testNewBridgeWithUsers(t, m)
- defer cleanUpBridgeUserData(bridge)
+ users := testNewUsersWithUsers(t, m)
+ defer cleanUpUsersData(users)
gomock.InOrder(
m.pmapiClient.EXPECT().Logout().Return(),
@@ -90,9 +90,9 @@ func TestDeleteUser(t *testing.T) {
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")
- err := bridge.DeleteUser("user", true)
+ err := users.DeleteUser("user", true)
assert.NoError(t, err)
- assert.Equal(t, 1, len(bridge.users))
+ assert.Equal(t, 1, len(users.users))
}
// Even when logout fails, delete is done.
@@ -103,8 +103,8 @@ func TestDeleteUserWithFailingLogout(t *testing.T) {
m.clientManager.EXPECT().GetClient("user").Return(m.pmapiClient).MinTimes(1)
m.clientManager.EXPECT().GetClient("users").Return(m.pmapiClient).MinTimes(1)
- bridge := testNewBridgeWithUsers(t, m)
- defer cleanUpBridgeUserData(bridge)
+ users := testNewUsersWithUsers(t, m)
+ defer cleanUpUsersData(users)
gomock.InOrder(
m.pmapiClient.EXPECT().Logout().Return(),
@@ -116,16 +116,16 @@ func TestDeleteUserWithFailingLogout(t *testing.T) {
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")
- err := bridge.DeleteUser("user", true)
+ err := users.DeleteUser("user", true)
assert.NoError(t, err)
- assert.Equal(t, 1, len(bridge.users))
+ assert.Equal(t, 1, len(users.users))
}
-func checkBridgeGetUser(t *testing.T, m mocks, query string, index int, expectedError string) {
- bridge := testNewBridgeWithUsers(t, m)
- defer cleanUpBridgeUserData(bridge)
+func checkUsersGetUser(t *testing.T, m mocks, query string, index int, expectedError string) {
+ users := testNewUsersWithUsers(t, m)
+ defer cleanUpUsersData(users)
- user, err := bridge.GetUser(query)
+ user, err := users.GetUser(query)
waitForEvents()
if expectedError != "" {
@@ -136,7 +136,7 @@ func checkBridgeGetUser(t *testing.T, m mocks, query string, index int, expected
var expectedUser *User
if index >= 0 {
- expectedUser = bridge.users[index]
+ expectedUser = users.users[index]
}
assert.Equal(m.t, expectedUser, user)
diff --git a/internal/bridge/bridge_login_test.go b/internal/users/users_login_test.go
similarity index 82%
rename from internal/bridge/bridge_login_test.go
rename to internal/users/users_login_test.go
index 65f73c3e..b16d50d1 100644
--- a/internal/bridge/bridge_login_test.go
+++ b/internal/users/users_login_test.go
@@ -15,27 +15,27 @@
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see .
-package bridge
+package users
import (
"errors"
"testing"
- "github.com/ProtonMail/proton-bridge/internal/bridge/credentials"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/metrics"
+ "github.com/ProtonMail/proton-bridge/internal/users/credentials"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
gomock "github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
)
-func TestBridgeFinishLoginBadMailboxPassword(t *testing.T) {
+func TestUsersFinishLoginBadMailboxPassword(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
err := errors.New("bad password")
gomock.InOrder(
- // Init bridge with no user from keychain.
+ // Init users with no user from keychain.
m.credentialsStore.EXPECT().List().Return([]string{}, nil),
// Set up mocks for FinishLogin.
@@ -44,16 +44,16 @@ func TestBridgeFinishLoginBadMailboxPassword(t *testing.T) {
m.pmapiClient.EXPECT().Logout(),
)
- checkBridgeFinishLogin(t, m, testAuth, testCredentials.MailboxPassword, "", err)
+ checkUsersFinishLogin(t, m, testAuth, testCredentials.MailboxPassword, "", err)
}
-func TestBridgeFinishLoginUpgradeApplication(t *testing.T) {
+func TestUsersFinishLoginUpgradeApplication(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
err := errors.New("Cannot logout when upgrade needed")
gomock.InOrder(
- // Init bridge with no user from keychain.
+ // Init users with no user from keychain.
m.credentialsStore.EXPECT().List().Return([]string{}, nil),
// Set up mocks for FinishLogin.
@@ -64,7 +64,7 @@ func TestBridgeFinishLoginUpgradeApplication(t *testing.T) {
m.pmapiClient.EXPECT().Logout(),
)
- checkBridgeFinishLogin(t, m, testAuth, testCredentials.MailboxPassword, "", pmapi.ErrUpgradeApplication)
+ checkUsersFinishLogin(t, m, testAuth, testCredentials.MailboxPassword, "", pmapi.ErrUpgradeApplication)
}
func refreshWithToken(token string) *pmapi.Auth {
@@ -81,7 +81,7 @@ func credentialsWithToken(token string) *credentials.Credentials {
return tmp
}
-func TestBridgeFinishLoginNewUser(t *testing.T) {
+func TestUsersFinishLoginNewUser(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
@@ -89,7 +89,7 @@ func TestBridgeFinishLoginNewUser(t *testing.T) {
m.clientManager.EXPECT().GetClient("user").Return(m.pmapiClient).MinTimes(1)
gomock.InOrder(
- // bridge.New() finds no users in keychain.
+ // users.New() finds no users in keychain.
m.credentialsStore.EXPECT().List().Return([]string{}, nil),
// getAPIUser() loads user info from API (e.g. userID).
@@ -128,12 +128,12 @@ func TestBridgeFinishLoginNewUser(t *testing.T) {
mockEventLoopNoAction(m)
- user := checkBridgeFinishLogin(t, m, testAuth, testCredentials.MailboxPassword, "user", nil)
+ user := checkUsersFinishLogin(t, m, testAuth, testCredentials.MailboxPassword, "user", nil)
mockAuthUpdate(user, "afterCredentials", m)
}
-func TestBridgeFinishLoginExistingDisconnectedUser(t *testing.T) {
+func TestUsersFinishLoginExistingDisconnectedUser(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
@@ -144,7 +144,7 @@ func TestBridgeFinishLoginExistingDisconnectedUser(t *testing.T) {
m.clientManager.EXPECT().GetClient("user").Return(m.pmapiClient).MinTimes(1)
gomock.InOrder(
- // bridge.New() finds one existing user in keychain.
+ // users.New() finds one existing user in keychain.
m.credentialsStore.EXPECT().List().Return([]string{"user"}, nil),
// newUser()
@@ -186,12 +186,12 @@ func TestBridgeFinishLoginExistingDisconnectedUser(t *testing.T) {
mockEventLoopNoAction(m)
- user := checkBridgeFinishLogin(t, m, testAuth, testCredentials.MailboxPassword, "user", nil)
+ user := checkUsersFinishLogin(t, m, testAuth, testCredentials.MailboxPassword, "user", nil)
mockAuthUpdate(user, "afterCredentials", m)
}
-func TestBridgeFinishLoginConnectedUser(t *testing.T) {
+func TestUsersFinishLoginConnectedUser(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
@@ -201,8 +201,8 @@ func TestBridgeFinishLoginConnectedUser(t *testing.T) {
mockConnectedUser(m)
mockEventLoopNoAction(m)
- bridge := testNewBridge(t, m)
- defer cleanUpBridgeUserData(bridge)
+ users := testNewUsers(t, m)
+ defer cleanUpUsersData(users)
// Then, try to log in again...
gomock.InOrder(
@@ -212,15 +212,15 @@ func TestBridgeFinishLoginConnectedUser(t *testing.T) {
m.pmapiClient.EXPECT().Logout(),
)
- _, err := bridge.FinishLogin(m.pmapiClient, testAuth, testCredentials.MailboxPassword)
+ _, err := users.FinishLogin(m.pmapiClient, testAuth, testCredentials.MailboxPassword)
assert.Equal(t, "user is already connected", err.Error())
}
-func checkBridgeFinishLogin(t *testing.T, m mocks, auth *pmapi.Auth, mailboxPassword string, expectedUserID string, expectedErr error) *User {
- bridge := testNewBridge(t, m)
- defer cleanUpBridgeUserData(bridge)
+func checkUsersFinishLogin(t *testing.T, m mocks, auth *pmapi.Auth, mailboxPassword string, expectedUserID string, expectedErr error) *User {
+ users := testNewUsers(t, m)
+ defer cleanUpUsersData(users)
- user, err := bridge.FinishLogin(m.pmapiClient, auth, mailboxPassword)
+ user, err := users.FinishLogin(m.pmapiClient, auth, mailboxPassword)
waitForEvents()
@@ -228,11 +228,11 @@ func checkBridgeFinishLogin(t *testing.T, m mocks, auth *pmapi.Auth, mailboxPass
if expectedUserID != "" {
assert.Equal(t, expectedUserID, user.ID())
- assert.Equal(t, 1, len(bridge.users))
- assert.Equal(t, expectedUserID, bridge.users[0].ID())
+ assert.Equal(t, 1, len(users.users))
+ assert.Equal(t, expectedUserID, users.users[0].ID())
} else {
assert.Equal(t, (*User)(nil), user)
- assert.Equal(t, 0, len(bridge.users))
+ assert.Equal(t, 0, len(users.users))
}
return user
diff --git a/internal/bridge/bridge_new_test.go b/internal/users/users_new_test.go
similarity index 78%
rename from internal/bridge/bridge_new_test.go
rename to internal/users/users_new_test.go
index 67aae3f9..2dceb5ba 100644
--- a/internal/bridge/bridge_new_test.go
+++ b/internal/users/users_new_test.go
@@ -15,40 +15,40 @@
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see .
-package bridge
+package users
import (
"errors"
"testing"
- credentials "github.com/ProtonMail/proton-bridge/internal/bridge/credentials"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/metrics"
"github.com/ProtonMail/proton-bridge/internal/preferences"
+ "github.com/ProtonMail/proton-bridge/internal/users/credentials"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
gomock "github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
)
-func TestNewBridgeNoKeychain(t *testing.T) {
+func TestNewUsersNoKeychain(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
m.credentialsStore.EXPECT().List().Return([]string{}, errors.New("no keychain"))
- checkBridgeNew(t, m, []*credentials.Credentials{})
+ checkUsersNew(t, m, []*credentials.Credentials{})
}
-func TestNewBridgeWithoutUsersInCredentialsStore(t *testing.T) {
+func TestNewUsersWithoutUsersInCredentialsStore(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
m.credentialsStore.EXPECT().List().Return([]string{}, nil)
- checkBridgeNew(t, m, []*credentials.Credentials{})
+ checkUsersNew(t, m, []*credentials.Credentials{})
}
-func TestNewBridgeWithDisconnectedUser(t *testing.T) {
+func TestNewUsersWithDisconnectedUser(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
@@ -63,10 +63,10 @@ func TestNewBridgeWithDisconnectedUser(t *testing.T) {
m.pmapiClient.EXPECT().Addresses().Return(nil),
)
- checkBridgeNew(t, m, []*credentials.Credentials{testCredentialsDisconnected})
+ checkUsersNew(t, m, []*credentials.Credentials{testCredentialsDisconnected})
}
-func TestNewBridgeWithConnectedUserWithBadToken(t *testing.T) {
+func TestNewUsersWithConnectedUserWithBadToken(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
@@ -85,7 +85,7 @@ func TestNewBridgeWithConnectedUserWithBadToken(t *testing.T) {
m.credentialsStore.EXPECT().Get("user").Return(testCredentialsDisconnected, nil)
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")
- checkBridgeNew(t, m, []*credentials.Credentials{testCredentialsDisconnected})
+ checkUsersNew(t, m, []*credentials.Credentials{testCredentialsDisconnected})
}
func mockConnectedUser(m mocks) {
@@ -105,9 +105,9 @@ func mockConnectedUser(m mocks) {
)
}
-// mockAuthUpdate simulates bridge calling UpdateAuthToken on the given user.
-// This would normally be done by Bridge when it receives an auth from the ClientManager,
-// but as we don't have a full bridge instance here, we do this manually.
+// mockAuthUpdate simulates users calling UpdateAuthToken on the given user.
+// This would normally be done by users when it receives an auth from the ClientManager,
+// but as we don't have a full users instance here, we do this manually.
func mockAuthUpdate(user *User, token string, m mocks) {
gomock.InOrder(
m.credentialsStore.EXPECT().UpdateToken("user", ":"+token).Return(nil),
@@ -119,7 +119,7 @@ func mockAuthUpdate(user *User, token string, m mocks) {
waitForEvents()
}
-func TestNewBridgeWithConnectedUser(t *testing.T) {
+func TestNewUsersWithConnectedUser(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
@@ -129,12 +129,12 @@ func TestNewBridgeWithConnectedUser(t *testing.T) {
mockConnectedUser(m)
mockEventLoopNoAction(m)
- checkBridgeNew(t, m, []*credentials.Credentials{testCredentials})
+ checkUsersNew(t, m, []*credentials.Credentials{testCredentials})
}
// Tests two users with different states and checks also the order from
-// credentials store is kept also in array of Bridge users.
-func TestNewBridgeWithUsers(t *testing.T) {
+// credentials store is kept also in array of users.
+func TestNewUsersWithUsers(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
@@ -155,10 +155,10 @@ func TestNewBridgeWithUsers(t *testing.T) {
mockEventLoopNoAction(m)
- checkBridgeNew(t, m, []*credentials.Credentials{testCredentialsDisconnected, testCredentials})
+ checkUsersNew(t, m, []*credentials.Credentials{testCredentialsDisconnected, testCredentials})
}
-func TestNewBridgeFirstStart(t *testing.T) {
+func TestNewUsersFirstStart(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
@@ -170,17 +170,17 @@ func TestNewBridgeFirstStart(t *testing.T) {
m.pmapiClient.EXPECT().Logout(),
)
- testNewBridge(t, m)
+ testNewUsers(t, m)
}
-func checkBridgeNew(t *testing.T, m mocks, expectedCredentials []*credentials.Credentials) {
- bridge := testNewBridge(t, m)
- defer cleanUpBridgeUserData(bridge)
+func checkUsersNew(t *testing.T, m mocks, expectedCredentials []*credentials.Credentials) {
+ users := testNewUsers(t, m)
+ defer cleanUpUsersData(users)
- assert.Equal(m.t, len(expectedCredentials), len(bridge.GetUsers()))
+ assert.Equal(m.t, len(expectedCredentials), len(users.GetUsers()))
credentials := []*credentials.Credentials{}
- for _, user := range bridge.users {
+ for _, user := range users.users {
credentials = append(credentials, user.creds)
}
diff --git a/internal/bridge/bridge_test.go b/internal/users/users_test.go
similarity index 88%
rename from internal/bridge/bridge_test.go
rename to internal/users/users_test.go
index b23700ae..6709d79e 100644
--- a/internal/bridge/bridge_test.go
+++ b/internal/users/users_test.go
@@ -15,7 +15,7 @@
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see .
-package bridge
+package users
import (
"fmt"
@@ -25,12 +25,12 @@ import (
"testing"
"time"
- "github.com/ProtonMail/proton-bridge/internal/bridge/credentials"
- bridgemocks "github.com/ProtonMail/proton-bridge/internal/bridge/mocks"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/metrics"
"github.com/ProtonMail/proton-bridge/internal/preferences"
"github.com/ProtonMail/proton-bridge/internal/store"
+ "github.com/ProtonMail/proton-bridge/internal/users/credentials"
+ usersmocks "github.com/ProtonMail/proton-bridge/internal/users/mocks"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
pmapimocks "github.com/ProtonMail/proton-bridge/pkg/pmapi/mocks"
gomock "github.com/golang/mock/gomock"
@@ -131,11 +131,11 @@ type mocks struct {
t *testing.T
ctrl *gomock.Controller
- config *bridgemocks.MockConfiger
- PanicHandler *bridgemocks.MockPanicHandler
- prefProvider *bridgemocks.MockPreferenceProvider
- clientManager *bridgemocks.MockClientManager
- credentialsStore *bridgemocks.MockCredentialsStorer
+ config *usersmocks.MockConfiger
+ PanicHandler *usersmocks.MockPanicHandler
+ prefProvider *usersmocks.MockPreferenceProvider
+ clientManager *usersmocks.MockClientManager
+ credentialsStore *usersmocks.MockCredentialsStorer
eventListener *MockListener
pmapiClient *pmapimocks.MockClient
@@ -172,11 +172,11 @@ func initMocks(t *testing.T) mocks {
t: t,
ctrl: mockCtrl,
- config: bridgemocks.NewMockConfiger(mockCtrl),
- PanicHandler: bridgemocks.NewMockPanicHandler(mockCtrl),
- prefProvider: bridgemocks.NewMockPreferenceProvider(mockCtrl),
- clientManager: bridgemocks.NewMockClientManager(mockCtrl),
- credentialsStore: bridgemocks.NewMockCredentialsStorer(mockCtrl),
+ config: usersmocks.NewMockConfiger(mockCtrl),
+ PanicHandler: usersmocks.NewMockPanicHandler(mockCtrl),
+ prefProvider: usersmocks.NewMockPreferenceProvider(mockCtrl),
+ clientManager: usersmocks.NewMockClientManager(mockCtrl),
+ credentialsStore: usersmocks.NewMockCredentialsStorer(mockCtrl),
eventListener: NewMockListener(mockCtrl),
pmapiClient: pmapimocks.NewMockClient(mockCtrl),
@@ -195,7 +195,7 @@ func initMocks(t *testing.T) mocks {
return m
}
-func testNewBridgeWithUsers(t *testing.T, m mocks) *Bridge {
+func testNewUsersWithUsers(t *testing.T, m mocks) *Users {
// Events are asynchronous
m.pmapiClient.EXPECT().GetEvent("").Return(testPMAPIEvent, nil).Times(2)
m.pmapiClient.EXPECT().GetEvent(testPMAPIEvent.EventID).Return(testPMAPIEvent, nil).Times(2)
@@ -225,18 +225,18 @@ func testNewBridgeWithUsers(t *testing.T, m mocks) *Bridge {
m.pmapiClient.EXPECT().Addresses().Return(testPMAPIAddresses),
)
- bridge := testNewBridge(t, m)
+ users := testNewUsers(t, m)
- user, _ := bridge.GetUser("user")
+ user, _ := users.GetUser("user")
mockAuthUpdate(user, "reftok", m)
- users, _ := bridge.GetUser("user")
- mockAuthUpdate(users, "reftok", m)
+ user, _ = users.GetUser("user")
+ mockAuthUpdate(user, "reftok", m)
- return bridge
+ return users
}
-func testNewBridge(t *testing.T, m mocks) *Bridge {
+func testNewUsers(t *testing.T, m mocks) *Users {
cacheFile, err := ioutil.TempFile("", "bridge-store-cache-*.db")
require.NoError(t, err, "could not get temporary file for store cache")
@@ -248,14 +248,14 @@ func testNewBridge(t *testing.T, m mocks) *Bridge {
m.eventListener.EXPECT().Add(events.UpgradeApplicationEvent, gomock.Any())
m.clientManager.EXPECT().GetAuthUpdateChannel().Return(make(chan pmapi.ClientAuth))
- bridge := New(m.config, m.prefProvider, m.PanicHandler, m.eventListener, m.clientManager, m.credentialsStore)
+ users := New(m.config, m.prefProvider, m.PanicHandler, m.eventListener, m.clientManager, m.credentialsStore)
waitForEvents()
- return bridge
+ return users
}
-func cleanUpBridgeUserData(b *Bridge) {
+func cleanUpUsersData(b *Users) {
for _, user := range b.users {
_ = user.clearStore()
}
@@ -268,8 +268,8 @@ func TestClearData(t *testing.T) {
m.clientManager.EXPECT().GetClient("user").Return(m.pmapiClient).MinTimes(1)
m.clientManager.EXPECT().GetClient("users").Return(m.pmapiClient).MinTimes(1)
- bridge := testNewBridgeWithUsers(t, m)
- defer cleanUpBridgeUserData(bridge)
+ users := testNewUsersWithUsers(t, m)
+ defer cleanUpUsersData(users)
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "users@pm.me")
@@ -286,7 +286,7 @@ func TestClearData(t *testing.T) {
m.config.EXPECT().ClearData().Return(nil)
- require.NoError(t, bridge.ClearData())
+ require.NoError(t, users.ClearData())
waitForEvents()
}
diff --git a/internal/bridge/bridge_test_exports.go b/internal/users/users_test_exports.go
similarity index 98%
rename from internal/bridge/bridge_test_exports.go
rename to internal/users/users_test_exports.go
index 9a81b887..d0d865a1 100644
--- a/internal/bridge/bridge_test_exports.go
+++ b/internal/users/users_test_exports.go
@@ -15,7 +15,7 @@
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see .
-package bridge
+package users
// IsAuthorized returns whether the user has received an Auth from the API yet.
func (u *User) IsAuthorized() bool {
diff --git a/test/context/bridge.go b/test/context/bridge.go
index 8c9214dd..a444b28f 100644
--- a/test/context/bridge.go
+++ b/test/context/bridge.go
@@ -20,6 +20,7 @@ package context
import (
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/preferences"
+ "github.com/ProtonMail/proton-bridge/internal/users"
"github.com/ProtonMail/proton-bridge/pkg/listener"
)
@@ -57,9 +58,9 @@ func (ctx *TestContext) RestartBridge() error {
func newBridgeInstance(
t *bddT,
cfg *fakeConfig,
- credStore bridge.CredentialsStorer,
+ credStore users.CredentialsStorer,
eventListener listener.Listener,
- clientManager bridge.ClientManager,
+ clientManager users.ClientManager,
) *bridge.Bridge {
panicHandler := &panicHandler{t: t}
pref := preferences.New(cfg)
diff --git a/test/context/bridge_user.go b/test/context/bridge_user.go
index e07e1d2b..cdfcbd02 100644
--- a/test/context/bridge_user.go
+++ b/test/context/bridge_user.go
@@ -23,8 +23,8 @@ import (
"path/filepath"
"time"
- "github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/store"
+ "github.com/ProtonMail/proton-bridge/internal/users"
"github.com/ProtonMail/proton-bridge/pkg/srp"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
@@ -56,7 +56,7 @@ func (ctx *TestContext) LoginUser(username, password, mailboxPassword string) (e
}
// GetUser retrieves the bridge user matching the given query string.
-func (ctx *TestContext) GetUser(username string) (*bridge.User, error) {
+func (ctx *TestContext) GetUser(username string) (*users.User, error) {
return ctx.bridge.GetUser(username)
}
diff --git a/test/context/context.go b/test/context/context.go
index 32530724..e6db35c7 100644
--- a/test/context/context.go
+++ b/test/context/context.go
@@ -20,6 +20,7 @@ package context
import (
"github.com/ProtonMail/proton-bridge/internal/bridge"
+ "github.com/ProtonMail/proton-bridge/internal/users"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/ProtonMail/proton-bridge/test/accounts"
@@ -48,7 +49,7 @@ type TestContext struct {
// Bridge core related variables.
bridge *bridge.Bridge
bridgeLastError error
- credStore bridge.CredentialsStorer
+ credStore users.CredentialsStorer
// IMAP related variables.
imapAddr string
diff --git a/test/context/credentials.go b/test/context/credentials.go
index b9527d92..78fb8c72 100644
--- a/test/context/credentials.go
+++ b/test/context/credentials.go
@@ -20,7 +20,7 @@ package context
import (
"strings"
- "github.com/ProtonMail/proton-bridge/internal/bridge/credentials"
+ "github.com/ProtonMail/proton-bridge/internal/users/credentials"
)
// bridgePassword is password to be used for IMAP or SMTP under tests.
diff --git a/test/features/imap/auth.feature b/test/features/imap/auth.feature
index 4ea83b8b..94ebdc2f 100644
--- a/test/features/imap/auth.feature
+++ b/test/features/imap/auth.feature
@@ -12,7 +12,7 @@ Feature: IMAP auth
Scenario: Authenticates with disconnected user
Given there is disconnected user "user"
When IMAP client authenticates "user"
- Then IMAP response is "IMAP error: NO bridge account is logged out, use bridge to login again"
+ Then IMAP response is "IMAP error: NO account is logged out, use the app to login again"
Scenario: Authenticates with connected user that was loaded without internet
Given there is connected user "user"
@@ -31,13 +31,13 @@ Feature: IMAP auth
Given there is connected user "user"
When "user" logs out from bridge
And IMAP client authenticates "user"
- Then IMAP response is "IMAP error: NO bridge account is logged out, use bridge to login again"
+ Then IMAP response is "IMAP error: NO account is logged out, use the app to login again"
Scenario: Authenticates user which was re-logged in
Given there is connected user "user"
When "user" logs out from bridge
And IMAP client authenticates "user"
- Then IMAP response is "IMAP error: NO bridge account is logged out, use bridge to login again"
+ Then IMAP response is "IMAP error: NO account is logged out, use the app to login again"
When "user" logs in to bridge
And IMAP client authenticates "user"
Then IMAP response is "OK"
diff --git a/test/features/smtp/auth.feature b/test/features/smtp/auth.feature
index b580e9e6..3933b4c1 100644
--- a/test/features/smtp/auth.feature
+++ b/test/features/smtp/auth.feature
@@ -19,7 +19,7 @@ Feature: SMTP auth
Scenario: Authenticates with disconnected user
Given there is disconnected user "user"
When SMTP client authenticates "user"
- Then SMTP response is "SMTP error: 454 bridge account is logged out, use bridge to login again"
+ Then SMTP response is "SMTP error: 454 account is logged out, use the app to login again"
Scenario: Authenticates with no user
When SMTP client authenticates with username "user@pm.me" and password "bridgepassword"