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

@ -38,6 +38,7 @@ Proton Mail Bridge includes the following 3rd party software:
* [go-imap](https://github.com/emersion/go-imap) available under [license](https://github.com/emersion/go-imap/blob/master/LICENSE)
* [go-imap-id](https://github.com/emersion/go-imap-id) available under [license](https://github.com/emersion/go-imap-id/blob/master/LICENSE)
* [go-message](https://github.com/emersion/go-message) available under [license](https://github.com/emersion/go-message/blob/master/LICENSE)
* [go-sasl](https://github.com/emersion/go-sasl) available under [license](https://github.com/emersion/go-sasl/blob/master/LICENSE)
* [go-smtp](https://github.com/emersion/go-smtp) available under [license](https://github.com/emersion/go-smtp/blob/master/LICENSE)
* [color](https://github.com/fatih/color) available under [license](https://github.com/fatih/color/blob/master/LICENSE)
* [sentry-go](https://github.com/getsentry/sentry-go) available under [license](https://github.com/getsentry/sentry-go/blob/master/LICENSE)
@ -85,7 +86,6 @@ Proton Mail Bridge includes the following 3rd party software:
* [wincred](https://github.com/danieljoos/wincred) available under [license](https://github.com/danieljoos/wincred/blob/master/LICENSE)
* [go-spew](https://github.com/davecgh/go-spew) available under [license](https://github.com/davecgh/go-spew/blob/master/LICENSE)
* [go-windows](https://github.com/elastic/go-windows) available under [license](https://github.com/elastic/go-windows/blob/master/LICENSE)
* [go-sasl](https://github.com/emersion/go-sasl) available under [license](https://github.com/emersion/go-sasl/blob/master/LICENSE)
* [go-textwrapper](https://github.com/emersion/go-textwrapper) available under [license](https://github.com/emersion/go-textwrapper/blob/master/LICENSE)
* [go-vcard](https://github.com/emersion/go-vcard) available under [license](https://github.com/emersion/go-vcard/blob/master/LICENSE)
* [go-shlex](https://github.com/flynn-archive/go-shlex) available under [license](https://github.com/flynn-archive/go-shlex/blob/master/LICENSE)

4
go.mod
View File

@ -20,7 +20,8 @@ require (
github.com/emersion/go-imap v1.2.1-0.20220429085312-746087b7a317
github.com/emersion/go-imap-id v0.0.0-20190926060100-f94a56b9ecde
github.com/emersion/go-message v0.16.0
github.com/emersion/go-smtp v0.15.1-0.20221018181223-201c9ab124e4
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead
github.com/emersion/go-smtp v0.15.1-0.20221021114529-49b17434419d
github.com/fatih/color v1.13.0
github.com/getsentry/sentry-go v0.13.0
github.com/go-resty/resty/v2 v2.7.0
@ -70,7 +71,6 @@ require (
github.com/danieljoos/wincred v1.1.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/elastic/go-windows v1.0.1 // indirect
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead // indirect
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 // indirect
github.com/emersion/go-vcard v0.0.0-20220507122617-d4056df0ec4a // indirect
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect

4
go.sum
View File

@ -126,8 +126,8 @@ github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b/go.mod h1:G/dpzLu
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead h1:fI1Jck0vUrXT8bnphprS1EoVRe2Q5CKCX8iDlpqjQ/Y=
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-smtp v0.15.1-0.20221018181223-201c9ab124e4 h1:KGRcxZDpW5w18HFaoOwC9oDKE/M2F2lkB1PtK4gsmgc=
github.com/emersion/go-smtp v0.15.1-0.20221018181223-201c9ab124e4/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
github.com/emersion/go-smtp v0.15.1-0.20221021114529-49b17434419d h1:hFRM6zCBSc+Xa0rBOqSlG6Qe9dKC/2vLhGAuZlWxTsc=
github.com/emersion/go-smtp v0.15.1-0.20221021114529-49b17434419d/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY=
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
github.com/emersion/go-vcard v0.0.0-20220507122617-d4056df0ec4a h1:cltZpe6s0SJtqK5c/5y2VrIYi8BAtDM6qjmiGYqfTik=

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("================================================")