forked from Silverfish/proton-bridge
Other: Safer user types
This commit is contained in:
@ -7,7 +7,6 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
@ -16,9 +15,10 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/focus"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/safe"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/try"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/user"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/vault"
|
||||
"github.com/bradenaw/juniper/xslices"
|
||||
"github.com/emersion/go-smtp"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/sirupsen/logrus"
|
||||
@ -30,17 +30,15 @@ type Bridge struct {
|
||||
vault *vault.Vault
|
||||
|
||||
// users holds authorized users.
|
||||
users map[string]*user.User
|
||||
users *safe.Map[string, *user.User]
|
||||
loadCh chan struct{}
|
||||
loadWG try.Group
|
||||
|
||||
// api manages user API clients.
|
||||
api *liteapi.Manager
|
||||
proxyCtl ProxyController
|
||||
identifier Identifier
|
||||
|
||||
// watchers holds all registered event watchers.
|
||||
watchers []*watcher.Watcher[events.Event]
|
||||
watchersLock sync.RWMutex
|
||||
|
||||
// tlsConfig holds the bridge TLS config used by the IMAP and SMTP servers.
|
||||
tlsConfig *tls.Config
|
||||
|
||||
@ -66,6 +64,9 @@ type Bridge struct {
|
||||
// locator is the bridge's locator.
|
||||
locator Locator
|
||||
|
||||
// watchers holds all registered event watchers.
|
||||
watchers *safe.Slice[*watcher.Watcher[events.Event]]
|
||||
|
||||
// errors contains errors encountered during startup.
|
||||
errors []error
|
||||
|
||||
@ -95,7 +96,7 @@ func New(
|
||||
|
||||
logIMAPClient, logIMAPServer bool, // whether to log IMAP client/server activity
|
||||
logSMTP bool, // whether to log SMTP activity
|
||||
) (*Bridge, error) {
|
||||
) (*Bridge, <-chan events.Event, error) {
|
||||
api := liteapi.New(
|
||||
liteapi.WithHostURL(apiURL),
|
||||
liteapi.WithAppVersion(constants.AppVersion(curVersion.Original())),
|
||||
@ -105,54 +106,62 @@ func New(
|
||||
|
||||
tlsConfig, err := loadTLSConfig(vault)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load TLS config: %w", err)
|
||||
return nil, nil, fmt.Errorf("failed to load TLS config: %w", err)
|
||||
}
|
||||
|
||||
gluonDir, err := getGluonDir(vault)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get Gluon directory: %w", err)
|
||||
return nil, nil, fmt.Errorf("failed to get Gluon directory: %w", err)
|
||||
}
|
||||
|
||||
smtpBackend, err := newSMTPBackend()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create SMTP backend: %w", err)
|
||||
return nil, nil, fmt.Errorf("failed to create SMTP backend: %w", err)
|
||||
}
|
||||
|
||||
imapServer, err := newIMAPServer(gluonDir, curVersion, tlsConfig, logIMAPClient, logIMAPServer)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create IMAP server: %w", err)
|
||||
return nil, nil, fmt.Errorf("failed to create IMAP server: %w", err)
|
||||
}
|
||||
|
||||
focusService, err := focus.NewService()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create focus service: %w", err)
|
||||
return nil, nil, fmt.Errorf("failed to create focus service: %w", err)
|
||||
}
|
||||
|
||||
bridge := newBridge(
|
||||
// App stuff
|
||||
locator,
|
||||
vault,
|
||||
autostarter,
|
||||
updater,
|
||||
curVersion,
|
||||
|
||||
// API stuff
|
||||
api,
|
||||
identifier,
|
||||
proxyCtl,
|
||||
|
||||
// Service stuff
|
||||
tlsConfig,
|
||||
imapServer,
|
||||
smtpBackend,
|
||||
focusService,
|
||||
|
||||
// Logging stuff
|
||||
logIMAPClient,
|
||||
logIMAPServer,
|
||||
logSMTP,
|
||||
)
|
||||
|
||||
// Get an event channel for all events (individual events can be subscribed to later).
|
||||
eventCh, _ := bridge.GetEvents()
|
||||
|
||||
if err := bridge.init(tlsReporter); err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize bridge: %w", err)
|
||||
return nil, nil, fmt.Errorf("failed to initialize bridge: %w", err)
|
||||
}
|
||||
|
||||
return bridge, nil
|
||||
return bridge, eventCh, nil
|
||||
}
|
||||
|
||||
func newBridge(
|
||||
@ -174,7 +183,9 @@ func newBridge(
|
||||
) *Bridge {
|
||||
return &Bridge{
|
||||
vault: vault,
|
||||
users: make(map[string]*user.User),
|
||||
|
||||
users: safe.NewMap[string, *user.User](nil),
|
||||
loadCh: make(chan struct{}, 1),
|
||||
|
||||
api: api,
|
||||
proxyCtl: proxyCtl,
|
||||
@ -193,6 +204,8 @@ func newBridge(
|
||||
autostarter: autostarter,
|
||||
locator: locator,
|
||||
|
||||
watchers: safe.NewSlice[*watcher.Watcher[events.Event]](),
|
||||
|
||||
logIMAPClient: logIMAPClient,
|
||||
logIMAPServer: logIMAPServer,
|
||||
logSMTP: logSMTP,
|
||||
@ -227,10 +240,6 @@ func (bridge *Bridge) init(tlsReporter TLSReporter) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
if err := bridge.loadUsers(); err != nil {
|
||||
return fmt.Errorf("failed to load users: %w", err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
for range tlsReporter.GetTLSIssueCh() {
|
||||
bridge.publish(events.TLSIssue{})
|
||||
@ -261,6 +270,8 @@ func (bridge *Bridge) init(tlsReporter TLSReporter) error {
|
||||
bridge.PushError(ErrWatchUpdates)
|
||||
}
|
||||
|
||||
go bridge.loadLoop()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -288,6 +299,9 @@ func (bridge *Bridge) Close(ctx context.Context) error {
|
||||
// Stop ongoing operations such as connectivity checks.
|
||||
close(bridge.stopCh)
|
||||
|
||||
// Wait for ongoing user load operations to finish.
|
||||
bridge.loadWG.Wait()
|
||||
|
||||
// Close the IMAP server.
|
||||
if err := bridge.closeIMAP(ctx); err != nil {
|
||||
logrus.WithError(err).Error("Failed to close IMAP server")
|
||||
@ -299,10 +313,10 @@ func (bridge *Bridge) Close(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// Close all users.
|
||||
for _, user := range bridge.users {
|
||||
if err := user.Close(); err != nil {
|
||||
logrus.WithError(err).Error("Failed to close user")
|
||||
}
|
||||
if err := bridge.users.IterValuesErr(func(user *user.User) error {
|
||||
return user.Close()
|
||||
}); err != nil {
|
||||
logrus.WithError(err).Error("Failed to close users")
|
||||
}
|
||||
|
||||
// Close the focus service.
|
||||
@ -317,49 +331,44 @@ func (bridge *Bridge) Close(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (bridge *Bridge) publish(event events.Event) {
|
||||
bridge.watchersLock.RLock()
|
||||
defer bridge.watchersLock.RUnlock()
|
||||
|
||||
for _, watcher := range bridge.watchers {
|
||||
bridge.watchers.Iter(func(watcher *watcher.Watcher[events.Event]) {
|
||||
if watcher.IsWatching(event) {
|
||||
if ok := watcher.Send(event); !ok {
|
||||
logrus.WithField("event", event).Warn("Failed to send event to watcher")
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (bridge *Bridge) addWatcher(ofType ...events.Event) *watcher.Watcher[events.Event] {
|
||||
bridge.watchersLock.Lock()
|
||||
defer bridge.watchersLock.Unlock()
|
||||
|
||||
newWatcher := watcher.New(ofType...)
|
||||
|
||||
bridge.watchers = append(bridge.watchers, newWatcher)
|
||||
bridge.watchers.Append(newWatcher)
|
||||
|
||||
return newWatcher
|
||||
}
|
||||
|
||||
func (bridge *Bridge) remWatcher(oldWatcher *watcher.Watcher[events.Event]) {
|
||||
bridge.watchersLock.Lock()
|
||||
defer bridge.watchersLock.Unlock()
|
||||
|
||||
bridge.watchers = xslices.Filter(bridge.watchers, func(other *watcher.Watcher[events.Event]) bool {
|
||||
return other != oldWatcher
|
||||
})
|
||||
bridge.watchers.Delete(oldWatcher)
|
||||
}
|
||||
|
||||
func (bridge *Bridge) onStatusUp() {
|
||||
bridge.publish(events.ConnStatusUp{})
|
||||
|
||||
if err := bridge.loadUsers(); err != nil {
|
||||
logrus.WithError(err).Error("Failed to load users")
|
||||
}
|
||||
bridge.loadCh <- struct{}{}
|
||||
|
||||
bridge.users.IterValues(func(user *user.User) {
|
||||
user.OnStatusUp()
|
||||
})
|
||||
}
|
||||
|
||||
func (bridge *Bridge) onStatusDown() {
|
||||
bridge.publish(events.ConnStatusDown{})
|
||||
|
||||
bridge.users.IterValues(func(user *user.User) {
|
||||
user.OnStatusDown()
|
||||
})
|
||||
|
||||
upCh, done := bridge.GetEvents(events.ConnStatusUp{})
|
||||
defer done()
|
||||
|
||||
|
||||
@ -136,7 +136,7 @@ func TestBridge_UserAgent(t *testing.T) {
|
||||
|
||||
func TestBridge_Cookies(t *testing.T) {
|
||||
withTLSEnv(t, func(ctx context.Context, s *server.Server, netCtl *liteapi.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||
sessionIDs := safe.NewSet[string]()
|
||||
sessionIDs := safe.NewValue([]string{})
|
||||
|
||||
// Save any session IDs we use.
|
||||
s.AddCallWatcher(func(call server.Call) {
|
||||
@ -145,7 +145,9 @@ func TestBridge_Cookies(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
sessionIDs.Insert(cookie.Value)
|
||||
sessionIDs.Mod(func(sessionIDs *[]string) {
|
||||
*sessionIDs = append(*sessionIDs, cookie.Value)
|
||||
})
|
||||
})
|
||||
|
||||
// Start bridge and add a user so that API assigns us a session ID via cookie.
|
||||
@ -160,8 +162,8 @@ func TestBridge_Cookies(t *testing.T) {
|
||||
})
|
||||
|
||||
// We should have used just one session ID.
|
||||
sessionIDs.Values(func(sessionIDs []string) {
|
||||
require.Len(t, sessionIDs, 1)
|
||||
sessionIDs.Load(func(sessionIDs []string) {
|
||||
require.Len(t, xslices.Unique(sessionIDs), 1)
|
||||
})
|
||||
})
|
||||
}
|
||||
@ -405,7 +407,7 @@ func withBridge(
|
||||
defer func() { require.NoError(t, cookieJar.PersistCookies()) }()
|
||||
|
||||
// Create a new bridge.
|
||||
bridge, err := bridge.New(
|
||||
bridge, eventCh, err := bridge.New(
|
||||
// The app stuff.
|
||||
locator,
|
||||
vault,
|
||||
@ -428,6 +430,9 @@ func withBridge(
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Wait for bridge to finish loading users.
|
||||
waitForEvent(t, eventCh, events.AllUsersLoaded{})
|
||||
|
||||
// Close the bridge when done.
|
||||
defer func() { require.NoError(t, bridge.Close(ctx)) }()
|
||||
|
||||
@ -435,6 +440,17 @@ func withBridge(
|
||||
tests(bridge, mocks)
|
||||
}
|
||||
|
||||
func waitForEvent[T any](t *testing.T, eventCh <-chan events.Event, wantEvent T) {
|
||||
t.Helper()
|
||||
|
||||
for event := range eventCh {
|
||||
switch event.(type) {
|
||||
case T:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// must is a helper function that panics on error.
|
||||
func must[T any](val T, err error) T {
|
||||
if err != nil {
|
||||
|
||||
@ -41,7 +41,12 @@ func (bridge *Bridge) ReportBug(ctx context.Context, osType, osVersion, descript
|
||||
if info, err := bridge.QueryUserInfo(username); err == nil {
|
||||
account = info.Username
|
||||
} else if userIDs := bridge.GetUserIDs(); len(userIDs) > 0 {
|
||||
account = bridge.users[userIDs[0]].Name()
|
||||
user, err := bridge.vault.GetUser(userIDs[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
account = user.Username()
|
||||
}
|
||||
|
||||
var atts []liteapi.ReportBugAttachment
|
||||
|
||||
@ -1,47 +1,52 @@
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/clientconfig"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/user"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/useragent"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/vault"
|
||||
)
|
||||
|
||||
func (bridge *Bridge) ConfigureAppleMail(userID, address string) error {
|
||||
user, ok := bridge.users[userID]
|
||||
if !ok {
|
||||
return ErrNoSuchUser
|
||||
}
|
||||
|
||||
if address == "" {
|
||||
address = user.Emails()[0]
|
||||
}
|
||||
|
||||
username := address
|
||||
addresses := address
|
||||
|
||||
if user.GetAddressMode() == vault.CombinedMode {
|
||||
username = user.Emails()[0]
|
||||
addresses = strings.Join(user.Emails(), ",")
|
||||
}
|
||||
|
||||
// If configuring apple mail for Catalina or newer, users should use SSL.
|
||||
if useragent.IsCatalinaOrNewer() && !bridge.vault.GetSMTPSSL() {
|
||||
if err := bridge.SetSMTPSSL(true); err != nil {
|
||||
return err
|
||||
if ok, err := bridge.users.GetErr(userID, func(user *user.User) error {
|
||||
if address == "" {
|
||||
address = user.Emails()[0]
|
||||
}
|
||||
|
||||
username := address
|
||||
addresses := address
|
||||
|
||||
if user.GetAddressMode() == vault.CombinedMode {
|
||||
username = user.Emails()[0]
|
||||
addresses = strings.Join(user.Emails(), ",")
|
||||
}
|
||||
|
||||
// If configuring apple mail for Catalina or newer, users should use SSL.
|
||||
if useragent.IsCatalinaOrNewer() && !bridge.vault.GetSMTPSSL() {
|
||||
if err := bridge.SetSMTPSSL(true); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return (&clientconfig.AppleMail{}).Configure(
|
||||
constants.Host,
|
||||
bridge.vault.GetIMAPPort(),
|
||||
bridge.vault.GetSMTPPort(),
|
||||
bridge.vault.GetIMAPSSL(),
|
||||
bridge.vault.GetSMTPSSL(),
|
||||
username,
|
||||
addresses,
|
||||
user.BridgePass(),
|
||||
)
|
||||
}); !ok {
|
||||
return ErrNoSuchUser
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("failed to configure apple mail: %w", err)
|
||||
}
|
||||
|
||||
return (&clientconfig.AppleMail{}).Configure(
|
||||
constants.Host,
|
||||
bridge.vault.GetIMAPPort(),
|
||||
bridge.vault.GetSMTPPort(),
|
||||
bridge.vault.GetIMAPSSL(),
|
||||
bridge.vault.GetSMTPSSL(),
|
||||
username,
|
||||
addresses,
|
||||
user.BridgePass(),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/updater"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/user"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/vault"
|
||||
)
|
||||
|
||||
@ -119,10 +120,10 @@ func (bridge *Bridge) SetGluonDir(ctx context.Context, newGluonDir string) error
|
||||
|
||||
bridge.imapServer = imapServer
|
||||
|
||||
for _, user := range bridge.users {
|
||||
if err := bridge.addIMAPUser(ctx, user); err != nil {
|
||||
return fmt.Errorf("failed to add IMAP user: %w", err)
|
||||
}
|
||||
if err := bridge.users.IterValuesErr(func(user *user.User) error {
|
||||
return bridge.addIMAPUser(ctx, user)
|
||||
}); err != nil {
|
||||
return fmt.Errorf("failed to add users to new IMAP server: %w", err)
|
||||
}
|
||||
|
||||
if err := bridge.serveIMAP(); err != nil {
|
||||
|
||||
@ -1,13 +1,10 @@
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/user"
|
||||
"github.com/emersion/go-smtp"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
type smtpBackend struct {
|
||||
@ -26,13 +23,12 @@ func (backend *smtpBackend) Login(state *smtp.ConnectionState, email, password s
|
||||
defer backend.usersLock.RUnlock()
|
||||
|
||||
for _, user := range backend.users {
|
||||
if subtle.ConstantTimeCompare(user.BridgePass(), []byte(password)) != 1 {
|
||||
session, err := user.NewSMTPSession(email, []byte(password))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if email := strings.ToLower(email); slices.Contains(user.Emails(), email) {
|
||||
return user.NewSMTPSession(email)
|
||||
}
|
||||
return session, nil
|
||||
}
|
||||
|
||||
return nil, ErrNoSuchUser
|
||||
|
||||
@ -19,7 +19,7 @@ func TestBridge_Sync(t *testing.T) {
|
||||
s := server.New()
|
||||
defer s.Close()
|
||||
|
||||
numMsg := 1 << 10
|
||||
numMsg := 1 << 8
|
||||
|
||||
withEnv(t, s, func(ctx context.Context, netCtl *liteapi.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
userID, addrID, err := s.CreateUser("imap", "imap@pm.me", password)
|
||||
@ -80,51 +80,51 @@ func TestBridge_Sync(t *testing.T) {
|
||||
|
||||
// Login the user; its sync should fail.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
syncCh, done := chToType[events.Event, events.SyncFailed](bridge.GetEvents(events.SyncFailed{}))
|
||||
defer done()
|
||||
{
|
||||
syncCh, done := chToType[events.Event, events.SyncFailed](bridge.GetEvents(events.SyncFailed{}))
|
||||
defer done()
|
||||
|
||||
userID, err := bridge.LoginFull(ctx, "imap", password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
userID, err := bridge.LoginFull(ctx, "imap", password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, userID, (<-syncCh).UserID)
|
||||
require.Equal(t, userID, (<-syncCh).UserID)
|
||||
|
||||
info, err := bridge.GetUserInfo(userID)
|
||||
require.NoError(t, err)
|
||||
require.True(t, info.Connected)
|
||||
info, err := bridge.GetUserInfo(userID)
|
||||
require.NoError(t, err)
|
||||
require.True(t, info.Connected)
|
||||
|
||||
client, err := client.Dial(fmt.Sprintf(":%v", bridge.GetIMAPPort()))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, client.Login("imap@pm.me", string(info.BridgePass)))
|
||||
defer func() { _ = client.Logout() }()
|
||||
client, err := client.Dial(fmt.Sprintf(":%v", bridge.GetIMAPPort()))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, client.Login("imap@pm.me", string(info.BridgePass)))
|
||||
defer func() { _ = client.Logout() }()
|
||||
|
||||
status, err := client.Select(`Folders/folder`, false)
|
||||
require.NoError(t, err)
|
||||
require.Less(t, status.Messages, uint32(numMsg))
|
||||
})
|
||||
status, err := client.Select(`Folders/folder`, false)
|
||||
require.NoError(t, err)
|
||||
require.Less(t, status.Messages, uint32(numMsg))
|
||||
}
|
||||
|
||||
// Remove the network limit, allowing the sync to finish.
|
||||
netCtl.SetReadLimit(0)
|
||||
// Remove the network limit, allowing the sync to finish.
|
||||
netCtl.SetReadLimit(0)
|
||||
|
||||
// Login the user; its sync should now finish.
|
||||
// If we then connect an IMAP client, it should eventually see all the messages.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
|
||||
defer done()
|
||||
{
|
||||
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
|
||||
defer done()
|
||||
|
||||
require.Equal(t, userID, (<-syncCh).UserID)
|
||||
require.Equal(t, userID, (<-syncCh).UserID)
|
||||
|
||||
info, err := bridge.GetUserInfo(userID)
|
||||
require.NoError(t, err)
|
||||
require.True(t, info.Connected)
|
||||
info, err := bridge.GetUserInfo(userID)
|
||||
require.NoError(t, err)
|
||||
require.True(t, info.Connected)
|
||||
|
||||
client, err := client.Dial(fmt.Sprintf(":%v", bridge.GetIMAPPort()))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, client.Login("imap@pm.me", string(info.BridgePass)))
|
||||
defer func() { _ = client.Logout() }()
|
||||
client, err := client.Dial(fmt.Sprintf(":%v", bridge.GetIMAPPort()))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, client.Login("imap@pm.me", string(info.BridgePass)))
|
||||
defer func() { _ = client.Logout() }()
|
||||
|
||||
status, err := client.Select(`Folders/folder`, false)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint32(numMsg), status.Messages)
|
||||
status, err := client.Select(`Folders/folder`, false)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint32(numMsg), status.Messages)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/ProtonMail/gluon/imap"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/safe"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/try"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/user"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/vault"
|
||||
@ -53,23 +54,26 @@ func (bridge *Bridge) GetUserInfo(userID string) (UserInfo, error) {
|
||||
return UserInfo{}, err
|
||||
}
|
||||
|
||||
user, ok := bridge.users[userID]
|
||||
if !ok {
|
||||
return getUserInfo(vaultUser.UserID(), vaultUser.Username(), vaultUser.AddressMode()), nil
|
||||
if info, ok := safe.MapGetRet(bridge.users, userID, func(user *user.User) UserInfo {
|
||||
return getConnUserInfo(user)
|
||||
}); ok {
|
||||
return info, nil
|
||||
}
|
||||
|
||||
return getConnUserInfo(user), nil
|
||||
return getUserInfo(vaultUser.UserID(), vaultUser.Username(), vaultUser.AddressMode()), nil
|
||||
}
|
||||
|
||||
// QueryUserInfo queries the user info by username or address.
|
||||
func (bridge *Bridge) QueryUserInfo(query string) (UserInfo, error) {
|
||||
for userID, user := range bridge.users {
|
||||
if user.Match(query) {
|
||||
return bridge.GetUserInfo(userID)
|
||||
return safe.MapValuesRetErr(bridge.users, func(users []*user.User) (UserInfo, error) {
|
||||
for _, user := range users {
|
||||
if user.Match(query) {
|
||||
return getConnUserInfo(user), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return UserInfo{}, ErrNoSuchUser
|
||||
return UserInfo{}, ErrNoSuchUser
|
||||
})
|
||||
}
|
||||
|
||||
// LoginAuth begins the login process. It returns an authorized client that might need 2FA.
|
||||
@ -79,7 +83,7 @@ func (bridge *Bridge) LoginAuth(ctx context.Context, username string, password [
|
||||
return nil, liteapi.Auth{}, fmt.Errorf("failed to create new API client: %w", err)
|
||||
}
|
||||
|
||||
if _, ok := bridge.users[auth.UserID]; ok {
|
||||
if bridge.users.Has(auth.UserID) {
|
||||
if err := client.AuthDelete(ctx); err != nil {
|
||||
logrus.WithError(err).Warn("Failed to delete auth")
|
||||
}
|
||||
@ -187,34 +191,37 @@ func (bridge *Bridge) DeleteUser(ctx context.Context, userID string) error {
|
||||
|
||||
// SetAddressMode sets the address mode for the given user.
|
||||
func (bridge *Bridge) SetAddressMode(ctx context.Context, userID string, mode vault.AddressMode) error {
|
||||
user, ok := bridge.users[userID]
|
||||
if !ok {
|
||||
return ErrNoSuchUser
|
||||
}
|
||||
|
||||
if user.GetAddressMode() == mode {
|
||||
return fmt.Errorf("address mode is already %q", mode)
|
||||
}
|
||||
|
||||
for _, gluonID := range user.GetGluonIDs() {
|
||||
if err := bridge.imapServer.RemoveUser(ctx, gluonID, true); err != nil {
|
||||
return fmt.Errorf("failed to remove user from IMAP server: %w", err)
|
||||
if ok, err := bridge.users.GetErr(userID, func(user *user.User) error {
|
||||
if user.GetAddressMode() == mode {
|
||||
return fmt.Errorf("address mode is already %q", mode)
|
||||
}
|
||||
}
|
||||
|
||||
if err := user.SetAddressMode(ctx, mode); err != nil {
|
||||
for _, gluonID := range user.GetGluonIDs() {
|
||||
if err := bridge.imapServer.RemoveUser(ctx, gluonID, true); err != nil {
|
||||
return fmt.Errorf("failed to remove user from IMAP server: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := user.SetAddressMode(ctx, mode); err != nil {
|
||||
return fmt.Errorf("failed to set address mode: %w", err)
|
||||
}
|
||||
|
||||
if err := bridge.addIMAPUser(ctx, user); err != nil {
|
||||
return fmt.Errorf("failed to add IMAP user: %w", err)
|
||||
}
|
||||
|
||||
bridge.publish(events.AddressModeChanged{
|
||||
UserID: userID,
|
||||
AddressMode: mode,
|
||||
})
|
||||
|
||||
return nil
|
||||
}); !ok {
|
||||
return ErrNoSuchUser
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("failed to set address mode: %w", err)
|
||||
}
|
||||
|
||||
if err := bridge.addIMAPUser(ctx, user); err != nil {
|
||||
return fmt.Errorf("failed to add IMAP user: %w", err)
|
||||
}
|
||||
|
||||
bridge.publish(events.AddressModeChanged{
|
||||
UserID: userID,
|
||||
AddressMode: mode,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -241,10 +248,30 @@ func (bridge *Bridge) loginUser(ctx context.Context, client *liteapi.Client, aut
|
||||
return apiUser.ID, nil
|
||||
}
|
||||
|
||||
// loadUsers is a loop that, when polled, attempts to load authorized users from the vault.
|
||||
// loadLoop is a loop that, when polled, attempts to load authorized users from the vault.
|
||||
func (bridge *Bridge) loadLoop() {
|
||||
for {
|
||||
bridge.loadWG.GoTry(func(ok bool) {
|
||||
if ok {
|
||||
if err := bridge.loadUsers(); err != nil {
|
||||
logrus.WithError(err).Error("Failed to load users")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
select {
|
||||
case <-bridge.stopCh:
|
||||
return
|
||||
|
||||
case <-bridge.loadCh:
|
||||
// ...
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (bridge *Bridge) loadUsers() error {
|
||||
return bridge.vault.ForUser(func(user *vault.User) error {
|
||||
if _, ok := bridge.users[user.UserID()]; ok {
|
||||
if err := bridge.vault.ForUser(func(user *vault.User) error {
|
||||
if bridge.users.Has(user.UserID()) {
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -271,7 +298,13 @@ func (bridge *Bridge) loadUsers() error {
|
||||
})
|
||||
|
||||
return nil
|
||||
})
|
||||
}); err != nil {
|
||||
return fmt.Errorf("failed to iterate over users: %w", err)
|
||||
}
|
||||
|
||||
bridge.publish(events.AllUsersLoaded{})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadUser loads an existing user from the vault.
|
||||
@ -387,7 +420,7 @@ func (bridge *Bridge) addNewUser(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bridge.users[apiUser.ID] = user
|
||||
bridge.users.Set(apiUser.ID, user)
|
||||
|
||||
return user, nil
|
||||
}
|
||||
@ -417,7 +450,7 @@ func (bridge *Bridge) addExistingUser(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bridge.users[apiUser.ID] = user
|
||||
bridge.users.Set(apiUser.ID, user)
|
||||
|
||||
return user, nil
|
||||
}
|
||||
@ -451,37 +484,38 @@ func (bridge *Bridge) addIMAPUser(ctx context.Context, user *user.User) error {
|
||||
|
||||
// logoutUser logs the given user out from bridge.
|
||||
func (bridge *Bridge) logoutUser(ctx context.Context, userID string) error {
|
||||
user, ok := bridge.users[userID]
|
||||
if !ok {
|
||||
return ErrNoSuchUser
|
||||
}
|
||||
|
||||
if err := bridge.smtpBackend.removeUser(user); err != nil {
|
||||
logrus.WithError(err).Error("Failed to remove user from SMTP backend")
|
||||
}
|
||||
|
||||
for _, gluonID := range user.GetGluonIDs() {
|
||||
if err := bridge.imapServer.RemoveUser(ctx, gluonID, false); err != nil {
|
||||
logrus.WithError(err).Error("Failed to remove IMAP user")
|
||||
if ok, err := bridge.users.GetDeleteErr(userID, func(user *user.User) error {
|
||||
if err := bridge.smtpBackend.removeUser(user); err != nil {
|
||||
logrus.WithError(err).Error("Failed to remove user from SMTP backend")
|
||||
}
|
||||
}
|
||||
|
||||
if err := user.Logout(ctx); err != nil {
|
||||
logrus.WithError(err).Error("Failed to logout user")
|
||||
}
|
||||
for _, gluonID := range user.GetGluonIDs() {
|
||||
if err := bridge.imapServer.RemoveUser(ctx, gluonID, false); err != nil {
|
||||
logrus.WithError(err).Error("Failed to remove IMAP user")
|
||||
}
|
||||
}
|
||||
|
||||
if err := user.Close(); err != nil {
|
||||
logrus.WithError(err).Error("Failed to close user")
|
||||
}
|
||||
if err := user.Logout(ctx); err != nil {
|
||||
logrus.WithError(err).Error("Failed to logout user")
|
||||
}
|
||||
|
||||
delete(bridge.users, userID)
|
||||
if err := user.Close(); err != nil {
|
||||
logrus.WithError(err).Error("Failed to close user")
|
||||
}
|
||||
|
||||
return nil
|
||||
}); !ok {
|
||||
return ErrNoSuchUser
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("failed to delete user: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// deleteUser deletes the given user from bridge.
|
||||
func (bridge *Bridge) deleteUser(ctx context.Context, userID string) {
|
||||
if user, ok := bridge.users[userID]; ok {
|
||||
if ok := bridge.users.GetDelete(userID, func(user *user.User) {
|
||||
if err := bridge.smtpBackend.removeUser(user); err != nil {
|
||||
logrus.WithError(err).Error("Failed to remove user from SMTP backend")
|
||||
}
|
||||
@ -499,13 +533,13 @@ func (bridge *Bridge) deleteUser(ctx context.Context, userID string) {
|
||||
if err := user.Close(); err != nil {
|
||||
logrus.WithError(err).Error("Failed to close user")
|
||||
}
|
||||
}); !ok {
|
||||
logrus.Debug("The bridge user was not connected")
|
||||
}
|
||||
|
||||
if err := bridge.vault.DeleteUser(userID); err != nil {
|
||||
logrus.WithError(err).Error("Failed to delete user from vault")
|
||||
}
|
||||
|
||||
delete(bridge.users, userID)
|
||||
}
|
||||
|
||||
// getUserInfo returns information about a disconnected user.
|
||||
|
||||
@ -43,23 +43,13 @@ func (bridge *Bridge) handleUserAddressCreated(ctx context.Context, user *user.U
|
||||
return fmt.Errorf("failed to remove user from IMAP server: %w", err)
|
||||
}
|
||||
|
||||
imapConn, err := user.NewIMAPConnector(addrID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create IMAP connector: %w", err)
|
||||
}
|
||||
|
||||
if err := bridge.imapServer.LoadUser(ctx, imapConn, gluonID, user.GluonKey()); err != nil {
|
||||
if err := bridge.imapServer.LoadUser(ctx, user.NewIMAPConnector(addrID), gluonID, user.GluonKey()); err != nil {
|
||||
return fmt.Errorf("failed to add user to IMAP server: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
case vault.SplitMode:
|
||||
imapConn, err := user.NewIMAPConnector(event.AddressID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create IMAP connector: %w", err)
|
||||
}
|
||||
|
||||
gluonID, err := bridge.imapServer.AddUser(ctx, imapConn, user.GluonKey())
|
||||
gluonID, err := bridge.imapServer.AddUser(ctx, user.NewIMAPConnector(event.AddressID), user.GluonKey())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add user to IMAP server: %w", err)
|
||||
}
|
||||
@ -93,12 +83,7 @@ func (bridge *Bridge) handleUserAddressDeleted(ctx context.Context, user *user.U
|
||||
return fmt.Errorf("failed to remove user from IMAP server: %w", err)
|
||||
}
|
||||
|
||||
imapConn, err := user.NewIMAPConnector(addrID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create IMAP connector: %w", err)
|
||||
}
|
||||
|
||||
if err := bridge.imapServer.LoadUser(ctx, imapConn, gluonID, user.GluonKey()); err != nil {
|
||||
if err := bridge.imapServer.LoadUser(ctx, user.NewIMAPConnector(addrID), gluonID, user.GluonKey()); err != nil {
|
||||
return fmt.Errorf("failed to add user to IMAP server: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user