diff --git a/internal/configuration/schema/notifier.go b/internal/configuration/schema/notifier.go index 6a3a7694..af51a9bd 100644 --- a/internal/configuration/schema/notifier.go +++ b/internal/configuration/schema/notifier.go @@ -17,10 +17,10 @@ type EmailNotifierConfiguration struct { type SMTPNotifierConfiguration struct { Username string `yaml:"username"` Password string `yaml:"password"` - Secure string `yaml:"secure"` + Sender string `yaml:"sender"` Host string `yaml:"host"` 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. diff --git a/internal/notification/smtp_login_auth.go b/internal/notification/smtp_login_auth.go new file mode 100644 index 00000000..075b3eeb --- /dev/null +++ b/internal/notification/smtp_login_auth.go @@ -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) + } +} diff --git a/internal/notification/smtp_notifier.go b/internal/notification/smtp_notifier.go index 861e195a..830a13b8 100644 --- a/internal/notification/smtp_notifier.go +++ b/internal/notification/smtp_notifier.go @@ -1,52 +1,112 @@ package notification import ( + "crypto/tls" + "errors" "fmt" "net/smtp" + "strings" "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. type SMTPNotifier struct { - address string - sender string username string password string + sender string host string port int + secure bool + address string } // NewSMTPNotifier create an SMTPNotifier targeting a given address. func NewSMTPNotifier(configuration schema.SMTPNotifierConfiguration) *SMTPNotifier { return &SMTPNotifier{ - host: configuration.Host, - port: configuration.Port, - address: fmt.Sprintf("%s:%d", configuration.Host, configuration.Port), - sender: configuration.Sender, username: configuration.Username, 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 { - auth := smtp.PlainAuth("", n.username, n.password, n.host) - err := smtp.SendMail(fmt.Sprintf("%s:%d", n.host, n.port), auth, n.sender, - []string{recipient}, []byte(msg)) - if err != nil { - return err - } - 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 -func (n *SMTPNotifier) unauthenticatedSend(recipient string, msg string) error { - // Connect to the remote SMTP server. c, err := smtp.Dial(n.address) if err != nil { 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 if err := c.Mail(n.sender); err != nil { return err @@ -77,18 +137,3 @@ func (n *SMTPNotifier) unauthenticatedSend(recipient string, msg string) error { } 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) -}