diff --git a/internal/notification/smtp_auth.go b/internal/notification/smtp_auth.go new file mode 100644 index 000000000..fb5d14f46 --- /dev/null +++ b/internal/notification/smtp_auth.go @@ -0,0 +1,86 @@ +package notification + +import ( + "fmt" + "net/smtp" + "strings" + + gomail "github.com/wneessen/go-mail" + "github.com/wneessen/go-mail/auth" + + "github.com/authelia/authelia/v4/internal/configuration/schema" + "github.com/authelia/authelia/v4/internal/utils" +) + +// NewOpportunisticSMTPAuth is an opportunistic smtp.Auth implementation. +func NewOpportunisticSMTPAuth(config *schema.SMTPNotifierConfiguration) *OpportunisticSMTPAuth { + if config.Username == "" && config.Password == "" { + return nil + } + + return &OpportunisticSMTPAuth{ + username: config.Username, + password: config.Password, + host: config.Host, + } +} + +// OpportunisticSMTPAuth is an opportunistic smtp.Auth implementation. +type OpportunisticSMTPAuth struct { + username, password, host string + + satPreference []gomail.SMTPAuthType + sa smtp.Auth +} + +// Start begins an authentication with a server. +// It returns the name of the authentication protocol +// and optionally data to include in the initial AUTH message +// sent to the server. +// If it returns a non-nil error, the SMTP client aborts +// the authentication attempt and closes the connection. +func (a *OpportunisticSMTPAuth) Start(server *smtp.ServerInfo) (proto string, toServer []byte, err error) { + for _, pref := range a.satPreference { + if utils.IsStringInSlice(string(pref), server.Auth) { + switch pref { + case gomail.SMTPAuthPlain: + a.sa = smtp.PlainAuth("", a.username, a.password, a.host) + case gomail.SMTPAuthLogin: + a.sa = auth.LoginAuth(a.username, a.password, a.host) + case gomail.SMTPAuthCramMD5: + a.sa = smtp.CRAMMD5Auth(a.username, a.password) + } + + break + } + } + + if a.sa == nil { + for _, sa := range server.Auth { + switch gomail.SMTPAuthType(sa) { + case gomail.SMTPAuthPlain: + a.sa = smtp.PlainAuth("", a.username, a.password, a.host) + case gomail.SMTPAuthLogin: + a.sa = auth.LoginAuth(a.username, a.password, a.host) + case gomail.SMTPAuthCramMD5: + a.sa = smtp.CRAMMD5Auth(a.username, a.password) + } + } + } + + if a.sa == nil { + return "", nil, fmt.Errorf("unsupported SMTP AUTH types: %s", strings.Join(server.Auth, ", ")) + } + + return a.sa.Start(server) +} + +// Next continues the authentication. The server has just sent +// the fromServer data. If more is true, the server expects a +// response, which Next should return as toServer; otherwise +// Next should return toServer == nil. +// If Next returns a non-nil error, the SMTP client aborts +// the authentication attempt and closes the connection. +func (a *OpportunisticSMTPAuth) Next(fromServer []byte, more bool) (toServer []byte, err error) { + return a.sa.Next(fromServer, more) +} diff --git a/internal/notification/smtp_notifier.go b/internal/notification/smtp_notifier.go index af4a954d0..0af580b73 100644 --- a/internal/notification/smtp_notifier.go +++ b/internal/notification/smtp_notifier.go @@ -22,7 +22,6 @@ func NewSMTPNotifier(config *schema.SMTPNotifierConfiguration, certPool *x509.Ce opts := []gomail.Option{ gomail.WithPort(config.Port), gomail.WithTLSConfig(utils.NewTLSConfig(config.TLS, certPool)), - gomail.WithPassword(config.Password), gomail.WithHELO(config.Identifier), } @@ -65,6 +64,7 @@ type SMTPNotifier struct { opts []gomail.Option } +// StartupCheck implements model.StartupCheck to perform startup check operations. func (n *SMTPNotifier) StartupCheck() (err error) { var client *gomail.Client @@ -85,6 +85,7 @@ func (n *SMTPNotifier) StartupCheck() (err error) { return nil } +// Send a notification via the SMTPNotifier. func (n *SMTPNotifier) Send(ctx context.Context, recipient mail.Address, subject string, et *templates.EmailTemplate, data any) (err error) { msg := gomail.NewMsg( gomail.WithMIMEVersion(gomail.Mime10), @@ -118,10 +119,16 @@ func (n *SMTPNotifier) Send(ctx context.Context, recipient mail.Address, subject var client *gomail.Client + n.log.Debugf("creating client with %d options: %+v", len(n.opts), n.opts) + if client, err = gomail.NewClient(n.config.Host, n.opts...); err != nil { return fmt.Errorf("notifier: smtp: failed to establish client: %w", err) } + if auth := NewOpportunisticSMTPAuth(n.config); auth != nil { + client.SetSMTPAuthCustom(auth) + } + if err = client.DialWithContext(ctx); err != nil { return fmt.Errorf("notifier: smtp: failed to dial connection: %w", err) }