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:
@ -23,7 +23,6 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@ -41,6 +40,7 @@ import (
|
|||||||
"github.com/ProtonMail/proton-bridge/v3/internal/identifier"
|
"github.com/ProtonMail/proton-bridge/v3/internal/identifier"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/sentry"
|
"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/telemetry"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
"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 triggers a check/sending if heartbeat is needed.
|
||||||
goHeartbeat func()
|
goHeartbeat func()
|
||||||
|
|
||||||
uidValidityGenerator imap.UIDValidityGenerator
|
serverManager *imapsmtpserver.Service
|
||||||
|
|
||||||
serverManager *ServerManager
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new bridge.
|
// New creates a new bridge.
|
||||||
@ -271,13 +269,19 @@ func newBridge(
|
|||||||
lastVersion: lastVersion,
|
lastVersion: lastVersion,
|
||||||
|
|
||||||
tasks: tasks,
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -528,24 +532,6 @@ func loadTLSConfig(vault *vault.Vault) (*tls.Config, error) {
|
|||||||
}, nil
|
}, 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 {
|
func min(a, b time.Duration) time.Duration {
|
||||||
if a < b {
|
if a < b {
|
||||||
return a
|
return a
|
||||||
|
|||||||
@ -44,6 +44,7 @@ import (
|
|||||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/focus"
|
"github.com/ProtonMail/proton-bridge/v3/internal/focus"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/locations"
|
"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/updater"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
|
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
|
||||||
@ -701,10 +702,10 @@ func TestBridge_InitGluonDirectory(t *testing.T) {
|
|||||||
configDir, err := b.GetGluonDataDir()
|
configDir, err := b.GetGluonDataDir()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
_, err = os.ReadDir(bridge.ApplyGluonCachePathSuffix(b.GetGluonCacheDir()))
|
_, err = os.ReadDir(imapsmtpserver.ApplyGluonCachePathSuffix(b.GetGluonCacheDir()))
|
||||||
require.False(t, os.IsNotExist(err))
|
require.False(t, os.IsNotExist(err))
|
||||||
|
|
||||||
_, err = os.ReadDir(bridge.ApplyGluonConfigPathSuffix(configDir))
|
_, err = os.ReadDir(imapsmtpserver.ApplyGluonConfigPathSuffix(configDir))
|
||||||
require.False(t, os.IsNotExist(err))
|
require.False(t, os.IsNotExist(err))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -777,16 +778,16 @@ func TestBridge_ChangeCacheDirectory(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Old store should no more exists.
|
// Old store should no more exists.
|
||||||
_, err = os.ReadDir(bridge.ApplyGluonCachePathSuffix(currentCacheDir))
|
_, err = os.ReadDir(imapsmtpserver.ApplyGluonCachePathSuffix(currentCacheDir))
|
||||||
require.True(t, os.IsNotExist(err))
|
require.True(t, os.IsNotExist(err))
|
||||||
// Database should not have changed.
|
// Database should not have changed.
|
||||||
_, err = os.ReadDir(bridge.ApplyGluonConfigPathSuffix(configDir))
|
_, err = os.ReadDir(imapsmtpserver.ApplyGluonConfigPathSuffix(configDir))
|
||||||
require.False(t, os.IsNotExist(err))
|
require.False(t, os.IsNotExist(err))
|
||||||
|
|
||||||
// New path should have Gluon sub-folder.
|
// New path should have Gluon sub-folder.
|
||||||
require.Equal(t, filepath.Join(newCacheDir, "gluon"), b.GetGluonCacheDir())
|
require.Equal(t, filepath.Join(newCacheDir, "gluon"), b.GetGluonCacheDir())
|
||||||
// And store should be inside it.
|
// 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))
|
require.False(t, os.IsNotExist(err))
|
||||||
|
|
||||||
// We should be able to fetch.
|
// We should be able to fetch.
|
||||||
|
|||||||
@ -18,6 +18,8 @@
|
|||||||
package bridge
|
package bridge
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
"github.com/ProtonMail/gluon/watcher"
|
"github.com/ProtonMail/gluon/watcher"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
"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]) {
|
func (b bridgeEventSubscription) Remove(watcher *watcher.Watcher[events.Event]) {
|
||||||
b.b.remWatcher(watcher)
|
b.b.remWatcher(watcher)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type bridgeEventPublisher struct {
|
||||||
|
b *Bridge
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b bridgeEventPublisher) PublishEvent(_ context.Context, event events.Event) {
|
||||||
|
b.b.publish(event)
|
||||||
|
}
|
||||||
|
|||||||
@ -20,22 +20,12 @@ package bridge
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/Masterminds/semver/v3"
|
"github.com/Masterminds/semver/v3"
|
||||||
"github.com/ProtonMail/gluon"
|
|
||||||
"github.com/ProtonMail/gluon/async"
|
|
||||||
imapEvents "github.com/ProtonMail/gluon/events"
|
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/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/ProtonMail/proton-bridge/v3/internal/useragent"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
@ -81,108 +71,59 @@ func (bridge *Bridge) handleIMAPEvent(event imapEvents.Event) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ApplyGluonCachePathSuffix(basePath string) string {
|
type bridgeIMAPSettings struct {
|
||||||
return filepath.Join(basePath, "backend", "store")
|
b *Bridge
|
||||||
}
|
}
|
||||||
|
|
||||||
func ApplyGluonConfigPathSuffix(basePath string) string {
|
func (b *bridgeIMAPSettings) EventPublisher() imapsmtpserver.IMAPEventPublisher {
|
||||||
return filepath.Join(basePath, "backend", "db")
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
func newIMAPServer(
|
func (b *bridgeIMAPSettings) TLSConfig() *tls.Config {
|
||||||
gluonCacheDir, gluonConfigDir string,
|
return b.b.tlsConfig
|
||||||
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)
|
|
||||||
|
|
||||||
logrus.WithFields(logrus.Fields{
|
func (b *bridgeIMAPSettings) LogClient() bool {
|
||||||
"gluonStore": gluonCacheDir,
|
return b.b.logIMAPClient
|
||||||
"gluonDB": gluonConfigDir,
|
}
|
||||||
"version": version,
|
|
||||||
"logClient": logClient,
|
|
||||||
"logServer": logServer,
|
|
||||||
}).Info("Creating IMAP server")
|
|
||||||
|
|
||||||
if logClient || logServer {
|
func (b *bridgeIMAPSettings) LogServer() bool {
|
||||||
log := logrus.WithField("protocol", "IMAP")
|
return b.b.logIMAPServer
|
||||||
log.Warning("================================================")
|
}
|
||||||
log.Warning("THIS LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA")
|
|
||||||
log.Warning("================================================")
|
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))
|
|
||||||
}
|
}
|
||||||
|
|||||||
26
internal/bridge/imapsmtp_telemetry.go
Normal file
26
internal/bridge/imapsmtp_telemetry.go
Normal 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)
|
||||||
|
}
|
||||||
@ -19,9 +19,6 @@ package bridge
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/Masterminds/semver/v3"
|
"github.com/Masterminds/semver/v3"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
"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)
|
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 {
|
func (bridge *Bridge) GetProxyAllowed() bool {
|
||||||
return bridge.vault.GetProxyAllowed()
|
return bridge.vault.GetProxyAllowed()
|
||||||
}
|
}
|
||||||
@ -318,16 +298,3 @@ func (bridge *Bridge) FactoryReset(ctx context.Context) {
|
|||||||
logrus.WithError(err).Error("Failed to clear data paths")
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -21,49 +21,14 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"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/ProtonMail/proton-bridge/v3/internal/user"
|
"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 {
|
func (bridge *Bridge) restartSMTP(ctx context.Context) error {
|
||||||
return bridge.serverManager.RestartSMTP(ctx)
|
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.
|
// addSMTPUser connects the given user to the smtp server.
|
||||||
func (bridge *Bridge) addSMTPUser(ctx context.Context, user *user.User) error {
|
func (bridge *Bridge) addSMTPUser(ctx context.Context, user *user.User) error {
|
||||||
return bridge.serverManager.AddSMTPAccount(ctx, user.GetSMTPService())
|
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 {
|
func (bridge *Bridge) removeSMTPUser(ctx context.Context, user *user.User) error {
|
||||||
return bridge.serverManager.RemoveSMTPAccount(ctx, user.GetSMTPService())
|
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}
|
||||||
|
}
|
||||||
|
|||||||
@ -15,7 +15,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// 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/>.
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
package bridge
|
package files
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -24,7 +24,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
|
||||||
func moveDir(from, to string) error {
|
func MoveDir(from, to string) error {
|
||||||
entries, err := os.ReadDir(from)
|
entries, err := os.ReadDir(from)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -36,7 +36,7 @@ func moveDir(from, to string) error {
|
|||||||
return err
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,12 +61,12 @@ func moveFile(from, to string) error {
|
|||||||
return os.Rename(from, to)
|
return os.Rename(from, to)
|
||||||
}
|
}
|
||||||
|
|
||||||
func copyDir(from, to string) error {
|
func CopyDir(from, to string) error {
|
||||||
entries, err := os.ReadDir(from)
|
entries, err := os.ReadDir(from)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := createIfNotExists(to, 0o700); err != nil {
|
if err := CreateIfNotExists(to, 0o700); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
@ -74,11 +74,11 @@ func copyDir(from, to string) error {
|
|||||||
destPath := filepath.Join(to, entry.Name())
|
destPath := filepath.Join(to, entry.Name())
|
||||||
|
|
||||||
if entry.IsDir() {
|
if entry.IsDir() {
|
||||||
if err := copyDir(sourcePath, destPath); err != nil {
|
if err := CopyDir(sourcePath, destPath); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if err := copyFile(sourcePath, destPath); err != nil {
|
if err := CopyFile(sourcePath, destPath); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -86,7 +86,7 @@ func copyDir(from, to string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func copyFile(srcFile, dstFile string) error {
|
func CopyFile(srcFile, dstFile string) error {
|
||||||
out, err := os.Create(filepath.Clean(dstFile))
|
out, err := os.Create(filepath.Clean(dstFile))
|
||||||
defer func(out *os.File) {
|
defer func(out *os.File) {
|
||||||
_ = out.Close()
|
_ = out.Close()
|
||||||
@ -113,7 +113,7 @@ func copyFile(srcFile, dstFile string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func exists(filePath string) bool {
|
func Exists(filePath string) bool {
|
||||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -121,8 +121,8 @@ func exists(filePath string) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func createIfNotExists(dir string, perm os.FileMode) error {
|
func CreateIfNotExists(dir string, perm os.FileMode) error {
|
||||||
if exists(dir) {
|
if Exists(dir) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -15,7 +15,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// 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/>.
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
package bridge
|
package files
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
@ -41,7 +41,7 @@ func TestMoveDir(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Move the files.
|
// Move the files.
|
||||||
if err := moveDir(from, to); err != nil {
|
if err := MoveDir(from, to); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
194
internal/services/imapsmtpserver/imap.go
Normal file
194
internal/services/imapsmtpserver/imap.go
Normal 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
|
||||||
|
}
|
||||||
57
internal/services/imapsmtpserver/listener.go
Normal file
57
internal/services/imapsmtpserver/listener.go
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -15,7 +15,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// 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/>.
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
package bridge
|
package imapsmtpserver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@ -24,10 +24,12 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/ProtonMail/gluon"
|
"github.com/ProtonMail/gluon"
|
||||||
|
"github.com/ProtonMail/gluon/async"
|
||||||
"github.com/ProtonMail/gluon/connector"
|
"github.com/ProtonMail/gluon/connector"
|
||||||
|
"github.com/ProtonMail/gluon/imap"
|
||||||
"github.com/ProtonMail/gluon/logging"
|
"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/events"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice"
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice"
|
||||||
bridgesmtp "github.com/ProtonMail/proton-bridge/v3/internal/services/smtp"
|
bridgesmtp "github.com/ProtonMail/proton-bridge/v3/internal/services/smtp"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/pkg/cpc"
|
"github.com/ProtonMail/proton-bridge/v3/pkg/cpc"
|
||||||
@ -35,8 +37,8 @@ import (
|
|||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ServerManager manages the IMAP & SMTP servers and their listeners.
|
// Service manages the IMAP & SMTP servers and their listeners.
|
||||||
type ServerManager struct {
|
type Service struct {
|
||||||
requests *cpc.CPC
|
requests *cpc.CPC
|
||||||
|
|
||||||
imapServer *gluon.Server
|
imapServer *gluon.Server
|
||||||
@ -46,30 +48,60 @@ type ServerManager struct {
|
|||||||
smtpListener net.Listener
|
smtpListener net.Listener
|
||||||
smtpAccounts *bridgesmtp.Accounts
|
smtpAccounts *bridgesmtp.Accounts
|
||||||
|
|
||||||
|
smtpSettings SMTPSettingsProvider
|
||||||
|
imapSettings IMAPSettingsProvider
|
||||||
|
eventPublisher events.EventPublisher
|
||||||
|
panicHandler async.PanicHandler
|
||||||
|
reporter reporter.Reporter
|
||||||
|
|
||||||
loadedUserCount int
|
loadedUserCount int
|
||||||
|
log *logrus.Entry
|
||||||
|
tasks *async.Group
|
||||||
|
|
||||||
|
uidValidityGenerator imap.UIDValidityGenerator
|
||||||
|
telemetry Telemetry
|
||||||
}
|
}
|
||||||
|
|
||||||
func newServerManager() *ServerManager {
|
func NewService(
|
||||||
return &ServerManager{
|
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(),
|
requests: cpc.NewCPC(),
|
||||||
smtpAccounts: bridgesmtp.NewAccounts(),
|
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 {
|
func (sm *Service) Init(ctx context.Context, group *async.Group, subscription events.Subscription) error {
|
||||||
imapServer, err := createIMAPServer(bridge)
|
imapServer, err := sm.createIMAPServer(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
smtpServer := createSMTPServer(bridge, sm.smtpAccounts)
|
smtpServer := sm.createSMTPServer()
|
||||||
|
|
||||||
sm.imapServer = imapServer
|
sm.imapServer = imapServer
|
||||||
sm.smtpServer = smtpServer
|
sm.smtpServer = smtpServer
|
||||||
|
|
||||||
bridge.tasks.Once(func(ctx context.Context) {
|
group.Once(func(ctx context.Context) {
|
||||||
logging.DoAnnotated(ctx, func(ctx context.Context) {
|
logging.DoAnnotated(ctx, func(ctx context.Context) {
|
||||||
sm.run(ctx, bridge)
|
sm.run(ctx, subscription)
|
||||||
}, logging.Labels{
|
}, logging.Labels{
|
||||||
"service": "server-manager",
|
"service": "server-manager",
|
||||||
})
|
})
|
||||||
@ -78,26 +110,26 @@ func (sm *ServerManager) Init(bridge *Bridge) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sm *ServerManager) CloseServers(ctx context.Context) error {
|
func (sm *Service) CloseServers(ctx context.Context) error {
|
||||||
defer sm.requests.Close()
|
defer sm.requests.Close()
|
||||||
_, err := sm.requests.Send(ctx, &smRequestClose{})
|
_, err := sm.requests.Send(ctx, &smRequestClose{})
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sm *ServerManager) RestartIMAP(ctx context.Context) error {
|
func (sm *Service) RestartIMAP(ctx context.Context) error {
|
||||||
_, err := sm.requests.Send(ctx, &smRequestRestartIMAP{})
|
_, err := sm.requests.Send(ctx, &smRequestRestartIMAP{})
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sm *ServerManager) RestartSMTP(ctx context.Context) error {
|
func (sm *Service) RestartSMTP(ctx context.Context) error {
|
||||||
_, err := sm.requests.Send(ctx, &smRequestRestartSMTP{})
|
_, err := sm.requests.Send(ctx, &smRequestRestartSMTP{})
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sm *ServerManager) AddIMAPUser(
|
func (sm *Service) AddIMAPUser(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
connector connector.Connector,
|
connector connector.Connector,
|
||||||
addrID string,
|
addrID string,
|
||||||
@ -114,7 +146,7 @@ func (sm *ServerManager) AddIMAPUser(
|
|||||||
return err
|
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{
|
_, err := sm.requests.Send(ctx, &smRequestSetGluonDir{
|
||||||
dir: gluonDir,
|
dir: gluonDir,
|
||||||
})
|
})
|
||||||
@ -122,7 +154,7 @@ func (sm *ServerManager) SetGluonDir(ctx context.Context, gluonDir string) error
|
|||||||
return err
|
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{
|
_, err := sm.requests.Send(ctx, &smRequestRemoveIMAPUser{
|
||||||
withData: deleteData,
|
withData: deleteData,
|
||||||
addrID: addrID,
|
addrID: addrID,
|
||||||
@ -132,42 +164,42 @@ func (sm *ServerManager) RemoveIMAPUser(ctx context.Context, deleteData bool, pr
|
|||||||
return err
|
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})
|
_, err := sm.requests.Send(ctx, &smRequestAddSMTPAccount{account: service})
|
||||||
|
|
||||||
return err
|
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})
|
_, err := sm.requests.Send(ctx, &smRequestRemoveSMTPAccount{account: service})
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sm *ServerManager) run(ctx context.Context, bridge *Bridge) {
|
func (sm *Service) run(ctx context.Context, subscription events.Subscription) {
|
||||||
eventCh, cancel := bridge.GetEvents()
|
eventSub := subscription.Add()
|
||||||
defer cancel()
|
defer subscription.Remove(eventSub)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
sm.handleClose(ctx, bridge)
|
sm.handleClose(ctx)
|
||||||
return
|
return
|
||||||
|
|
||||||
case evt := <-eventCh:
|
case evt := <-eventSub.GetChannel():
|
||||||
switch evt.(type) {
|
switch evt.(type) {
|
||||||
case events.ConnStatusDown:
|
case events.ConnStatusDown:
|
||||||
logrus.Info("Server Manager, network down stopping listeners")
|
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")
|
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)
|
logrus.WithError(err)
|
||||||
}
|
}
|
||||||
case events.ConnStatusUp:
|
case events.ConnStatusUp:
|
||||||
logrus.Info("Server Manager, network up starting listeners")
|
logrus.Info("Server Manager, network up starting listeners")
|
||||||
sm.handleLoadedUserCountChange(ctx, bridge)
|
sm.handleLoadedUserCountChange(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
case request, ok := <-sm.requests.ReceiveCh():
|
case request, ok := <-sm.requests.ReceiveCh():
|
||||||
@ -177,34 +209,34 @@ func (sm *ServerManager) run(ctx context.Context, bridge *Bridge) {
|
|||||||
|
|
||||||
switch r := request.Value().(type) {
|
switch r := request.Value().(type) {
|
||||||
case *smRequestClose:
|
case *smRequestClose:
|
||||||
sm.handleClose(ctx, bridge)
|
sm.handleClose(ctx)
|
||||||
request.Reply(ctx, nil, nil)
|
request.Reply(ctx, nil, nil)
|
||||||
return
|
return
|
||||||
|
|
||||||
case *smRequestRestartSMTP:
|
case *smRequestRestartSMTP:
|
||||||
err := sm.restartSMTP(bridge)
|
err := sm.restartSMTP(ctx)
|
||||||
request.Reply(ctx, nil, err)
|
request.Reply(ctx, nil, err)
|
||||||
|
|
||||||
case *smRequestRestartIMAP:
|
case *smRequestRestartIMAP:
|
||||||
err := sm.restartIMAP(ctx, bridge)
|
err := sm.restartIMAP(ctx)
|
||||||
request.Reply(ctx, nil, err)
|
request.Reply(ctx, nil, err)
|
||||||
|
|
||||||
case *smRequestAddIMAPUser:
|
case *smRequestAddIMAPUser:
|
||||||
err := sm.handleAddIMAPUser(ctx, r.connector, r.addrID, r.idProvider, r.syncStateProvider)
|
err := sm.handleAddIMAPUser(ctx, r.connector, r.addrID, r.idProvider, r.syncStateProvider)
|
||||||
request.Reply(ctx, nil, err)
|
request.Reply(ctx, nil, err)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
sm.handleLoadedUserCountChange(ctx, bridge)
|
sm.handleLoadedUserCountChange(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
case *smRequestRemoveIMAPUser:
|
case *smRequestRemoveIMAPUser:
|
||||||
err := sm.handleRemoveIMAPUser(ctx, r.withData, r.idProvider, r.addrID...)
|
err := sm.handleRemoveIMAPUser(ctx, r.withData, r.idProvider, r.addrID...)
|
||||||
request.Reply(ctx, nil, err)
|
request.Reply(ctx, nil, err)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
sm.handleLoadedUserCountChange(ctx, bridge)
|
sm.handleLoadedUserCountChange(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
case *smRequestSetGluonDir:
|
case *smRequestSetGluonDir:
|
||||||
err := sm.handleSetGluonDir(ctx, bridge, r.dir)
|
err := sm.handleSetGluonDir(ctx, r.dir)
|
||||||
request.Reply(ctx, nil, err)
|
request.Reply(ctx, nil, err)
|
||||||
|
|
||||||
case *smRequestAddSMTPAccount:
|
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)
|
logrus.Infof("Validating Listener State %v", sm.loadedUserCount)
|
||||||
if sm.shouldStartServers() {
|
if sm.shouldStartServers() {
|
||||||
if sm.imapListener == nil {
|
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")
|
logrus.WithError(err).Error("Failed to start IMAP server")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if sm.smtpListener == nil {
|
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")
|
logrus.WithError(err).Error("Failed to start SMTP server")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if sm.imapListener != nil {
|
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")
|
logrus.WithError(err).Error("Failed to stop IMAP server")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if sm.smtpListener != nil {
|
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")
|
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.
|
// 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")
|
logrus.WithError(err).Error("Failed to close IMAP server")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close the SMTP 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")
|
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,
|
connector connector.Connector,
|
||||||
addrID string,
|
addrID string,
|
||||||
idProvider imapservice.GluonIDProvider,
|
idProvider imapservice.GluonIDProvider,
|
||||||
@ -278,7 +312,7 @@ func (sm *ServerManager) handleAddIMAPUser(ctx context.Context,
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sm *ServerManager) handleAddIMAPUserImpl(ctx context.Context,
|
func (sm *Service) handleAddIMAPUserImpl(ctx context.Context,
|
||||||
connector connector.Connector,
|
connector connector.Connector,
|
||||||
addrID string,
|
addrID string,
|
||||||
idProvider imapservice.GluonIDProvider,
|
idProvider imapservice.GluonIDProvider,
|
||||||
@ -361,7 +395,7 @@ func (sm *ServerManager) handleAddIMAPUserImpl(ctx context.Context,
|
|||||||
return nil
|
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 {
|
if sm.imapServer == nil {
|
||||||
return fmt.Errorf("no imap server instance running")
|
return fmt.Errorf("no imap server instance running")
|
||||||
}
|
}
|
||||||
@ -394,37 +428,37 @@ func (sm *ServerManager) handleRemoveIMAPUser(ctx context.Context, withData bool
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func createIMAPServer(bridge *Bridge) (*gluon.Server, error) {
|
func (sm *Service) createIMAPServer(ctx context.Context) (*gluon.Server, error) {
|
||||||
gluonDataDir, err := bridge.GetGluonDataDir()
|
gluonDataDir, err := sm.imapSettings.DataDirectory()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get Gluon Database directory: %w", err)
|
return nil, fmt.Errorf("failed to get Gluon Database directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
server, err := newIMAPServer(
|
server, err := newIMAPServer(
|
||||||
bridge.vault.GetGluonCacheDir(),
|
sm.imapSettings.CacheDirectory(),
|
||||||
gluonDataDir,
|
gluonDataDir,
|
||||||
bridge.curVersion,
|
sm.imapSettings.Version(),
|
||||||
bridge.tlsConfig,
|
sm.imapSettings.TLSConfig(),
|
||||||
bridge.reporter,
|
sm.reporter,
|
||||||
bridge.logIMAPClient,
|
sm.imapSettings.LogClient(),
|
||||||
bridge.logIMAPServer,
|
sm.imapSettings.LogServer(),
|
||||||
bridge.imapEventCh,
|
sm.imapSettings.EventPublisher(),
|
||||||
bridge.tasks,
|
sm.tasks,
|
||||||
bridge.uidValidityGenerator,
|
sm.uidValidityGenerator,
|
||||||
bridge.panicHandler,
|
sm.panicHandler,
|
||||||
)
|
)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
bridge.publish(events.IMAPServerCreated{})
|
sm.eventPublisher.PublishEvent(ctx, events.IMAPServerCreated{})
|
||||||
}
|
}
|
||||||
|
|
||||||
return server, err
|
return server, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func createSMTPServer(bridge *Bridge, accounts *bridgesmtp.Accounts) *smtp.Server {
|
func (sm *Service) createSMTPServer() *smtp.Server {
|
||||||
return newSMTPServer(bridge, accounts, bridge.tlsConfig, bridge.logSMTP)
|
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().
|
// 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
|
// 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
|
// 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
|
sm.smtpServer = nil
|
||||||
|
|
||||||
bridge.publish(events.SMTPServerStopped{})
|
sm.eventPublisher.PublishEvent(ctx, events.SMTPServerStopped{})
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sm *ServerManager) closeIMAPServer(ctx context.Context, bridge *Bridge) error {
|
func (sm *Service) closeIMAPServer(ctx context.Context) error {
|
||||||
if sm.imapListener != nil {
|
if sm.imapListener != nil {
|
||||||
logrus.Info("Closing IMAP Listener")
|
logrus.Info("Closing IMAP Listener")
|
||||||
|
|
||||||
@ -463,7 +497,7 @@ func (sm *ServerManager) closeIMAPServer(ctx context.Context, bridge *Bridge) er
|
|||||||
|
|
||||||
sm.imapListener = nil
|
sm.imapListener = nil
|
||||||
|
|
||||||
bridge.publish(events.IMAPServerStopped{})
|
sm.eventPublisher.PublishEvent(ctx, events.IMAPServerStopped{})
|
||||||
}
|
}
|
||||||
|
|
||||||
if sm.imapServer != nil {
|
if sm.imapServer != nil {
|
||||||
@ -474,13 +508,13 @@ func (sm *ServerManager) closeIMAPServer(ctx context.Context, bridge *Bridge) er
|
|||||||
|
|
||||||
sm.imapServer = nil
|
sm.imapServer = nil
|
||||||
|
|
||||||
bridge.publish(events.IMAPServerClosed{})
|
sm.eventPublisher.PublishEvent(ctx, events.IMAPServerClosed{})
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
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")
|
logrus.Info("Restarting IMAP server")
|
||||||
|
|
||||||
if sm.imapListener != nil {
|
if sm.imapListener != nil {
|
||||||
@ -490,55 +524,55 @@ func (sm *ServerManager) restartIMAP(ctx context.Context, bridge *Bridge) error
|
|||||||
|
|
||||||
sm.imapListener = nil
|
sm.imapListener = nil
|
||||||
|
|
||||||
bridge.publish(events.IMAPServerStopped{})
|
sm.eventPublisher.PublishEvent(ctx, events.IMAPServerStopped{})
|
||||||
}
|
}
|
||||||
|
|
||||||
if sm.shouldStartServers() {
|
if sm.shouldStartServers() {
|
||||||
return sm.serveIMAP(ctx, bridge)
|
return sm.serveIMAP(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sm *ServerManager) restartSMTP(bridge *Bridge) error {
|
func (sm *Service) restartSMTP(ctx context.Context) error {
|
||||||
logrus.Info("Restarting SMTP server")
|
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)
|
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() {
|
if sm.shouldStartServers() {
|
||||||
return sm.serveSMTP(bridge)
|
return sm.serveSMTP(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sm *ServerManager) serveSMTP(bridge *Bridge) error {
|
func (sm *Service) serveSMTP(ctx context.Context) error {
|
||||||
port, err := func() (int, error) {
|
port, err := func() (int, error) {
|
||||||
logrus.WithFields(logrus.Fields{
|
logrus.WithFields(logrus.Fields{
|
||||||
"port": bridge.vault.GetSMTPPort(),
|
"port": sm.smtpSettings.Port(),
|
||||||
"ssl": bridge.vault.GetSMTPSSL(),
|
"ssl": sm.smtpSettings.UseSSL(),
|
||||||
}).Info("Starting SMTP server")
|
}).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 {
|
if err != nil {
|
||||||
return 0, fmt.Errorf("failed to create SMTP listener: %w", err)
|
return 0, fmt.Errorf("failed to create SMTP listener: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
sm.smtpListener = smtpListener
|
sm.smtpListener = smtpListener
|
||||||
|
|
||||||
bridge.tasks.Once(func(context.Context) {
|
sm.tasks.Once(func(context.Context) {
|
||||||
if err := sm.smtpServer.Serve(smtpListener); err != nil {
|
if err := sm.smtpServer.Serve(smtpListener); err != nil {
|
||||||
logrus.WithError(err).Info("SMTP server stopped")
|
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)
|
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 {
|
if err != nil {
|
||||||
bridge.publish(events.SMTPServerError{
|
sm.eventPublisher.PublishEvent(ctx, events.SMTPServerError{
|
||||||
Error: err,
|
Error: err,
|
||||||
})
|
})
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
bridge.publish(events.SMTPServerReady{
|
sm.eventPublisher.PublishEvent(ctx, events.SMTPServerReady{
|
||||||
Port: port,
|
Port: port,
|
||||||
})
|
})
|
||||||
|
|
||||||
return nil
|
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) {
|
port, err := func() (int, error) {
|
||||||
if sm.imapServer == nil {
|
if sm.imapServer == nil {
|
||||||
return 0, fmt.Errorf("no IMAP server instance running")
|
return 0, fmt.Errorf("no IMAP server instance running")
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.WithFields(logrus.Fields{
|
logrus.WithFields(logrus.Fields{
|
||||||
"port": bridge.vault.GetIMAPPort(),
|
"port": sm.imapSettings.Port(),
|
||||||
"ssl": bridge.vault.GetIMAPSSL(),
|
"ssl": sm.imapSettings.UseSSL(),
|
||||||
}).Info("Starting IMAP server")
|
}).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 {
|
if err != nil {
|
||||||
return 0, fmt.Errorf("failed to create IMAP listener: %w", err)
|
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)
|
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)
|
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 {
|
if err != nil {
|
||||||
bridge.publish(events.IMAPServerError{
|
sm.eventPublisher.PublishEvent(ctx, events.IMAPServerError{
|
||||||
Error: err,
|
Error: err,
|
||||||
})
|
})
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
bridge.publish(events.IMAPServerReady{
|
sm.eventPublisher.PublishEvent(ctx, events.IMAPServerReady{
|
||||||
Port: port,
|
Port: port,
|
||||||
})
|
})
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sm *ServerManager) stopIMAPListener(bridge *Bridge) error {
|
func (sm *Service) stopIMAPListener(ctx context.Context) error {
|
||||||
logrus.Info("Stopping IMAP listener")
|
logrus.Info("Stopping IMAP listener")
|
||||||
if sm.imapListener != nil {
|
if sm.imapListener != nil {
|
||||||
if err := sm.imapListener.Close(); err != nil {
|
if err := sm.imapListener.Close(); err != nil {
|
||||||
@ -613,54 +647,54 @@ func (sm *ServerManager) stopIMAPListener(bridge *Bridge) error {
|
|||||||
|
|
||||||
sm.imapListener = nil
|
sm.imapListener = nil
|
||||||
|
|
||||||
bridge.publish(events.IMAPServerStopped{})
|
sm.eventPublisher.PublishEvent(ctx, events.IMAPServerStopped{})
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sm *ServerManager) handleSetGluonDir(ctx context.Context, bridge *Bridge, newGluonDir string) error {
|
func (sm *Service) handleSetGluonDir(ctx context.Context, newGluonDir string) error {
|
||||||
return safe.RLockRet(func() error {
|
currentGluonDir := sm.imapSettings.CacheDirectory()
|
||||||
currentGluonDir := bridge.GetGluonCacheDir()
|
newGluonDir = filepath.Join(newGluonDir, "gluon")
|
||||||
newGluonDir = filepath.Join(newGluonDir, "gluon")
|
if newGluonDir == currentGluonDir {
|
||||||
if newGluonDir == currentGluonDir {
|
return fmt.Errorf("new gluon dir is the same as the old one")
|
||||||
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 err
|
||||||
return fmt.Errorf("failed to close IMAP: %w", 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
|
return nil
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sm *ServerManager) shouldStartServers() bool {
|
func (sm *Service) shouldStartServers() bool {
|
||||||
return sm.loadedUserCount >= 1
|
return sm.loadedUserCount >= 1
|
||||||
}
|
}
|
||||||
|
|
||||||
69
internal/services/imapsmtpserver/smtp.go
Normal file
69
internal/services/imapsmtpserver/smtp.go
Normal 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
|
||||||
|
}
|
||||||
22
internal/services/imapsmtpserver/telemetry.go
Normal file
22
internal/services/imapsmtpserver/telemetry.go
Normal 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)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user