Other(refactor): Remove bridgeWrap from frontend interface

This commit is contained in:
James Houlahan 2022-08-26 15:12:19 +02:00 committed by Jakub
parent 9786deef48
commit 6bbe2d0e00
16 changed files with 307 additions and 168 deletions

View File

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

10
go.mod
View File

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

12
go.sum
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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