From 6bbe2d0e001692a0f81c28c136b2bfb26d811c5c Mon Sep 17 00:00:00 2001 From: James Houlahan Date: Fri, 26 Aug 2022 15:12:19 +0200 Subject: [PATCH] Other(refactor): Remove bridgeWrap from frontend interface --- COPYING_NOTES.md | 3 +- go.mod | 10 ++- go.sum | 12 ++- internal/frontend/cli/account_utils.go | 54 +++++++----- internal/frontend/cli/accounts.go | 87 +++++++++++-------- internal/frontend/cli/frontend.go | 4 +- internal/frontend/frontend.go | 5 +- internal/frontend/grpc/bridge_grpc.pb.go | 1 + internal/frontend/grpc/service.go | 14 +-- internal/frontend/grpc/service_user.go | 41 ++++++--- internal/frontend/grpc/utils.go | 24 +++--- internal/frontend/types/types.go | 63 +++----------- internal/users/types.go | 35 ++++++++ internal/users/users.go | 104 +++++++++++++++++++++-- internal/users/users_login_test.go | 6 +- test/context/users.go | 12 ++- 16 files changed, 307 insertions(+), 168 deletions(-) diff --git a/COPYING_NOTES.md b/COPYING_NOTES.md index ce35d3b3..9d5e3c9b 100644 --- a/COPYING_NOTES.md +++ b/COPYING_NOTES.md @@ -85,6 +85,8 @@ Proton Mail Bridge includes the following 3rd party software: * [grpc](https://google.golang.org/grpc) available under [license](https://github.com/grpc/grpc-go/blob/master/LICENSE) * [protobuf](https://google.golang.org/protobuf) available under [license](https://github.com/protocolbuffers/protobuf/blob/main/LICENSE) * [plist](https://howett.net/plist) available under [license](https://github.com/DHowett/go-plist/blob/main/LICENSE) +* [juniper](https://github.com/bradenaw/juniper) available under [license](https://github.com/bradenaw/juniper/blob/master/LICENSE) +* [exp](https://golang.org/x/exp) available under [license](https://cs.opensource.google/go/x/exp/+/master:LICENSE) * [go-mime](https://github.com/ProtonMail/go-mime) available under [license](https://github.com/ProtonMail/go-mime/blob/master/LICENSE) * [cascadia](https://github.com/andybalholm/cascadia) available under [license](https://github.com/andybalholm/cascadia/blob/master/LICENSE) * [antlr4](https://github.com/antlr/antlr4) available under [license](https://github.com/antlr/antlr4/blob/master/LICENSE) @@ -109,7 +111,6 @@ Proton Mail Bridge includes the following 3rd party software: github.com/shurcooL/sanitized_anchor_name * [pflag](https://github.com/spf13/pflag) available under [license](https://github.com/spf13/pflag/blob/master/LICENSE) * [tagparser](https://github.com/vmihailenco/tagparser) available under [license](https://github.com/vmihailenco/tagparser/blob/master/LICENSE) -* [xerrors](https://golang.org/x/xerrors) available under [license](https://cs.opensource.google/go/x/xerrors/+/master:LICENSE) * [genproto](https://google.golang.org/genproto) gopkg.in/yaml.v3 * [docker-credential-helpers](https://github.com/ProtonMail/docker-credential-helpers) available under [license](https://github.com/ProtonMail/docker-credential-helpers/blob/master/LICENSE) diff --git a/go.mod b/go.mod index e6a71ea7..66dad4e8 100644 --- a/go.mod +++ b/go.mod @@ -44,7 +44,7 @@ require ( github.com/go-resty/resty/v2 v2.6.0 github.com/godbus/dbus v4.1.0+incompatible github.com/golang/mock v1.4.4 - github.com/google/go-cmp v0.5.6 + github.com/google/go-cmp v0.5.8 github.com/google/uuid v1.1.2 github.com/gopherjs/gopherjs v0.0.0-20190411002643-bd77b112433e // indirect github.com/hashicorp/go-multierror v1.1.0 @@ -67,13 +67,18 @@ require ( go.etcd.io/bbolt v1.3.6 golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 - golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b + golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f golang.org/x/text v0.3.7 google.golang.org/grpc v1.46.2 google.golang.org/protobuf v1.28.0 howett.net/plist v1.0.0 ) +require ( + github.com/bradenaw/juniper v0.7.0 + golang.org/x/exp v0.0.0-20220823124025-807a23277127 +) + require ( github.com/ProtonMail/go-mime v0.0.0-20220302105931-303f85f7fe0f // indirect github.com/andybalholm/cascadia v1.1.0 // indirect @@ -99,7 +104,6 @@ require ( github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/vmihailenco/tagparser v0.1.2 // indirect - golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 // indirect ) diff --git a/go.sum b/go.sum index 3acf4994..9e48b53d 100644 --- a/go.sum +++ b/go.sum @@ -75,6 +75,8 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= +github.com/bradenaw/juniper v0.7.0 h1:8JaJpY2Sm+EheEows6ZsS7s8ZM86Fa3yfaq5xXQH4SI= +github.com/bradenaw/juniper v0.7.0/go.mod h1:Z2B7aJlQ7xbfWsnMLROj5t/5FQ94/MkIdKC30J4WvzI= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -221,8 +223,9 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= @@ -500,6 +503,8 @@ golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxT golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20220823124025-807a23277127 h1:S4NrSKDfihhl3+4jSTgwoIevKxX9p7Iv9x++OEIptDo= +golang.org/x/exp v0.0.0-20220823124025-807a23277127/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -593,8 +598,8 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b h1:2n253B2r0pYSmEV+UNCQoPfU/FiaizQEK5Gu4Bq4JE8= -golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -632,7 +637,6 @@ golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapK golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= diff --git a/internal/frontend/cli/account_utils.go b/internal/frontend/cli/account_utils.go index 3e620d7b..8f04e644 100644 --- a/internal/frontend/cli/account_utils.go +++ b/internal/frontend/cli/account_utils.go @@ -22,7 +22,7 @@ import ( "strconv" "strings" - "github.com/ProtonMail/proton-bridge/v2/internal/frontend/types" + "github.com/ProtonMail/proton-bridge/v2/internal/users" "github.com/abiosoft/ishell" ) @@ -35,9 +35,13 @@ func (f *frontendCLI) completeUsernames(args []string) (usernames []string) { if len(args) == 1 { arg = args[0] } - for _, user := range f.bridge.GetUsers() { - if strings.HasPrefix(strings.ToLower(user.Username()), strings.ToLower(arg)) { - usernames = append(usernames, user.Username()) + for _, userID := range f.bridge.GetUserIDs() { + user, err := f.bridge.GetUserInfo(userID) + if err != nil { + panic(err) + } + if strings.HasPrefix(strings.ToLower(user.Username), strings.ToLower(arg)) { + usernames = append(usernames, user.Username) } } return @@ -46,7 +50,7 @@ func (f *frontendCLI) completeUsernames(args []string) (usernames []string) { // noAccountWrapper is a decorator for functions which need any account to be properly functional. func (f *frontendCLI) noAccountWrapper(callback func(*ishell.Context)) func(*ishell.Context) { return func(c *ishell.Context) { - users := f.bridge.GetUsers() + users := f.bridge.GetUserIDs() if len(users) == 0 { f.Println("No active accounts. Please add account to continue.") } else { @@ -55,46 +59,54 @@ func (f *frontendCLI) noAccountWrapper(callback func(*ishell.Context)) func(*ish } } -func (f *frontendCLI) askUserByIndexOrName(c *ishell.Context) types.User { +func (f *frontendCLI) askUserByIndexOrName(c *ishell.Context) users.UserInfo { user := f.getUserByIndexOrName("") - if user != nil { + if user.ID != "" { return user } - numberOfAccounts := len(f.bridge.GetUsers()) + numberOfAccounts := len(f.bridge.GetUserIDs()) indexRange := fmt.Sprintf("number between 0 and %d", numberOfAccounts-1) if len(c.Args) == 0 { f.Printf("Please choose %s or username.\n", indexRange) - return nil + return users.UserInfo{} } arg := c.Args[0] user = f.getUserByIndexOrName(arg) - if user == nil { + if user.ID == "" { f.Printf("Wrong input '%s'. Choose %s or username.\n", bold(arg), indexRange) - return nil + return users.UserInfo{} } return user } -func (f *frontendCLI) getUserByIndexOrName(arg string) types.User { - users := f.bridge.GetUsers() - numberOfAccounts := len(users) +func (f *frontendCLI) getUserByIndexOrName(arg string) users.UserInfo { + userIDs := f.bridge.GetUserIDs() + numberOfAccounts := len(userIDs) if numberOfAccounts == 0 { - return nil + return users.UserInfo{} + } + res := make([]users.UserInfo, len(userIDs)) + for idx, userID := range userIDs { + user, err := f.bridge.GetUserInfo(userID) + if err != nil { + panic(err) + } + res[idx] = user } if numberOfAccounts == 1 { - return users[0] + return res[0] } if index, err := strconv.Atoi(arg); err == nil { if index < 0 || index >= numberOfAccounts { - return nil + return users.UserInfo{} } - return users[index] + return res[index] } - for _, user := range users { - if user.Username() == arg { + for _, user := range res { + if user.Username == arg { return user } } - return nil + return users.UserInfo{} } diff --git a/internal/frontend/cli/accounts.go b/internal/frontend/cli/accounts.go index 451f1041..71960de3 100644 --- a/internal/frontend/cli/accounts.go +++ b/internal/frontend/cli/accounts.go @@ -23,48 +23,52 @@ import ( "github.com/ProtonMail/proton-bridge/v2/internal/bridge" "github.com/ProtonMail/proton-bridge/v2/internal/config/settings" - "github.com/ProtonMail/proton-bridge/v2/internal/frontend/types" + "github.com/ProtonMail/proton-bridge/v2/internal/users" "github.com/abiosoft/ishell" ) func (f *frontendCLI) listAccounts(c *ishell.Context) { spacing := "%-2d: %-20s (%-15s, %-15s)\n" f.Printf(bold(strings.ReplaceAll(spacing, "d", "s")), "#", "account", "status", "address mode") - for idx, user := range f.bridge.GetUsers() { + for idx, userID := range f.bridge.GetUserIDs() { + user, err := f.bridge.GetUserInfo(userID) + if err != nil { + panic(err) + } connected := "disconnected" - if user.IsConnected() { + if user.Connected { connected = "connected" } mode := "split" - if user.IsCombinedAddressMode() { + if user.Mode == users.CombinedMode { mode = "combined" } - f.Printf(spacing, idx, user.Username(), connected, mode) + f.Printf(spacing, idx, user.Username, connected, mode) } f.Println() } func (f *frontendCLI) showAccountInfo(c *ishell.Context) { user := f.askUserByIndexOrName(c) - if user == nil { + if user.ID == "" { return } - if !user.IsConnected() { - f.Printf("Please login to %s to get email client configuration.\n", bold(user.Username())) + if !user.Connected { + f.Printf("Please login to %s to get email client configuration.\n", bold(user.Username)) return } - if user.IsCombinedAddressMode() { - f.showAccountAddressInfo(user, user.GetPrimaryAddress()) + if user.Mode == users.CombinedMode { + f.showAccountAddressInfo(user, user.Addresses[user.Primary]) } else { - for _, address := range user.GetAddresses() { + for _, address := range user.Addresses { f.showAccountAddressInfo(user, address) } } } -func (f *frontendCLI) showAccountAddressInfo(user types.User, address string) { +func (f *frontendCLI) showAccountAddressInfo(user users.UserInfo, address string) { smtpSecurity := "STARTTLS" if f.bridge.GetBool(settings.SMTPSSLKey) { smtpSecurity = "SSL" @@ -74,7 +78,7 @@ func (f *frontendCLI) showAccountAddressInfo(user types.User, address string) { bridge.Host, f.bridge.GetInt(settings.IMAPPortKey), address, - user.GetBridgePassword(), + user.Password, "STARTTLS", ) f.Println("") @@ -82,7 +86,7 @@ func (f *frontendCLI) showAccountAddressInfo(user types.User, address string) { bridge.Host, f.bridge.GetInt(settings.SMTPPortKey), address, - user.GetBridgePassword(), + user.Password, smtpSecurity, ) f.Println("") @@ -95,8 +99,8 @@ func (f *frontendCLI) loginAccount(c *ishell.Context) { //nolint:funlen loginName := "" if len(c.Args) > 0 { user := f.getUserByIndexOrName(c.Args[0]) - if user != nil { - loginName = user.GetPrimaryAddress() + if user.ID != "" { + loginName = user.Addresses[user.Primary] } } @@ -143,14 +147,19 @@ func (f *frontendCLI) loginAccount(c *ishell.Context) { //nolint:funlen } f.Println("Adding account ...") - user, err := f.bridge.FinishLogin(client, auth, []byte(mailboxPassword)) + userID, err := f.bridge.FinishLogin(client, auth, []byte(mailboxPassword)) if err != nil { log.WithField("username", loginName).WithError(err).Error("Login was unsuccessful") f.Println("Adding account was unsuccessful:", err) return } - f.Printf("Account %s was added successfully.\n", bold(user.Username())) + user, err := f.bridge.GetUserInfo(userID) + if err != nil { + panic(err) + } + + f.Printf("Account %s was added successfully.\n", bold(user.Username)) } func (f *frontendCLI) logoutAccount(c *ishell.Context) { @@ -158,11 +167,11 @@ func (f *frontendCLI) logoutAccount(c *ishell.Context) { defer f.ShowPrompt(true) user := f.askUserByIndexOrName(c) - if user == nil { + if user.ID == "" { return } - if f.yesNoQuestion("Are you sure you want to logout account " + bold(user.Username())) { - if err := user.Logout(); err != nil { + if f.yesNoQuestion("Are you sure you want to logout account " + bold(user.Username)) { + if err := f.bridge.LogoutUser(user.ID); err != nil { f.printAndLogError("Logging out failed: ", err) } } @@ -173,12 +182,12 @@ func (f *frontendCLI) deleteAccount(c *ishell.Context) { defer f.ShowPrompt(true) user := f.askUserByIndexOrName(c) - if user == nil { + if user.ID == "" { return } - if f.yesNoQuestion("Are you sure you want to " + bold("remove account "+user.Username())) { + if f.yesNoQuestion("Are you sure you want to " + bold("remove account "+user.Username)) { clearCache := f.yesNoQuestion("Do you want to remove cache for this account") - if err := f.bridge.DeleteUser(user.ID(), clearCache); err != nil { + if err := f.bridge.DeleteUser(user.ID, clearCache); err != nil { f.printAndLogError("Cannot delete account: ", err) return } @@ -193,9 +202,13 @@ func (f *frontendCLI) deleteAccounts(c *ishell.Context) { return } - for _, user := range f.bridge.GetUsers() { - if err := f.bridge.DeleteUser(user.ID(), false); err != nil { - f.printAndLogError("Cannot delete account ", user.Username(), ": ", err) + for _, userID := range f.bridge.GetUserIDs() { + user, err := f.bridge.GetUserInfo(userID) + if err != nil { + panic(err) + } + if err := f.bridge.DeleteUser(user.ID, false); err != nil { + f.printAndLogError("Cannot delete account ", user.Username, ": ", err) } } @@ -222,19 +235,25 @@ func (f *frontendCLI) deleteEverything(c *ishell.Context) { func (f *frontendCLI) changeMode(c *ishell.Context) { user := f.askUserByIndexOrName(c) - if user == nil { + if user.ID == "" { return } - newMode := "combined mode" - if user.IsCombinedAddressMode() { - newMode = "split mode" + var targetMode users.AddressMode + + if user.Mode == users.CombinedMode { + targetMode = users.SplitMode + } else { + targetMode = users.CombinedMode } - if !f.yesNoQuestion("Are you sure you want to change the mode for account " + bold(user.Username()) + " to " + bold(newMode)) { + + if !f.yesNoQuestion("Are you sure you want to change the mode for account " + bold(user.Username) + " to " + bold(targetMode)) { return } - if err := user.SwitchAddressMode(); err != nil { + + if err := f.bridge.SetAddressMode(user.ID, targetMode); err != nil { f.printAndLogError("Cannot switch address mode:", err) } - f.Printf("Address mode for account %s changed to %s\n", user.Username(), newMode) + + f.Printf("Address mode for account %s changed to %s\n", user.Username, targetMode) } diff --git a/internal/frontend/cli/frontend.go b/internal/frontend/cli/frontend.go index aae4bd08..10ad573c 100644 --- a/internal/frontend/cli/frontend.go +++ b/internal/frontend/cli/frontend.go @@ -307,11 +307,11 @@ func (f *frontendCLI) watchEvents() { case address := <-addressChangedLogoutCh: f.notifyLogout(address) case userID := <-logoutCh: - user, err := f.bridge.GetUser(userID) + user, err := f.bridge.GetUserInfo(userID) if err != nil { return } - f.notifyLogout(user.Username()) + f.notifyLogout(user.Username) case <-certIssue: f.notifyCertIssue() } diff --git a/internal/frontend/frontend.go b/internal/frontend/frontend.go index dce7a460..8cae6995 100644 --- a/internal/frontend/frontend.go +++ b/internal/frontend/frontend.go @@ -46,7 +46,6 @@ func New( bridge *bridge.Bridge, restarter types.Restarter, ) Frontend { - bridgeWrap := types.NewBridgeWrap(bridge) switch frontendType { case "grpc": return grpc.NewService( @@ -54,7 +53,7 @@ func New( panicHandler, eventListener, updater, - bridgeWrap, + bridge, restarter, ) @@ -63,7 +62,7 @@ func New( panicHandler, eventListener, updater, - bridgeWrap, + bridge, restarter, ) diff --git a/internal/frontend/grpc/bridge_grpc.pb.go b/internal/frontend/grpc/bridge_grpc.pb.go index b30c8cd6..8241cae0 100644 --- a/internal/frontend/grpc/bridge_grpc.pb.go +++ b/internal/frontend/grpc/bridge_grpc.pb.go @@ -8,6 +8,7 @@ package grpc import ( context "context" + grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" status "google.golang.org/grpc/status" diff --git a/internal/frontend/grpc/service.go b/internal/frontend/grpc/service.go index 8f325e4f..579349c6 100644 --- a/internal/frontend/grpc/service.go +++ b/internal/frontend/grpc/service.go @@ -229,11 +229,11 @@ func (s *Service) watchEvents() { // nolint:funlen case address := <-addressChangedLogoutCh: _ = s.SendEvent(NewMailAddressChangeLogoutEvent(address)) case userID := <-logoutCh: - user, err := s.bridge.GetUser(userID) + user, err := s.bridge.GetUserInfo(userID) if err != nil { return } - _ = s.SendEvent(NewUserDisconnectedEvent(user.Username())) + _ = s.SendEvent(NewUserDisconnectedEvent(user.Username)) case <-updateApplicationCh: s.updateForce() case userID := <-userChangedCh: @@ -275,7 +275,7 @@ func (s *Service) finishLogin() { s.eventListener.Add(events.UserChangeDone, done) defer s.eventListener.Remove(events.UserChangeDone, done) - user, err := s.bridge.FinishLogin(s.authClient, s.auth, s.password) + userID, err := s.bridge.FinishLogin(s.authClient, s.auth, s.password) if err != nil && err != users.ErrUserAlreadyConnected { s.log.WithError(err).Errorf("Finish login failed") @@ -286,14 +286,14 @@ func (s *Service) finishLogin() { // The user changed should be triggered by FinishLogin, but it is not // guaranteed when this is going to happen. Therefor we should wait // until we receive the signal from userChanged function. - s.waitForUserChangeDone(done, user.ID()) + s.waitForUserChangeDone(done, userID) - s.log.WithField("userID", user.ID()).Debug("Login finished") - _ = s.SendEvent(NewLoginFinishedEvent(user.ID())) + s.log.WithField("userID", userID).Debug("Login finished") + _ = s.SendEvent(NewLoginFinishedEvent(userID)) if err == users.ErrUserAlreadyConnected { s.log.WithError(err).Error("User already logged in") - _ = s.SendEvent(NewLoginAlreadyLoggedInEvent(user.ID())) + _ = s.SendEvent(NewLoginAlreadyLoggedInEvent(userID)) } } diff --git a/internal/frontend/grpc/service_user.go b/internal/frontend/grpc/service_user.go index 4e12a111..f4dc069c 100644 --- a/internal/frontend/grpc/service_user.go +++ b/internal/frontend/grpc/service_user.go @@ -21,6 +21,8 @@ import ( "context" "time" + "github.com/ProtonMail/proton-bridge/v2/internal/users" + "github.com/sirupsen/logrus" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/emptypb" @@ -30,11 +32,15 @@ import ( func (s *Service) GetUserList(context.Context, *emptypb.Empty) (*UserListResponse, error) { s.log.Info("GetUserList") - users := s.bridge.GetUsers() + var userList []*User - userList := make([]*User, len(users)) - for i, user := range users { - userList[i] = grpcUserFromBridge(user) + for idx, userID := range s.bridge.GetUserIDs() { + user, err := s.bridge.GetUserInfo(userID) + if err != nil { + return nil, err + } + + userList[idx] = grpcUserFromInfo(user) } // If there are no active accounts. @@ -48,18 +54,18 @@ func (s *Service) GetUserList(context.Context, *emptypb.Empty) (*UserListRespons func (s *Service) GetUser(_ context.Context, userID *wrapperspb.StringValue) (*User, error) { s.log.WithField("userID", userID).Info("GetUser") - user, err := s.bridge.GetUser(userID.Value) + user, err := s.bridge.GetUserInfo(userID.Value) if err != nil { return nil, status.Errorf(codes.NotFound, "user not found %v", userID.Value) } - return grpcUserFromBridge(user), nil + return grpcUserFromInfo(user), nil } func (s *Service) SetUserSplitMode(_ context.Context, splitMode *UserSplitModeRequest) (*emptypb.Empty, error) { s.log.WithField("UserID", splitMode.UserID).WithField("Active", splitMode.Active).Info("SetUserSplitMode") - user, err := s.bridge.GetUser(splitMode.UserID) + user, err := s.bridge.GetUserInfo(splitMode.UserID) if err != nil { return nil, status.Errorf(codes.NotFound, "user not found %v", splitMode.UserID) } @@ -67,8 +73,17 @@ func (s *Service) SetUserSplitMode(_ context.Context, splitMode *UserSplitModeRe go func() { defer s.panicHandler.HandlePanic() defer func() { _ = s.SendEvent(NewUserToggleSplitModeFinishedEvent(splitMode.UserID)) }() - if splitMode.Active == user.IsCombinedAddressMode() { - _ = user.SwitchAddressMode() // check for errors + + var targetMode users.AddressMode + + if splitMode.Active && user.Mode == users.CombinedMode { + targetMode = users.SplitMode + } else if !splitMode.Active && user.Mode == users.SplitMode { + targetMode = users.CombinedMode + } + + if err := s.bridge.SetAddressMode(user.ID, targetMode); err != nil { + logrus.WithError(err).Error("Failed to set address mode") } }() @@ -78,14 +93,16 @@ func (s *Service) SetUserSplitMode(_ context.Context, splitMode *UserSplitModeRe func (s *Service) LogoutUser(_ context.Context, userID *wrapperspb.StringValue) (*emptypb.Empty, error) { s.log.WithField("UserID", userID.Value).Info("LogoutUser") - user, err := s.bridge.GetUser(userID.Value) - if err != nil { + if _, err := s.bridge.GetUserInfo(userID.Value); err != nil { return nil, status.Errorf(codes.NotFound, "user not found %v", userID.Value) } go func() { defer s.panicHandler.HandlePanic() - _ = user.Logout() + + if err := s.bridge.LogoutUser(userID.Value); err != nil { + logrus.WithError(err).Error("Failed to log user out") + } }() return &emptypb.Empty{}, nil diff --git a/internal/frontend/grpc/utils.go b/internal/frontend/grpc/utils.go index ec5b2dec..a1f29767 100644 --- a/internal/frontend/grpc/utils.go +++ b/internal/frontend/grpc/utils.go @@ -21,7 +21,7 @@ import ( "regexp" "strings" - "github.com/ProtonMail/proton-bridge/v2/internal/frontend/types" + "github.com/ProtonMail/proton-bridge/v2/internal/users" "github.com/sirupsen/logrus" ) @@ -57,19 +57,19 @@ func getInitials(fullName string) string { return strings.ToUpper(initials) } -// grpcUserFromBridge converts a bridge user to a gRPC user. -func grpcUserFromBridge(user types.User) *User { +// grpcUserFromInfo converts a bridge user to a gRPC user. +func grpcUserFromInfo(user users.UserInfo) *User { return &User{ - Id: user.ID(), - Username: user.Username(), - AvatarText: getInitials(user.Username()), - LoggedIn: user.IsConnected(), - SplitMode: !user.IsCombinedAddressMode(), + Id: user.ID, + Username: user.Username, + AvatarText: getInitials(user.Username), + LoggedIn: user.Connected, + SplitMode: user.Mode == users.SplitMode, SetupGuideSeen: true, // users listed have already seen the setup guide. - UsedBytes: user.UsedBytes(), - TotalBytes: user.TotalBytes(), - Password: user.GetBridgePassword(), - Addresses: user.GetAddresses(), + UsedBytes: user.UsedBytes, + TotalBytes: user.TotalBytes, + Password: user.Password, + Addresses: user.Addresses, } } diff --git a/internal/frontend/types/types.go b/internal/frontend/types/types.go index d5150f69..8c234450 100644 --- a/internal/frontend/types/types.go +++ b/internal/frontend/types/types.go @@ -21,9 +21,9 @@ package types import ( "crypto/tls" - "github.com/ProtonMail/proton-bridge/v2/internal/bridge" "github.com/ProtonMail/proton-bridge/v2/internal/config/settings" "github.com/ProtonMail/proton-bridge/v2/internal/updater" + "github.com/ProtonMail/proton-bridge/v2/internal/users" "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" ) @@ -45,39 +45,22 @@ type Updater interface { CanInstall(updater.VersionInfo) bool } -// UserManager is an interface of users needed by frontend. -type UserManager interface { +// Bridger is an interface of bridge needed by frontend. +type Bridger interface { Login(username string, password []byte) (pmapi.Client, *pmapi.Auth, error) - FinishLogin(client pmapi.Client, auth *pmapi.Auth, mailboxPassword []byte) (User, error) - GetUsers() []User - GetUser(query string) (User, error) + FinishLogin(client pmapi.Client, auth *pmapi.Auth, mailboxPassword []byte) (string, error) + + GetUserIDs() []string + GetUserInfo(string) (users.UserInfo, error) + LogoutUser(userID string) error DeleteUser(userID string, clearCache bool) error + SetAddressMode(userID string, split users.AddressMode) error + ClearData() error ClearUsers() error FactoryReset() -} - -// User is an interface of user needed by frontend. -type User interface { - ID() string - UsedBytes() int64 - TotalBytes() int64 - Username() string - IsConnected() bool - IsCombinedAddressMode() bool - GetPrimaryAddress() string - GetAddresses() []string - GetBridgePassword() string - SwitchAddressMode() error - Logout() error -} - -// Bridger is an interface of bridge needed by frontend. -type Bridger interface { - UserManager GetTLSConfig() (*tls.Config, error) - ProvideLogsPath() (string, error) GetLicenseFilePath() string GetDependencyLicensesLink() string @@ -115,29 +98,3 @@ type Bridger interface { IsAllMailVisible() bool SetIsAllMailVisible(bool) } - -type bridgeWrap struct { - *bridge.Bridge -} - -// NewBridgeWrap wraps bridge struct into local bridgeWrap to implement local interface. -// The problem is that Bridge returns the bridge package's User type. -// Every method which returns User therefore has to be overridden to fulfill the interface. -func NewBridgeWrap(bridge *bridge.Bridge) *bridgeWrap { //nolint:revive - return &bridgeWrap{Bridge: bridge} -} - -func (b *bridgeWrap) FinishLogin(client pmapi.Client, auth *pmapi.Auth, mailboxPassword []byte) (User, error) { - return b.Bridge.FinishLogin(client, auth, mailboxPassword) -} - -func (b *bridgeWrap) GetUsers() (users []User) { - for _, user := range b.Bridge.GetUsers() { - users = append(users, user) - } - return -} - -func (b *bridgeWrap) GetUser(query string) (User, error) { - return b.Bridge.GetUser(query) -} diff --git a/internal/users/types.go b/internal/users/types.go index ce33734c..b4bd74b7 100644 --- a/internal/users/types.go +++ b/internal/users/types.go @@ -46,3 +46,38 @@ type StoreMaker interface { New(user store.BridgeUser) (*store.Store, error) Remove(userID string) error } + +type UserInfo struct { + ID string + Username string + Password string + + Addresses []string + Primary int + + UsedBytes int64 + TotalBytes int64 + + Connected bool + Mode AddressMode +} + +type AddressMode int + +const ( + SplitMode AddressMode = iota + CombinedMode +) + +func (mode AddressMode) String() string { + switch mode { + case SplitMode: + return "split mode" + + case CombinedMode: + return "combined mode" + + default: + return "unknown mode" + } +} diff --git a/internal/users/users.go b/internal/users/users.go index 450ca238..09ffb894 100644 --- a/internal/users/users.go +++ b/internal/users/users.go @@ -30,9 +30,11 @@ import ( "github.com/ProtonMail/proton-bridge/v2/pkg/keychain" "github.com/ProtonMail/proton-bridge/v2/pkg/listener" "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" + "github.com/bradenaw/juniper/xslices" "github.com/hashicorp/go-multierror" "github.com/pkg/errors" logrus "github.com/sirupsen/logrus" + "golang.org/x/exp/slices" ) var ( @@ -215,10 +217,10 @@ func (u *Users) Login(username string, password []byte) (authClient pmapi.Client } // FinishLogin finishes the login procedure and adds the user into the credentials store. -func (u *Users) FinishLogin(client pmapi.Client, auth *pmapi.Auth, password []byte) (user *User, err error) { //nolint:funlen +func (u *Users) FinishLogin(client pmapi.Client, auth *pmapi.Auth, password []byte) (userID string, err error) { //nolint:funlen apiUser, passphrase, err := getAPIUser(context.Background(), client, password) if err != nil { - return nil, err + return "", err } if user, ok := u.hasUser(apiUser.ID); ok { @@ -227,39 +229,39 @@ func (u *Users) FinishLogin(client pmapi.Client, auth *pmapi.Auth, password []by logrus.WithError(err).Warn("Failed to delete new auth session") } - return user, ErrUserAlreadyConnected + return user.ID(), ErrUserAlreadyConnected } // Update the user's credentials with the latest auth used to connect this user. if _, err := u.credStorer.UpdateToken(auth.UserID, auth.UID, auth.RefreshToken); err != nil { notifyKeychainRepair(u.events, err) - return nil, errors.Wrap(err, "failed to load user credentials") + return "", errors.Wrap(err, "failed to load user credentials") } // Update the password in case the user changed it. creds, err := u.credStorer.UpdatePassword(apiUser.ID, passphrase) if err != nil { notifyKeychainRepair(u.events, err) - return nil, errors.Wrap(err, "failed to update password of user in credentials store") + return "", errors.Wrap(err, "failed to update password of user in credentials store") } // will go and unlock cache if not already done if err := user.connect(client, creds); err != nil { - return nil, errors.Wrap(err, "failed to reconnect existing user") + return "", errors.Wrap(err, "failed to reconnect existing user") } u.events.Emit(events.UserRefreshEvent, apiUser.ID) - return user, nil + return user.ID(), nil } if err := u.addNewUser(client, apiUser, auth, passphrase); err != nil { - return nil, errors.Wrap(err, "failed to add new user") + return "", errors.Wrap(err, "failed to add new user") } u.events.Emit(events.UserRefreshEvent, apiUser.ID) - return u.GetUser(apiUser.ID) + return apiUser.ID, nil } // addNewUser adds a new user. @@ -322,6 +324,16 @@ func (u *Users) GetUsers() []*User { return u.users } +// GetUserIDs returns IDs of all added users into keychain (even logged out users). +func (u *Users) GetUserIDs() []string { + u.lock.RLock() + defer u.lock.RUnlock() + + return xslices.Map(u.users, func(user *User) string { + return user.ID() + }) +} + // 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) @@ -343,6 +355,44 @@ func (u *Users) GetUser(query string) (*User, error) { return nil, errors.New("user " + query + " not found") } +// GetUserInfo returns user about the user with the given ID. +func (u *Users) GetUserInfo(userID string) (UserInfo, error) { + u.lock.RLock() + defer u.lock.RUnlock() + + idx := slices.IndexFunc(u.users, func(user *User) bool { + return user.userID == userID + }) + if idx < 0 { + return UserInfo{}, errors.New("no such user") + } + + user := u.users[idx] + + var mode AddressMode + + if user.IsCombinedAddressMode() { + mode = CombinedMode + } else { + mode = SplitMode + } + + return UserInfo{ + ID: userID, + Username: user.Username(), + Password: user.GetBridgePassword(), + + Addresses: user.GetAddresses(), + Primary: slices.Index(user.GetAddresses(), user.GetPrimaryAddress()), + + UsedBytes: user.UsedBytes(), + TotalBytes: user.TotalBytes(), + + Connected: user.IsConnected(), + Mode: mode, + }, nil +} + // ClearData closes all connections (to release db files and so on) and clears all data. func (u *Users) ClearData() error { var result error @@ -364,6 +414,42 @@ func (u *Users) ClearData() error { return result } +func (u *Users) LogoutUser(userID string) error { + u.lock.RLock() + defer u.lock.RUnlock() + + idx := slices.IndexFunc(u.users, func(user *User) bool { + return user.userID == userID + }) + if idx < 0 { + return errors.New("no such user") + } + + return u.users[idx].Logout() +} + +func (u *Users) SetAddressMode(userID string, mode AddressMode) error { + u.lock.RLock() + defer u.lock.RUnlock() + + idx := slices.IndexFunc(u.users, func(user *User) bool { + return user.userID == userID + }) + if idx < 0 { + return errors.New("no such user") + } + + if mode == CombinedMode && u.users[idx].IsCombinedAddressMode() { + return nil + } + + if mode == SplitMode && !u.users[idx].IsCombinedAddressMode() { + return nil + } + + return u.users[idx].SwitchAddressMode() +} + // 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 { diff --git a/internal/users/users_login_test.go b/internal/users/users_login_test.go index bff40ff9..502180c2 100644 --- a/internal/users/users_login_test.go +++ b/internal/users/users_login_test.go @@ -117,16 +117,16 @@ func checkUsersFinishLogin(t *testing.T, m mocks, auth *pmapi.Auth, mailboxPassw users := testNewUsers(t, m) defer cleanUpUsersData(users) - user, err := users.FinishLogin(m.pmapiClient, auth, mailboxPassword) + userID, err := users.FinishLogin(m.pmapiClient, auth, mailboxPassword) r.Equal(t, expectedErr, err) if expectedUserID != "" { - r.Equal(t, expectedUserID, user.ID()) + r.Equal(t, expectedUserID, userID) r.Equal(t, 1, len(users.users)) r.Equal(t, expectedUserID, users.users[0].ID()) } else { - r.Equal(t, (*User)(nil), user) + r.Equal(t, "", userID) r.Equal(t, 0, len(users.users)) } } diff --git a/test/context/users.go b/test/context/users.go index 7110c3f3..b6aaafd0 100644 --- a/test/context/users.go +++ b/test/context/users.go @@ -52,12 +52,14 @@ func (ctx *TestContext) LoginUser(username string, password, mailboxPassword []b } } - user, err := ctx.users.FinishLogin(client, auth, mailboxPassword) + userID, err := ctx.users.FinishLogin(client, auth, mailboxPassword) if err != nil { return errors.Wrap(err, "failed to finish login") } - ctx.addCleanupChecked(user.Logout, "Logging out user") + ctx.addCleanupChecked(func() error { + return ctx.bridge.LogoutUser(userID) + }, "Logging out user") return nil } @@ -73,12 +75,14 @@ func (ctx *TestContext) FinishLogin(client pmapi.Client, mailboxPassword []byte) return errors.New("cannot get current auth tokens from client") } - user, err := ctx.users.FinishLogin(client, c.GetCurrentAuth(), mailboxPassword) + userID, err := ctx.users.FinishLogin(client, c.GetCurrentAuth(), mailboxPassword) if err != nil { return errors.Wrap(err, "failed to finish login") } - ctx.addCleanupChecked(user.Logout, "Logging out user") + ctx.addCleanupChecked(func() error { + return ctx.bridge.LogoutUser(userID) + }, "Logging out user") return nil }