Implement SMTP StartTLS and Adaptive Auth
- If the STARTTLS extension is advertised we automatically STARTTLS before authenticating or sending - Uses the secure config key to determine if we should verify the cert. By default it does not verify the cert (should not break any configs) - Attempt auth when the config has a SMTP password and the server supports the AUTH extension and either the PLAIN or LOGIN mechanism - Check the mechanisms supported by the server and use PLAIN or LOGIN depending on which is supported - Changed secure key to use boolean values instead of strings - Arranged SMTP notifier properties/vars to be in the same order - Log the steps for STARTTLS (debug only) - Log the steps for AUTH (debug only)pull/525/head
parent
716e017521
commit
c4b56a6002
|
@ -17,10 +17,10 @@ type EmailNotifierConfiguration struct {
|
||||||
type SMTPNotifierConfiguration struct {
|
type SMTPNotifierConfiguration struct {
|
||||||
Username string `yaml:"username"`
|
Username string `yaml:"username"`
|
||||||
Password string `yaml:"password"`
|
Password string `yaml:"password"`
|
||||||
Secure string `yaml:"secure"`
|
Sender string `yaml:"sender"`
|
||||||
Host string `yaml:"host"`
|
Host string `yaml:"host"`
|
||||||
Port int `yaml:"port"`
|
Port int `yaml:"port"`
|
||||||
Sender string `yaml:"sender"`
|
Secure bool `yaml:"secure"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NotifierConfiguration representes the configuration of the notifier to use when sending notifications to users.
|
// NotifierConfiguration representes the configuration of the notifier to use when sending notifications to users.
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
package notification
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"net/smtp"
|
||||||
|
)
|
||||||
|
|
||||||
|
type loginAuth struct {
|
||||||
|
username string
|
||||||
|
password string
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoginAuth(username, password string) smtp.Auth {
|
||||||
|
return &loginAuth{username, password}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
|
||||||
|
return "LOGIN", []byte{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
|
||||||
|
if !more {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case bytes.Equal(fromServer, []byte("Username:")):
|
||||||
|
return []byte(a.username), nil
|
||||||
|
case bytes.Equal(fromServer, []byte("Password:")):
|
||||||
|
return []byte(a.password), nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("Unexpected challenge/data from server: %s.", fromServer)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,52 +1,112 @@
|
||||||
package notification
|
package notification
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/smtp"
|
"net/smtp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/authelia/authelia/internal/configuration/schema"
|
"github.com/authelia/authelia/internal/configuration/schema"
|
||||||
|
"github.com/authelia/authelia/internal/utils"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SMTPNotifier a notifier to send emails to SMTP servers.
|
// SMTPNotifier a notifier to send emails to SMTP servers.
|
||||||
type SMTPNotifier struct {
|
type SMTPNotifier struct {
|
||||||
address string
|
|
||||||
sender string
|
|
||||||
username string
|
username string
|
||||||
password string
|
password string
|
||||||
|
sender string
|
||||||
host string
|
host string
|
||||||
port int
|
port int
|
||||||
|
secure bool
|
||||||
|
address string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSMTPNotifier create an SMTPNotifier targeting a given address.
|
// NewSMTPNotifier create an SMTPNotifier targeting a given address.
|
||||||
func NewSMTPNotifier(configuration schema.SMTPNotifierConfiguration) *SMTPNotifier {
|
func NewSMTPNotifier(configuration schema.SMTPNotifierConfiguration) *SMTPNotifier {
|
||||||
return &SMTPNotifier{
|
return &SMTPNotifier{
|
||||||
host: configuration.Host,
|
|
||||||
port: configuration.Port,
|
|
||||||
address: fmt.Sprintf("%s:%d", configuration.Host, configuration.Port),
|
|
||||||
sender: configuration.Sender,
|
|
||||||
username: configuration.Username,
|
username: configuration.Username,
|
||||||
password: configuration.Password,
|
password: configuration.Password,
|
||||||
|
sender: configuration.Sender,
|
||||||
|
host: configuration.Host,
|
||||||
|
port: configuration.Port,
|
||||||
|
secure: configuration.Secure,
|
||||||
|
address: fmt.Sprintf("%s:%d", configuration.Host, configuration.Port),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *SMTPNotifier) authenticatedSend(recipient string, msg string) error {
|
// Send send a identity verification link to a user.
|
||||||
auth := smtp.PlainAuth("", n.username, n.password, n.host)
|
func (n *SMTPNotifier) Send(recipient string, subject string, body string) error {
|
||||||
err := smtp.SendMail(fmt.Sprintf("%s:%d", n.host, n.port), auth, n.sender,
|
msg := "From: " + n.sender + "\n" +
|
||||||
[]string{recipient}, []byte(msg))
|
"To: " + recipient + "\n" +
|
||||||
if err != nil {
|
"Subject: " + subject + "\n" +
|
||||||
return err
|
"Content-Type: text/html\n" +
|
||||||
}
|
"MIME-version: 1.0;\nContent-Type: text/html; charset=\"UTF-8\";\n\n" +
|
||||||
return nil
|
body
|
||||||
}
|
|
||||||
|
|
||||||
func (n *SMTPNotifier) unauthenticatedSend(recipient string, msg string) error {
|
|
||||||
// Connect to the remote SMTP server.
|
|
||||||
c, err := smtp.Dial(n.address)
|
c, err := smtp.Dial(n.address)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Do StartTLS if available (some servers only provide the auth extnesion after, and encrpytion is preferred)
|
||||||
|
starttls, _ := c.Extension("STARTTLS")
|
||||||
|
if starttls {
|
||||||
|
tlsconfig := &tls.Config{
|
||||||
|
InsecureSkipVerify: !n.secure,
|
||||||
|
ServerName: n.host,
|
||||||
|
}
|
||||||
|
log.Debugf("SMTP server supports STARTTLS (InsecureSkipVerify: %t, ServerName: %s), attempting", tlsconfig.InsecureSkipVerify, tlsconfig.ServerName)
|
||||||
|
err := c.StartTLS(tlsconfig)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
log.Debug("SMTP STARTTLS completed without error")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Debug("SMTP server does not support STARTTLS, skipping")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt AUTH if password is specified only
|
||||||
|
if n.password != "" {
|
||||||
|
|
||||||
|
// Check the server supports AUTH, and get the mechanisms
|
||||||
|
authExtension, m := c.Extension("AUTH")
|
||||||
|
if authExtension {
|
||||||
|
log.Debugf("Config has SMTP password and server supports AUTH with the following mechanisms: %s.", m)
|
||||||
|
mechanisms := strings.Split(m, " ")
|
||||||
|
var auth smtp.Auth
|
||||||
|
|
||||||
|
// Adaptively select the AUTH mechanism to use based on what the server advertised
|
||||||
|
if utils.IsStringInSlice("PLAIN", mechanisms) {
|
||||||
|
auth = smtp.PlainAuth("", n.username, n.password, n.host)
|
||||||
|
log.Debug("SMTP server supports AUTH PLAIN, attempting...")
|
||||||
|
} else if utils.IsStringInSlice("LOGIN", mechanisms) {
|
||||||
|
auth = LoginAuth(n.username, n.password)
|
||||||
|
log.Debug("SMTP server supports AUTH LOGIN, attempting...")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Throw error since AUTH extension is not supported
|
||||||
|
if auth == nil {
|
||||||
|
return fmt.Errorf("SMTP server does not advertise a AUTH mechanism that Authelia supports. Advertised mechanisms: %s.", m)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authenticate
|
||||||
|
err := c.Auth(auth)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
log.Debug("SMTP AUTH completed successfully.")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return errors.New("SMTP server does not advertise the AUTH extension but a password was specified. Either disable auth (don't specify a password/comment the password), or specify an SMTP host and port that supports AUTH PLAIN or AUTH LOGIN.")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Debug("SMTP config has no password specified for use with AUTH, skipping.")
|
||||||
|
}
|
||||||
|
|
||||||
// Set the sender and recipient first
|
// Set the sender and recipient first
|
||||||
if err := c.Mail(n.sender); err != nil {
|
if err := c.Mail(n.sender); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -77,18 +137,3 @@ func (n *SMTPNotifier) unauthenticatedSend(recipient string, msg string) error {
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send send a identity verification link to a user.
|
|
||||||
func (n *SMTPNotifier) Send(recipient string, subject string, body string) error {
|
|
||||||
msg := "From: " + n.sender + "\n" +
|
|
||||||
"To: " + recipient + "\n" +
|
|
||||||
"Subject: " + subject + "\n" +
|
|
||||||
"Content-Type: text/html\n" +
|
|
||||||
"MIME-version: 1.0;\nContent-Type: text/html; charset=\"UTF-8\";\n\n" +
|
|
||||||
body
|
|
||||||
|
|
||||||
if n.password != "" {
|
|
||||||
return n.authenticatedSend(recipient, msg)
|
|
||||||
}
|
|
||||||
return n.unauthenticatedSend(recipient, msg)
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in New Issue