[#1217] add support for smtp LOGIN auth

This commit is contained in:
Gani Georgiev
2022-12-13 11:45:59 +02:00
parent 6d46cefd1f
commit 1f45b858a6
36 changed files with 421 additions and 127 deletions
+2 -2
View File
@@ -10,8 +10,8 @@ import (
var _ Mailer = (*Sendmail)(nil)
// Sendmail implements `mailer.Mailer` interface and defines a mail
// client that sends emails via the `sendmail` *nix command.
// Sendmail implements [mailer.Mailer] interface and defines a mail
// client that sends emails via the "sendmail" *nix command.
//
// This client is usually recommended only for development and testing.
type Sendmail struct {
+86 -16
View File
@@ -1,6 +1,7 @@
package mailer
import (
"errors"
"fmt"
"net/smtp"
"strings"
@@ -11,7 +12,14 @@ import (
var _ Mailer = (*SmtpClient)(nil)
// NewSmtpClient creates new `SmtpClient` with the provided configuration.
const (
SmtpAuthPlain = "PLAIN"
SmtpAuthLogin = "LOGIN"
)
// Deprecated: Use directly the SmtpClient struct literal.
//
// NewSmtpClient creates new SmtpClient with the provided configuration.
func NewSmtpClient(
host string,
port int,
@@ -20,41 +28,47 @@ func NewSmtpClient(
tls bool,
) *SmtpClient {
return &SmtpClient{
host: host,
port: port,
username: username,
password: password,
tls: tls,
Host: host,
Port: port,
Username: username,
Password: password,
Tls: tls,
}
}
// SmtpClient defines a SMTP mail client structure that implements
// `mailer.Mailer` interface.
type SmtpClient struct {
host string
port int
username string
password string
tls bool
Host string
Port int
Username string
Password string
Tls bool
AuthMethod string // default to "PLAIN"
}
// Send implements `mailer.Mailer` interface.
func (c *SmtpClient) Send(m *Message) error {
var smtpAuth smtp.Auth
if c.username != "" || c.password != "" {
smtpAuth = smtp.PlainAuth("", c.username, c.password, c.host)
if c.Username != "" || c.Password != "" {
switch c.AuthMethod {
case SmtpAuthLogin:
smtpAuth = &smtpLoginAuth{c.Username, c.Password}
default:
smtpAuth = smtp.PlainAuth("", c.Username, c.Password, c.Host)
}
}
// create mail instance
var yak *mailyak.MailYak
if c.tls {
if c.Tls {
var tlsErr error
yak, tlsErr = mailyak.NewWithTLS(fmt.Sprintf("%s:%d", c.host, c.port), smtpAuth, nil)
yak, tlsErr = mailyak.NewWithTLS(fmt.Sprintf("%s:%d", c.Host, c.Port), smtpAuth, nil)
if tlsErr != nil {
return tlsErr
}
} else {
yak = mailyak.New(fmt.Sprintf("%s:%d", c.host, c.port), smtpAuth)
yak = mailyak.New(fmt.Sprintf("%s:%d", c.Host, c.Port), smtpAuth)
}
if m.From.Name != "" {
@@ -108,3 +122,59 @@ func (c *SmtpClient) Send(m *Message) error {
return yak.Send()
}
// -------------------------------------------------------------------
// AUTH LOGIN
// -------------------------------------------------------------------
var _ smtp.Auth = (*smtpLoginAuth)(nil)
// smtpLoginAuth defines an AUTH that implements the LOGIN authentication mechanism.
//
// AUTH LOGIN is obsolete[1] but some mail services like outlook requires it [2].
//
// NB!
// It 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.
//
// [1]: https://github.com/golang/go/issues/40817
// [2]: https://support.microsoft.com/en-us/office/outlook-com-no-longer-supports-auth-plain-authentication-07f7d5e9-1697-465f-84d2-4513d4ff0145?ui=en-us&rs=en-us&ad=us
type smtpLoginAuth struct {
username, password string
}
// Start initializes an authentication with the server.
//
// It is part of the [smtp.Auth] interface.
func (a *smtpLoginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
// Must have TLS, or else localhost server.
// Note: If TLS is not true, then we can't trust ANYTHING in ServerInfo.
// In particular, it doesn't matter if the server advertises LOGIN auth.
// That might just be the attacker saying
// "it's ok, you can trust me with your password."
if !server.TLS && !isLocalhost(server.Name) {
return "", nil, errors.New("unencrypted connection")
}
return "LOGIN", nil, nil
}
// Next "continues" the auth process by feeding the server with the requested data.
//
// It is part of the [smtp.Auth] interface.
func (a *smtpLoginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
if more {
switch strings.ToLower(string(fromServer)) {
case "username:":
return []byte(a.username), nil
case "password:":
return []byte(a.password), nil
}
}
return nil, nil
}
func isLocalhost(name string) bool {
return name == "localhost" || name == "127.0.0.1" || name == "::1"
}
+164
View File
@@ -0,0 +1,164 @@
package mailer
import (
"net/smtp"
"testing"
)
func TestLoginAuthStart(t *testing.T) {
auth := smtpLoginAuth{username: "test", password: "123456"}
scenarios := []struct {
name string
serverInfo *smtp.ServerInfo
expectError bool
}{
{
"localhost without tls",
&smtp.ServerInfo{TLS: false, Name: "localhost"},
false,
},
{
"localhost with tls",
&smtp.ServerInfo{TLS: true, Name: "localhost"},
false,
},
{
"127.0.0.1 without tls",
&smtp.ServerInfo{TLS: false, Name: "127.0.0.1"},
false,
},
{
"127.0.0.1 with tls",
&smtp.ServerInfo{TLS: false, Name: "127.0.0.1"},
false,
},
{
"::1 without tls",
&smtp.ServerInfo{TLS: false, Name: "::1"},
false,
},
{
"::1 with tls",
&smtp.ServerInfo{TLS: false, Name: "::1"},
false,
},
{
"non-localhost without tls",
&smtp.ServerInfo{TLS: false, Name: "example.com"},
true,
},
{
"non-localhost with tls",
&smtp.ServerInfo{TLS: true, Name: "example.com"},
false,
},
}
for _, s := range scenarios {
method, resp, err := auth.Start(s.serverInfo)
hasErr := err != nil
if hasErr != s.expectError {
t.Fatalf("[%s] Expected hasErr %v, got %v", s.name, s.expectError, hasErr)
}
if hasErr {
continue
}
if len(resp) != 0 {
t.Fatalf("[%s] Expected emtpy data response, got %v", s.name, resp)
}
if method != "LOGIN" {
t.Fatalf("[%s] Expected LOGIN, got %v", s.name, method)
}
}
}
func TestLoginAuthNext(t *testing.T) {
auth := smtpLoginAuth{username: "test", password: "123456"}
{
// example|false
r1, err := auth.Next([]byte("example:"), false)
if err != nil {
t.Fatalf("[example|false] Unexpected error %v", err)
}
if len(r1) != 0 {
t.Fatalf("[example|false] Expected empty part, got %v", r1)
}
// example|true
r2, err := auth.Next([]byte("example:"), true)
if err != nil {
t.Fatalf("[example|true] Unexpected error %v", err)
}
if len(r2) != 0 {
t.Fatalf("[example|true] Expected empty part, got %v", r2)
}
}
// ---------------------------------------------------------------
{
// username:|false
r1, err := auth.Next([]byte("username:"), false)
if err != nil {
t.Fatalf("[username|false] Unexpected error %v", err)
}
if len(r1) != 0 {
t.Fatalf("[username|false] Expected empty part, got %v", r1)
}
// username:|true
r2, err := auth.Next([]byte("username:"), true)
if err != nil {
t.Fatalf("[username|true] Unexpected error %v", err)
}
if str := string(r2); str != auth.username {
t.Fatalf("[username|true] Expected %s, got %s", auth.username, str)
}
// uSeRnAmE:|true
r3, err := auth.Next([]byte("uSeRnAmE:"), true)
if err != nil {
t.Fatalf("[uSeRnAmE|true] Unexpected error %v", err)
}
if str := string(r3); str != auth.username {
t.Fatalf("[uSeRnAmE|true] Expected %s, got %s", auth.username, str)
}
}
// ---------------------------------------------------------------
{
// password:|false
r1, err := auth.Next([]byte("password:"), false)
if err != nil {
t.Fatalf("[password|false] Unexpected error %v", err)
}
if len(r1) != 0 {
t.Fatalf("[password|false] Expected empty part, got %v", r1)
}
// password:|true
r2, err := auth.Next([]byte("password:"), true)
if err != nil {
t.Fatalf("[password|true] Unexpected error %v", err)
}
if str := string(r2); str != auth.password {
t.Fatalf("[password|true] Expected %s, got %s", auth.password, str)
}
// pAsSwOrD:|true
r3, err := auth.Next([]byte("pAsSwOrD:"), true)
if err != nil {
t.Fatalf("[pAsSwOrD|true] Unexpected error %v", err)
}
if str := string(r3); str != auth.password {
t.Fatalf("[pAsSwOrD|true] Expected %s, got %s", auth.password, str)
}
}
}