2019-04-24 21:52:08 +00:00
package notification
import (
2019-12-20 18:40:01 +00:00
"crypto/tls"
2019-12-30 02:03:51 +00:00
"crypto/x509"
2019-12-20 18:40:01 +00:00
"errors"
2019-04-24 21:52:08 +00:00
"fmt"
2022-07-09 02:40:02 +00:00
"io"
2022-08-26 21:39:20 +00:00
"mime/multipart"
2021-08-10 00:52:41 +00:00
"net"
2022-07-18 00:56:09 +00:00
"net/mail"
2019-04-24 21:52:08 +00:00
"net/smtp"
2022-07-18 00:56:09 +00:00
"os"
2019-12-20 18:40:01 +00:00
"strings"
2020-08-21 02:16:23 +00:00
"time"
2019-04-24 21:52:08 +00:00
2022-07-18 00:56:09 +00:00
"github.com/google/uuid"
2021-11-30 11:15:21 +00:00
"github.com/sirupsen/logrus"
2021-08-11 01:04:35 +00:00
"github.com/authelia/authelia/v4/internal/configuration/schema"
"github.com/authelia/authelia/v4/internal/logging"
2022-07-18 00:56:09 +00:00
"github.com/authelia/authelia/v4/internal/templates"
2021-08-11 01:04:35 +00:00
"github.com/authelia/authelia/v4/internal/utils"
2019-04-24 21:52:08 +00:00
)
2022-07-18 00:56:09 +00:00
// 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 , tls . VersionTLS12 , certPool ) ,
log : logging . Logger ( ) ,
templates : templateProvider ,
}
at := strings . LastIndex ( config . Sender . Address , "@" )
if at >= 0 {
notifier . domain = config . Sender . Address [ at : ]
}
return notifier
}
[FEATURE] Notifier Startup Checks (#889)
* implement SMTP notifier startup check
* check dial, starttls, auth, mail from, rcpt to, reset, and quit
* log the error on failure
* implement mock
* misc optimizations, adjustments, and refactoring
* implement validate_skip config option
* fix comments to end with period
* fix suites that used smtp notifier without a smtp container
* add docs
* add file notifier startup check
* move file mode into const.go
* disable gosec linting on insecureskipverify since it's intended, warned, and discouraged
* minor PR commentary adjustment
* apply suggestions from code review
Co-Authored-By: Amir Zarrinkafsh <nightah@me.com>
2020-04-21 04:59:38 +00:00
// SMTPNotifier a notifier to send emails to SMTP servers.
2019-04-24 21:52:08 +00:00
type SMTPNotifier struct {
2022-07-18 00:56:09 +00:00
config * schema . SMTPNotifierConfiguration
domain string
tlsConfig * tls . Config
log * logrus . Logger
templates * templates . Provider
client * smtp . Client
2019-04-24 21:52:08 +00:00
}
2022-07-18 00:56:09 +00:00
// Send is used to email a recipient.
2022-08-26 21:39:20 +00:00
func ( n * SMTPNotifier ) Send ( recipient mail . Address , subject string , bodyText , bodyHTML [ ] byte ) ( err error ) {
2022-07-18 00:56:09 +00:00
if err = n . dial ( ) ; err != nil {
return fmt . Errorf ( fmtSMTPDialError , err )
2019-04-24 21:52:08 +00:00
}
2020-05-05 19:35:32 +00:00
2022-07-18 00:56:09 +00:00
// 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.
2022-08-26 21:39:20 +00:00
if err = n . compose ( recipient , subject , bodyText , bodyHTML ) ; err != nil {
2022-07-18 00:56:09 +00:00
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
2019-04-24 21:52:08 +00:00
}
[FEATURE] Notifier Startup Checks (#889)
* implement SMTP notifier startup check
* check dial, starttls, auth, mail from, rcpt to, reset, and quit
* log the error on failure
* implement mock
* misc optimizations, adjustments, and refactoring
* implement validate_skip config option
* fix comments to end with period
* fix suites that used smtp notifier without a smtp container
* add docs
* add file notifier startup check
* move file mode into const.go
* disable gosec linting on insecureskipverify since it's intended, warned, and discouraged
* minor PR commentary adjustment
* apply suggestions from code review
Co-Authored-By: Amir Zarrinkafsh <nightah@me.com>
2020-04-21 04:59:38 +00:00
// Do startTLS if available (some servers only provide the auth extension after, and encryption is preferred).
2020-04-23 02:01:24 +00:00
func ( n * SMTPNotifier ) startTLS ( ) error {
2022-01-31 05:25:15 +00:00
// Only start if not already encrypted.
2019-12-30 02:03:51 +00:00
if _ , ok := n . client . TLSConnectionState ( ) ; ok {
2021-11-30 11:15:21 +00:00
n . log . Debugf ( "Notifier SMTP connection is already encrypted, skipping STARTTLS" )
2020-04-23 02:01:24 +00:00
return nil
2019-12-30 02:03:51 +00:00
}
2022-08-26 21:39:20 +00:00
switch ok , _ := n . client . Extension ( smtpExtSTARTTLS ) ; ok {
2020-05-06 00:52:06 +00:00
case true :
2021-11-30 11:15:21 +00:00
n . log . Debugf ( "Notifier SMTP server supports STARTTLS (disableVerifyCert: %t, ServerName: %s), attempting" , n . tlsConfig . InsecureSkipVerify , n . tlsConfig . ServerName )
2019-12-30 02:03:51 +00:00
2020-04-23 02:01:24 +00:00
if err := n . client . StartTLS ( n . tlsConfig ) ; err != nil {
return err
2019-12-20 18:40:01 +00:00
}
2020-05-05 19:35:32 +00:00
2021-11-30 11:15:21 +00:00
n . log . Debug ( "Notifier SMTP STARTTLS completed without error" )
2020-05-06 00:52:06 +00:00
default :
2022-07-18 00:56:09 +00:00
switch n . config . DisableRequireTLS {
2020-05-06 00:52:06 +00:00
case true :
2021-11-30 11:15:21 +00:00
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)" )
2020-05-06 00:52:06 +00:00
default :
2022-07-18 00:56:09 +00:00
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)" )
2020-05-06 00:52:06 +00:00
}
2019-12-20 18:40:01 +00:00
}
2020-05-05 19:35:32 +00:00
2020-04-23 02:01:24 +00:00
return nil
2019-12-30 02:03:51 +00:00
}
2019-12-20 18:40:01 +00:00
[FEATURE] Notifier Startup Checks (#889)
* implement SMTP notifier startup check
* check dial, starttls, auth, mail from, rcpt to, reset, and quit
* log the error on failure
* implement mock
* misc optimizations, adjustments, and refactoring
* implement validate_skip config option
* fix comments to end with period
* fix suites that used smtp notifier without a smtp container
* add docs
* add file notifier startup check
* move file mode into const.go
* disable gosec linting on insecureskipverify since it's intended, warned, and discouraged
* minor PR commentary adjustment
* apply suggestions from code review
Co-Authored-By: Amir Zarrinkafsh <nightah@me.com>
2020-04-21 04:59:38 +00:00
// Attempt Authentication.
2022-07-18 00:56:09 +00:00
func ( n * SMTPNotifier ) auth ( ) ( err error ) {
[FEATURE] Notifier Startup Checks (#889)
* implement SMTP notifier startup check
* check dial, starttls, auth, mail from, rcpt to, reset, and quit
* log the error on failure
* implement mock
* misc optimizations, adjustments, and refactoring
* implement validate_skip config option
* fix comments to end with period
* fix suites that used smtp notifier without a smtp container
* add docs
* add file notifier startup check
* move file mode into const.go
* disable gosec linting on insecureskipverify since it's intended, warned, and discouraged
* minor PR commentary adjustment
* apply suggestions from code review
Co-Authored-By: Amir Zarrinkafsh <nightah@me.com>
2020-04-21 04:59:38 +00:00
// Attempt AUTH if password is specified only.
2022-07-18 00:56:09 +00:00
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" )
2019-12-28 02:49:29 +00:00
}
2019-12-20 18:40:01 +00:00
[FEATURE] Notifier Startup Checks (#889)
* implement SMTP notifier startup check
* check dial, starttls, auth, mail from, rcpt to, reset, and quit
* log the error on failure
* implement mock
* misc optimizations, adjustments, and refactoring
* implement validate_skip config option
* fix comments to end with period
* fix suites that used smtp notifier without a smtp container
* add docs
* add file notifier startup check
* move file mode into const.go
* disable gosec linting on insecureskipverify since it's intended, warned, and discouraged
* minor PR commentary adjustment
* apply suggestions from code review
Co-Authored-By: Amir Zarrinkafsh <nightah@me.com>
2020-04-21 04:59:38 +00:00
// Check the server supports AUTH, and get the mechanisms.
2022-07-18 00:56:09 +00:00
if ok , m = n . client . Extension ( smtpCommandAUTH ) ; ok {
2020-05-05 19:35:32 +00:00
var auth smtp . Auth
2021-11-30 11:15:21 +00:00
n . log . Debugf ( "Notifier SMTP server supports authentication with the following mechanisms: %s" , m )
2022-07-18 00:56:09 +00:00
2019-12-20 18:40:01 +00:00
mechanisms := strings . Split ( m , " " )
[FEATURE] Notifier Startup Checks (#889)
* implement SMTP notifier startup check
* check dial, starttls, auth, mail from, rcpt to, reset, and quit
* log the error on failure
* implement mock
* misc optimizations, adjustments, and refactoring
* implement validate_skip config option
* fix comments to end with period
* fix suites that used smtp notifier without a smtp container
* add docs
* add file notifier startup check
* move file mode into const.go
* disable gosec linting on insecureskipverify since it's intended, warned, and discouraged
* minor PR commentary adjustment
* apply suggestions from code review
Co-Authored-By: Amir Zarrinkafsh <nightah@me.com>
2020-04-21 04:59:38 +00:00
// Adaptively select the AUTH mechanism to use based on what the server advertised.
2022-07-18 00:56:09 +00:00
if utils . IsStringInSlice ( smtpAUTHMechanismPlain , mechanisms ) {
auth = smtp . PlainAuth ( "" , n . config . Username , n . config . Password , n . config . Host )
2020-05-05 19:35:32 +00:00
2021-11-30 11:15:21 +00:00
n . log . Debug ( "Notifier SMTP client attempting AUTH PLAIN with server" )
2022-07-18 00:56:09 +00:00
} else if utils . IsStringInSlice ( smtpAUTHMechanismLogin , mechanisms ) {
auth = newLoginAuth ( n . config . Username , n . config . Password , n . config . Host )
2020-05-05 19:35:32 +00:00
2021-11-30 11:15:21 +00:00
n . log . Debug ( "Notifier SMTP client attempting AUTH LOGIN with server" )
2019-12-20 18:40:01 +00:00
}
[FEATURE] Notifier Startup Checks (#889)
* implement SMTP notifier startup check
* check dial, starttls, auth, mail from, rcpt to, reset, and quit
* log the error on failure
* implement mock
* misc optimizations, adjustments, and refactoring
* implement validate_skip config option
* fix comments to end with period
* fix suites that used smtp notifier without a smtp container
* add docs
* add file notifier startup check
* move file mode into const.go
* disable gosec linting on insecureskipverify since it's intended, warned, and discouraged
* minor PR commentary adjustment
* apply suggestions from code review
Co-Authored-By: Amir Zarrinkafsh <nightah@me.com>
2020-04-21 04:59:38 +00:00
// Throw error since AUTH extension is not supported.
2019-12-20 18:40:01 +00:00
if auth == nil {
2022-07-18 00:56:09 +00:00
return fmt . Errorf ( "server does not advertise an AUTH mechanism that is supported (PLAIN or LOGIN are supported, but server advertised mechanisms '%s')" , m )
2019-12-20 18:40:01 +00:00
}
[FEATURE] Notifier Startup Checks (#889)
* implement SMTP notifier startup check
* check dial, starttls, auth, mail from, rcpt to, reset, and quit
* log the error on failure
* implement mock
* misc optimizations, adjustments, and refactoring
* implement validate_skip config option
* fix comments to end with period
* fix suites that used smtp notifier without a smtp container
* add docs
* add file notifier startup check
* move file mode into const.go
* disable gosec linting on insecureskipverify since it's intended, warned, and discouraged
* minor PR commentary adjustment
* apply suggestions from code review
Co-Authored-By: Amir Zarrinkafsh <nightah@me.com>
2020-04-21 04:59:38 +00:00
// Authenticate.
2022-07-18 00:56:09 +00:00
if err = n . client . Auth ( auth ) ; err != nil {
2020-04-23 02:01:24 +00:00
return err
2019-12-20 18:40:01 +00:00
}
2020-05-05 19:35:32 +00:00
2021-11-30 11:15:21 +00:00
n . log . Debug ( "Notifier SMTP client authenticated successfully with the server" )
2020-05-05 19:35:32 +00:00
2020-04-21 05:53:47 +00:00
return nil
2019-12-20 18:40:01 +00:00
}
2020-05-05 19:35:32 +00:00
2022-07-18 00:56:09 +00:00
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" )
2019-12-20 18:40:01 +00:00
}
2020-05-05 19:35:32 +00:00
2021-11-30 11:15:21 +00:00
n . log . Debug ( "Notifier SMTP config has no password specified so authentication is being skipped" )
2020-05-05 19:35:32 +00:00
2020-04-21 05:53:47 +00:00
return nil
2019-12-30 02:03:51 +00:00
}
2019-12-20 18:40:01 +00:00
2022-08-26 21:39:20 +00:00
func ( n * SMTPNotifier ) compose ( recipient mail . Address , subject string , bodyText , bodyHTML [ ] byte ) ( err error ) {
2022-07-18 00:56:09 +00:00
n . log . Debugf ( "Notifier SMTP client attempting to send email body to %s" , recipient . String ( ) )
2020-05-05 19:35:32 +00:00
2022-07-18 00:56:09 +00:00
if ! n . config . DisableRequireTLS {
2019-12-30 02:03:51 +00:00
_ , ok := n . client . TLSConnectionState ( )
if ! ok {
2022-07-18 00:56:09 +00:00
return errors . New ( "client can't send an email over plain text connection" )
2019-12-30 02:03:51 +00:00
}
}
2020-05-05 19:35:32 +00:00
2022-07-18 00:56:09 +00:00
var (
wc io . WriteCloser
muuid uuid . UUID
)
2019-12-30 02:03:51 +00:00
2022-07-18 00:56:09 +00:00
if wc , err = n . client . Data ( ) ; err != nil {
n . log . Debugf ( "Notifier SMTP client error while obtaining WriteCloser: %v" , err )
2019-04-24 21:52:08 +00:00
return err
}
2022-07-18 00:56:09 +00:00
if muuid , err = uuid . NewRandom ( ) ; err != nil {
2019-04-24 21:52:08 +00:00
return err
}
2020-05-05 19:35:32 +00:00
2022-08-26 21:39:20 +00:00
data := templates . EmailEnvelopeValues {
2022-07-18 00:56:09 +00:00
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 ( ) ,
2022-08-26 21:39:20 +00:00
Subject : strings . ReplaceAll ( n . config . Subject , "{title}" , subject ) ,
2022-07-18 00:56:09 +00:00
Date : time . Now ( ) ,
2022-07-09 02:40:02 +00:00
}
2022-08-26 21:39:20 +00:00
if err = n . templates . ExecuteEmailEnvelope ( wc , data ) ; err != nil {
2022-07-18 00:56:09 +00:00
n . log . Debugf ( "Notifier SMTP client error while sending email body over WriteCloser: %v" , err )
2022-07-09 02:40:02 +00:00
return err
2021-08-10 00:52:41 +00:00
}
2020-05-05 19:35:32 +00:00
2022-08-26 21:39:20 +00:00
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 )
}
2022-07-18 00:56:09 +00:00
if err = wc . Close ( ) ; err != nil {
n . log . Debugf ( "Notifier SMTP client error while closing the WriteCloser: %v" , err )
2021-08-10 00:52:41 +00:00
return err
2019-04-24 21:52:08 +00:00
}
2020-05-05 19:35:32 +00:00
2019-12-30 02:03:51 +00:00
return nil
}
[FEATURE] Notifier Startup Checks (#889)
* implement SMTP notifier startup check
* check dial, starttls, auth, mail from, rcpt to, reset, and quit
* log the error on failure
* implement mock
* misc optimizations, adjustments, and refactoring
* implement validate_skip config option
* fix comments to end with period
* fix suites that used smtp notifier without a smtp container
* add docs
* add file notifier startup check
* move file mode into const.go
* disable gosec linting on insecureskipverify since it's intended, warned, and discouraged
* minor PR commentary adjustment
* apply suggestions from code review
Co-Authored-By: Amir Zarrinkafsh <nightah@me.com>
2020-04-21 04:59:38 +00:00
// Closes the connection properly.
2020-01-28 04:00:43 +00:00
func ( n * SMTPNotifier ) cleanup ( ) {
2022-07-18 00:56:09 +00:00
if err := n . client . Quit ( ) ; err != nil {
n . log . Warnf ( "Notifier SMTP client encountered error during cleanup: %v" , err )
[FEATURE] Notifier Startup Checks (#889)
* implement SMTP notifier startup check
* check dial, starttls, auth, mail from, rcpt to, reset, and quit
* log the error on failure
* implement mock
* misc optimizations, adjustments, and refactoring
* implement validate_skip config option
* fix comments to end with period
* fix suites that used smtp notifier without a smtp container
* add docs
* add file notifier startup check
* move file mode into const.go
* disable gosec linting on insecureskipverify since it's intended, warned, and discouraged
* minor PR commentary adjustment
* apply suggestions from code review
Co-Authored-By: Amir Zarrinkafsh <nightah@me.com>
2020-04-21 04:59:38 +00:00
}
2022-07-18 00:56:09 +00:00
n . client = nil
2019-04-24 21:52:08 +00:00
}