From 31fb878bbd579a2ee47063a2f0a48c9cbaf2029e Mon Sep 17 00:00:00 2001 From: James Houlahan Date: Mon, 14 Nov 2022 12:14:27 +0100 Subject: [PATCH] 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). --- COPYING_NOTES.md | 2 +- go.mod | 4 +- go.sum | 4 +- internal/bridge/send_test.go | 91 +++++++++++++++++++++--------------- internal/bridge/smtp.go | 8 ++++ 5 files changed, 67 insertions(+), 42 deletions(-) diff --git a/COPYING_NOTES.md b/COPYING_NOTES.md index b9af649d..c66d176a 100644 --- a/COPYING_NOTES.md +++ b/COPYING_NOTES.md @@ -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) diff --git a/go.mod b/go.mod index 7c2f61e0..fb59b194 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index cf497abf..e6dc15f7 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/bridge/send_test.go b/internal/bridge/send_test.go index 6b0feb77..03af9382 100644 --- a/internal/bridge/send_test.go +++ b/internal/bridge/send_test.go @@ -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) }) }) } diff --git a/internal/bridge/smtp.go b/internal/bridge/smtp.go index d5dde962..f9240a32 100644 --- a/internal/bridge/smtp.go +++ b/internal/bridge/smtp.go @@ -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("================================================")