GODT-1187: Remove IMAP/SMTP blocking when no internet.

This commit is contained in:
Jakub
2021-05-28 09:59:31 +02:00
committed by Jakub Cuth
parent f0ee82fdd2
commit 21dcac9fac
10 changed files with 604 additions and 321 deletions

View File

@ -23,13 +23,11 @@ import (
"io" "io"
"net" "net"
"strings" "strings"
"sync/atomic"
"time" "time"
imapid "github.com/ProtonMail/go-imap-id" imapid "github.com/ProtonMail/go-imap-id"
"github.com/ProtonMail/proton-bridge/internal/bridge" "github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/config/useragent" "github.com/ProtonMail/proton-bridge/internal/config/useragent"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/imap/id" "github.com/ProtonMail/proton-bridge/internal/imap/id"
"github.com/ProtonMail/proton-bridge/internal/imap/idle" "github.com/ProtonMail/proton-bridge/internal/imap/idle"
"github.com/ProtonMail/proton-bridge/internal/imap/uidplus" "github.com/ProtonMail/proton-bridge/internal/imap/uidplus"
@ -43,43 +41,58 @@ import (
"github.com/emersion/go-imap/backend" "github.com/emersion/go-imap/backend"
imapserver "github.com/emersion/go-imap/server" imapserver "github.com/emersion/go-imap/server"
"github.com/emersion/go-sasl" "github.com/emersion/go-sasl"
"github.com/sirupsen/logrus"
) )
type imapServer struct { // Server takes care of IMAP listening serving. It implements serverutil.Server.
panicHandler panicHandler type Server struct {
server *imapserver.Server panicHandler panicHandler
userAgent *useragent.UserAgent userAgent *useragent.UserAgent
eventListener listener.Listener debugClient bool
debugClient bool debugServer bool
debugServer bool port int
port int
isRunning atomic.Value server *imapserver.Server
controller serverutil.Controller
} }
// NewIMAPServer constructs a new IMAP server configured with the given options. // NewIMAPServer constructs a new IMAP server configured with the given options.
func NewIMAPServer(panicHandler panicHandler, debugClient, debugServer bool, port int, tls *tls.Config, imapBackend backend.Backend, userAgent *useragent.UserAgent, eventListener listener.Listener) *imapServer { // nolint[golint] func NewIMAPServer(
s := imapserver.New(imapBackend) panicHandler panicHandler,
s.Addr = fmt.Sprintf("%v:%v", bridge.Host, port) debugClient, debugServer bool,
s.TLSConfig = tls port int,
s.AllowInsecureAuth = true tls *tls.Config,
s.ErrorLog = newServerErrorLogger("server-imap") imapBackend backend.Backend,
s.AutoLogout = 30 * time.Minute userAgent *useragent.UserAgent,
eventListener listener.Listener,
if debugServer { ) *Server {
fmt.Println("THE LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA") server := &Server{
log.Warning("================================================") panicHandler: panicHandler,
log.Warning("THIS LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA") userAgent: userAgent,
log.Warning("================================================") debugClient: debugClient,
debugServer: debugServer,
port: port,
} }
server.server = newGoIMAPServer(tls, imapBackend, server.Address(), userAgent)
server.controller = serverutil.NewController(server, eventListener)
return server
}
func newGoIMAPServer(tls *tls.Config, backend backend.Backend, address string, userAgent *useragent.UserAgent) *imapserver.Server {
server := imapserver.New(backend)
server.TLSConfig = tls
server.AllowInsecureAuth = true
server.ErrorLog = serverutil.NewServerErrorLogger(serverutil.IMAP)
server.AutoLogout = 30 * time.Minute
server.Addr = address
serverID := imapid.ID{ serverID := imapid.ID{
imapid.FieldName: "ProtonMail Bridge", imapid.FieldName: "ProtonMail Bridge",
imapid.FieldVendor: "Proton Technologies AG", imapid.FieldVendor: "Proton Technologies AG",
imapid.FieldSupportURL: "https://protonmail.com/support", imapid.FieldSupportURL: "https://protonmail.com/support",
} }
s.EnableAuth(sasl.Login, func(conn imapserver.Conn) sasl.Server { server.EnableAuth(sasl.Login, func(conn imapserver.Conn) sasl.Server {
return sasl.NewLoginServer(func(address, password string) error { return sasl.NewLoginServer(func(address, password string) error {
user, err := conn.Server().Backend.Login(nil, address, password) user, err := conn.Server().Backend.Login(nil, address, password)
if err != nil { if err != nil {
@ -93,7 +106,7 @@ func NewIMAPServer(panicHandler panicHandler, debugClient, debugServer bool, por
}) })
}) })
s.Enable( server.Enable(
idle.NewExtension(), idle.NewExtension(),
imapmove.NewExtension(), imapmove.NewExtension(),
id.NewExtension(serverID, userAgent), id.NewExtension(serverID, userAgent),
@ -103,87 +116,35 @@ func NewIMAPServer(panicHandler panicHandler, debugClient, debugServer bool, por
uidplus.NewExtension(), uidplus.NewExtension(),
) )
server := &imapServer{
panicHandler: panicHandler,
server: s,
userAgent: userAgent,
eventListener: eventListener,
debugClient: debugClient,
debugServer: debugServer,
port: port,
}
server.isRunning.Store(false)
return server return server
} }
func (s *imapServer) HandlePanic() { s.panicHandler.HandlePanic() } // ListenAndServe will run server and all monitors.
func (s *imapServer) IsRunning() bool { return s.isRunning.Load().(bool) } func (s *Server) ListenAndServe() { s.controller.ListenAndServe() }
func (s *imapServer) Port() int { return s.port }
// ListenAndServe starts the server and keeps it on based on internet // Close turns off server and monitors.
// availability. func (s *Server) Close() { s.controller.Close() }
func (s *imapServer) ListenAndServe() {
serverutil.ListenAndServe(s, s.eventListener)
}
// ListenRetryAndServe will start listener. If port is occupied it will try // Implements serverutil.Server interface.
// again after coolDown time. Once listener is OK it will serve.
func (s *imapServer) ListenRetryAndServe(retries int, retryAfter time.Duration) {
if s.IsRunning() {
return
}
s.isRunning.Store(true)
l := log.WithField("address", s.server.Addr) func (Server) Protocol() serverutil.Protocol { return serverutil.IMAP }
l.Info("IMAP server is starting") func (s *Server) UseSSL() bool { return false }
listener, err := net.Listen("tcp", s.server.Addr) func (s *Server) Address() string { return fmt.Sprintf("%s:%d", bridge.Host, s.port) }
if err != nil { func (s *Server) TLSConfig() *tls.Config { return s.server.TLSConfig }
s.isRunning.Store(false) func (s *Server) HandlePanic() { s.panicHandler.HandlePanic() }
if retries > 0 {
l.WithError(err).WithField("retries", retries).Warn("IMAP listener failed")
time.Sleep(retryAfter)
s.ListenRetryAndServe(retries-1, retryAfter)
return
}
l.WithError(err).Error("IMAP listener failed") func (s *Server) DebugServer() bool { return s.debugServer }
s.eventListener.Emit(events.ErrorEvent, "IMAP failed: "+err.Error()) func (s *Server) DebugClient() bool { return s.debugClient }
return
}
err = s.server.Serve(&connListener{ func (s *Server) SetLoggers(localDebug, remoteDebug io.Writer) {
Listener: listener, s.server.Debug = imap.NewDebugWriter(localDebug, remoteDebug)
server: s,
userAgent: s.userAgent,
})
// Serve returns error every time, even after closing the server.
// User shouldn't be notified about error if server shouldn't be running,
// but it should in case it was not closed by `s.Close()`.
if err != nil && s.IsRunning() {
s.isRunning.Store(false)
l.WithError(err).Error("IMAP server failed")
s.eventListener.Emit(events.ErrorEvent, "IMAP failed: "+err.Error())
return
}
defer s.server.Close() //nolint[errcheck]
l.Info("IMAP server stopped") if !s.userAgent.HasClient() {
} s.userAgent.SetClient("UnknownClient", "0.0.1")
// Stops the server.
func (s *imapServer) Close() {
if !s.IsRunning() {
return
}
s.isRunning.Store(false)
log.Info("Closing IMAP server")
if err := s.server.Close(); err != nil {
log.WithError(err).Error("Failed to close the connection")
} }
} }
func (s *imapServer) DisconnectUser(address string) { func (s *Server) DisconnectUser(address string) {
log.Info("Disconnecting all open IMAP connections for ", address) log.Info("Disconnecting all open IMAP connections for ", address)
s.server.ForEachConn(func(conn imapserver.Conn) { s.server.ForEachConn(func(conn imapserver.Conn) {
connUser := conn.Context().User connUser := conn.Context().User
@ -195,60 +156,5 @@ func (s *imapServer) DisconnectUser(address string) {
}) })
} }
// connListener sets debug loggers on server containing fields with local func (s *Server) Serve(listener net.Listener) error { return s.server.Serve(listener) }
// and remote addresses right after new connection is accepted. func (s *Server) StopServe() error { return s.server.Close() }
type connListener struct {
net.Listener
server *imapServer
userAgent *useragent.UserAgent
}
func (l *connListener) Accept() (net.Conn, error) {
conn, err := l.Listener.Accept()
if err == nil && (l.server.debugServer || l.server.debugClient) {
debugLog := log
if addr := conn.LocalAddr(); addr != nil {
debugLog = debugLog.WithField("loc", addr.String())
}
if addr := conn.RemoteAddr(); addr != nil {
debugLog = debugLog.WithField("rem", addr.String())
}
var localDebug, remoteDebug io.Writer
if l.server.debugServer {
localDebug = debugLog.WithField("pkg", "imap/server").WriterLevel(logrus.DebugLevel)
}
if l.server.debugClient {
remoteDebug = debugLog.WithField("pkg", "imap/client").WriterLevel(logrus.DebugLevel)
}
l.server.server.Debug = imap.NewDebugWriter(localDebug, remoteDebug)
}
if !l.userAgent.HasClient() {
l.userAgent.SetClient("UnknownClient", "0.0.1")
}
return conn, err
}
// serverErrorLogger implements go-imap/logger interface.
type serverErrorLogger struct {
tag string
}
func newServerErrorLogger(tag string) *serverErrorLogger {
return &serverErrorLogger{tag}
}
func (s *serverErrorLogger) Printf(format string, args ...interface{}) {
err := fmt.Sprintf(format, args...)
log.WithField("pkg", s.tag).Error(err)
}
func (s *serverErrorLogger) Println(args ...interface{}) {
err := fmt.Sprintln(args...)
log.WithField("pkg", s.tag).Error(err)
}

View File

@ -0,0 +1,117 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail 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.
//
// ProtonMail 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 ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package serverutil
import (
"crypto/tls"
"fmt"
"net"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/sirupsen/logrus"
)
// Controller will make sure that server is listening and serving and if needed
// users are disconnected.
type Controller interface {
ListenAndServe()
Close()
}
// NewController return simple server controller.
func NewController(s Server, l listener.Listener) Controller {
log := logrus.WithField("pkg", "serverutil").WithField("protocol", s.Protocol())
c := &controller{
server: s,
signals: l,
log: log,
closeDisconnectUsers: make(chan void),
}
if s.DebugServer() {
fmt.Println("THE LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA")
log.Warning("================================================")
log.Warning("THIS LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA")
log.Warning("================================================")
}
return c
}
type void struct{}
type controller struct {
server Server
signals listener.Listener
log *logrus.Entry
closeDisconnectUsers chan void
}
func (c *controller) Close() {
c.closeDisconnectUsers <- void{}
if err := c.server.StopServe(); err != nil {
c.log.WithError(err).Error("Issue when closing server")
}
}
// ListenAndServe starts the server and keeps it on based on internet
// availability. It also monitors and disconnect users if requested.
func (c *controller) ListenAndServe() {
go monitorDisconnectedUsers(c.server, c.signals, c.closeDisconnectUsers)
defer c.server.HandlePanic()
l := c.log.WithField("useSSL", c.server.UseSSL()).
WithField("address", c.server.Address())
var listener net.Listener
var err error
if c.server.UseSSL() {
listener, err = tls.Listen("tcp", c.server.Address(), c.server.TLSConfig())
} else {
listener, err = net.Listen("tcp", c.server.Address())
}
if err != nil {
l.WithError(err).Error("Cannot start listner.")
c.signals.Emit(events.ErrorEvent, string(c.server.Protocol())+" failed: "+err.Error())
return
}
// When starting the Bridge, we don't want to retry to notify user
// quickly about the issue. Very probably retry will not help anyway.
l.Info("Starting server")
err = c.server.Serve(&connListener{listener, c.server})
l.WithError(err).Debug("GoSMTP not serving")
}
func monitorDisconnectedUsers(s Server, l listener.Listener, done <-chan void) {
ch := make(chan string)
l.Add(events.CloseConnectionEvent, ch)
for {
select {
case <-done:
return
case address := <-ch:
s.DisconnectUser(address)
}
}
}

View File

@ -0,0 +1,39 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail 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.
//
// ProtonMail 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 ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package serverutil
import (
"github.com/sirupsen/logrus"
)
// ServerErrorLogger implements go-imap/logger interface.
type ServerErrorLogger struct {
l *logrus.Entry
}
func NewServerErrorLogger(protocol Protocol) *ServerErrorLogger {
return &ServerErrorLogger{l: logrus.WithField("protocol", protocol)}
}
func (s *ServerErrorLogger) Printf(format string, args ...interface{}) {
s.l.Errorf(format, args...)
}
func (s *ServerErrorLogger) Println(args ...interface{}) {
s.l.Errorln(args...)
}

View File

@ -0,0 +1,59 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail 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.
//
// ProtonMail 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 ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package serverutil
import (
"io"
"net"
"github.com/sirupsen/logrus"
)
// connListener sets debug loggers on server containing fields with local
// and remote addresses right after new connection is accepted.
type connListener struct {
net.Listener
server Server
}
func (l *connListener) Accept() (net.Conn, error) {
conn, err := l.Listener.Accept()
if err == nil && (l.server.DebugServer() || l.server.DebugClient()) {
debugLog := logrus.WithField("pkg", l.server.Protocol())
if addr := conn.LocalAddr(); addr != nil {
debugLog = debugLog.WithField("loc", addr.String())
}
if addr := conn.RemoteAddr(); addr != nil {
debugLog = debugLog.WithField("rem", addr.String())
}
var localDebug, remoteDebug io.Writer
if l.server.DebugServer() {
localDebug = debugLog.WithField("comm", "server").WriterLevel(logrus.DebugLevel)
}
if l.server.DebugClient() {
remoteDebug = debugLog.WithField("comm", "client").WriterLevel(logrus.DebugLevel)
}
l.server.SetLoggers(localDebug, remoteDebug)
}
return conn, err
}

View File

@ -0,0 +1,26 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail 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.
//
// ProtonMail 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 ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package serverutil
type Protocol string
const (
HTTP = Protocol("HTTP")
IMAP = Protocol("IMAP")
SMTP = Protocol("SMTP")
)

View File

@ -18,32 +18,24 @@
package serverutil package serverutil
import ( import (
"time" "crypto/tls"
"io"
"github.com/ProtonMail/proton-bridge/internal/events" "net"
"github.com/ProtonMail/proton-bridge/pkg/listener"
) )
// Server which can handle disconnected users and lost internet connection. // Server can handle disconnected users.
type Server interface { type Server interface {
Protocol() Protocol
UseSSL() bool
Address() string
TLSConfig() *tls.Config
DebugServer() bool
DebugClient() bool
SetLoggers(localDebug, remoteDebug io.Writer)
HandlePanic()
DisconnectUser(string) DisconnectUser(string)
ListenRetryAndServe(int, time.Duration) Serve(net.Listener) error
} StopServe() error
func monitorDisconnectedUsers(s Server, l listener.Listener) {
ch := make(chan string)
l.Add(events.CloseConnectionEvent, ch)
for address := range ch {
s.DisconnectUser(address)
}
}
// ListenAndServe starts the server and keeps it on based on internet
// availability. It also monitors and disconnect users if requested.
func ListenAndServe(s Server, l listener.Listener) {
go monitorDisconnectedUsers(s, l)
// When starting the Bridge, we don't want to retry to notify user
// quickly about the issue. Very probably retry will not help anyway.
s.ListenRetryAndServe(0, 0)
} }

View File

@ -0,0 +1,153 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail 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.
//
// ProtonMail 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 ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package test
import (
"net/http"
"testing"
"time"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/serverutil"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/stretchr/testify/require"
)
func setup(t *testing.T) (*require.Assertions, *testServer, listener.Listener, serverutil.Controller) {
r := require.New(t)
s := newTestServer()
l := listener.New()
c := serverutil.NewController(s, l)
return r, s, l, c
}
func TestControllerListernServeClose(t *testing.T) {
r, s, l, c := setup(t)
errorCh := l.ProvideChannel(events.ErrorEvent)
r.True(s.portIsFree())
go c.ListenAndServe()
r.Eventually(s.portIsOccupied, time.Second, 50*time.Millisecond)
r.NoError(s.ping())
r.Nil(s.localDebug)
r.Nil(s.remoteDebug)
c.Close()
r.Eventually(s.portIsFree, time.Second, 50*time.Millisecond)
select {
case msg := <-errorCh:
r.Fail("Expected no error but have %q", msg)
case <-time.Tick(100 * time.Millisecond):
break
}
}
func TestControllerFailOnBusyPort(t *testing.T) {
r, s, l, c := setup(t)
ocupator := http.Server{Addr: s.Address()}
defer ocupator.Close() //nolint[errcheck]
go ocupator.ListenAndServe() //nolint[errcheck]
r.Eventually(s.portIsOccupied, time.Second, 50*time.Millisecond)
errorCh := l.ProvideChannel(events.ErrorEvent)
go c.ListenAndServe()
r.Eventually(s.portIsOccupied, time.Second, 50*time.Millisecond)
select {
case <-errorCh:
break
case <-time.Tick(time.Second):
r.Fail("Expected error but have none.")
}
}
func TestControllerCallDisconnectUser(t *testing.T) {
r, s, l, c := setup(t)
go c.ListenAndServe()
r.Eventually(s.portIsOccupied, time.Second, 50*time.Millisecond)
r.NoError(s.ping())
l.Emit(events.CloseConnectionEvent, "")
r.Eventually(func() bool { return s.calledDisconnected == 1 }, time.Second, 50*time.Millisecond)
c.Close()
r.Eventually(s.portIsFree, time.Second, 50*time.Millisecond)
l.Emit(events.CloseConnectionEvent, "")
r.Equal(1, s.calledDisconnected)
}
func TestDebugClient(t *testing.T) {
r, s, _, c := setup(t)
s.debugServer = false
s.debugClient = true
go c.ListenAndServe()
r.Eventually(s.portIsOccupied, time.Second, 50*time.Millisecond)
r.NoError(s.ping())
r.Nil(s.localDebug)
r.NotNil(s.remoteDebug)
c.Close()
r.Eventually(s.portIsFree, time.Second, 50*time.Millisecond)
}
func TestDebugServer(t *testing.T) {
r, s, _, c := setup(t)
s.debugServer = true
s.debugClient = false
go c.ListenAndServe()
r.Eventually(s.portIsOccupied, time.Second, 50*time.Millisecond)
r.NoError(s.ping())
r.NotNil(s.localDebug)
r.Nil(s.remoteDebug)
c.Close()
r.Eventually(s.portIsFree, time.Second, 50*time.Millisecond)
}
func TestDebugBoth(t *testing.T) {
r, s, _, c := setup(t)
s.debugServer = true
s.debugClient = true
go c.ListenAndServe()
r.Eventually(s.portIsOccupied, time.Second, 50*time.Millisecond)
r.NoError(s.ping())
r.NotNil(s.localDebug)
r.NotNil(s.remoteDebug)
c.Close()
r.Eventually(s.portIsFree, time.Second, 50*time.Millisecond)
}

View File

@ -0,0 +1,88 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail 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.
//
// ProtonMail 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 ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package test
import (
"crypto/tls"
"fmt"
"io"
"net"
"net/http"
"github.com/ProtonMail/proton-bridge/internal/serverutil"
"github.com/ProtonMail/proton-bridge/pkg/ports"
)
func newTestServer() *testServer {
return &testServer{port: 11188}
}
type testServer struct {
http http.Server
useSSL,
debugServer,
debugClient bool
calledDisconnected int
port int
tls *tls.Config
localDebug, remoteDebug io.Writer
}
func (*testServer) Protocol() serverutil.Protocol { return serverutil.HTTP }
func (s *testServer) UseSSL() bool { return s.useSSL }
func (s *testServer) Address() string { return fmt.Sprintf("127.0.0.1:%d", s.port) }
func (s *testServer) TLSConfig() *tls.Config { return s.tls }
func (s *testServer) HandlePanic() {}
func (s *testServer) DebugServer() bool { return s.debugServer }
func (s *testServer) DebugClient() bool { return s.debugClient }
func (s *testServer) SetLoggers(localDebug, remoteDebug io.Writer) {
s.localDebug = localDebug
s.remoteDebug = remoteDebug
}
func (s *testServer) DisconnectUser(string) {
s.calledDisconnected++
}
func (s *testServer) Serve(l net.Listener) error {
return s.http.Serve(l)
}
func (s *testServer) StopServe() error { return s.http.Close() }
func (s *testServer) portIsFree() bool {
return ports.IsPortFree(s.port)
}
func (s *testServer) portIsOccupied() bool {
return !ports.IsPortFree(s.port)
}
func (s *testServer) ping() error {
client := &http.Client{}
resp, err := client.Get("http://" + s.Address() + "/ping")
if err != nil {
return err
}
return resp.Body.Close()
}

View File

@ -20,72 +20,60 @@ package smtp
import ( import (
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"io"
"net" "net"
"sync/atomic"
"time"
"github.com/ProtonMail/proton-bridge/internal/bridge" "github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/serverutil" "github.com/ProtonMail/proton-bridge/internal/serverutil"
"github.com/ProtonMail/proton-bridge/pkg/listener" "github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/emersion/go-sasl" "github.com/emersion/go-sasl"
goSMTP "github.com/emersion/go-smtp" goSMTP "github.com/emersion/go-smtp"
"github.com/sirupsen/logrus"
) )
// Server is Bridge SMTP server implementation. // Server is Bridge SMTP server implementation.
type Server struct { type Server struct {
panicHandler panicHandler panicHandler panicHandler
backend goSMTP.Backend backend goSMTP.Backend
server *goSMTP.Server debug bool
eventListener listener.Listener useSSL bool
debug bool port int
useSSL bool tls *tls.Config
port int
tls *tls.Config server *goSMTP.Server
isRunning atomic.Value controller serverutil.Controller
} }
// NewSMTPServer returns an SMTP server configured with the given options. // NewSMTPServer returns an SMTP server configured with the given options.
func NewSMTPServer(panicHandler panicHandler, debug bool, port int, useSSL bool, tls *tls.Config, smtpBackend goSMTP.Backend, eventListener listener.Listener) *Server { func NewSMTPServer(
if debug { panicHandler panicHandler,
fmt.Println("THE LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA") debug bool, port int, useSSL bool,
log.Warning("================================================") tls *tls.Config,
log.Warning("THIS LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA") smtpBackend goSMTP.Backend,
log.Warning("================================================") eventListener listener.Listener,
) *Server {
server := &Server{
panicHandler: panicHandler,
backend: smtpBackend,
debug: debug,
useSSL: useSSL,
port: port,
tls: tls,
} }
server := &Server{ server.server = newGoSMTPServer(server)
panicHandler: panicHandler, server.controller = serverutil.NewController(server, eventListener)
backend: smtpBackend,
eventListener: eventListener,
debug: debug,
useSSL: useSSL,
port: port,
tls: tls,
}
server.isRunning.Store(false)
return server return server
} }
func (s *Server) HandlePanic() { s.panicHandler.HandlePanic() } func newGoSMTPServer(s *Server) *goSMTP.Server {
func (s *Server) IsRunning() bool { return s.isRunning.Load().(bool) } newSMTP := goSMTP.NewServer(s.backend)
func (s *Server) Port() int { return s.port } newSMTP.Addr = s.Address()
newSMTP.TLSConfig = s.tls
func newGoSMTPServer(debug bool, smtpBackend goSMTP.Backend, port int, tls *tls.Config) *goSMTP.Server {
newSMTP := goSMTP.NewServer(smtpBackend)
newSMTP.Addr = fmt.Sprintf("%v:%v", bridge.Host, port)
newSMTP.TLSConfig = tls
newSMTP.Domain = bridge.Host newSMTP.Domain = bridge.Host
newSMTP.ErrorLog = serverutil.NewServerErrorLogger(serverutil.SMTP)
newSMTP.AllowInsecureAuth = true newSMTP.AllowInsecureAuth = true
newSMTP.MaxLineLength = 1 << 16 newSMTP.MaxLineLength = 1 << 16
if debug {
newSMTP.Debug = logrus.
WithField("pkg", "smtp/server").
WriterLevel(logrus.DebugLevel)
}
newSMTP.EnableAuth(sasl.Login, func(conn *goSMTP.Conn) sasl.Server { newSMTP.EnableAuth(sasl.Login, func(conn *goSMTP.Conn) sasl.Server {
return sasl.NewLoginServer(func(address, password string) error { return sasl.NewLoginServer(func(address, password string) error {
user, err := conn.Server().Backend.Login(nil, address, password) user, err := conn.Server().Backend.Login(nil, address, password)
@ -100,80 +88,24 @@ func newGoSMTPServer(debug bool, smtpBackend goSMTP.Backend, port int, tls *tls.
return newSMTP return newSMTP
} }
// ListenAndServe starts the server and keeps it on based on internet // ListenAndServe will run server and all monitors.
// availability. func (s *Server) ListenAndServe() { s.controller.ListenAndServe() }
func (s *Server) ListenAndServe() {
serverutil.ListenAndServe(s, s.eventListener)
}
func (s *Server) ListenRetryAndServe(retries int, retryAfter time.Duration) { // Close turns off server and monitors.
if s.IsRunning() { func (s *Server) Close() { s.controller.Close() }
return
}
s.isRunning.Store(true)
s.server = newGoSMTPServer(s.debug, s.backend, s.port, s.tls) // Implements servertutil.Server interface.
l := log.WithField("useSSL", s.useSSL).WithField("address", s.server.Addr) func (Server) Protocol() serverutil.Protocol { return serverutil.SMTP }
l.Info("SMTP server is starting") func (s *Server) UseSSL() bool { return s.useSSL }
func (s *Server) Address() string { return fmt.Sprintf("%s:%d", bridge.Host, s.port) }
func (s *Server) TLSConfig() *tls.Config { return s.tls }
func (s *Server) HandlePanic() { s.panicHandler.HandlePanic() }
var listener net.Listener func (s *Server) DebugServer() bool { return s.debug }
var err error func (s *Server) DebugClient() bool { return s.debug }
if s.useSSL {
listener, err = tls.Listen("tcp", s.server.Addr, s.server.TLSConfig)
} else {
listener, err = net.Listen("tcp", s.server.Addr)
}
l.WithError(err).Debug("Listener for SMTP created")
if err != nil {
s.isRunning.Store(false)
if retries > 0 {
l.WithError(err).WithField("retries", retries).Warn("SMTP listener failed")
time.Sleep(retryAfter)
s.ListenRetryAndServe(retries-1, retryAfter)
return
}
l.WithError(err).Error("SMTP listener failed") func (s *Server) SetLoggers(localDebug, remoteDebug io.Writer) { s.server.Debug = localDebug }
s.eventListener.Emit(events.ErrorEvent, "SMTP failed: "+err.Error())
return
}
err = s.server.Serve(listener)
l.WithError(err).Debug("GoSMTP not serving")
// Serve returns error every time, even after closing the server.
// User shouldn't be notified about error if server shouldn't be running,
// but it should in case it was not closed by `s.Close()`.
if err != nil && s.IsRunning() {
s.isRunning.Store(false)
l.WithError(err).Error("SMTP server failed")
s.eventListener.Emit(events.ErrorEvent, "SMTP failed: "+err.Error())
return
}
defer func() {
// Go SMTP server instance can be closed only once. Otherwise
// it returns an error. The error is not export therefore we
// will check the string value.
err := s.server.Close()
if err == nil || err.Error() != "smtp: server already closed" {
l.WithError(err).Warn("Server was not closed")
}
}()
l.Info("SMTP server closed")
}
// Close stops the server.
func (s *Server) Close() {
if !s.IsRunning() {
return
}
s.isRunning.Store(false)
if err := s.server.Close(); err != nil {
log.WithError(err).Error("Cannot close the server")
}
}
func (s *Server) DisconnectUser(address string) { func (s *Server) DisconnectUser(address string) {
log.Info("Disconnecting all open SMTP connections for ", address) log.Info("Disconnecting all open SMTP connections for ", address)
@ -186,3 +118,6 @@ func (s *Server) DisconnectUser(address string) {
} }
}) })
} }
func (s *Server) Serve(l net.Listener) error { return s.server.Serve(l) }
func (s *Server) StopServe() error { return s.server.Close() }

View File

@ -1,32 +0,0 @@
Feature: Servers are closed when no internet
# FIXME: Locally works, has lags on CI. Looks like it breaks other tests as well.
@ignore
Scenario: All connection are closed and then restored multiple times
Given there is connected user "user"
And there is IMAP client "i1" logged in as "user"
And there is SMTP client "s1" logged in as "user"
When there is no internet connection
And 3 seconds pass
Then IMAP client "i1" is logged out
And SMTP client "s1" is logged out
Given the internet connection is restored
And 3 seconds pass
And there is IMAP client "i2" logged in as "user"
And there is SMTP client "s2" logged in as "user"
When IMAP client "i2" gets info of "INBOX"
When SMTP client "s2" sends "HELO example.com"
Then IMAP response to "i2" is "OK"
Then SMTP response to "s2" is "OK"
When there is no internet connection
And 3 seconds pass
Then IMAP client "i2" is logged out
And SMTP client "s2" is logged out
Given the internet connection is restored
And 3 seconds pass
And there is IMAP client "i3" logged in as "user"
And there is SMTP client "s3" logged in as "user"
When IMAP client "i3" gets info of "INBOX"
When SMTP client "s3" sends "HELO example.com"
Then IMAP response to "i3" is "OK"
Then SMTP response to "s3" is "OK"