diff --git a/auth.go b/auth.go index 314cb50..90a032a 100644 --- a/auth.go +++ b/auth.go @@ -62,6 +62,20 @@ const ( // https://datatracker.ietf.org/doc/html/rfc4616/ SMTPAuthPlain SMTPAuthType = "PLAIN" + // SMTPAuthPlainNoEnc is the "PLAIN" authentication mechanism as described in RFC 4616. + // + // Since the "PLAIN" SASL authentication mechanism transmits the username and password in + // plaintext over the internet connection, bye default we only allow this mechanism over + // a TLS secured connection. This authentiation mechanism overrides this default and will + // allow PLAIN authentication via an unencrypted channel. This can be useful if the + // connection has already been secured in a different way (e. g. a SSH tunnel) + // + // Note: Use this authentication method with caution. If used in the wrong way, you might + // expose your authentication information over unencrypted channels! + // + // https://datatracker.ietf.org/doc/html/rfc4616/ + SMTPAuthPlainNoEnc SMTPAuthType = "PLAIN-NOENC" + // SMTPAuthXOAUTH2 is the "XOAUTH2" SASL authentication mechanism. // https://developers.google.com/gmail/imap/xoauth2-protocol SMTPAuthXOAUTH2 SMTPAuthType = "XOAUTH2" diff --git a/client.go b/client.go index fbb01fb..a3e8067 100644 --- a/client.go +++ b/client.go @@ -1096,7 +1096,12 @@ func (c *Client) auth() error { if !strings.Contains(smtpAuthType, string(SMTPAuthPlain)) { return ErrPlainAuthNotSupported } - c.smtpAuth = smtp.PlainAuth("", c.user, c.pass, c.host) + c.smtpAuth = smtp.PlainAuth("", c.user, c.pass, c.host, false) + case SMTPAuthPlainNoEnc: + if !strings.Contains(smtpAuthType, string(SMTPAuthPlain)) { + return ErrPlainAuthNotSupported + } + c.smtpAuth = smtp.PlainAuth("", c.user, c.pass, c.host, true) case SMTPAuthLogin: if !strings.Contains(smtpAuthType, string(SMTPAuthLogin)) { return ErrLoginAuthNotSupported diff --git a/client_test.go b/client_test.go index 6da44e1..86bc60e 100644 --- a/client_test.go +++ b/client_test.go @@ -110,7 +110,7 @@ func TestNewClientWithOptions(t *testing.T) { {"WithSMTPAuth()", WithSMTPAuth(SMTPAuthLogin), false}, { "WithSMTPAuthCustom()", - WithSMTPAuthCustom(smtp.PlainAuth("", "", "", "")), + WithSMTPAuthCustom(smtp.PlainAuth("", "", "", "", false)), false, }, {"WithUsername()", WithUsername("test"), false}, @@ -606,7 +606,7 @@ func TestSetSMTPAuthCustom(t *testing.T) { }{ {"SMTPAuth: CRAM-MD5", smtp.CRAMMD5Auth("", ""), "CRAM-MD5", false}, {"SMTPAuth: LOGIN", smtp.LoginAuth("", "", ""), "LOGIN", false}, - {"SMTPAuth: PLAIN", smtp.PlainAuth("", "", "", ""), "PLAIN", false}, + {"SMTPAuth: PLAIN", smtp.PlainAuth("", "", "", "", false), "PLAIN", false}, } si := smtp.ServerInfo{TLS: true} for _, tt := range tests { @@ -1227,7 +1227,7 @@ func TestClient_DialWithContext_switchAuth(t *testing.T) { // We switch to CUSTOM by providing PLAIN auth as function - the server supports this client.SetSMTPAuthCustom(smtp.PlainAuth("", os.Getenv("TEST_SMTPAUTH_USER"), - os.Getenv("TEST_SMTPAUTH_PASS"), os.Getenv("TEST_HOST"))) + os.Getenv("TEST_SMTPAUTH_PASS"), os.Getenv("TEST_HOST"), false)) if client.smtpAuthType != SMTPAuthCustom { t.Errorf("expected auth type to be Custom, got: %s", client.smtpAuthType) } diff --git a/smtp/auth_plain.go b/smtp/auth_plain.go index e6e0ad9..f2ea8ac 100644 --- a/smtp/auth_plain.go +++ b/smtp/auth_plain.go @@ -17,6 +17,7 @@ package smtp type plainAuth struct { identity, username, password string host string + allowUnencryptedAuth bool } // PlainAuth returns an [Auth] that implements the PLAIN authentication @@ -27,8 +28,8 @@ type plainAuth struct { // PlainAuth will only send the credentials if the connection is using TLS // or is connected to localhost. Otherwise authentication will fail with an // error, without sending the credentials. -func PlainAuth(identity, username, password, host string) Auth { - return &plainAuth{identity, username, password, host} +func PlainAuth(identity, username, password, host string, allowUnEnc bool) Auth { + return &plainAuth{identity, username, password, host, allowUnEnc} } func (a *plainAuth) Start(server *ServerInfo) (string, []byte, error) { @@ -37,7 +38,7 @@ func (a *plainAuth) Start(server *ServerInfo) (string, []byte, error) { // In particular, it doesn't matter if the server advertises PLAIN auth. // That might just be the attacker saying // "it's ok, you can trust me with your password." - if !server.TLS && !isLocalhost(server.Name) { + if !a.allowUnencryptedAuth && !server.TLS && !isLocalhost(server.Name) { return "", nil, ErrUnencrypted } if server.Name != a.host {