Other: Bump go-smtp version to fix race condition

There was a race condition internal to the go-smtp library.
In order to fix it, a version bump was necessary.
However, this significantly changed the library interface.
This commit is contained in:
James Houlahan
2022-10-21 11:07:53 +02:00
parent 974735d415
commit 0f125196a6
13 changed files with 210 additions and 316 deletions

View File

@ -64,8 +64,7 @@ type Bridge struct {
imapListener net.Listener
// smtpServer is the bridge's SMTP server.
smtpServer *smtp.Server
smtpBackend *smtpBackend
smtpServer *smtp.Server
// updater is the bridge's updater.
updater Updater
@ -131,8 +130,6 @@ func New( //nolint:funlen
return nil, nil, fmt.Errorf("failed to get Gluon directory: %w", err)
}
smtpBackend := newSMTPBackend()
imapServer, err := newIMAPServer(gluonDir, curVersion, tlsConfig, logIMAPClient, logIMAPServer)
if err != nil {
return nil, nil, fmt.Errorf("failed to create IMAP server: %w", err)
@ -159,7 +156,6 @@ func New( //nolint:funlen
// Service stuff
tlsConfig,
imapServer,
smtpBackend,
focusService,
// Logging stuff
@ -191,11 +187,10 @@ func newBridge(
tlsConfig *tls.Config,
imapServer *gluon.Server,
smtpBackend *smtpBackend,
focusService *focus.Service,
logIMAPClient, logIMAPServer, logSMTP bool,
) *Bridge {
return &Bridge{
bridge := &Bridge{
vault: vault,
users: safe.NewMap[string, *user.User](nil),
@ -205,10 +200,8 @@ func newBridge(
proxyCtl: proxyCtl,
identifier: identifier,
tlsConfig: tlsConfig,
imapServer: imapServer,
smtpServer: newSMTPServer(smtpBackend, tlsConfig, logSMTP),
smtpBackend: smtpBackend,
tlsConfig: tlsConfig,
imapServer: imapServer,
updater: updater,
curVersion: curVersion,
@ -226,6 +219,10 @@ func newBridge(
stopCh: make(chan struct{}),
}
bridge.smtpServer = newSMTPServer(&smtpBackend{bridge}, tlsConfig, logSMTP)
return bridge
}
func (bridge *Bridge) init(tlsReporter TLSReporter) error {

View File

@ -26,7 +26,6 @@ import (
"github.com/ProtonMail/proton-bridge/v2/internal/logging"
"github.com/ProtonMail/proton-bridge/v2/internal/constants"
"github.com/emersion/go-sasl"
"github.com/emersion/go-smtp"
"github.com/sirupsen/logrus"
)
@ -67,7 +66,7 @@ func (bridge *Bridge) restartSMTP() error {
return err
}
bridge.smtpServer = newSMTPServer(bridge.smtpBackend, bridge.tlsConfig, bridge.logSMTP)
bridge.smtpServer = newSMTPServer(&smtpBackend{bridge}, bridge.tlsConfig, bridge.logSMTP)
return bridge.serveSMTP()
}
@ -99,18 +98,5 @@ func newSMTPServer(smtpBackend *smtpBackend, tlsConfig *tls.Config, shouldLog bo
smtpServer.Debug = logging.NewSMTPDebugLogger()
}
smtpServer.EnableAuth(sasl.Login, func(conn *smtp.Conn) sasl.Server {
return sasl.NewLoginServer(func(address, password string) error {
user, err := conn.Server().Backend.Login(nil, address, password)
if err != nil {
return err
}
conn.SetSession(user)
return nil
})
})
return smtpServer
}

View File

@ -18,69 +18,82 @@
package bridge
import (
"sync"
"fmt"
"io"
"github.com/ProtonMail/proton-bridge/v2/internal/user"
"github.com/emersion/go-smtp"
)
type smtpBackend struct {
users map[string]*user.User
usersLock sync.RWMutex
bridge *Bridge
}
func newSMTPBackend() *smtpBackend {
return &smtpBackend{
users: make(map[string]*user.User),
}
type smtpSession struct {
bridge *Bridge
userID string
authID string
from string
to []string
}
func (backend *smtpBackend) Login(_ *smtp.ConnectionState, email, password string) (smtp.Session, error) {
backend.usersLock.RLock()
defer backend.usersLock.RUnlock()
func (be *smtpBackend) NewSession(_ *smtp.Conn) (smtp.Session, error) {
return &smtpSession{
bridge: be.bridge,
}, nil
}
for _, user := range backend.users {
session, err := user.NewSMTPSession(email, []byte(password))
if err != nil {
continue
func (s *smtpSession) AuthPlain(username, password string) error {
return s.bridge.users.ValuesErr(func(users []*user.User) error {
for _, user := range users {
addrID, err := user.CheckAuth(username, []byte(password))
if err != nil {
continue
}
s.userID = user.ID()
s.authID = addrID
return nil
}
return session, nil
}
return nil, ErrNoSuchUser
return fmt.Errorf("invalid username or password")
})
}
func (backend *smtpBackend) AnonymousLogin(state *smtp.ConnectionState) (smtp.Session, error) {
return nil, ErrNotImplemented
func (s *smtpSession) Reset() {
s.from = ""
s.to = nil
}
// addUser adds the given user to the backend.
// It returns an error if a user with the same ID already exists.
func (backend *smtpBackend) addUser(newUser *user.User) error {
backend.usersLock.Lock()
defer backend.usersLock.Unlock()
func (s *smtpSession) Logout() error {
s.Reset()
return nil
}
if _, ok := backend.users[newUser.ID()]; ok {
return ErrUserAlreadyExists
func (s *smtpSession) Mail(from string, opts *smtp.MailOptions) error {
s.from = from
return nil
}
func (s *smtpSession) Rcpt(to string) error {
if len(to) > 0 {
s.to = append(s.to, to)
}
backend.users[newUser.ID()] = newUser
return nil
}
// removeUser removes the given user from the backend.
// It returns an error if the user doesn't exist.
func (backend *smtpBackend) removeUser(user *user.User) error {
backend.usersLock.Lock()
defer backend.usersLock.Unlock()
if _, ok := backend.users[user.ID()]; !ok {
return ErrNoSuchUser
func (s *smtpSession) Data(r io.Reader) error {
if ok, err := s.bridge.users.GetErr(s.userID, func(user *user.User) error {
return user.SendMail(s.authID, s.from, s.to, r)
}); !ok {
return fmt.Errorf("no such user %q", s.userID)
} else if err != nil {
return fmt.Errorf("failed to send mail: %w", err)
}
delete(backend.users, user.ID())
return nil
}

View File

@ -401,11 +401,6 @@ func (bridge *Bridge) addUserWithVault(
return fmt.Errorf("failed to add IMAP user: %w", err)
}
// Connect the user's address(es) to the SMTP server.
if err := bridge.smtpBackend.addUser(user); err != nil {
return fmt.Errorf("failed to add user to SMTP backend: %w", err)
}
// Handle events coming from the user before forwarding them to the bridge.
// For example, if the user's addresses change, we need to update them in gluon.
go func() {
@ -497,10 +492,6 @@ func (bridge *Bridge) addIMAPUser(ctx context.Context, user *user.User) error {
// logoutUser logs the given user out from bridge.
func (bridge *Bridge) logoutUser(ctx context.Context, userID string) error {
if ok, err := bridge.users.GetDeleteErr(userID, func(user *user.User) error {
if err := bridge.smtpBackend.removeUser(user); err != nil {
logrus.WithError(err).Error("Failed to remove user from SMTP backend")
}
for _, gluonID := range user.GetGluonIDs() {
if err := bridge.imapServer.RemoveUser(ctx, gluonID, false); err != nil {
logrus.WithError(err).Error("Failed to remove IMAP user")
@ -528,10 +519,6 @@ func (bridge *Bridge) logoutUser(ctx context.Context, userID string) error {
// deleteUser deletes the given user from bridge.
func (bridge *Bridge) deleteUser(ctx context.Context, userID string) {
if ok := bridge.users.GetDelete(userID, func(user *user.User) {
if err := bridge.smtpBackend.removeUser(user); err != nil {
logrus.WithError(err).Error("Failed to remove user from SMTP backend")
}
for _, gluonID := range user.GetGluonIDs() {
if err := bridge.imapServer.RemoveUser(ctx, gluonID, true); err != nil {
logrus.WithError(err).Error("Failed to remove IMAP user")