mirror of
https://github.com/ProtonMail/proton-bridge.git
synced 2025-12-15 22:56:48 +00:00
chore: merge release/perth_narrows into devel
This commit is contained in:
@ -32,14 +32,12 @@ func defaultAPIOptions(
|
||||
version *semver.Version,
|
||||
cookieJar http.CookieJar,
|
||||
transport http.RoundTripper,
|
||||
poolSize int,
|
||||
) []proton.Option {
|
||||
return []proton.Option{
|
||||
proton.WithHostURL(apiURL),
|
||||
proton.WithAppVersion(constants.AppVersion(version.Original())),
|
||||
proton.WithCookieJar(cookieJar),
|
||||
proton.WithTransport(transport),
|
||||
proton.WithAttPoolSize(poolSize),
|
||||
proton.WithLogger(logrus.StandardLogger()),
|
||||
}
|
||||
}
|
||||
|
||||
@ -32,7 +32,6 @@ func newAPIOptions(
|
||||
version *semver.Version,
|
||||
cookieJar http.CookieJar,
|
||||
transport http.RoundTripper,
|
||||
poolSize int,
|
||||
) []proton.Option {
|
||||
return defaultAPIOptions(apiURL, version, cookieJar, transport, poolSize)
|
||||
return defaultAPIOptions(apiURL, version, cookieJar, transport)
|
||||
}
|
||||
|
||||
@ -33,9 +33,8 @@ func newAPIOptions(
|
||||
version *semver.Version,
|
||||
cookieJar http.CookieJar,
|
||||
transport http.RoundTripper,
|
||||
poolSize int,
|
||||
) []proton.Option {
|
||||
opt := defaultAPIOptions(apiURL, version, cookieJar, transport, poolSize)
|
||||
opt := defaultAPIOptions(apiURL, version, cookieJar, transport)
|
||||
|
||||
if host := os.Getenv("BRIDGE_API_HOST"); host != "" {
|
||||
opt = append(opt, proton.WithHostURL(host))
|
||||
|
||||
@ -31,6 +31,7 @@ import (
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/gluon"
|
||||
imapEvents "github.com/ProtonMail/gluon/events"
|
||||
"github.com/ProtonMail/gluon/imap"
|
||||
"github.com/ProtonMail/gluon/reporter"
|
||||
"github.com/ProtonMail/gluon/watcher"
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
@ -124,10 +125,12 @@ type Bridge struct {
|
||||
|
||||
// goUpdate triggers a check/install of updates.
|
||||
goUpdate func()
|
||||
|
||||
uidValidityGenerator imap.UIDValidityGenerator
|
||||
}
|
||||
|
||||
// New creates a new bridge.
|
||||
func New( //nolint:funlen
|
||||
func New(
|
||||
locator Locator, // the locator to provide paths to store data
|
||||
vault *vault.Vault, // the bridge's encrypted data store
|
||||
autostarter Autostarter, // the autostarter to manage autostart settings
|
||||
@ -142,12 +145,13 @@ func New( //nolint:funlen
|
||||
proxyCtl ProxyController, // the DoH controller
|
||||
crashHandler async.PanicHandler,
|
||||
reporter reporter.Reporter,
|
||||
uidValidityGenerator imap.UIDValidityGenerator,
|
||||
|
||||
logIMAPClient, logIMAPServer bool, // whether to log IMAP client/server activity
|
||||
logSMTP bool, // whether to log SMTP activity
|
||||
) (*Bridge, <-chan events.Event, error) {
|
||||
// api is the user's API manager.
|
||||
api := proton.New(newAPIOptions(apiURL, curVersion, cookieJar, roundTripper, vault.SyncAttPool())...)
|
||||
api := proton.New(newAPIOptions(apiURL, curVersion, cookieJar, roundTripper)...)
|
||||
|
||||
// tasks holds all the bridge's background tasks.
|
||||
tasks := async.NewGroup(context.Background(), crashHandler)
|
||||
@ -171,6 +175,7 @@ func New( //nolint:funlen
|
||||
api,
|
||||
identifier,
|
||||
proxyCtl,
|
||||
uidValidityGenerator,
|
||||
logIMAPClient, logIMAPServer, logSMTP,
|
||||
)
|
||||
if err != nil {
|
||||
@ -185,22 +190,9 @@ func New( //nolint:funlen
|
||||
return nil, nil, fmt.Errorf("failed to initialize bridge: %w", err)
|
||||
}
|
||||
|
||||
// Start serving IMAP.
|
||||
if err := bridge.serveIMAP(); err != nil {
|
||||
logrus.WithError(err).Error("IMAP error")
|
||||
bridge.PushError(ErrServeIMAP)
|
||||
}
|
||||
|
||||
// Start serving SMTP.
|
||||
if err := bridge.serveSMTP(); err != nil {
|
||||
logrus.WithError(err).Error("SMTP error")
|
||||
bridge.PushError(ErrServeSMTP)
|
||||
}
|
||||
|
||||
return bridge, eventCh, nil
|
||||
}
|
||||
|
||||
// nolint:funlen
|
||||
func newBridge(
|
||||
tasks *async.Group,
|
||||
imapEventCh chan imapEvents.Event,
|
||||
@ -216,6 +208,7 @@ func newBridge(
|
||||
api *proton.Manager,
|
||||
identifier Identifier,
|
||||
proxyCtl ProxyController,
|
||||
uidValidityGenerator imap.UIDValidityGenerator,
|
||||
|
||||
logIMAPClient, logIMAPServer, logSMTP bool,
|
||||
) (*Bridge, error) {
|
||||
@ -254,12 +247,13 @@ func newBridge(
|
||||
logIMAPServer,
|
||||
imapEventCh,
|
||||
tasks,
|
||||
uidValidityGenerator,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create IMAP server: %w", err)
|
||||
}
|
||||
|
||||
focusService, err := focus.NewService(curVersion)
|
||||
focusService, err := focus.NewService(locator, curVersion)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create focus service: %w", err)
|
||||
}
|
||||
@ -300,6 +294,8 @@ func newBridge(
|
||||
lastVersion: lastVersion,
|
||||
|
||||
tasks: tasks,
|
||||
|
||||
uidValidityGenerator: uidValidityGenerator,
|
||||
}
|
||||
|
||||
bridge.smtpServer = newSMTPServer(bridge, tlsConfig, logSMTP)
|
||||
@ -307,7 +303,6 @@ func newBridge(
|
||||
return bridge, nil
|
||||
}
|
||||
|
||||
// nolint:funlen
|
||||
func (bridge *Bridge) init(tlsReporter TLSReporter) error {
|
||||
// Enable or disable the proxy at startup.
|
||||
if bridge.vault.GetProxyAllowed() {
|
||||
@ -376,16 +371,32 @@ func (bridge *Bridge) init(tlsReporter TLSReporter) error {
|
||||
})
|
||||
})
|
||||
|
||||
// Attempt to lazy load users when triggered.
|
||||
// We need to load users before we can start the IMAP and SMTP servers.
|
||||
// We must only start the servers once.
|
||||
var once sync.Once
|
||||
|
||||
// Attempt to load users from the vault when triggered.
|
||||
bridge.goLoad = bridge.tasks.Trigger(func(ctx context.Context) {
|
||||
if err := bridge.loadUsers(ctx); err != nil {
|
||||
logrus.WithError(err).Error("Failed to load users")
|
||||
if netErr := new(proton.NetError); !errors.As(err, &netErr) {
|
||||
sentry.ReportError(bridge.reporter, "Failed to load users", err)
|
||||
}
|
||||
} else {
|
||||
bridge.publish(events.AllUsersLoaded{})
|
||||
return
|
||||
}
|
||||
|
||||
bridge.publish(events.AllUsersLoaded{})
|
||||
|
||||
// Once all users have been loaded, start the bridge's IMAP and SMTP servers.
|
||||
once.Do(func() {
|
||||
if err := bridge.serveIMAP(); err != nil {
|
||||
logrus.WithError(err).Error("Failed to start IMAP server")
|
||||
}
|
||||
|
||||
if err := bridge.serveSMTP(); err != nil {
|
||||
logrus.WithError(err).Error("Failed to start SMTP server")
|
||||
}
|
||||
})
|
||||
})
|
||||
defer bridge.goLoad()
|
||||
|
||||
@ -545,7 +556,7 @@ func (bridge *Bridge) onStatusDown(ctx context.Context) {
|
||||
}
|
||||
|
||||
func loadTLSConfig(vault *vault.Vault) (*tls.Config, error) {
|
||||
cert, err := tls.X509KeyPair(vault.GetBridgeTLSCert(), vault.GetBridgeTLSKey())
|
||||
cert, err := tls.X509KeyPair(vault.GetBridgeTLSCert())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -21,6 +21,7 @@ import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@ -29,6 +30,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/gluon/imap"
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/go-proton-api/server"
|
||||
"github.com/ProtonMail/go-proton-api/server/backend"
|
||||
@ -121,8 +123,11 @@ func TestBridge_Focus(t *testing.T) {
|
||||
raiseCh, done := bridge.GetEvents(events.Raise{})
|
||||
defer done()
|
||||
|
||||
settingsFolder, err := locator.ProvideSettingsPath()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Simulate a focus event.
|
||||
focus.TryRaise()
|
||||
focus.TryRaise(settingsFolder)
|
||||
|
||||
// Wait for the event.
|
||||
require.IsType(t, events.Raise{}, <-raiseCh)
|
||||
@ -496,6 +501,21 @@ func TestBridge_InitGluonDirectory(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_LoginFailed(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
failCh, done := chToType[events.Event, events.IMAPLoginFailed](bridge.GetEvents(events.IMAPLoginFailed{}))
|
||||
defer done()
|
||||
|
||||
imapClient, err := client.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Error(t, imapClient.Login("badUser", "badPass"))
|
||||
require.Equal(t, "badUser", (<-failCh).Username)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_ChangeCacheDirectory(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||
userID, addrID, err := s.CreateUser("imap", password)
|
||||
@ -657,6 +677,9 @@ func withMocks(t *testing.T, tests func(*bridge.Mocks)) {
|
||||
tests(mocks)
|
||||
}
|
||||
|
||||
// Needs to be global to survive bridge shutdown/startup in unit tests as they happen to fast.
|
||||
var testUIDValidityGenerator = imap.DefaultEpochUIDValidityGenerator()
|
||||
|
||||
// withBridge creates a new bridge which points to the given API URL and uses the given keychain, and closes it when done.
|
||||
func withBridgeNoMocks(
|
||||
ctx context.Context,
|
||||
@ -702,6 +725,7 @@ func withBridgeNoMocks(
|
||||
mocks.ProxyCtl,
|
||||
mocks.CrashHandler,
|
||||
mocks.Reporter,
|
||||
testUIDValidityGenerator,
|
||||
|
||||
// The logging stuff.
|
||||
os.Getenv("BRIDGE_LOG_IMAP_CLIENT") == "1",
|
||||
@ -713,6 +737,10 @@ func withBridgeNoMocks(
|
||||
|
||||
// Wait for bridge to finish loading users.
|
||||
waitForEvent(t, eventCh, events.AllUsersLoaded{})
|
||||
// Wait for bridge to start the IMAP server.
|
||||
waitForEvent(t, eventCh, events.IMAPServerReady{})
|
||||
// Wait for bridge to start the SMTP server.
|
||||
waitForEvent(t, eventCh, events.SMTPServerReady{})
|
||||
|
||||
// Set random IMAP and SMTP ports for the tests.
|
||||
require.NoError(t, bridge.SetIMAPPort(0))
|
||||
@ -742,7 +770,7 @@ func withBridge(
|
||||
})
|
||||
}
|
||||
|
||||
func waitForEvent[T any](t *testing.T, eventCh <-chan events.Event, wantEvent T) {
|
||||
func waitForEvent[T any](t *testing.T, eventCh <-chan events.Event, _ T) {
|
||||
t.Helper()
|
||||
|
||||
for event := range eventCh {
|
||||
|
||||
@ -37,7 +37,7 @@ const (
|
||||
MaxCompressedFilesCount = 6
|
||||
)
|
||||
|
||||
func (bridge *Bridge) ReportBug(ctx context.Context, osType, osVersion, description, username, email, client string, attachLogs bool) error { //nolint:funlen
|
||||
func (bridge *Bridge) ReportBug(ctx context.Context, osType, osVersion, description, username, email, client string, attachLogs bool) error {
|
||||
var account string
|
||||
|
||||
if info, err := bridge.QueryUserInfo(username); err == nil {
|
||||
|
||||
@ -22,10 +22,7 @@ import "errors"
|
||||
var (
|
||||
ErrVaultInsecure = errors.New("the vault is insecure")
|
||||
ErrVaultCorrupt = errors.New("the vault is corrupt")
|
||||
|
||||
ErrServeIMAP = errors.New("failed to serve IMAP")
|
||||
ErrServeSMTP = errors.New("failed to serve SMTP")
|
||||
ErrWatchUpdates = errors.New("failed to watch for updates")
|
||||
ErrWatchUpdates = errors.New("failed to watch for updates")
|
||||
|
||||
ErrNoSuchUser = errors.New("no such user")
|
||||
ErrUserAlreadyExists = errors.New("user already exists")
|
||||
|
||||
@ -28,10 +28,13 @@ import (
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/gluon"
|
||||
imapEvents "github.com/ProtonMail/gluon/events"
|
||||
"github.com/ProtonMail/gluon/imap"
|
||||
"github.com/ProtonMail/gluon/reporter"
|
||||
"github.com/ProtonMail/gluon/store"
|
||||
"github.com/ProtonMail/gluon/store/fallback_v0"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/async"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||
@ -44,26 +47,42 @@ const (
|
||||
)
|
||||
|
||||
func (bridge *Bridge) serveIMAP() error {
|
||||
if bridge.imapServer == nil {
|
||||
return fmt.Errorf("no imap server instance running")
|
||||
}
|
||||
port, err := func() (int, error) {
|
||||
if bridge.imapServer == nil {
|
||||
return 0, fmt.Errorf("no IMAP server instance running")
|
||||
}
|
||||
|
||||
logrus.Info("Starting IMAP server")
|
||||
logrus.Info("Starting IMAP server")
|
||||
|
||||
imapListener, err := newListener(bridge.vault.GetIMAPPort(), bridge.vault.GetIMAPSSL(), bridge.tlsConfig)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to create IMAP listener: %w", err)
|
||||
}
|
||||
|
||||
bridge.imapListener = imapListener
|
||||
|
||||
if err := bridge.imapServer.Serve(context.Background(), bridge.imapListener); err != nil {
|
||||
return 0, fmt.Errorf("failed to serve IMAP: %w", err)
|
||||
}
|
||||
|
||||
if err := bridge.vault.SetIMAPPort(getPort(imapListener.Addr())); err != nil {
|
||||
return 0, fmt.Errorf("failed to store IMAP port in vault: %w", err)
|
||||
}
|
||||
|
||||
return getPort(imapListener.Addr()), nil
|
||||
}()
|
||||
|
||||
imapListener, err := newListener(bridge.vault.GetIMAPPort(), bridge.vault.GetIMAPSSL(), bridge.tlsConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create IMAP listener: %w", err)
|
||||
bridge.publish(events.IMAPServerError{
|
||||
Error: err,
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
bridge.imapListener = imapListener
|
||||
|
||||
if err := bridge.imapServer.Serve(context.Background(), bridge.imapListener); err != nil {
|
||||
return fmt.Errorf("failed to serve IMAP: %w", err)
|
||||
}
|
||||
|
||||
if err := bridge.vault.SetIMAPPort(getPort(imapListener.Addr())); err != nil {
|
||||
return fmt.Errorf("failed to store IMAP port in vault: %w", err)
|
||||
}
|
||||
bridge.publish(events.IMAPServerReady{
|
||||
Port: port,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -75,6 +94,8 @@ func (bridge *Bridge) restartIMAP() error {
|
||||
if err := bridge.imapListener.Close(); err != nil {
|
||||
return fmt.Errorf("failed to close IMAP listener: %w", err)
|
||||
}
|
||||
|
||||
bridge.publish(events.IMAPServerStopped{})
|
||||
}
|
||||
|
||||
return bridge.serveIMAP()
|
||||
@ -87,6 +108,7 @@ func (bridge *Bridge) closeIMAP(ctx context.Context) error {
|
||||
if err := bridge.imapServer.Close(ctx); err != nil {
|
||||
return fmt.Errorf("failed to close IMAP server: %w", err)
|
||||
}
|
||||
|
||||
bridge.imapServer = nil
|
||||
}
|
||||
|
||||
@ -96,12 +118,12 @@ func (bridge *Bridge) closeIMAP(ctx context.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
bridge.publish(events.IMAPServerStopped{})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// addIMAPUser connects the given user to gluon.
|
||||
//
|
||||
//nolint:funlen
|
||||
func (bridge *Bridge) addIMAPUser(ctx context.Context, user *user.User) error {
|
||||
if bridge.imapServer == nil {
|
||||
return fmt.Errorf("no imap server instance running")
|
||||
@ -242,6 +264,13 @@ func (bridge *Bridge) handleIMAPEvent(event imapEvents.Event) {
|
||||
if event.IMAPID.Name != "" && event.IMAPID.Version != "" {
|
||||
bridge.identifier.SetClient(event.IMAPID.Name, event.IMAPID.Version)
|
||||
}
|
||||
|
||||
case imapEvents.LoginFailed:
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"sessionID": event.SessionID,
|
||||
"username": event.Username,
|
||||
}).Info("Received IMAP login failure notification")
|
||||
bridge.publish(events.IMAPLoginFailed{Username: event.Username})
|
||||
}
|
||||
}
|
||||
|
||||
@ -261,7 +290,6 @@ func ApplyGluonConfigPathSuffix(basePath string) string {
|
||||
return filepath.Join(basePath, "backend", "db")
|
||||
}
|
||||
|
||||
// nolint:funlen
|
||||
func newIMAPServer(
|
||||
gluonCacheDir, gluonConfigDir string,
|
||||
version *semver.Version,
|
||||
@ -270,6 +298,7 @@ func newIMAPServer(
|
||||
logClient, logServer bool,
|
||||
eventCh chan<- imapEvents.Event,
|
||||
tasks *async.Group,
|
||||
uidValidityGenerator imap.UIDValidityGenerator,
|
||||
) (*gluon.Server, error) {
|
||||
gluonCacheDir = ApplyGluonCachePathSuffix(gluonCacheDir)
|
||||
gluonConfigDir = ApplyGluonConfigPathSuffix(gluonConfigDir)
|
||||
@ -313,6 +342,7 @@ func newIMAPServer(
|
||||
gluon.WithLogger(imapClientLog, imapServerLog),
|
||||
getGluonVersionInfo(version),
|
||||
gluon.WithReporter(reporter),
|
||||
gluon.WithUIDValidityGenerator(uidValidityGenerator),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -348,7 +378,7 @@ func (*storeBuilder) New(path, userID string, passphrase []byte) (store.Store, e
|
||||
return store.NewOnDiskStore(
|
||||
filepath.Join(path, userID),
|
||||
passphrase,
|
||||
store.WithCompressor(new(store.GZipCompressor)),
|
||||
store.WithFallback(fallback_v0.NewOnDiskStoreV0WithCompressor(&fallback_v0.GZipCompressor{})),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -57,6 +57,7 @@ func TestBridge_Refresh(t *testing.T) {
|
||||
require.Equal(t, userID, (<-syncCh).UserID)
|
||||
})
|
||||
|
||||
var uidValidities = make(map[string]uint32, len(names))
|
||||
// If we then connect an IMAP client, it should see all the labels with UID validity of 1.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
mocks.Reporter.EXPECT().ReportMessageWithContext(gomock.Any(), gomock.Any()).AnyTimes()
|
||||
@ -73,7 +74,7 @@ func TestBridge_Refresh(t *testing.T) {
|
||||
for _, name := range names {
|
||||
status, err := client.Select("Folders/"+name, false)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint32(1000), status.UidValidity)
|
||||
uidValidities[name] = status.UidValidity
|
||||
}
|
||||
})
|
||||
|
||||
@ -106,7 +107,7 @@ func TestBridge_Refresh(t *testing.T) {
|
||||
for _, name := range names {
|
||||
status, err := client.Select("Folders/"+name, false)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint32(1001), status.UidValidity)
|
||||
require.Greater(t, status.UidValidity, uidValidities[name])
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@ -25,11 +25,9 @@ import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
@ -131,26 +129,21 @@ func (bridge *Bridge) SetGluonDir(ctx context.Context, newGluonDir string) error
|
||||
return fmt.Errorf("new gluon dir is the same as the old one")
|
||||
}
|
||||
|
||||
if err := bridge.stopEventLoops(); err != nil {
|
||||
return err
|
||||
if err := bridge.closeIMAP(context.Background()); err != nil {
|
||||
return fmt.Errorf("failed to close IMAP: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
err := bridge.startEventLoops(ctx)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
if err := bridge.moveGluonCacheDir(currentGluonDir, newGluonDir); err != nil {
|
||||
logrus.WithError(err).Error("failed to move GluonCacheDir")
|
||||
|
||||
if err := bridge.vault.SetGluonDir(currentGluonDir); err != nil {
|
||||
panic(err)
|
||||
return fmt.Errorf("failed to revert GluonCacheDir: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
gluonDataDir, err := bridge.GetGluonDataDir()
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("failed to get Gluon Database directory: %w", err))
|
||||
return fmt.Errorf("failed to get Gluon Database directory: %w", err)
|
||||
}
|
||||
|
||||
imapServer, err := newIMAPServer(
|
||||
@ -163,13 +156,24 @@ func (bridge *Bridge) SetGluonDir(ctx context.Context, newGluonDir string) error
|
||||
bridge.logIMAPServer,
|
||||
bridge.imapEventCh,
|
||||
bridge.tasks,
|
||||
bridge.uidValidityGenerator,
|
||||
)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("failed to create new IMAP server: %w", err))
|
||||
return fmt.Errorf("failed to create new IMAP server: %w", err)
|
||||
}
|
||||
|
||||
bridge.imapServer = imapServer
|
||||
|
||||
for _, user := range bridge.users {
|
||||
if err := 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 {
|
||||
return fmt.Errorf("failed to serve IMAP: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}, bridge.usersLock)
|
||||
}
|
||||
@ -191,34 +195,6 @@ func (bridge *Bridge) moveGluonCacheDir(oldGluonDir, newGluonDir string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bridge *Bridge) stopEventLoops() error {
|
||||
if err := bridge.closeIMAP(context.Background()); err != nil {
|
||||
return fmt.Errorf("failed to close IMAP: %w", err)
|
||||
}
|
||||
|
||||
if err := bridge.closeSMTP(); err != nil {
|
||||
return fmt.Errorf("failed to close SMTP: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bridge *Bridge) startEventLoops(ctx context.Context) error {
|
||||
for _, user := range bridge.users {
|
||||
if err := 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 {
|
||||
panic(fmt.Errorf("failed to serve IMAP: %w", err))
|
||||
}
|
||||
|
||||
if err := bridge.serveSMTP(); err != nil {
|
||||
panic(fmt.Errorf("failed to serve SMTP: %w", err))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bridge *Bridge) GetProxyAllowed() bool {
|
||||
return bridge.vault.GetProxyAllowed()
|
||||
}
|
||||
@ -332,6 +308,9 @@ func (bridge *Bridge) SetColorScheme(colorScheme string) error {
|
||||
return bridge.vault.SetColorScheme(colorScheme)
|
||||
}
|
||||
|
||||
// FactoryReset deletes all users, wipes the vault, and deletes all files.
|
||||
// Note: it does not clear the keychain. The only entry in the keychain is the vault password,
|
||||
// which we need at next startup to decrypt the vault.
|
||||
func (bridge *Bridge) FactoryReset(ctx context.Context) {
|
||||
// Delete all the users.
|
||||
safe.Lock(func() {
|
||||
@ -348,22 +327,10 @@ func (bridge *Bridge) FactoryReset(ctx context.Context) {
|
||||
logrus.WithError(err).Error("Failed to reset vault")
|
||||
}
|
||||
|
||||
// Then delete all files.
|
||||
if err := bridge.locator.Clear(); err != nil {
|
||||
// Lastly, delete all files except the vault.
|
||||
if err := bridge.locator.Clear(bridge.vault.Path()); err != nil {
|
||||
logrus.WithError(err).Error("Failed to clear data paths")
|
||||
}
|
||||
|
||||
// Lastly clear the keychain.
|
||||
vaultDir, err := bridge.locator.ProvideSettingsPath()
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("Failed to get vault dir")
|
||||
} else if helper, err := vault.GetHelper(vaultDir); err != nil {
|
||||
logrus.WithError(err).Error("Failed to get keychain helper")
|
||||
} else if keychain, err := keychain.NewKeychain(helper, constants.KeyChainName); err != nil {
|
||||
logrus.WithError(err).Error("Failed to get keychain")
|
||||
} else if err := keychain.Clear(); err != nil {
|
||||
logrus.WithError(err).Error("Failed to clear keychain")
|
||||
}
|
||||
}
|
||||
|
||||
func getPort(addr net.Addr) int {
|
||||
|
||||
@ -22,6 +22,7 @@ import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||
@ -31,25 +32,41 @@ import (
|
||||
)
|
||||
|
||||
func (bridge *Bridge) serveSMTP() error {
|
||||
logrus.Info("Starting SMTP server")
|
||||
port, err := func() (int, error) {
|
||||
logrus.Info("Starting SMTP server")
|
||||
|
||||
smtpListener, err := newListener(bridge.vault.GetSMTPPort(), bridge.vault.GetSMTPSSL(), bridge.tlsConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create SMTP listener: %w", err)
|
||||
}
|
||||
|
||||
bridge.smtpListener = smtpListener
|
||||
|
||||
bridge.tasks.Once(func(context.Context) {
|
||||
if err := bridge.smtpServer.Serve(smtpListener); err != nil {
|
||||
logrus.WithError(err).Info("SMTP server stopped")
|
||||
smtpListener, err := newListener(bridge.vault.GetSMTPPort(), bridge.vault.GetSMTPSSL(), bridge.tlsConfig)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to create SMTP listener: %w", err)
|
||||
}
|
||||
})
|
||||
|
||||
if err := bridge.vault.SetSMTPPort(getPort(smtpListener.Addr())); err != nil {
|
||||
return fmt.Errorf("failed to store SMTP port in vault: %w", err)
|
||||
bridge.smtpListener = smtpListener
|
||||
|
||||
bridge.tasks.Once(func(context.Context) {
|
||||
if err := bridge.smtpServer.Serve(smtpListener); err != nil {
|
||||
logrus.WithError(err).Info("SMTP server stopped")
|
||||
}
|
||||
})
|
||||
|
||||
if err := bridge.vault.SetSMTPPort(getPort(smtpListener.Addr())); err != nil {
|
||||
return 0, fmt.Errorf("failed to store SMTP port in vault: %w", err)
|
||||
}
|
||||
|
||||
return getPort(smtpListener.Addr()), nil
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
bridge.publish(events.SMTPServerError{
|
||||
Error: err,
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
bridge.publish(events.SMTPServerReady{
|
||||
Port: port,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -60,6 +77,8 @@ func (bridge *Bridge) restartSMTP() error {
|
||||
return fmt.Errorf("failed to close SMTP: %w", err)
|
||||
}
|
||||
|
||||
bridge.publish(events.SMTPServerStopped{})
|
||||
|
||||
bridge.smtpServer = newSMTPServer(bridge, bridge.tlsConfig, bridge.logSMTP)
|
||||
|
||||
return bridge.serveSMTP()
|
||||
@ -82,6 +101,8 @@ func (bridge *Bridge) closeSMTP() error {
|
||||
logrus.WithError(err).Debug("Failed to close SMTP server (expected -- we close the listener ourselves)")
|
||||
}
|
||||
|
||||
bridge.publish(events.SMTPServerStopped{})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@ -431,7 +431,7 @@ func createMessages(ctx context.Context, t *testing.T, c *proton.Client, addrID,
|
||||
_, ok := addrKRs[addrID]
|
||||
require.True(t, ok)
|
||||
|
||||
res, err := stream.Collect(ctx, c.ImportMessages(
|
||||
str, err := c.ImportMessages(
|
||||
ctx,
|
||||
addrKRs[addrID],
|
||||
runtime.NumCPU(),
|
||||
@ -446,7 +446,10 @@ func createMessages(ctx context.Context, t *testing.T, c *proton.Client, addrID,
|
||||
Message: message,
|
||||
}
|
||||
})...,
|
||||
))
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err := stream.Collect(ctx, str)
|
||||
require.NoError(t, err)
|
||||
|
||||
return xslices.Map(res, func(res proton.ImportRes) string {
|
||||
|
||||
@ -18,5 +18,9 @@
|
||||
package bridge
|
||||
|
||||
func (bridge *Bridge) GetBridgeTLSCert() ([]byte, []byte) {
|
||||
return bridge.vault.GetBridgeTLSCert(), bridge.vault.GetBridgeTLSKey()
|
||||
return bridge.vault.GetBridgeTLSCert()
|
||||
}
|
||||
|
||||
func (bridge *Bridge) SetBridgeTLSCertPath(certPath, keyPath string) error {
|
||||
return bridge.vault.SetBridgeTLSCertPath(certPath, keyPath)
|
||||
}
|
||||
|
||||
@ -30,7 +30,7 @@ type Locator interface {
|
||||
ProvideGluonDataPath() (string, error)
|
||||
GetLicenseFilePath() string
|
||||
GetDependencyLicensesLink() string
|
||||
Clear() error
|
||||
Clear(...string) error
|
||||
}
|
||||
|
||||
type Identifier interface {
|
||||
|
||||
@ -32,19 +32,7 @@ func (bridge *Bridge) CheckForUpdates() {
|
||||
}
|
||||
|
||||
func (bridge *Bridge) InstallUpdate(version updater.VersionInfo) {
|
||||
log := logrus.WithFields(logrus.Fields{
|
||||
"version": version.Version,
|
||||
"current": bridge.curVersion,
|
||||
"channel": bridge.vault.GetUpdateChannel(),
|
||||
})
|
||||
|
||||
select {
|
||||
case bridge.installCh <- installJob{version: version, silent: false}:
|
||||
log.Info("The update will be installed manually")
|
||||
|
||||
default:
|
||||
log.Info("An update is already being installed")
|
||||
}
|
||||
bridge.installCh <- installJob{version: version, silent: false}
|
||||
}
|
||||
|
||||
func (bridge *Bridge) handleUpdate(version updater.VersionInfo) {
|
||||
@ -89,17 +77,7 @@ func (bridge *Bridge) handleUpdate(version updater.VersionInfo) {
|
||||
|
||||
default:
|
||||
safe.RLock(func() {
|
||||
if version.Version.GreaterThan(bridge.newVersion) {
|
||||
log.Info("An update is available")
|
||||
|
||||
select {
|
||||
case bridge.installCh <- installJob{version: version, silent: true}:
|
||||
log.Info("The update will be installed silently")
|
||||
|
||||
default:
|
||||
log.Info("An update is already being installed")
|
||||
}
|
||||
}
|
||||
bridge.installCh <- installJob{version: version, silent: true}
|
||||
}, bridge.newVersionLock)
|
||||
}
|
||||
}
|
||||
@ -117,6 +95,12 @@ func (bridge *Bridge) installUpdate(ctx context.Context, job installJob) {
|
||||
"channel": bridge.vault.GetUpdateChannel(),
|
||||
})
|
||||
|
||||
if !job.version.Version.GreaterThan(bridge.newVersion) {
|
||||
return
|
||||
}
|
||||
|
||||
log.WithField("silent", job.silent).Info("An update is available")
|
||||
|
||||
bridge.publish(events.UpdateAvailable{
|
||||
Version: job.version,
|
||||
Compatible: true,
|
||||
@ -142,6 +126,7 @@ func (bridge *Bridge) installUpdate(ctx context.Context, job installJob) {
|
||||
Silent: job.silent,
|
||||
Error: err,
|
||||
})
|
||||
|
||||
default:
|
||||
log.Info("The update was installed successfully")
|
||||
|
||||
|
||||
@ -434,6 +434,7 @@ func (bridge *Bridge) loadUser(ctx context.Context, user *vault.User) error {
|
||||
logrus.WithError(err).Warn("Failed to clear user secrets")
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("failed to create API client: %w", err)
|
||||
}
|
||||
|
||||
@ -516,8 +517,8 @@ func (bridge *Bridge) addUserWithVault(
|
||||
bridge.reporter,
|
||||
apiUser,
|
||||
bridge.crashHandler,
|
||||
bridge.vault.SyncWorkers(),
|
||||
bridge.vault.GetShowAllMail(),
|
||||
bridge.vault.GetMaxSyncMemory(),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create user: %w", err)
|
||||
|
||||
@ -20,18 +20,23 @@ package bridge_test
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/mail"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/gluon/rfc822"
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/go-proton-api/server"
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
||||
"github.com/bradenaw/juniper/stream"
|
||||
"github.com/bradenaw/juniper/xslices"
|
||||
"github.com/emersion/go-imap"
|
||||
"github.com/emersion/go-imap/client"
|
||||
@ -192,12 +197,13 @@ func TestBridge_User_BadMessage_NoBadEvent(t *testing.T) {
|
||||
|
||||
var messageIDs []string
|
||||
|
||||
// Create 10 more messages for the user, generating events.
|
||||
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||
messageIDs = createNumMessages(ctx, t, c, addrID, proton.InboxLabel, 10)
|
||||
})
|
||||
|
||||
// If bridge attempts to sync the new messages, it should get a BadRequest error.
|
||||
s.AddStatusHook(func(req *http.Request) (int, bool) {
|
||||
if len(messageIDs) < 3 {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
if strings.Contains(req.URL.Path, "/mail/v4/messages/"+messageIDs[2]) {
|
||||
return http.StatusUnprocessableEntity, true
|
||||
}
|
||||
@ -205,11 +211,6 @@ func TestBridge_User_BadMessage_NoBadEvent(t *testing.T) {
|
||||
return 0, false
|
||||
})
|
||||
|
||||
// Create 10 more messages for the user, generating events.
|
||||
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||
messageIDs = createNumMessages(ctx, t, c, addrID, proton.InboxLabel, 10)
|
||||
})
|
||||
|
||||
// Remove messages
|
||||
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||
require.NoError(t, c.DeleteMessage(ctx, messageIDs...))
|
||||
@ -374,6 +375,396 @@ func TestBridge_User_Network_NoBadEvents(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_User_DropConn_NoBadEvent(t *testing.T) {
|
||||
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
|
||||
dropListener := proton.NewListener(l, proton.NewDropConn)
|
||||
defer func() { _ = dropListener.Close() }()
|
||||
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
// Create a user.
|
||||
_, addrID, err := s.CreateUser("user", password)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create 10 messages for the user.
|
||||
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||
createNumMessages(ctx, t, c, addrID, proton.InboxLabel, 10)
|
||||
})
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
userLoginAndSync(ctx, t, bridge, "user", password)
|
||||
|
||||
mocks.Reporter.EXPECT().ReportMessageWithContext(gomock.Any(), gomock.Any()).AnyTimes()
|
||||
|
||||
// Create 10 more messages for the user, generating events.
|
||||
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||
createNumMessages(ctx, t, c, addrID, proton.InboxLabel, 10)
|
||||
})
|
||||
|
||||
var count int
|
||||
|
||||
// The first 10 times bridge attempts to sync any of the messages, drop the connection.
|
||||
s.AddStatusHook(func(req *http.Request) (int, bool) {
|
||||
if strings.Contains(req.URL.Path, "/mail/v4/messages") {
|
||||
if count++; count < 10 {
|
||||
dropListener.DropAll()
|
||||
}
|
||||
}
|
||||
|
||||
return 0, false
|
||||
})
|
||||
|
||||
info, err := bridge.QueryUserInfo("user")
|
||||
require.NoError(t, err)
|
||||
|
||||
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
|
||||
defer func() { _ = client.Logout() }()
|
||||
|
||||
// The IMAP client will eventually see 20 messages.
|
||||
require.Eventually(t, func() bool {
|
||||
status, err := client.Status("INBOX", []imap.StatusItem{imap.StatusMessages})
|
||||
return err == nil && status.Messages == 20
|
||||
}, 10*time.Second, 100*time.Millisecond)
|
||||
})
|
||||
}, server.WithListener(dropListener))
|
||||
}
|
||||
|
||||
func TestBridge_User_UpdateDraftAndCreateOtherMessage(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
// Create a bridge user.
|
||||
_, _, err := s.CreateUser("user", password)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Initially sync the user.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
userLoginAndSync(ctx, t, bridge, "user", password)
|
||||
})
|
||||
|
||||
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||
user, err := c.GetUser(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
addrs, err := c.GetAddresses(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
salts, err := c.GetSalts(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
keyPass, err := salts.SaltForKey(password, user.Keys.Primary().ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, addrKRs, err := proton.Unlock(user, addrs, keyPass)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a draft (generating a "create draft message" event).
|
||||
draft, err := c.CreateDraft(ctx, addrKRs[addrs[0].ID], proton.CreateDraftReq{
|
||||
Message: proton.DraftTemplate{
|
||||
Subject: "subject",
|
||||
Sender: &mail.Address{Name: "sender", Address: addrs[0].Email},
|
||||
Body: "body",
|
||||
MIMEType: rfc822.TextPlain,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Process those events
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
userContinueEventProcess(ctx, t, s, bridge)
|
||||
})
|
||||
|
||||
// Update the draft (generating an "update draft message" event).
|
||||
require.NoError(t, getErr(c.UpdateDraft(ctx, draft.ID, addrKRs[addrs[0].ID], proton.UpdateDraftReq{
|
||||
Message: proton.DraftTemplate{
|
||||
Subject: "subject 2",
|
||||
Sender: &mail.Address{Name: "sender", Address: addrs[0].Email},
|
||||
Body: "body 2",
|
||||
MIMEType: rfc822.TextPlain,
|
||||
},
|
||||
})))
|
||||
|
||||
// Import a message (generating a "create message" event).
|
||||
str, err := c.ImportMessages(ctx, addrKRs[addrs[0].ID], 1, 1, proton.ImportReq{
|
||||
Metadata: proton.ImportMetadata{
|
||||
AddressID: addrs[0].ID,
|
||||
Flags: proton.MessageFlagReceived,
|
||||
},
|
||||
Message: []byte("From: someone@example.com\r\nTo: blabla@example.com\r\n\r\nhello"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err := stream.Collect(ctx, str)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Process those events.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
userContinueEventProcess(ctx, t, s, bridge)
|
||||
})
|
||||
|
||||
// Update the imported message (generating an "update message" event).
|
||||
require.NoError(t, c.MarkMessagesUnread(ctx, res[0].MessageID))
|
||||
|
||||
// Process those events.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
userContinueEventProcess(ctx, t, s, bridge)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_User_SendDraftRemoveDraftFlag(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
// Create a bridge user.
|
||||
_, _, err := s.CreateUser("user", password)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Initially sync the user.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
userLoginAndSync(ctx, t, bridge, "user", password)
|
||||
})
|
||||
|
||||
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||
user, err := c.GetUser(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
addrs, err := c.GetAddresses(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
salts, err := c.GetSalts(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
keyPass, err := salts.SaltForKey(password, user.Keys.Primary().ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, addrKRs, err := proton.Unlock(user, addrs, keyPass)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a draft (generating a "create draft message" event).
|
||||
draft, err := c.CreateDraft(ctx, addrKRs[addrs[0].ID], proton.CreateDraftReq{
|
||||
Message: proton.DraftTemplate{
|
||||
Subject: "subject",
|
||||
ToList: []*mail.Address{{Address: addrs[0].Email}},
|
||||
Sender: &mail.Address{Name: "sender", Address: addrs[0].Email},
|
||||
Body: "body",
|
||||
MIMEType: rfc822.TextPlain,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Process those events
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
userContinueEventProcess(ctx, t, s, bridge)
|
||||
|
||||
info, err := bridge.QueryUserInfo("user")
|
||||
require.NoError(t, err)
|
||||
|
||||
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
|
||||
defer func() { _ = client.Logout() }()
|
||||
|
||||
messages, err := clientFetch(client, "Drafts")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, messages, 1)
|
||||
require.Contains(t, messages[0].Flags, imap.DraftFlag)
|
||||
})
|
||||
|
||||
// Send the draft (generating an "update message" event).
|
||||
{
|
||||
pubKeys, recType, err := c.GetPublicKeys(ctx, addrs[0].Email)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, recType, proton.RecipientTypeInternal)
|
||||
|
||||
var req proton.SendDraftReq
|
||||
|
||||
require.NoError(t, req.AddTextPackage(addrKRs[addrs[0].ID], "body", rfc822.TextPlain, map[string]proton.SendPreferences{
|
||||
addrs[0].Email: {
|
||||
Encrypt: true,
|
||||
PubKey: must(crypto.NewKeyRing(must(crypto.NewKeyFromArmored(pubKeys[0].PublicKey)))),
|
||||
SignatureType: proton.DetachedSignature,
|
||||
EncryptionScheme: proton.InternalScheme,
|
||||
MIMEType: rfc822.TextPlain,
|
||||
},
|
||||
}, nil))
|
||||
|
||||
require.NoError(t, getErr(c.SendDraft(ctx, draft.ID, req)))
|
||||
}
|
||||
|
||||
// Process those events; the draft will move to the sent folder and lose the draft flag.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
userContinueEventProcess(ctx, t, s, bridge)
|
||||
|
||||
info, err := bridge.QueryUserInfo("user")
|
||||
require.NoError(t, err)
|
||||
|
||||
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
|
||||
defer func() { _ = client.Logout() }()
|
||||
|
||||
messages, err := clientFetch(client, "Sent")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, messages, 1)
|
||||
require.NotContains(t, messages[0].Flags, imap.DraftFlag)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_User_DisableEnableAddress(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
// Create a user.
|
||||
userID, _, err := s.CreateUser("user", password)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create an additional address for the user.
|
||||
aliasID, err := s.CreateAddress(userID, "alias@"+s.GetDomain(), password)
|
||||
require.NoError(t, err)
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
require.NoError(t, getErr(bridge.LoginFull(ctx, "user", password, nil, nil)))
|
||||
|
||||
// Initially we should list the address.
|
||||
info, err := bridge.QueryUserInfo("user")
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, info.Addresses, "alias@"+s.GetDomain())
|
||||
})
|
||||
|
||||
// Disable the address.
|
||||
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||
require.NoError(t, c.DisableAddress(ctx, aliasID))
|
||||
})
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// Eventually we shouldn't list the address.
|
||||
require.Eventually(t, func() bool {
|
||||
info, err := bridge.QueryUserInfo("user")
|
||||
require.NoError(t, err)
|
||||
|
||||
return xslices.Index(info.Addresses, "alias@"+s.GetDomain()) < 0
|
||||
}, 5*time.Second, 100*time.Millisecond)
|
||||
})
|
||||
|
||||
// Enable the address.
|
||||
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||
require.NoError(t, c.EnableAddress(ctx, aliasID))
|
||||
})
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// Eventually we should list the address.
|
||||
require.Eventually(t, func() bool {
|
||||
info, err := bridge.QueryUserInfo("user")
|
||||
require.NoError(t, err)
|
||||
|
||||
return xslices.Index(info.Addresses, "alias@"+s.GetDomain()) >= 0
|
||||
}, 5*time.Second, 100*time.Millisecond)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_User_CreateDisabledAddress(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
// Create a user.
|
||||
userID, _, err := s.CreateUser("user", password)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create an additional address for the user.
|
||||
aliasID, err := s.CreateAddress(userID, "alias@"+s.GetDomain(), password)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Immediately disable the address.
|
||||
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||
require.NoError(t, c.DisableAddress(ctx, aliasID))
|
||||
})
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
require.NoError(t, getErr(bridge.LoginFull(ctx, "user", password, nil, nil)))
|
||||
|
||||
// Initially we shouldn't list the address.
|
||||
info, err := bridge.QueryUserInfo("user")
|
||||
require.NoError(t, err)
|
||||
require.NotContains(t, info.Addresses, "alias@"+s.GetDomain())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_User_HandleParentLabelRename(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
require.NoError(t, getErr(bridge.LoginFull(ctx, username, password, nil, nil)))
|
||||
|
||||
info, err := bridge.QueryUserInfo(username)
|
||||
require.NoError(t, err)
|
||||
|
||||
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
|
||||
defer func() { _ = client.Logout() }()
|
||||
|
||||
withClient(ctx, t, s, username, password, func(ctx context.Context, c *proton.Client) {
|
||||
parentName := uuid.NewString()
|
||||
childName := uuid.NewString()
|
||||
|
||||
// Create a folder.
|
||||
parentLabel, err := c.CreateLabel(ctx, proton.CreateLabelReq{
|
||||
Name: parentName,
|
||||
Type: proton.LabelTypeFolder,
|
||||
Color: "#f66",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Wait for the parent folder to be created.
|
||||
require.Eventually(t, func() bool {
|
||||
return xslices.IndexFunc(clientList(client), func(mailbox *imap.MailboxInfo) bool {
|
||||
return mailbox.Name == fmt.Sprintf("Folders/%v", parentName)
|
||||
}) >= 0
|
||||
}, 100*user.EventPeriod, user.EventPeriod)
|
||||
|
||||
// Create a subfolder.
|
||||
childLabel, err := c.CreateLabel(ctx, proton.CreateLabelReq{
|
||||
Name: childName,
|
||||
Type: proton.LabelTypeFolder,
|
||||
Color: "#f66",
|
||||
ParentID: parentLabel.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, parentLabel.ID, childLabel.ParentID)
|
||||
|
||||
// Wait for the parent folder to be created.
|
||||
require.Eventually(t, func() bool {
|
||||
return xslices.IndexFunc(clientList(client), func(mailbox *imap.MailboxInfo) bool {
|
||||
return mailbox.Name == fmt.Sprintf("Folders/%v/%v", parentName, childName)
|
||||
}) >= 0
|
||||
}, 100*user.EventPeriod, user.EventPeriod)
|
||||
|
||||
newParentName := uuid.NewString()
|
||||
|
||||
// Rename the parent folder.
|
||||
require.NoError(t, getErr(c.UpdateLabel(ctx, parentLabel.ID, proton.UpdateLabelReq{
|
||||
Color: "#f66",
|
||||
Name: newParentName,
|
||||
})))
|
||||
|
||||
// Wait for the parent folder to be renamed.
|
||||
require.Eventually(t, func() bool {
|
||||
return xslices.IndexFunc(clientList(client), func(mailbox *imap.MailboxInfo) bool {
|
||||
return mailbox.Name == fmt.Sprintf("Folders/%v", newParentName)
|
||||
}) >= 0
|
||||
}, 100*user.EventPeriod, user.EventPeriod)
|
||||
|
||||
// Wait for the child folder to be renamed.
|
||||
require.Eventually(t, func() bool {
|
||||
return xslices.IndexFunc(clientList(client), func(mailbox *imap.MailboxInfo) bool {
|
||||
return mailbox.Name == fmt.Sprintf("Folders/%v/%v", newParentName, childName)
|
||||
}) >= 0
|
||||
}, 100*user.EventPeriod, user.EventPeriod)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// userLoginAndSync logs in user and waits until user is fully synced.
|
||||
func userLoginAndSync(
|
||||
ctx context.Context,
|
||||
|
||||
@ -37,9 +37,14 @@ func (bridge *Bridge) handleUserEvent(ctx context.Context, user *user.User, even
|
||||
return fmt.Errorf("failed to handle user address created event: %w", err)
|
||||
}
|
||||
|
||||
case events.UserAddressUpdated:
|
||||
if err := bridge.handleUserAddressUpdated(ctx, user, event); err != nil {
|
||||
return fmt.Errorf("failed to handle user address updated event: %w", err)
|
||||
case events.UserAddressEnabled:
|
||||
if err := bridge.handleUserAddressEnabled(ctx, user, event); err != nil {
|
||||
return fmt.Errorf("failed to handle user address enabled event: %w", err)
|
||||
}
|
||||
|
||||
case events.UserAddressDisabled:
|
||||
if err := bridge.handleUserAddressDisabled(ctx, user, event); err != nil {
|
||||
return fmt.Errorf("failed to handle user address disabled event: %w", err)
|
||||
}
|
||||
|
||||
case events.UserAddressDeleted:
|
||||
@ -66,55 +71,84 @@ func (bridge *Bridge) handleUserEvent(ctx context.Context, user *user.User, even
|
||||
}
|
||||
|
||||
func (bridge *Bridge) handleUserAddressCreated(ctx context.Context, user *user.User, event events.UserAddressCreated) error {
|
||||
if user.GetAddressMode() == vault.SplitMode {
|
||||
if bridge.imapServer == nil {
|
||||
return fmt.Errorf("no imap server instance running")
|
||||
}
|
||||
if user.GetAddressMode() != vault.SplitMode {
|
||||
return nil
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
if bridge.imapServer == nil {
|
||||
return fmt.Errorf("no imap server instance running")
|
||||
}
|
||||
|
||||
if err := user.SetGluonID(event.AddressID, gluonID); err != nil {
|
||||
return fmt.Errorf("failed to set gluon ID: %w", err)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
if err := user.SetGluonID(event.AddressID, gluonID); err != nil {
|
||||
return fmt.Errorf("failed to set gluon ID: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GODT-1948: Handle addresses that have been disabled!
|
||||
func (bridge *Bridge) handleUserAddressUpdated(_ context.Context, user *user.User, _ events.UserAddressUpdated) error {
|
||||
switch user.GetAddressMode() {
|
||||
case vault.CombinedMode:
|
||||
return fmt.Errorf("not implemented")
|
||||
func (bridge *Bridge) handleUserAddressEnabled(ctx context.Context, user *user.User, event events.UserAddressEnabled) error {
|
||||
if user.GetAddressMode() != vault.SplitMode {
|
||||
return nil
|
||||
}
|
||||
|
||||
case vault.SplitMode:
|
||||
return fmt.Errorf("not implemented")
|
||||
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)
|
||||
}
|
||||
|
||||
if err := user.SetGluonID(event.AddressID, gluonID); err != nil {
|
||||
return fmt.Errorf("failed to set gluon ID: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bridge *Bridge) handleUserAddressDisabled(ctx context.Context, user *user.User, event events.UserAddressDisabled) error {
|
||||
if user.GetAddressMode() != vault.SplitMode {
|
||||
return nil
|
||||
}
|
||||
|
||||
gluonID, ok := user.GetGluonID(event.AddressID)
|
||||
if !ok {
|
||||
return fmt.Errorf("gluon ID not found for address %s", event.AddressID)
|
||||
}
|
||||
|
||||
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.RemoveGluonID(event.AddressID, gluonID); err != nil {
|
||||
return fmt.Errorf("failed to remove gluon ID for address: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bridge *Bridge) handleUserAddressDeleted(ctx context.Context, user *user.User, event events.UserAddressDeleted) error {
|
||||
if user.GetAddressMode() == vault.SplitMode {
|
||||
if bridge.imapServer == nil {
|
||||
return fmt.Errorf("no imap server instance running")
|
||||
}
|
||||
if user.GetAddressMode() != vault.SplitMode {
|
||||
return nil
|
||||
}
|
||||
|
||||
gluonID, ok := user.GetGluonID(event.AddressID)
|
||||
if !ok {
|
||||
return fmt.Errorf("gluon ID not found for address %s", event.AddressID)
|
||||
}
|
||||
if bridge.imapServer == nil {
|
||||
return fmt.Errorf("no imap server instance running")
|
||||
}
|
||||
|
||||
if err := bridge.imapServer.RemoveUser(ctx, gluonID, true); err != nil {
|
||||
return fmt.Errorf("failed to remove user from IMAP server: %w", err)
|
||||
}
|
||||
gluonID, ok := user.GetGluonID(event.AddressID)
|
||||
if !ok {
|
||||
return fmt.Errorf("gluon ID not found for address %s", event.AddressID)
|
||||
}
|
||||
|
||||
if err := user.RemoveGluonID(event.AddressID, gluonID); err != nil {
|
||||
return fmt.Errorf("failed to remove gluon ID for address: %w", err)
|
||||
}
|
||||
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.RemoveGluonID(event.AddressID, gluonID); err != nil {
|
||||
return fmt.Errorf("failed to remove gluon ID for address: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@ -20,6 +20,8 @@ package bridge_test
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@ -61,6 +63,50 @@ func TestBridge_Login(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_Login_DropConn(t *testing.T) {
|
||||
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
|
||||
dropListener := proton.NewListener(l, proton.NewDropConn)
|
||||
defer func() { _ = dropListener.Close() }()
|
||||
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// Login the user.
|
||||
userID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// The user is now connected.
|
||||
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
||||
require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge))
|
||||
})
|
||||
|
||||
// Whether to allow the user to be created.
|
||||
var allowUser bool
|
||||
|
||||
s.AddStatusHook(func(req *http.Request) (int, bool) {
|
||||
// Drop any request to the users endpoint.
|
||||
if !allowUser && req.URL.Path == "/core/v4/users" {
|
||||
dropListener.DropAll()
|
||||
}
|
||||
|
||||
// After the ping request, allow the user to be created.
|
||||
if req.URL.Path == "/tests/ping" {
|
||||
allowUser = true
|
||||
}
|
||||
|
||||
return 0, false
|
||||
})
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// The user is eventually connected.
|
||||
require.Eventually(t, func() bool {
|
||||
return len(bridge.GetUserIDs()) == 1 && len(getConnectedUserIDs(t, bridge)) == 1
|
||||
}, 5*time.Second, 100*time.Millisecond)
|
||||
})
|
||||
}, server.WithListener(dropListener))
|
||||
}
|
||||
|
||||
func TestBridge_LoginTwice(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
|
||||
Reference in New Issue
Block a user