feat(GODT-2814): Standalone Server Manager

Convert ServerManger into a standalone service so that it can become a
self contained module.
This commit is contained in:
Leander Beernaert 2023-08-07 16:47:41 +02:00
parent 1f2c573803
commit 9a96588afb
14 changed files with 647 additions and 347 deletions

View File

@ -23,7 +23,6 @@ import (
"crypto/tls"
"errors"
"fmt"
"net"
"net/http"
"sync"
"time"
@ -41,6 +40,7 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/identifier"
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
"github.com/ProtonMail/proton-bridge/v3/internal/sentry"
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapsmtpserver"
"github.com/ProtonMail/proton-bridge/v3/internal/telemetry"
"github.com/ProtonMail/proton-bridge/v3/internal/user"
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
@ -126,9 +126,7 @@ type Bridge struct {
// goHeartbeat triggers a check/sending if heartbeat is needed.
goHeartbeat func()
uidValidityGenerator imap.UIDValidityGenerator
serverManager *ServerManager
serverManager *imapsmtpserver.Service
}
// New creates a new bridge.
@ -271,13 +269,19 @@ func newBridge(
lastVersion: lastVersion,
tasks: tasks,
uidValidityGenerator: uidValidityGenerator,
serverManager: newServerManager(),
}
if err := bridge.serverManager.Init(bridge); err != nil {
bridge.serverManager = imapsmtpserver.NewService(context.Background(),
&bridgeSMTPSettings{b: bridge},
&bridgeIMAPSettings{b: bridge},
&bridgeEventPublisher{b: bridge},
panicHandler,
reporter,
uidValidityGenerator,
&bridgeIMAPSMTPTelemetry{b: bridge},
)
if err := bridge.serverManager.Init(context.Background(), bridge.tasks, &bridgeEventSubscription{b: bridge}); err != nil {
return nil, err
}
@ -528,24 +532,6 @@ func loadTLSConfig(vault *vault.Vault) (*tls.Config, error) {
}, nil
}
func newListener(port int, useTLS bool, tlsConfig *tls.Config) (net.Listener, error) {
if useTLS {
tlsListener, err := tls.Listen("tcp", fmt.Sprintf("%v:%v", constants.Host, port), tlsConfig)
if err != nil {
return nil, err
}
return tlsListener, nil
}
netListener, err := net.Listen("tcp", fmt.Sprintf("%v:%v", constants.Host, port))
if err != nil {
return nil, err
}
return netListener, nil
}
func min(a, b time.Duration) time.Duration {
if a < b {
return a

View File

@ -44,6 +44,7 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/ProtonMail/proton-bridge/v3/internal/focus"
"github.com/ProtonMail/proton-bridge/v3/internal/locations"
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapsmtpserver"
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
"github.com/ProtonMail/proton-bridge/v3/internal/user"
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
@ -701,10 +702,10 @@ func TestBridge_InitGluonDirectory(t *testing.T) {
configDir, err := b.GetGluonDataDir()
require.NoError(t, err)
_, err = os.ReadDir(bridge.ApplyGluonCachePathSuffix(b.GetGluonCacheDir()))
_, err = os.ReadDir(imapsmtpserver.ApplyGluonCachePathSuffix(b.GetGluonCacheDir()))
require.False(t, os.IsNotExist(err))
_, err = os.ReadDir(bridge.ApplyGluonConfigPathSuffix(configDir))
_, err = os.ReadDir(imapsmtpserver.ApplyGluonConfigPathSuffix(configDir))
require.False(t, os.IsNotExist(err))
})
})
@ -777,16 +778,16 @@ func TestBridge_ChangeCacheDirectory(t *testing.T) {
require.NoError(t, err)
// Old store should no more exists.
_, err = os.ReadDir(bridge.ApplyGluonCachePathSuffix(currentCacheDir))
_, err = os.ReadDir(imapsmtpserver.ApplyGluonCachePathSuffix(currentCacheDir))
require.True(t, os.IsNotExist(err))
// Database should not have changed.
_, err = os.ReadDir(bridge.ApplyGluonConfigPathSuffix(configDir))
_, err = os.ReadDir(imapsmtpserver.ApplyGluonConfigPathSuffix(configDir))
require.False(t, os.IsNotExist(err))
// New path should have Gluon sub-folder.
require.Equal(t, filepath.Join(newCacheDir, "gluon"), b.GetGluonCacheDir())
// And store should be inside it.
_, err = os.ReadDir(bridge.ApplyGluonCachePathSuffix(b.GetGluonCacheDir()))
_, err = os.ReadDir(imapsmtpserver.ApplyGluonCachePathSuffix(b.GetGluonCacheDir()))
require.False(t, os.IsNotExist(err))
// We should be able to fetch.

View File

@ -18,6 +18,8 @@
package bridge
import (
"context"
"github.com/ProtonMail/gluon/watcher"
"github.com/ProtonMail/proton-bridge/v3/internal/events"
)
@ -33,3 +35,11 @@ func (b bridgeEventSubscription) Add(ofType ...events.Event) *watcher.Watcher[ev
func (b bridgeEventSubscription) Remove(watcher *watcher.Watcher[events.Event]) {
b.b.remWatcher(watcher)
}
type bridgeEventPublisher struct {
b *Bridge
}
func (b bridgeEventPublisher) PublishEvent(_ context.Context, event events.Event) {
b.b.publish(event)
}

View File

@ -20,22 +20,12 @@ package bridge
import (
"context"
"crypto/tls"
"io"
"os"
"path/filepath"
"strings"
"github.com/Masterminds/semver/v3"
"github.com/ProtonMail/gluon"
"github.com/ProtonMail/gluon/async"
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/constants"
"github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapsmtpserver"
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
"github.com/sirupsen/logrus"
)
@ -81,108 +71,59 @@ func (bridge *Bridge) handleIMAPEvent(event imapEvents.Event) {
}
}
func ApplyGluonCachePathSuffix(basePath string) string {
return filepath.Join(basePath, "backend", "store")
type bridgeIMAPSettings struct {
b *Bridge
}
func ApplyGluonConfigPathSuffix(basePath string) string {
return filepath.Join(basePath, "backend", "db")
func (b *bridgeIMAPSettings) EventPublisher() imapsmtpserver.IMAPEventPublisher {
return b
}
func newIMAPServer(
gluonCacheDir, gluonConfigDir string,
version *semver.Version,
tlsConfig *tls.Config,
reporter reporter.Reporter,
logClient, logServer bool,
eventCh chan<- imapEvents.Event,
tasks *async.Group,
uidValidityGenerator imap.UIDValidityGenerator,
panicHandler async.PanicHandler,
) (*gluon.Server, error) {
gluonCacheDir = ApplyGluonCachePathSuffix(gluonCacheDir)
gluonConfigDir = ApplyGluonConfigPathSuffix(gluonConfigDir)
func (b *bridgeIMAPSettings) TLSConfig() *tls.Config {
return b.b.tlsConfig
}
logrus.WithFields(logrus.Fields{
"gluonStore": gluonCacheDir,
"gluonDB": gluonConfigDir,
"version": version,
"logClient": logClient,
"logServer": logServer,
}).Info("Creating IMAP server")
func (b *bridgeIMAPSettings) LogClient() bool {
return b.b.logIMAPClient
}
if logClient || logServer {
log := logrus.WithField("protocol", "IMAP")
log.Warning("================================================")
log.Warning("THIS LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA")
log.Warning("================================================")
func (b *bridgeIMAPSettings) LogServer() bool {
return b.b.logIMAPServer
}
func (b *bridgeIMAPSettings) Port() int {
return b.b.vault.GetIMAPPort()
}
func (b *bridgeIMAPSettings) SetPort(i int) error {
return b.b.vault.SetIMAPPort(i)
}
func (b *bridgeIMAPSettings) UseSSL() bool {
return b.b.vault.GetIMAPSSL()
}
func (b *bridgeIMAPSettings) CacheDirectory() string {
return b.b.GetGluonCacheDir()
}
func (b *bridgeIMAPSettings) DataDirectory() (string, error) {
return b.b.GetGluonDataDir()
}
func (b *bridgeIMAPSettings) SetCacheDirectory(s string) error {
return b.b.vault.SetGluonDir(s)
}
func (b *bridgeIMAPSettings) Version() *semver.Version {
return b.b.curVersion
}
func (b *bridgeIMAPSettings) PublishIMAPEvent(ctx context.Context, event imapEvents.Event) {
select {
case <-ctx.Done():
return
case b.b.imapEventCh <- event:
// do nothing
}
var imapClientLog io.Writer
if logClient {
imapClientLog = logging.NewIMAPLogger()
} else {
imapClientLog = io.Discard
}
var imapServerLog io.Writer
if logServer {
imapServerLog = logging.NewIMAPLogger()
} else {
imapServerLog = io.Discard
}
imapServer, err := gluon.New(
gluon.WithTLS(tlsConfig),
gluon.WithDataDir(gluonCacheDir),
gluon.WithDatabaseDir(gluonConfigDir),
gluon.WithStoreBuilder(new(storeBuilder)),
gluon.WithLogger(imapClientLog, imapServerLog),
getGluonVersionInfo(version),
gluon.WithReporter(reporter),
gluon.WithUIDValidityGenerator(uidValidityGenerator),
gluon.WithPanicHandler(panicHandler),
)
if err != nil {
return nil, err
}
tasks.Once(func(ctx context.Context) {
async.ForwardContext(ctx, eventCh, imapServer.AddWatcher())
})
tasks.Once(func(ctx context.Context) {
async.RangeContext(ctx, imapServer.GetErrorCh(), func(err error) {
logrus.WithError(err).Error("IMAP server error")
})
})
return imapServer, nil
}
func getGluonVersionInfo(version *semver.Version) gluon.Option {
return gluon.WithVersionInfo(
int(version.Major()),
int(version.Minor()),
int(version.Patch()),
constants.FullAppName,
"TODO",
"TODO",
)
}
type storeBuilder struct{}
func (*storeBuilder) New(path, userID string, passphrase []byte) (store.Store, error) {
return store.NewOnDiskStore(
filepath.Join(path, userID),
passphrase,
store.WithFallback(fallback_v0.NewOnDiskStoreV0WithCompressor(&fallback_v0.GZipCompressor{})),
)
}
func (*storeBuilder) Delete(path, userID string) error {
return os.RemoveAll(filepath.Join(path, userID))
}

View File

@ -0,0 +1,26 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package bridge
type bridgeIMAPSMTPTelemetry struct {
b *Bridge
}
func (b bridgeIMAPSMTPTelemetry) SetCacheLocation(s string) {
b.b.heartbeat.SetCacheLocation(s)
}

View File

@ -19,9 +19,6 @@ package bridge
import (
"context"
"fmt"
"net"
"os"
"github.com/Masterminds/semver/v3"
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
@ -134,23 +131,6 @@ func (bridge *Bridge) SetGluonDir(ctx context.Context, newGluonDir string) error
return bridge.serverManager.SetGluonDir(ctx, newGluonDir)
}
func (bridge *Bridge) moveGluonCacheDir(oldGluonDir, newGluonDir string) error {
logrus.Infof("gluon cache moving from %s to %s", oldGluonDir, newGluonDir)
oldCacheDir := ApplyGluonCachePathSuffix(oldGluonDir)
if err := copyDir(oldCacheDir, ApplyGluonCachePathSuffix(newGluonDir)); err != nil {
return fmt.Errorf("failed to copy gluon dir: %w", err)
}
if err := bridge.vault.SetGluonDir(newGluonDir); err != nil {
return fmt.Errorf("failed to set new gluon cache dir: %w", err)
}
if err := os.RemoveAll(oldCacheDir); err != nil {
logrus.WithError(err).Error("failed to remove old gluon cache dir")
}
return nil
}
func (bridge *Bridge) GetProxyAllowed() bool {
return bridge.vault.GetProxyAllowed()
}
@ -318,16 +298,3 @@ func (bridge *Bridge) FactoryReset(ctx context.Context) {
logrus.WithError(err).Error("Failed to clear data paths")
}
}
func getPort(addr net.Addr) int {
switch addr := addr.(type) {
case *net.TCPAddr:
return addr.Port
case *net.UDPAddr:
return addr.Port
default:
return 0
}
}

View File

@ -21,49 +21,14 @@ import (
"context"
"crypto/tls"
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
smtpservice "github.com/ProtonMail/proton-bridge/v3/internal/services/smtp"
"github.com/ProtonMail/proton-bridge/v3/internal/identifier"
"github.com/ProtonMail/proton-bridge/v3/internal/user"
"github.com/emersion/go-sasl"
"github.com/emersion/go-smtp"
"github.com/sirupsen/logrus"
)
func (bridge *Bridge) restartSMTP(ctx context.Context) error {
return bridge.serverManager.RestartSMTP(ctx)
}
func newSMTPServer(bridge *Bridge, accounts *smtpservice.Accounts, tlsConfig *tls.Config, logSMTP bool) *smtp.Server {
logrus.WithField("logSMTP", logSMTP).Info("Creating SMTP server")
smtpServer := smtp.NewServer(smtpservice.NewBackend(accounts, &bridgeUserAgentUpdater{Bridge: bridge}))
smtpServer.TLSConfig = tlsConfig
smtpServer.Domain = constants.Host
smtpServer.AllowInsecureAuth = true
smtpServer.MaxLineLength = 1 << 16
smtpServer.ErrorLog = logging.NewSMTPLogger()
// go-smtp suppors SASL PLAIN but not LOGIN. We need to add LOGIN support ourselves.
smtpServer.EnableAuth(sasl.Login, func(conn *smtp.Conn) sasl.Server {
return sasl.NewLoginServer(func(username, password string) error {
return conn.Session().AuthPlain(username, password)
})
})
if logSMTP {
log := logrus.WithField("protocol", "SMTP")
log.Warning("================================================")
log.Warning("THIS LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA")
log.Warning("================================================")
smtpServer.Debug = logging.NewSMTPDebugLogger()
}
return smtpServer
}
// addSMTPUser connects the given user to the smtp server.
func (bridge *Bridge) addSMTPUser(ctx context.Context, user *user.User) error {
return bridge.serverManager.AddSMTPAccount(ctx, user.GetSMTPService())
@ -73,3 +38,31 @@ func (bridge *Bridge) addSMTPUser(ctx context.Context, user *user.User) error {
func (bridge *Bridge) removeSMTPUser(ctx context.Context, user *user.User) error {
return bridge.serverManager.RemoveSMTPAccount(ctx, user.GetSMTPService())
}
type bridgeSMTPSettings struct {
b *Bridge
}
func (b *bridgeSMTPSettings) TLSConfig() *tls.Config {
return b.b.tlsConfig
}
func (b *bridgeSMTPSettings) Log() bool {
return b.b.logSMTP
}
func (b *bridgeSMTPSettings) Port() int {
return b.b.vault.GetSMTPPort()
}
func (b *bridgeSMTPSettings) SetPort(i int) error {
return b.b.vault.SetSMTPPort(i)
}
func (b *bridgeSMTPSettings) UseSSL() bool {
return b.b.vault.GetSMTPSSL()
}
func (b *bridgeSMTPSettings) Identifier() identifier.UserAgentUpdater {
return &bridgeUserAgentUpdater{Bridge: b.b}
}

View File

@ -15,7 +15,7 @@
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package bridge
package files
import (
"fmt"
@ -24,7 +24,7 @@ import (
"path/filepath"
)
func moveDir(from, to string) error {
func MoveDir(from, to string) error {
entries, err := os.ReadDir(from)
if err != nil {
return err
@ -36,7 +36,7 @@ func moveDir(from, to string) error {
return err
}
if err := moveDir(filepath.Join(from, entry.Name()), filepath.Join(to, entry.Name())); err != nil {
if err := MoveDir(filepath.Join(from, entry.Name()), filepath.Join(to, entry.Name())); err != nil {
return err
}
@ -61,12 +61,12 @@ func moveFile(from, to string) error {
return os.Rename(from, to)
}
func copyDir(from, to string) error {
func CopyDir(from, to string) error {
entries, err := os.ReadDir(from)
if err != nil {
return err
}
if err := createIfNotExists(to, 0o700); err != nil {
if err := CreateIfNotExists(to, 0o700); err != nil {
return err
}
for _, entry := range entries {
@ -74,11 +74,11 @@ func copyDir(from, to string) error {
destPath := filepath.Join(to, entry.Name())
if entry.IsDir() {
if err := copyDir(sourcePath, destPath); err != nil {
if err := CopyDir(sourcePath, destPath); err != nil {
return err
}
} else {
if err := copyFile(sourcePath, destPath); err != nil {
if err := CopyFile(sourcePath, destPath); err != nil {
return err
}
}
@ -86,7 +86,7 @@ func copyDir(from, to string) error {
return nil
}
func copyFile(srcFile, dstFile string) error {
func CopyFile(srcFile, dstFile string) error {
out, err := os.Create(filepath.Clean(dstFile))
defer func(out *os.File) {
_ = out.Close()
@ -113,7 +113,7 @@ func copyFile(srcFile, dstFile string) error {
return nil
}
func exists(filePath string) bool {
func Exists(filePath string) bool {
if _, err := os.Stat(filePath); os.IsNotExist(err) {
return false
}
@ -121,8 +121,8 @@ func exists(filePath string) bool {
return true
}
func createIfNotExists(dir string, perm os.FileMode) error {
if exists(dir) {
func CreateIfNotExists(dir string, perm os.FileMode) error {
if Exists(dir) {
return nil
}

View File

@ -15,7 +15,7 @@
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package bridge
package files
import (
"os"
@ -41,7 +41,7 @@ func TestMoveDir(t *testing.T) {
}
// Move the files.
if err := moveDir(from, to); err != nil {
if err := MoveDir(from, to); err != nil {
t.Fatal(err)
}

View File

@ -0,0 +1,194 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package imapsmtpserver
import (
"context"
"crypto/tls"
"fmt"
"io"
"os"
"path/filepath"
"github.com/Masterminds/semver/v3"
"github.com/ProtonMail/gluon"
"github.com/ProtonMail/gluon/async"
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/constants"
"github.com/ProtonMail/proton-bridge/v3/internal/files"
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
"github.com/sirupsen/logrus"
)
type IMAPSettingsProvider interface {
TLSConfig() *tls.Config
LogClient() bool
LogServer() bool
Port() int
SetPort(int) error
UseSSL() bool
CacheDirectory() string
DataDirectory() (string, error)
SetCacheDirectory(string) error
EventPublisher() IMAPEventPublisher
Version() *semver.Version
}
type IMAPEventPublisher interface {
PublishIMAPEvent(ctx context.Context, event imapEvents.Event)
}
func ApplyGluonCachePathSuffix(basePath string) string {
return filepath.Join(basePath, "backend", "store")
}
func ApplyGluonConfigPathSuffix(basePath string) string {
return filepath.Join(basePath, "backend", "db")
}
func newIMAPServer(
gluonCacheDir, gluonConfigDir string,
version *semver.Version,
tlsConfig *tls.Config,
reporter reporter.Reporter,
logClient, logServer bool,
eventPublisher IMAPEventPublisher,
tasks *async.Group,
uidValidityGenerator imap.UIDValidityGenerator,
panicHandler async.PanicHandler,
) (*gluon.Server, error) {
gluonCacheDir = ApplyGluonCachePathSuffix(gluonCacheDir)
gluonConfigDir = ApplyGluonConfigPathSuffix(gluonConfigDir)
logrus.WithFields(logrus.Fields{
"gluonStore": gluonCacheDir,
"gluonDB": gluonConfigDir,
"version": version,
"logClient": logClient,
"logServer": logServer,
}).Info("Creating IMAP server")
if logClient || logServer {
log := logrus.WithField("protocol", "IMAP")
log.Warning("================================================")
log.Warning("THIS LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA")
log.Warning("================================================")
}
var imapClientLog io.Writer
if logClient {
imapClientLog = logging.NewIMAPLogger()
} else {
imapClientLog = io.Discard
}
var imapServerLog io.Writer
if logServer {
imapServerLog = logging.NewIMAPLogger()
} else {
imapServerLog = io.Discard
}
imapServer, err := gluon.New(
gluon.WithTLS(tlsConfig),
gluon.WithDataDir(gluonCacheDir),
gluon.WithDatabaseDir(gluonConfigDir),
gluon.WithStoreBuilder(new(storeBuilder)),
gluon.WithLogger(imapClientLog, imapServerLog),
getGluonVersionInfo(version),
gluon.WithReporter(reporter),
gluon.WithUIDValidityGenerator(uidValidityGenerator),
gluon.WithPanicHandler(panicHandler),
)
if err != nil {
return nil, err
}
tasks.Once(func(ctx context.Context) {
watcher := imapServer.AddWatcher()
for {
select {
case <-ctx.Done():
return
case e, ok := <-watcher:
if !ok {
return
}
eventPublisher.PublishIMAPEvent(ctx, e)
}
}
})
tasks.Once(func(ctx context.Context) {
async.RangeContext(ctx, imapServer.GetErrorCh(), func(err error) {
logrus.WithError(err).Error("IMAP server error")
})
})
return imapServer, nil
}
func getGluonVersionInfo(version *semver.Version) gluon.Option {
return gluon.WithVersionInfo(
int(version.Major()),
int(version.Minor()),
int(version.Patch()),
constants.FullAppName,
"TODO",
"TODO",
)
}
type storeBuilder struct{}
func (*storeBuilder) New(path, userID string, passphrase []byte) (store.Store, error) {
return store.NewOnDiskStore(
filepath.Join(path, userID),
passphrase,
store.WithFallback(fallback_v0.NewOnDiskStoreV0WithCompressor(&fallback_v0.GZipCompressor{})),
)
}
func (*storeBuilder) Delete(path, userID string) error {
return os.RemoveAll(filepath.Join(path, userID))
}
func moveGluonCacheDir(settings IMAPSettingsProvider, oldGluonDir, newGluonDir string) error {
logrus.Infof("gluon cache moving from %s to %s", oldGluonDir, newGluonDir)
oldCacheDir := ApplyGluonCachePathSuffix(oldGluonDir)
if err := files.CopyDir(oldCacheDir, ApplyGluonCachePathSuffix(newGluonDir)); err != nil {
return fmt.Errorf("failed to copy gluon dir: %w", err)
}
if err := settings.SetCacheDirectory(newGluonDir); err != nil {
return fmt.Errorf("failed to set new gluon cache dir: %w", err)
}
if err := os.RemoveAll(oldCacheDir); err != nil {
logrus.WithError(err).Error("failed to remove old gluon cache dir")
}
return nil
}

View File

@ -0,0 +1,57 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package imapsmtpserver
import (
"crypto/tls"
"fmt"
"net"
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
)
func newListener(port int, useTLS bool, tlsConfig *tls.Config) (net.Listener, error) {
if useTLS {
tlsListener, err := tls.Listen("tcp", fmt.Sprintf("%v:%v", constants.Host, port), tlsConfig)
if err != nil {
return nil, err
}
return tlsListener, nil
}
netListener, err := net.Listen("tcp", fmt.Sprintf("%v:%v", constants.Host, port))
if err != nil {
return nil, err
}
return netListener, nil
}
func getPort(addr net.Addr) int {
switch addr := addr.(type) {
case *net.TCPAddr:
return addr.Port
case *net.UDPAddr:
return addr.Port
default:
return 0
}
}

View File

@ -15,7 +15,7 @@
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package bridge
package imapsmtpserver
import (
"context"
@ -24,10 +24,12 @@ import (
"path/filepath"
"github.com/ProtonMail/gluon"
"github.com/ProtonMail/gluon/async"
"github.com/ProtonMail/gluon/connector"
"github.com/ProtonMail/gluon/imap"
"github.com/ProtonMail/gluon/logging"
"github.com/ProtonMail/gluon/reporter"
"github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice"
bridgesmtp "github.com/ProtonMail/proton-bridge/v3/internal/services/smtp"
"github.com/ProtonMail/proton-bridge/v3/pkg/cpc"
@ -35,8 +37,8 @@ import (
"github.com/sirupsen/logrus"
)
// ServerManager manages the IMAP & SMTP servers and their listeners.
type ServerManager struct {
// Service manages the IMAP & SMTP servers and their listeners.
type Service struct {
requests *cpc.CPC
imapServer *gluon.Server
@ -46,30 +48,60 @@ type ServerManager struct {
smtpListener net.Listener
smtpAccounts *bridgesmtp.Accounts
smtpSettings SMTPSettingsProvider
imapSettings IMAPSettingsProvider
eventPublisher events.EventPublisher
panicHandler async.PanicHandler
reporter reporter.Reporter
loadedUserCount int
log *logrus.Entry
tasks *async.Group
uidValidityGenerator imap.UIDValidityGenerator
telemetry Telemetry
}
func newServerManager() *ServerManager {
return &ServerManager{
func NewService(
ctx context.Context,
smtpSettings SMTPSettingsProvider,
imapSettings IMAPSettingsProvider,
eventPublisher events.EventPublisher,
panicHandler async.PanicHandler,
reporter reporter.Reporter,
uidValidityGenerator imap.UIDValidityGenerator,
telemetry Telemetry,
) *Service {
return &Service{
requests: cpc.NewCPC(),
smtpAccounts: bridgesmtp.NewAccounts(),
panicHandler: panicHandler,
reporter: reporter,
smtpSettings: smtpSettings,
imapSettings: imapSettings,
eventPublisher: eventPublisher,
log: logrus.WithField("service", "server-manager"),
tasks: async.NewGroup(ctx, panicHandler),
uidValidityGenerator: uidValidityGenerator,
telemetry: telemetry,
}
}
func (sm *ServerManager) Init(bridge *Bridge) error {
imapServer, err := createIMAPServer(bridge)
func (sm *Service) Init(ctx context.Context, group *async.Group, subscription events.Subscription) error {
imapServer, err := sm.createIMAPServer(ctx)
if err != nil {
return err
}
smtpServer := createSMTPServer(bridge, sm.smtpAccounts)
smtpServer := sm.createSMTPServer()
sm.imapServer = imapServer
sm.smtpServer = smtpServer
bridge.tasks.Once(func(ctx context.Context) {
group.Once(func(ctx context.Context) {
logging.DoAnnotated(ctx, func(ctx context.Context) {
sm.run(ctx, bridge)
sm.run(ctx, subscription)
}, logging.Labels{
"service": "server-manager",
})
@ -78,26 +110,26 @@ func (sm *ServerManager) Init(bridge *Bridge) error {
return nil
}
func (sm *ServerManager) CloseServers(ctx context.Context) error {
func (sm *Service) CloseServers(ctx context.Context) error {
defer sm.requests.Close()
_, err := sm.requests.Send(ctx, &smRequestClose{})
return err
}
func (sm *ServerManager) RestartIMAP(ctx context.Context) error {
func (sm *Service) RestartIMAP(ctx context.Context) error {
_, err := sm.requests.Send(ctx, &smRequestRestartIMAP{})
return err
}
func (sm *ServerManager) RestartSMTP(ctx context.Context) error {
func (sm *Service) RestartSMTP(ctx context.Context) error {
_, err := sm.requests.Send(ctx, &smRequestRestartSMTP{})
return err
}
func (sm *ServerManager) AddIMAPUser(
func (sm *Service) AddIMAPUser(
ctx context.Context,
connector connector.Connector,
addrID string,
@ -114,7 +146,7 @@ func (sm *ServerManager) AddIMAPUser(
return err
}
func (sm *ServerManager) SetGluonDir(ctx context.Context, gluonDir string) error {
func (sm *Service) SetGluonDir(ctx context.Context, gluonDir string) error {
_, err := sm.requests.Send(ctx, &smRequestSetGluonDir{
dir: gluonDir,
})
@ -122,7 +154,7 @@ func (sm *ServerManager) SetGluonDir(ctx context.Context, gluonDir string) error
return err
}
func (sm *ServerManager) RemoveIMAPUser(ctx context.Context, deleteData bool, provider imapservice.GluonIDProvider, addrID ...string) error {
func (sm *Service) RemoveIMAPUser(ctx context.Context, deleteData bool, provider imapservice.GluonIDProvider, addrID ...string) error {
_, err := sm.requests.Send(ctx, &smRequestRemoveIMAPUser{
withData: deleteData,
addrID: addrID,
@ -132,42 +164,42 @@ func (sm *ServerManager) RemoveIMAPUser(ctx context.Context, deleteData bool, pr
return err
}
func (sm *ServerManager) AddSMTPAccount(ctx context.Context, service *bridgesmtp.Service) error {
func (sm *Service) AddSMTPAccount(ctx context.Context, service *bridgesmtp.Service) error {
_, err := sm.requests.Send(ctx, &smRequestAddSMTPAccount{account: service})
return err
}
func (sm *ServerManager) RemoveSMTPAccount(ctx context.Context, service *bridgesmtp.Service) error {
func (sm *Service) RemoveSMTPAccount(ctx context.Context, service *bridgesmtp.Service) error {
_, err := sm.requests.Send(ctx, &smRequestRemoveSMTPAccount{account: service})
return err
}
func (sm *ServerManager) run(ctx context.Context, bridge *Bridge) {
eventCh, cancel := bridge.GetEvents()
defer cancel()
func (sm *Service) run(ctx context.Context, subscription events.Subscription) {
eventSub := subscription.Add()
defer subscription.Remove(eventSub)
for {
select {
case <-ctx.Done():
sm.handleClose(ctx, bridge)
sm.handleClose(ctx)
return
case evt := <-eventCh:
case evt := <-eventSub.GetChannel():
switch evt.(type) {
case events.ConnStatusDown:
logrus.Info("Server Manager, network down stopping listeners")
if err := sm.closeSMTPServer(bridge); err != nil {
if err := sm.closeSMTPServer(ctx); err != nil {
logrus.WithError(err).Error("Failed to close SMTP server")
}
if err := sm.stopIMAPListener(bridge); err != nil {
if err := sm.stopIMAPListener(ctx); err != nil {
logrus.WithError(err)
}
case events.ConnStatusUp:
logrus.Info("Server Manager, network up starting listeners")
sm.handleLoadedUserCountChange(ctx, bridge)
sm.handleLoadedUserCountChange(ctx)
}
case request, ok := <-sm.requests.ReceiveCh():
@ -177,34 +209,34 @@ func (sm *ServerManager) run(ctx context.Context, bridge *Bridge) {
switch r := request.Value().(type) {
case *smRequestClose:
sm.handleClose(ctx, bridge)
sm.handleClose(ctx)
request.Reply(ctx, nil, nil)
return
case *smRequestRestartSMTP:
err := sm.restartSMTP(bridge)
err := sm.restartSMTP(ctx)
request.Reply(ctx, nil, err)
case *smRequestRestartIMAP:
err := sm.restartIMAP(ctx, bridge)
err := sm.restartIMAP(ctx)
request.Reply(ctx, nil, err)
case *smRequestAddIMAPUser:
err := sm.handleAddIMAPUser(ctx, r.connector, r.addrID, r.idProvider, r.syncStateProvider)
request.Reply(ctx, nil, err)
if err == nil {
sm.handleLoadedUserCountChange(ctx, bridge)
sm.handleLoadedUserCountChange(ctx)
}
case *smRequestRemoveIMAPUser:
err := sm.handleRemoveIMAPUser(ctx, r.withData, r.idProvider, r.addrID...)
request.Reply(ctx, nil, err)
if err == nil {
sm.handleLoadedUserCountChange(ctx, bridge)
sm.handleLoadedUserCountChange(ctx)
}
case *smRequestSetGluonDir:
err := sm.handleSetGluonDir(ctx, bridge, r.dir)
err := sm.handleSetGluonDir(ctx, r.dir)
request.Reply(ctx, nil, err)
case *smRequestAddSMTPAccount:
@ -221,48 +253,50 @@ func (sm *ServerManager) run(ctx context.Context, bridge *Bridge) {
}
}
func (sm *ServerManager) handleLoadedUserCountChange(ctx context.Context, bridge *Bridge) {
func (sm *Service) handleLoadedUserCountChange(ctx context.Context) {
logrus.Infof("Validating Listener State %v", sm.loadedUserCount)
if sm.shouldStartServers() {
if sm.imapListener == nil {
if err := sm.serveIMAP(ctx, bridge); err != nil {
if err := sm.serveIMAP(ctx); err != nil {
logrus.WithError(err).Error("Failed to start IMAP server")
}
}
if sm.smtpListener == nil {
if err := sm.restartSMTP(bridge); err != nil {
if err := sm.restartSMTP(ctx); err != nil {
logrus.WithError(err).Error("Failed to start SMTP server")
}
}
} else {
if sm.imapListener != nil {
if err := sm.stopIMAPListener(bridge); err != nil {
if err := sm.stopIMAPListener(ctx); err != nil {
logrus.WithError(err).Error("Failed to stop IMAP server")
}
}
if sm.smtpListener != nil {
if err := sm.closeSMTPServer(bridge); err != nil {
if err := sm.closeSMTPServer(ctx); err != nil {
logrus.WithError(err).Error("Failed to stop SMTP server")
}
}
}
}
func (sm *ServerManager) handleClose(ctx context.Context, bridge *Bridge) {
func (sm *Service) handleClose(ctx context.Context) {
sm.tasks.Cancel()
// Close the IMAP server.
if err := sm.closeIMAPServer(ctx, bridge); err != nil {
if err := sm.closeIMAPServer(ctx); err != nil {
logrus.WithError(err).Error("Failed to close IMAP server")
}
// Close the SMTP server.
if err := sm.closeSMTPServer(bridge); err != nil {
if err := sm.closeSMTPServer(ctx); err != nil {
logrus.WithError(err).Error("Failed to close SMTP server")
}
}
func (sm *ServerManager) handleAddIMAPUser(ctx context.Context,
func (sm *Service) handleAddIMAPUser(ctx context.Context,
connector connector.Connector,
addrID string,
idProvider imapservice.GluonIDProvider,
@ -278,7 +312,7 @@ func (sm *ServerManager) handleAddIMAPUser(ctx context.Context,
return err
}
func (sm *ServerManager) handleAddIMAPUserImpl(ctx context.Context,
func (sm *Service) handleAddIMAPUserImpl(ctx context.Context,
connector connector.Connector,
addrID string,
idProvider imapservice.GluonIDProvider,
@ -361,7 +395,7 @@ func (sm *ServerManager) handleAddIMAPUserImpl(ctx context.Context,
return nil
}
func (sm *ServerManager) handleRemoveIMAPUser(ctx context.Context, withData bool, idProvider imapservice.GluonIDProvider, addrIDs ...string) error {
func (sm *Service) handleRemoveIMAPUser(ctx context.Context, withData bool, idProvider imapservice.GluonIDProvider, addrIDs ...string) error {
if sm.imapServer == nil {
return fmt.Errorf("no imap server instance running")
}
@ -394,37 +428,37 @@ func (sm *ServerManager) handleRemoveIMAPUser(ctx context.Context, withData bool
return nil
}
func createIMAPServer(bridge *Bridge) (*gluon.Server, error) {
gluonDataDir, err := bridge.GetGluonDataDir()
func (sm *Service) createIMAPServer(ctx context.Context) (*gluon.Server, error) {
gluonDataDir, err := sm.imapSettings.DataDirectory()
if err != nil {
return nil, fmt.Errorf("failed to get Gluon Database directory: %w", err)
}
server, err := newIMAPServer(
bridge.vault.GetGluonCacheDir(),
sm.imapSettings.CacheDirectory(),
gluonDataDir,
bridge.curVersion,
bridge.tlsConfig,
bridge.reporter,
bridge.logIMAPClient,
bridge.logIMAPServer,
bridge.imapEventCh,
bridge.tasks,
bridge.uidValidityGenerator,
bridge.panicHandler,
sm.imapSettings.Version(),
sm.imapSettings.TLSConfig(),
sm.reporter,
sm.imapSettings.LogClient(),
sm.imapSettings.LogServer(),
sm.imapSettings.EventPublisher(),
sm.tasks,
sm.uidValidityGenerator,
sm.panicHandler,
)
if err == nil {
bridge.publish(events.IMAPServerCreated{})
sm.eventPublisher.PublishEvent(ctx, events.IMAPServerCreated{})
}
return server, err
}
func createSMTPServer(bridge *Bridge, accounts *bridgesmtp.Accounts) *smtp.Server {
return newSMTPServer(bridge, accounts, bridge.tlsConfig, bridge.logSMTP)
func (sm *Service) createSMTPServer() *smtp.Server {
return newSMTPServer(sm.smtpAccounts, sm.smtpSettings)
}
func (sm *ServerManager) closeSMTPServer(bridge *Bridge) error {
func (sm *Service) closeSMTPServer(ctx context.Context) error {
// We close the listener ourselves even though it's also closed by smtpServer.Close().
// This is because smtpServer.Serve() is called in a separate goroutine and might be executed
// after we've already closed the server. However, go-smtp has a bug; it blocks on the listener
@ -447,13 +481,13 @@ func (sm *ServerManager) closeSMTPServer(bridge *Bridge) error {
sm.smtpServer = nil
bridge.publish(events.SMTPServerStopped{})
sm.eventPublisher.PublishEvent(ctx, events.SMTPServerStopped{})
}
return nil
}
func (sm *ServerManager) closeIMAPServer(ctx context.Context, bridge *Bridge) error {
func (sm *Service) closeIMAPServer(ctx context.Context) error {
if sm.imapListener != nil {
logrus.Info("Closing IMAP Listener")
@ -463,7 +497,7 @@ func (sm *ServerManager) closeIMAPServer(ctx context.Context, bridge *Bridge) er
sm.imapListener = nil
bridge.publish(events.IMAPServerStopped{})
sm.eventPublisher.PublishEvent(ctx, events.IMAPServerStopped{})
}
if sm.imapServer != nil {
@ -474,13 +508,13 @@ func (sm *ServerManager) closeIMAPServer(ctx context.Context, bridge *Bridge) er
sm.imapServer = nil
bridge.publish(events.IMAPServerClosed{})
sm.eventPublisher.PublishEvent(ctx, events.IMAPServerClosed{})
}
return nil
}
func (sm *ServerManager) restartIMAP(ctx context.Context, bridge *Bridge) error {
func (sm *Service) restartIMAP(ctx context.Context) error {
logrus.Info("Restarting IMAP server")
if sm.imapListener != nil {
@ -490,55 +524,55 @@ func (sm *ServerManager) restartIMAP(ctx context.Context, bridge *Bridge) error
sm.imapListener = nil
bridge.publish(events.IMAPServerStopped{})
sm.eventPublisher.PublishEvent(ctx, events.IMAPServerStopped{})
}
if sm.shouldStartServers() {
return sm.serveIMAP(ctx, bridge)
return sm.serveIMAP(ctx)
}
return nil
}
func (sm *ServerManager) restartSMTP(bridge *Bridge) error {
func (sm *Service) restartSMTP(ctx context.Context) error {
logrus.Info("Restarting SMTP server")
if err := sm.closeSMTPServer(bridge); err != nil {
if err := sm.closeSMTPServer(ctx); err != nil {
return fmt.Errorf("failed to close SMTP: %w", err)
}
bridge.publish(events.SMTPServerStopped{})
sm.eventPublisher.PublishEvent(ctx, events.SMTPServerStopped{})
sm.smtpServer = newSMTPServer(bridge, sm.smtpAccounts, bridge.tlsConfig, bridge.logSMTP)
sm.smtpServer = newSMTPServer(sm.smtpAccounts, sm.smtpSettings)
if sm.shouldStartServers() {
return sm.serveSMTP(bridge)
return sm.serveSMTP(ctx)
}
return nil
}
func (sm *ServerManager) serveSMTP(bridge *Bridge) error {
func (sm *Service) serveSMTP(ctx context.Context) error {
port, err := func() (int, error) {
logrus.WithFields(logrus.Fields{
"port": bridge.vault.GetSMTPPort(),
"ssl": bridge.vault.GetSMTPSSL(),
"port": sm.smtpSettings.Port(),
"ssl": sm.smtpSettings.UseSSL(),
}).Info("Starting SMTP server")
smtpListener, err := newListener(bridge.vault.GetSMTPPort(), bridge.vault.GetSMTPSSL(), bridge.tlsConfig)
smtpListener, err := newListener(sm.smtpSettings.Port(), sm.smtpSettings.UseSSL(), sm.smtpSettings.TLSConfig())
if err != nil {
return 0, fmt.Errorf("failed to create SMTP listener: %w", err)
}
sm.smtpListener = smtpListener
bridge.tasks.Once(func(context.Context) {
sm.tasks.Once(func(context.Context) {
if err := sm.smtpServer.Serve(smtpListener); err != nil {
logrus.WithError(err).Info("SMTP server stopped")
}
})
if err := bridge.vault.SetSMTPPort(getPort(smtpListener.Addr())); err != nil {
if err := sm.smtpSettings.SetPort(getPort(smtpListener.Addr())); err != nil {
return 0, fmt.Errorf("failed to store SMTP port in vault: %w", err)
}
@ -546,32 +580,32 @@ func (sm *ServerManager) serveSMTP(bridge *Bridge) error {
}()
if err != nil {
bridge.publish(events.SMTPServerError{
sm.eventPublisher.PublishEvent(ctx, events.SMTPServerError{
Error: err,
})
return err
}
bridge.publish(events.SMTPServerReady{
sm.eventPublisher.PublishEvent(ctx, events.SMTPServerReady{
Port: port,
})
return nil
}
func (sm *ServerManager) serveIMAP(ctx context.Context, bridge *Bridge) error {
func (sm *Service) serveIMAP(ctx context.Context) error {
port, err := func() (int, error) {
if sm.imapServer == nil {
return 0, fmt.Errorf("no IMAP server instance running")
}
logrus.WithFields(logrus.Fields{
"port": bridge.vault.GetIMAPPort(),
"ssl": bridge.vault.GetIMAPSSL(),
"port": sm.imapSettings.Port(),
"ssl": sm.imapSettings.UseSSL(),
}).Info("Starting IMAP server")
imapListener, err := newListener(bridge.vault.GetIMAPPort(), bridge.vault.GetIMAPSSL(), bridge.tlsConfig)
imapListener, err := newListener(sm.imapSettings.Port(), sm.imapSettings.UseSSL(), sm.imapSettings.TLSConfig())
if err != nil {
return 0, fmt.Errorf("failed to create IMAP listener: %w", err)
}
@ -582,7 +616,7 @@ func (sm *ServerManager) serveIMAP(ctx context.Context, bridge *Bridge) error {
return 0, fmt.Errorf("failed to serve IMAP: %w", err)
}
if err := bridge.vault.SetIMAPPort(getPort(imapListener.Addr())); err != nil {
if err := sm.imapSettings.SetPort(getPort(imapListener.Addr())); err != nil {
return 0, fmt.Errorf("failed to store IMAP port in vault: %w", err)
}
@ -590,21 +624,21 @@ func (sm *ServerManager) serveIMAP(ctx context.Context, bridge *Bridge) error {
}()
if err != nil {
bridge.publish(events.IMAPServerError{
sm.eventPublisher.PublishEvent(ctx, events.IMAPServerError{
Error: err,
})
return err
}
bridge.publish(events.IMAPServerReady{
sm.eventPublisher.PublishEvent(ctx, events.IMAPServerReady{
Port: port,
})
return nil
}
func (sm *ServerManager) stopIMAPListener(bridge *Bridge) error {
func (sm *Service) stopIMAPListener(ctx context.Context) error {
logrus.Info("Stopping IMAP listener")
if sm.imapListener != nil {
if err := sm.imapListener.Close(); err != nil {
@ -613,54 +647,54 @@ func (sm *ServerManager) stopIMAPListener(bridge *Bridge) error {
sm.imapListener = nil
bridge.publish(events.IMAPServerStopped{})
sm.eventPublisher.PublishEvent(ctx, events.IMAPServerStopped{})
}
return nil
}
func (sm *ServerManager) handleSetGluonDir(ctx context.Context, bridge *Bridge, newGluonDir string) error {
return safe.RLockRet(func() error {
currentGluonDir := bridge.GetGluonCacheDir()
newGluonDir = filepath.Join(newGluonDir, "gluon")
if newGluonDir == currentGluonDir {
return fmt.Errorf("new gluon dir is the same as the old one")
func (sm *Service) handleSetGluonDir(ctx context.Context, newGluonDir string) error {
currentGluonDir := sm.imapSettings.CacheDirectory()
newGluonDir = filepath.Join(newGluonDir, "gluon")
if newGluonDir == currentGluonDir {
return fmt.Errorf("new gluon dir is the same as the old one")
}
if err := sm.closeIMAPServer(ctx); err != nil {
return fmt.Errorf("failed to close IMAP: %w", err)
}
sm.loadedUserCount = 0
if err := moveGluonCacheDir(sm.imapSettings, currentGluonDir, newGluonDir); err != nil {
logrus.WithError(err).Error("failed to move GluonCacheDir")
if err := sm.imapSettings.SetCacheDirectory(currentGluonDir); err != nil {
return fmt.Errorf("failed to revert GluonCacheDir: %w", err)
}
if err := sm.closeIMAPServer(context.Background(), bridge); err != nil {
return fmt.Errorf("failed to close IMAP: %w", err)
return err
}
sm.telemetry.SetCacheLocation(newGluonDir)
imapServer, err := sm.createIMAPServer(ctx)
if err != nil {
return fmt.Errorf("failed to create new IMAP server: %w", err)
}
sm.imapServer = imapServer
if sm.shouldStartServers() {
if err := sm.serveIMAP(ctx); err != nil {
return fmt.Errorf("failed to serve IMAP: %w", err)
}
}
sm.loadedUserCount = 0
if err := bridge.moveGluonCacheDir(currentGluonDir, newGluonDir); err != nil {
logrus.WithError(err).Error("failed to move GluonCacheDir")
if err := bridge.vault.SetGluonDir(currentGluonDir); err != nil {
return fmt.Errorf("failed to revert GluonCacheDir: %w", err)
}
}
bridge.heartbeat.SetCacheLocation(newGluonDir)
imapServer, err := createIMAPServer(bridge)
if err != nil {
return fmt.Errorf("failed to create new IMAP server: %w", err)
}
sm.imapServer = imapServer
if sm.shouldStartServers() {
if err := sm.serveIMAP(ctx, bridge); err != nil {
return fmt.Errorf("failed to serve IMAP: %w", err)
}
}
return nil
}, bridge.usersLock)
return nil
}
func (sm *ServerManager) shouldStartServers() bool {
func (sm *Service) shouldStartServers() bool {
return sm.loadedUserCount >= 1
}

View File

@ -0,0 +1,69 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package imapsmtpserver
import (
"crypto/tls"
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/ProtonMail/proton-bridge/v3/internal/identifier"
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
smtpservice "github.com/ProtonMail/proton-bridge/v3/internal/services/smtp"
"github.com/emersion/go-sasl"
"github.com/emersion/go-smtp"
"github.com/sirupsen/logrus"
)
type SMTPSettingsProvider interface {
TLSConfig() *tls.Config
Log() bool
Port() int
SetPort(int) error
UseSSL() bool
Identifier() identifier.UserAgentUpdater
}
func newSMTPServer(accounts *smtpservice.Accounts, settings SMTPSettingsProvider) *smtp.Server {
logrus.WithField("logSMTP", settings.Log()).Info("Creating SMTP server")
smtpServer := smtp.NewServer(smtpservice.NewBackend(accounts, settings.Identifier()))
smtpServer.TLSConfig = settings.TLSConfig()
smtpServer.Domain = constants.Host
smtpServer.AllowInsecureAuth = true
smtpServer.MaxLineLength = 1 << 16
smtpServer.ErrorLog = logging.NewSMTPLogger()
// go-smtp suppors SASL PLAIN but not LOGIN. We need to add LOGIN support ourselves.
smtpServer.EnableAuth(sasl.Login, func(conn *smtp.Conn) sasl.Server {
return sasl.NewLoginServer(func(username, password string) error {
return conn.Session().AuthPlain(username, password)
})
})
if settings.Log() {
log := logrus.WithField("protocol", "SMTP")
log.Warning("================================================")
log.Warning("THIS LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA")
log.Warning("================================================")
smtpServer.Debug = logging.NewSMTPDebugLogger()
}
return smtpServer
}

View File

@ -0,0 +1,22 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package imapsmtpserver
type Telemetry interface {
SetCacheLocation(string)
}