333 lines
9.9 KiB
Go
333 lines
9.9 KiB
Go
package notification
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"mime/multipart"
|
|
"net"
|
|
"net/mail"
|
|
"net/smtp"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/sirupsen/logrus"
|
|
|
|
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
|
"github.com/authelia/authelia/v4/internal/logging"
|
|
"github.com/authelia/authelia/v4/internal/templates"
|
|
"github.com/authelia/authelia/v4/internal/utils"
|
|
)
|
|
|
|
// NewSMTPNotifier creates a SMTPNotifier using the notifier configuration.
|
|
func NewSMTPNotifier(config *schema.SMTPNotifierConfiguration, certPool *x509.CertPool, templateProvider *templates.Provider) *SMTPNotifier {
|
|
notifier := &SMTPNotifier{
|
|
config: config,
|
|
tlsConfig: utils.NewTLSConfig(config.TLS, certPool),
|
|
log: logging.Logger(),
|
|
templates: templateProvider,
|
|
}
|
|
|
|
at := strings.LastIndex(config.Sender.Address, "@")
|
|
|
|
if at >= 0 {
|
|
notifier.domain = config.Sender.Address[at:]
|
|
}
|
|
|
|
return notifier
|
|
}
|
|
|
|
// SMTPNotifier a notifier to send emails to SMTP servers.
|
|
type SMTPNotifier struct {
|
|
config *schema.SMTPNotifierConfiguration
|
|
domain string
|
|
tlsConfig *tls.Config
|
|
log *logrus.Logger
|
|
templates *templates.Provider
|
|
|
|
client *smtp.Client
|
|
}
|
|
|
|
// Send is used to email a recipient.
|
|
func (n *SMTPNotifier) Send(recipient mail.Address, subject string, bodyText, bodyHTML []byte) (err error) {
|
|
if err = n.dial(); err != nil {
|
|
return fmt.Errorf(fmtSMTPDialError, err)
|
|
}
|
|
|
|
// Always execute QUIT at the end once we're connected.
|
|
defer n.cleanup()
|
|
|
|
if err = n.preamble(recipient); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Compose and send the email body to the server.
|
|
if err = n.compose(recipient, subject, bodyText, bodyHTML); err != nil {
|
|
return fmt.Errorf(fmtSMTPGenericError, smtpCommandDATA, err)
|
|
}
|
|
|
|
n.log.Debug("Notifier SMTP client successfully sent email")
|
|
|
|
return nil
|
|
}
|
|
|
|
// StartupCheck implements the startup check provider interface.
|
|
func (n *SMTPNotifier) StartupCheck() (err error) {
|
|
if err = n.dial(); err != nil {
|
|
return fmt.Errorf(fmtSMTPDialError, err)
|
|
}
|
|
|
|
// Always execute QUIT at the end once we're connected.
|
|
defer n.cleanup()
|
|
|
|
if err = n.preamble(n.config.StartupCheckAddress); err != nil {
|
|
return err
|
|
}
|
|
|
|
return n.client.Reset()
|
|
}
|
|
|
|
// preamble performs generic preamble requirements for sending messages via SMTP.
|
|
func (n *SMTPNotifier) preamble(recipient mail.Address) (err error) {
|
|
if err = n.client.Hello(n.config.Identifier); err != nil {
|
|
return fmt.Errorf(fmtSMTPGenericError, smtpCommandHELLO, err)
|
|
}
|
|
|
|
if err = n.startTLS(); err != nil {
|
|
return fmt.Errorf(fmtSMTPGenericError, smtpCommandSTARTTLS, err)
|
|
}
|
|
|
|
if err = n.auth(); err != nil {
|
|
return fmt.Errorf(fmtSMTPGenericError, smtpCommandAUTH, err)
|
|
}
|
|
|
|
if err = n.client.Mail(n.config.Sender.Address); err != nil {
|
|
return fmt.Errorf(fmtSMTPGenericError, smtpCommandMAIL, err)
|
|
}
|
|
|
|
if err = n.client.Rcpt(recipient.Address); err != nil {
|
|
return fmt.Errorf(fmtSMTPGenericError, smtpCommandRCPT, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Dial the SMTP server with the SMTPNotifier config.
|
|
func (n *SMTPNotifier) dial() (err error) {
|
|
var (
|
|
client *smtp.Client
|
|
conn net.Conn
|
|
dialer = &net.Dialer{Timeout: n.config.Timeout}
|
|
)
|
|
|
|
n.log.Debugf("Notifier SMTP client attempting connection to %s:%d", n.config.Host, n.config.Port)
|
|
|
|
if n.config.Port == smtpPortSUBMISSIONS {
|
|
n.log.Debugf("Notifier SMTP client using submissions port 465. Make sure the mail server you are connecting to is configured for submissions and not SMTPS.")
|
|
|
|
conn, err = tls.DialWithDialer(dialer, "tcp", fmt.Sprintf("%s:%d", n.config.Host, n.config.Port), n.tlsConfig)
|
|
} else {
|
|
conn, err = dialer.Dial("tcp", fmt.Sprintf("%s:%d", n.config.Host, n.config.Port))
|
|
}
|
|
|
|
switch {
|
|
case err == nil:
|
|
break
|
|
case errors.Is(err, io.EOF):
|
|
return fmt.Errorf("received %w error: this error often occurs due to network errors such as a firewall, network policies, or closed ports which may be due to smtp service not running or an incorrect port specified in configuration", err)
|
|
default:
|
|
return err
|
|
}
|
|
|
|
if client, err = smtp.NewClient(conn, n.config.Host); err != nil {
|
|
return err
|
|
}
|
|
|
|
n.client = client
|
|
|
|
n.log.Debug("Notifier SMTP client connected successfully")
|
|
|
|
return nil
|
|
}
|
|
|
|
// Do startTLS if available (some servers only provide the auth extension after, and encryption is preferred).
|
|
func (n *SMTPNotifier) startTLS() error {
|
|
// Skips STARTTLS if is disabled in configuration.
|
|
if n.config.DisableStartTLS {
|
|
n.log.Warn("Notifier SMTP connection has opportunistic STARTTLS explicitly disabled which means all emails will be sent insecurely over plain text and this setting is only necessary for non-compliant SMTP servers which advertise they support STARTTLS when they actually don't support STARTTLS")
|
|
return nil
|
|
}
|
|
|
|
// Only start if not already encrypted.
|
|
if _, ok := n.client.TLSConnectionState(); ok {
|
|
n.log.Debugf("Notifier SMTP connection is already encrypted, skipping STARTTLS")
|
|
return nil
|
|
}
|
|
|
|
switch ok, _ := n.client.Extension(smtpExtSTARTTLS); ok {
|
|
case true:
|
|
n.log.Debugf("Notifier SMTP server supports STARTTLS (disableVerifyCert: %t, ServerName: %s), attempting", n.tlsConfig.InsecureSkipVerify, n.tlsConfig.ServerName)
|
|
|
|
if err := n.client.StartTLS(n.tlsConfig); err != nil {
|
|
return err
|
|
}
|
|
|
|
n.log.Debug("Notifier SMTP STARTTLS completed without error")
|
|
default:
|
|
switch n.config.DisableRequireTLS {
|
|
case true:
|
|
n.log.Warn("Notifier SMTP server does not support STARTTLS and SMTP configuration is set to disable the TLS requirement (only useful for unauthenticated emails over plain text)")
|
|
default:
|
|
return errors.New("server does not support TLS and it is required by default (see documentation if you want to disable this highly recommended requirement)")
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Attempt Authentication.
|
|
func (n *SMTPNotifier) auth() (err error) {
|
|
// Attempt AUTH if password is specified only.
|
|
if n.config.Password != "" {
|
|
var (
|
|
ok bool
|
|
m string
|
|
)
|
|
|
|
if _, ok = n.client.TLSConnectionState(); !ok {
|
|
return errors.New("client does not support authentication over plain text and the connection is currently plain text")
|
|
}
|
|
|
|
// Check the server supports AUTH, and get the mechanisms.
|
|
if ok, m = n.client.Extension(smtpCommandAUTH); ok {
|
|
var auth smtp.Auth
|
|
|
|
n.log.Debugf("Notifier SMTP server supports authentication with the following mechanisms: %s", m)
|
|
|
|
mechanisms := strings.Split(m, " ")
|
|
|
|
// Adaptively select the AUTH mechanism to use based on what the server advertised.
|
|
if utils.IsStringInSlice(smtpAUTHMechanismPlain, mechanisms) {
|
|
auth = smtp.PlainAuth("", n.config.Username, n.config.Password, n.config.Host)
|
|
|
|
n.log.Debug("Notifier SMTP client attempting AUTH PLAIN with server")
|
|
} else if utils.IsStringInSlice(smtpAUTHMechanismLogin, mechanisms) {
|
|
auth = newLoginAuth(n.config.Username, n.config.Password, n.config.Host)
|
|
|
|
n.log.Debug("Notifier SMTP client attempting AUTH LOGIN with server")
|
|
}
|
|
|
|
// Throw error since AUTH extension is not supported.
|
|
if auth == nil {
|
|
return fmt.Errorf("server does not advertise an AUTH mechanism that is supported (PLAIN or LOGIN are supported, but server advertised mechanisms '%s')", m)
|
|
}
|
|
|
|
// Authenticate.
|
|
if err = n.client.Auth(auth); err != nil {
|
|
return err
|
|
}
|
|
|
|
n.log.Debug("Notifier SMTP client authenticated successfully with the server")
|
|
|
|
return nil
|
|
}
|
|
|
|
return errors.New("server does not advertise the AUTH extension but config requires AUTH (password specified), either disable AUTH, or use an SMTP host that supports AUTH PLAIN or AUTH LOGIN")
|
|
}
|
|
|
|
n.log.Debug("Notifier SMTP config has no password specified so authentication is being skipped")
|
|
|
|
return nil
|
|
}
|
|
|
|
func (n *SMTPNotifier) compose(recipient mail.Address, subject string, bodyText, bodyHTML []byte) (err error) {
|
|
n.log.Debugf("Notifier SMTP client attempting to send email body to %s", recipient.String())
|
|
|
|
if !n.config.DisableRequireTLS {
|
|
_, ok := n.client.TLSConnectionState()
|
|
if !ok {
|
|
return errors.New("client can't send an email over plain text connection")
|
|
}
|
|
}
|
|
|
|
var (
|
|
wc io.WriteCloser
|
|
muuid uuid.UUID
|
|
)
|
|
|
|
if wc, err = n.client.Data(); err != nil {
|
|
n.log.Debugf("Notifier SMTP client error while obtaining WriteCloser: %v", err)
|
|
return err
|
|
}
|
|
|
|
if muuid, err = uuid.NewRandom(); err != nil {
|
|
return err
|
|
}
|
|
|
|
data := templates.EmailEnvelopeValues{
|
|
ProcessID: os.Getpid(),
|
|
UUID: muuid.String(),
|
|
Host: n.config.Host,
|
|
ServerName: n.config.TLS.ServerName,
|
|
SenderDomain: n.domain,
|
|
Identifier: n.config.Identifier,
|
|
From: n.config.Sender.String(),
|
|
To: recipient.String(),
|
|
Subject: strings.ReplaceAll(n.config.Subject, "{title}", subject),
|
|
Date: time.Now(),
|
|
}
|
|
|
|
if err = n.templates.ExecuteEmailEnvelope(wc, data); err != nil {
|
|
n.log.Debugf("Notifier SMTP client error while sending email body over WriteCloser: %v", err)
|
|
|
|
return err
|
|
}
|
|
|
|
mwr := multipart.NewWriter(wc)
|
|
|
|
if _, err = wc.Write([]byte(fmt.Sprintf(`Content-Type: multipart/alternative; boundary="%s"`, mwr.Boundary()))); err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, err = wc.Write(rfc2822DoubleNewLine); err != nil {
|
|
return err
|
|
}
|
|
|
|
ext8BITMIME, _ := n.client.Extension(smtpExt8BITMIME)
|
|
|
|
if err = multipartWrite(mwr, smtpMIMEHeaders(ext8BITMIME, smtpContentTypeTextPlain, bodyText), bodyText); err != nil {
|
|
return fmt.Errorf("failed to write text/plain part: %w", err)
|
|
}
|
|
|
|
if len(bodyHTML) != 0 {
|
|
if err = multipartWrite(mwr,
|
|
smtpMIMEHeaders(ext8BITMIME, smtpContentTypeTextHTML, bodyText), bodyHTML); err != nil {
|
|
return fmt.Errorf("failed to write text/html part: %w", err)
|
|
}
|
|
}
|
|
|
|
if err = mwr.Close(); err != nil {
|
|
return fmt.Errorf("failed to finalize the multipart content: %w", err)
|
|
}
|
|
|
|
if err = wc.Close(); err != nil {
|
|
n.log.Debugf("Notifier SMTP client error while closing the WriteCloser: %v", err)
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Closes the connection properly.
|
|
func (n *SMTPNotifier) cleanup() {
|
|
if err := n.client.Quit(); err != nil {
|
|
n.log.Warnf("Notifier SMTP client encountered error during cleanup: %v", err)
|
|
}
|
|
|
|
n.client = nil
|
|
}
|