[#1217] add support for smtp LOGIN auth
This commit is contained in:
@@ -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
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user