GODT-2070: Implement SASL login for SMTP

go-smtp now comes with out of the box support for SASL PLAIN but it
still requires manual implementation of SASL LOGIN (deprecated).
This commit is contained in:
James Houlahan
2022-11-14 12:14:27 +01:00
parent 59278913ca
commit 31fb878bbd
5 changed files with 67 additions and 42 deletions

View File

@ -19,8 +19,10 @@ package bridge_test
import (
"context"
"crypto/tls"
"fmt"
"net/smtp"
"net"
"strings"
"testing"
"time"
@ -28,6 +30,8 @@ import (
"github.com/ProtonMail/proton-bridge/v2/internal/constants"
"github.com/emersion/go-imap"
"github.com/emersion/go-imap/client"
"github.com/emersion/go-sasl"
"github.com/emersion/go-smtp"
"github.com/stretchr/testify/require"
"gitlab.protontech.ch/go/liteapi"
"gitlab.protontech.ch/go/liteapi/server"
@ -52,47 +56,60 @@ func TestBridge_Send(t *testing.T) {
require.NoError(t, err)
for i := 0; i < 10; i++ {
// Send an email from sender to recipient.
smtpClient, err := smtp.Dial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetSMTPPort()))
// Dial the server.
client, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetSMTPPort())))
require.NoError(t, err)
defer smtpClient.Close() //nolint:errcheck
defer client.Close() //nolint:errcheck
require.NoError(t, smtpClient.Auth(smtp.PlainAuth("", senderInfo.Addresses[0], string(senderInfo.BridgePass), constants.Host)))
require.NoError(t, smtpClient.Mail(senderInfo.Addresses[0]))
require.NoError(t, smtpClient.Rcpt("recipient@pm.me"))
// Upgrade to TLS.
require.NoError(t, client.StartTLS(&tls.Config{InsecureSkipVerify: true}))
wc, err := smtpClient.Data()
require.NoError(t, err)
if i%2 == 0 {
// Authorize with SASL PLAIN.
require.NoError(t, client.Auth(sasl.NewPlainClient(
senderInfo.Addresses[0],
senderInfo.Addresses[0],
string(senderInfo.BridgePass)),
))
} else {
// Authorize with SASL LOGIN.
require.NoError(t, client.Auth(sasl.NewLoginClient(
senderInfo.Addresses[0],
string(senderInfo.BridgePass)),
))
}
n, err := fmt.Fprintf(wc, "Subject: Test %v\r\n\r\nHello world!", i)
require.NoError(t, err)
require.Greater(t, n, 0)
require.NoError(t, wc.Close())
// Sender should see the message in the Sent folder.
senderIMAPClient, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
require.NoError(t, err)
require.NoError(t, senderIMAPClient.Login(senderInfo.Addresses[0], string(senderInfo.BridgePass)))
defer senderIMAPClient.Logout() //nolint:errcheck
require.Eventually(t, func() bool {
status, err := senderIMAPClient.Status(`Sent`, []imap.StatusItem{imap.StatusMessages})
require.NoError(t, err)
return status.Messages == uint32(i+1)
}, 10*time.Second, 100*time.Millisecond)
// Recipient should see the message in the Inbox.
recipientIMAPClient, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
require.NoError(t, err)
require.NoError(t, recipientIMAPClient.Login(recipientInfo.Addresses[0], string(recipientInfo.BridgePass)))
defer recipientIMAPClient.Logout() //nolint:errcheck
require.Eventually(t, func() bool {
status, err := recipientIMAPClient.Status(`Inbox`, []imap.StatusItem{imap.StatusMessages})
require.NoError(t, err)
return status.Messages == uint32(i+1)
}, 10*time.Second, 100*time.Millisecond)
// Send the message.
require.NoError(t, client.SendMail(
senderInfo.Addresses[0],
[]string{recipientInfo.Addresses[0]},
strings.NewReader(fmt.Sprintf("Subject: Test %v\r\n\r\nHello world!", i)),
))
}
// Connect the sender IMAP client.
senderIMAPClient, err := client.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
require.NoError(t, err)
require.NoError(t, senderIMAPClient.Login(senderInfo.Addresses[0], string(senderInfo.BridgePass)))
defer senderIMAPClient.Logout() //nolint:errcheck
// Connect the recipient IMAP client.
recipientIMAPClient, err := client.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
require.NoError(t, err)
require.NoError(t, recipientIMAPClient.Login(recipientInfo.Addresses[0], string(recipientInfo.BridgePass)))
defer recipientIMAPClient.Logout() //nolint:errcheck
// Sender should have 10 messages in the sent folder.
// Recipient should have 0 messages in inbox.
require.Eventually(t, func() bool {
sent, err := senderIMAPClient.Status(`Sent`, []imap.StatusItem{imap.StatusMessages})
require.NoError(t, err)
inbox, err := recipientIMAPClient.Status(`Inbox`, []imap.StatusItem{imap.StatusMessages})
require.NoError(t, err)
return sent.Messages == 10 && inbox.Messages == 10
}, 10*time.Second, 100*time.Millisecond)
})
})
}

View File

@ -25,6 +25,7 @@ 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"
)
@ -95,6 +96,13 @@ func newSMTPServer(bridge *Bridge, tlsConfig *tls.Config, logSMTP bool) *smtp.Se
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("================================================")