fix(notifier): force use of sender email in smtp from cmd (#2616)

This change addresses an issue with the usage of the full sender configuration option in the MAIL FROM SMTP command. If a user includes a name in the sender this shouldn't be sent in the MAIL FROM command, instead we should extract it and use just the email portion.

Fixes #2571
pull/2647/head^2
James Elliott 2021-11-30 22:15:21 +11:00 committed by GitHub
parent 568f210b2b
commit ab8f9b0697
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 437 additions and 79 deletions

View File

@ -591,8 +591,10 @@ notifier:
## Can also be set using a secret: https://www.authelia.com/docs/configuration/secrets.html ## Can also be set using a secret: https://www.authelia.com/docs/configuration/secrets.html
password: password password: password
## The address to send the email FROM. ## The sender is used to is used for the MAIL FROM command and the FROM header.
sender: admin@example.com ## If this is not defined and the username is an email, we use the username as this value. This can either be just
## an email address or the RFC5322 'Name <email address>' format.
sender: "Authelia <admin@example.com>"
## HELO/EHLO Identifier. Some SMTP Servers may reject the default of localhost. ## HELO/EHLO Identifier. Some SMTP Servers may reject the default of localhost.
identifier: localhost identifier: localhost

View File

@ -21,7 +21,7 @@ notifier:
timeout: 5s timeout: 5s
username: test username: test
password: password password: password
sender: admin@example.com sender: "Authelia <admin@example.com>"
identifier: localhost identifier: localhost
subject: "[Authelia] {title}" subject: "[Authelia] {title}"
startup_check_address: test@authelia.com startup_check_address: test@authelia.com
@ -103,8 +103,13 @@ required: yes
{: .label .label-config .label-red } {: .label .label-config .label-red }
</div> </div>
The address sent in the FROM header for the email. Basically who the email appears to come from. It should be noted The sender is used to construct both the SMTP command `MAIL FROM` and to add the `FROM` header. This address must be
that some SMTP servers require the username provided to have access to send from the specific address listed here. in [RFC5322](https://datatracker.ietf.org/doc/html/rfc5322#section-3.4) format. This means it must one of two formats:
- jsmith@domain.com
- John Smith <jsmith@domain.com>
The `MAIL FROM` command sent to SMTP servers will not include the name portion, this is only set in the `FROM` as per
specifications.
### identifier ### identifier
<div markdown="1"> <div markdown="1">

View File

@ -591,8 +591,10 @@ notifier:
## Can also be set using a secret: https://www.authelia.com/docs/configuration/secrets.html ## Can also be set using a secret: https://www.authelia.com/docs/configuration/secrets.html
password: password password: password
## The address to send the email FROM. ## The sender is used to is used for the MAIL FROM command and the FROM header.
sender: admin@example.com ## If this is not defined and the username is an email, we use the username as this value. This can either be just
## an email address or the RFC5322 'Name <email address>' format.
sender: "Authelia <admin@example.com>"
## HELO/EHLO Identifier. Some SMTP Servers may reject the default of localhost. ## HELO/EHLO Identifier. Some SMTP Servers may reject the default of localhost.
identifier: localhost identifier: localhost

View File

@ -0,0 +1,35 @@
package configuration
import (
"fmt"
"net/mail"
"reflect"
"github.com/mitchellh/mapstructure"
)
// StringToMailAddressFunc decodes a string into a mail.Address.
func StringToMailAddressFunc() mapstructure.DecodeHookFunc {
return func(f reflect.Kind, t reflect.Kind, data interface{}) (value interface{}, err error) {
if f != reflect.String || t != reflect.TypeOf(mail.Address{}).Kind() {
return data, nil
}
dataStr := data.(string)
if dataStr == "" {
return mail.Address{}, nil
}
var (
mailAddress *mail.Address
)
mailAddress, err = mail.ParseAddress(dataStr)
if err != nil {
return nil, fmt.Errorf("could not parse '%s' as a RFC5322 address: %w", dataStr, err)
}
return *mailAddress, nil
}
}

View File

@ -45,6 +45,7 @@ func unmarshal(ko *koanf.Koanf, val *schema.StructValidator, path string, o inte
DecodeHook: mapstructure.ComposeDecodeHookFunc( DecodeHook: mapstructure.ComposeDecodeHookFunc(
mapstructure.StringToTimeDurationHookFunc(), mapstructure.StringToTimeDurationHookFunc(),
mapstructure.StringToSliceHookFunc(","), mapstructure.StringToSliceHookFunc(","),
StringToMailAddressFunc(),
), ),
Metadata: nil, Metadata: nil,
Result: o, Result: o,

View File

@ -224,6 +224,39 @@ func TestShouldValidateAndRaiseErrorsOnBadConfiguration(t *testing.T) {
assert.EqualError(t, val.Errors()[1], "invalid configuration key 'logs_level' was replaced by 'log.level'") assert.EqualError(t, val.Errors()[1], "invalid configuration key 'logs_level' was replaced by 'log.level'")
} }
func TestShouldRaiseErrOnInvalidNotifierSMTPSender(t *testing.T) {
testReset()
val := schema.NewStructValidator()
keys, _, err := Load(val, NewDefaultSources([]string{"./test_resources/config_smtp_sender_invalid.yml"}, DefaultEnvPrefix, DefaultEnvDelimiter)...)
assert.NoError(t, err)
validator.ValidateKeys(keys, DefaultEnvPrefix, val)
require.Len(t, val.Errors(), 1)
assert.Len(t, val.Warnings(), 0)
assert.EqualError(t, val.Errors()[0], "error occurred during unmarshalling configuration: 1 error(s) decoding:\n\n* error decoding 'notifier.smtp.sender': could not parse 'admin' as a RFC5322 address: mail: missing '@' or angle-addr")
}
func TestShouldHandleErrInvalidatorWhenSMTPSenderBlank(t *testing.T) {
testReset()
val := schema.NewStructValidator()
keys, config, err := Load(val, NewDefaultSources([]string{"./test_resources/config_smtp_sender_blank.yml"}, DefaultEnvPrefix, DefaultEnvDelimiter)...)
assert.NoError(t, err)
validator.ValidateKeys(keys, DefaultEnvPrefix, val)
assert.Len(t, val.Errors(), 0)
assert.Len(t, val.Warnings(), 0)
assert.Equal(t, "", config.Notifier.SMTP.Sender.Name)
assert.Equal(t, "", config.Notifier.SMTP.Sender.Address)
}
func TestShouldNotReadConfigurationOnFSAccessDenied(t *testing.T) { func TestShouldNotReadConfigurationOnFSAccessDenied(t *testing.T) {
if runtime.GOOS == constWindows { if runtime.GOOS == constWindows {
t.Skip("skipping test due to being on windows") t.Skip("skipping test due to being on windows")

View File

@ -1,6 +1,9 @@
package schema package schema
import "time" import (
"net/mail"
"time"
)
// FileSystemNotifierConfiguration represents the configuration of the notifier writing emails in a file. // FileSystemNotifierConfiguration represents the configuration of the notifier writing emails in a file.
type FileSystemNotifierConfiguration struct { type FileSystemNotifierConfiguration struct {
@ -15,7 +18,7 @@ type SMTPNotifierConfiguration struct {
Username string `koanf:"username"` Username string `koanf:"username"`
Password string `koanf:"password"` Password string `koanf:"password"`
Identifier string `koanf:"identifier"` Identifier string `koanf:"identifier"`
Sender string `koanf:"sender"` Sender mail.Address `koanf:"sender"`
Subject string `koanf:"subject"` Subject string `koanf:"subject"`
StartupCheckAddress string `koanf:"startup_check_address"` StartupCheckAddress string `koanf:"startup_check_address"`
DisableRequireTLS bool `koanf:"disable_require_tls"` DisableRequireTLS bool `koanf:"disable_require_tls"`
@ -35,6 +38,7 @@ var DefaultSMTPNotifierConfiguration = SMTPNotifierConfiguration{
Timeout: time.Second * 5, Timeout: time.Second * 5,
Subject: "[Authelia] {title}", Subject: "[Authelia] {title}",
Identifier: "localhost", Identifier: "localhost",
StartupCheckAddress: "test@authelia.com",
TLS: &TLSConfig{ TLS: &TLSConfig{
MinimumVersion: "TLS1.2", MinimumVersion: "TLS1.2",
}, },

View File

@ -0,0 +1,126 @@
---
default_redirection_url: https://home.example.com:8080/
server:
host: 127.0.0.1
port: 9091
log:
level: debug
totp:
issuer: authelia.com
duo_api:
hostname: api-123456789.example.com
integration_key: ABCDEF
authentication_backend:
ldap:
url: ldap://127.0.0.1
base_dn: dc=example,dc=com
username_attribute: uid
additional_users_dn: ou=users
users_filter: (&({username_attribute}={input})(objectCategory=person)(objectClass=user))
additional_groups_dn: ou=groups
groups_filter: (&(member={dn})(objectClass=groupOfNames))
group_name_attribute: cn
mail_attribute: mail
user: cn=admin,dc=example,dc=com
access_control:
default_policy: deny
rules:
# Rules applied to everyone
- domain: public.example.com
policy: bypass
- domain: secure.example.com
policy: one_factor
# Network based rule, if not provided any network matches.
networks:
- 192.168.1.0/24
- domain: secure.example.com
policy: two_factor
- domain: [singlefactor.example.com, onefactor.example.com]
policy: one_factor
# Rules applied to 'admins' group
- domain: "mx2.mail.example.com"
subject: "group:admins"
policy: deny
- domain: "*.example.com"
subject: "group:admins"
policy: two_factor
# Rules applied to 'dev' group
- domain: dev.example.com
resources:
- "^/groups/dev/.*$"
subject: "group:dev"
policy: two_factor
# Rules applied to user 'john'
- domain: dev.example.com
resources:
- "^/users/john/.*$"
subject: "user:john"
policy: two_factor
# Rules applied to 'dev' group and user 'john'
- domain: dev.example.com
resources:
- "^/deny-all.*$"
subject: ["group:dev", "user:john"]
policy: deny
# Rules applied to user 'harry'
- domain: dev.example.com
resources:
- "^/users/harry/.*$"
subject: "user:harry"
policy: two_factor
# Rules applied to user 'bob'
- domain: "*.mail.example.com"
subject: "user:bob"
policy: two_factor
- domain: "dev.example.com"
resources:
- "^/users/bob/.*$"
subject: "user:bob"
policy: two_factor
session:
name: authelia_session
expiration: 3600000 # 1 hour
inactivity: 300000 # 5 minutes
domain: example.com
redis:
host: 127.0.0.1
port: 6379
high_availability:
sentinel_name: test
regulation:
max_retries: 3
find_time: 120
ban_time: 300
storage:
mysql:
host: 127.0.0.1
port: 3306
database: authelia
username: authelia
notifier:
smtp:
username: test
host: 127.0.0.1
port: 1025
sender: ""
disable_require_tls: true
...

View File

@ -0,0 +1,126 @@
---
default_redirection_url: https://home.example.com:8080/
server:
host: 127.0.0.1
port: 9091
log:
level: debug
totp:
issuer: authelia.com
duo_api:
hostname: api-123456789.example.com
integration_key: ABCDEF
authentication_backend:
ldap:
url: ldap://127.0.0.1
base_dn: dc=example,dc=com
username_attribute: uid
additional_users_dn: ou=users
users_filter: (&({username_attribute}={input})(objectCategory=person)(objectClass=user))
additional_groups_dn: ou=groups
groups_filter: (&(member={dn})(objectClass=groupOfNames))
group_name_attribute: cn
mail_attribute: mail
user: cn=admin,dc=example,dc=com
access_control:
default_policy: deny
rules:
# Rules applied to everyone
- domain: public.example.com
policy: bypass
- domain: secure.example.com
policy: one_factor
# Network based rule, if not provided any network matches.
networks:
- 192.168.1.0/24
- domain: secure.example.com
policy: two_factor
- domain: [singlefactor.example.com, onefactor.example.com]
policy: one_factor
# Rules applied to 'admins' group
- domain: "mx2.mail.example.com"
subject: "group:admins"
policy: deny
- domain: "*.example.com"
subject: "group:admins"
policy: two_factor
# Rules applied to 'dev' group
- domain: dev.example.com
resources:
- "^/groups/dev/.*$"
subject: "group:dev"
policy: two_factor
# Rules applied to user 'john'
- domain: dev.example.com
resources:
- "^/users/john/.*$"
subject: "user:john"
policy: two_factor
# Rules applied to 'dev' group and user 'john'
- domain: dev.example.com
resources:
- "^/deny-all.*$"
subject: ["group:dev", "user:john"]
policy: deny
# Rules applied to user 'harry'
- domain: dev.example.com
resources:
- "^/users/harry/.*$"
subject: "user:harry"
policy: two_factor
# Rules applied to user 'bob'
- domain: "*.mail.example.com"
subject: "user:bob"
policy: two_factor
- domain: "dev.example.com"
resources:
- "^/users/bob/.*$"
subject: "user:bob"
policy: two_factor
session:
name: authelia_session
expiration: 3600000 # 1 hour
inactivity: 300000 # 5 minutes
domain: example.com
redis:
host: 127.0.0.1
port: 6379
high_availability:
sentinel_name: test
regulation:
max_retries: 3
find_time: 120
ban_time: 300
storage:
mysql:
host: 127.0.0.1
port: 3306
database: authelia
username: authelia
notifier:
smtp:
username: test
host: 127.0.0.1
port: 1025
sender: admin
disable_require_tls: true
...

View File

@ -31,7 +31,7 @@ func ValidateNotifier(configuration *schema.NotifierConfiguration, validator *sc
func validateSMTPNotifier(configuration *schema.SMTPNotifierConfiguration, validator *schema.StructValidator) { func validateSMTPNotifier(configuration *schema.SMTPNotifierConfiguration, validator *schema.StructValidator) {
if configuration.StartupCheckAddress == "" { if configuration.StartupCheckAddress == "" {
configuration.StartupCheckAddress = "test@authelia.com" configuration.StartupCheckAddress = schema.DefaultSMTPNotifierConfiguration.StartupCheckAddress
} }
if configuration.Host == "" { if configuration.Host == "" {
@ -46,7 +46,7 @@ func validateSMTPNotifier(configuration *schema.SMTPNotifierConfiguration, valid
configuration.Timeout = schema.DefaultSMTPNotifierConfiguration.Timeout configuration.Timeout = schema.DefaultSMTPNotifierConfiguration.Timeout
} }
if configuration.Sender == "" { if configuration.Sender.Address == "" {
validator.Push(fmt.Errorf(errFmtNotifierSMTPNotConfigured, "sender")) validator.Push(fmt.Errorf(errFmtNotifierSMTPNotConfigured, "sender"))
} }

View File

@ -2,6 +2,7 @@ package validator
import ( import (
"fmt" "fmt"
"net/mail"
"testing" "testing"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
@ -20,12 +21,16 @@ func (suite *NotifierSuite) SetupTest() {
suite.configuration.SMTP = &schema.SMTPNotifierConfiguration{ suite.configuration.SMTP = &schema.SMTPNotifierConfiguration{
Username: "john", Username: "john",
Password: "password", Password: "password",
Sender: "admin@example.com", Sender: mail.Address{Name: "Authelia", Address: "authelia@example.com"},
Host: "example.com", Host: "example.com",
Port: 25, Port: 25,
} }
suite.configuration.FileSystem = nil
} }
/*
Common Tests.
*/
func (suite *NotifierSuite) TestShouldEnsureAtLeastSMTPOrFilesystemIsProvided() { func (suite *NotifierSuite) TestShouldEnsureAtLeastSMTPOrFilesystemIsProvided() {
ValidateNotifier(&suite.configuration, suite.validator) ValidateNotifier(&suite.configuration, suite.validator)
@ -63,29 +68,37 @@ func (suite *NotifierSuite) TestShouldEnsureEitherSMTPOrFilesystemIsProvided() {
suite.Assert().EqualError(suite.validator.Errors()[0], errFmtNotifierMultipleConfigured) suite.Assert().EqualError(suite.validator.Errors()[0], errFmtNotifierMultipleConfigured)
} }
func (suite *NotifierSuite) TestShouldEnsureFilenameOfFilesystemNotifierIsProvided() { /*
suite.configuration.SMTP = nil SMTP Tests.
suite.configuration.FileSystem = &schema.FileSystemNotifierConfiguration{ */
Filename: "test", func (suite *NotifierSuite) TestSMTPShouldSetTLSDefaults() {
}
ValidateNotifier(&suite.configuration, suite.validator) ValidateNotifier(&suite.configuration, suite.validator)
suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().False(suite.validator.HasWarnings())
suite.Assert().False(suite.validator.HasErrors()) suite.Assert().False(suite.validator.HasErrors())
suite.configuration.FileSystem.Filename = "" suite.Assert().Equal("example.com", suite.configuration.SMTP.TLS.ServerName)
suite.Assert().Equal("TLS1.2", suite.configuration.SMTP.TLS.MinimumVersion)
suite.Assert().False(suite.configuration.SMTP.TLS.SkipVerify)
}
func (suite *NotifierSuite) TestSMTPShouldDefaultTLSServerNameToHost() {
suite.configuration.SMTP.Host = "google.com"
suite.configuration.SMTP.TLS = &schema.TLSConfig{
MinimumVersion: "TLS1.1",
}
ValidateNotifier(&suite.configuration, suite.validator) ValidateNotifier(&suite.configuration, suite.validator)
suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().False(suite.validator.HasWarnings())
suite.Require().True(suite.validator.HasErrors()) suite.Assert().False(suite.validator.HasErrors())
suite.Assert().Len(suite.validator.Errors(), 1) suite.Assert().Equal("google.com", suite.configuration.SMTP.TLS.ServerName)
suite.Assert().Equal("TLS1.1", suite.configuration.SMTP.TLS.MinimumVersion)
suite.Assert().EqualError(suite.validator.Errors()[0], errFmtNotifierFileSystemFileNameNotConfigured) suite.Assert().False(suite.configuration.SMTP.TLS.SkipVerify)
} }
func (suite *NotifierSuite) TestShouldEnsureHostAndPortOfSMTPNotifierAreProvided() { func (suite *NotifierSuite) TestSMTPShouldEnsureHostAndPortAreProvided() {
suite.configuration.FileSystem = nil suite.configuration.FileSystem = nil
ValidateNotifier(&suite.configuration, suite.validator) ValidateNotifier(&suite.configuration, suite.validator)
@ -108,15 +121,8 @@ func (suite *NotifierSuite) TestShouldEnsureHostAndPortOfSMTPNotifierAreProvided
suite.Assert().EqualError(errors[1], fmt.Sprintf(errFmtNotifierSMTPNotConfigured, "port")) suite.Assert().EqualError(errors[1], fmt.Sprintf(errFmtNotifierSMTPNotConfigured, "port"))
} }
func (suite *NotifierSuite) TestShouldEnsureSenderOfSMTPNotifierAreProvided() { func (suite *NotifierSuite) TestSMTPShouldEnsureSenderIsProvided() {
suite.configuration.FileSystem = nil suite.configuration.SMTP.Sender = mail.Address{}
ValidateNotifier(&suite.configuration, suite.validator)
suite.Assert().False(suite.validator.HasWarnings())
suite.Assert().False(suite.validator.HasErrors())
suite.configuration.SMTP.Sender = ""
ValidateNotifier(&suite.configuration, suite.validator) ValidateNotifier(&suite.configuration, suite.validator)
@ -128,6 +134,31 @@ func (suite *NotifierSuite) TestShouldEnsureSenderOfSMTPNotifierAreProvided() {
suite.Assert().EqualError(suite.validator.Errors()[0], fmt.Sprintf(errFmtNotifierSMTPNotConfigured, "sender")) suite.Assert().EqualError(suite.validator.Errors()[0], fmt.Sprintf(errFmtNotifierSMTPNotConfigured, "sender"))
} }
/*
File Tests.
*/
func (suite *NotifierSuite) TestFileShouldEnsureFilenameIsProvided() {
suite.configuration.SMTP = nil
suite.configuration.FileSystem = &schema.FileSystemNotifierConfiguration{
Filename: "test",
}
ValidateNotifier(&suite.configuration, suite.validator)
suite.Assert().False(suite.validator.HasWarnings())
suite.Assert().False(suite.validator.HasErrors())
suite.configuration.FileSystem.Filename = ""
ValidateNotifier(&suite.configuration, suite.validator)
suite.Assert().False(suite.validator.HasWarnings())
suite.Require().True(suite.validator.HasErrors())
suite.Assert().Len(suite.validator.Errors(), 1)
suite.Assert().EqualError(suite.validator.Errors()[0], errFmtNotifierFileSystemFileNameNotConfigured)
}
func TestNotifierSuite(t *testing.T) { func TestNotifierSuite(t *testing.T) {
suite.Run(t, new(NotifierSuite)) suite.Run(t, new(NotifierSuite))
} }

View File

@ -1,4 +1,9 @@
package notification package notification
const fileNotifierMode = 0600 const (
const rfc5322DateTimeLayout = "Mon, 2 Jan 2006 15:04:05 -0700" fileNotifierMode = 0600
)
const (
rfc5322DateTimeLayout = "Mon, 2 Jan 2006 15:04:05 -0700"
)

View File

@ -10,6 +10,8 @@ import (
"strings" "strings"
"time" "time"
"github.com/sirupsen/logrus"
"github.com/authelia/authelia/v4/internal/configuration/schema" "github.com/authelia/authelia/v4/internal/configuration/schema"
"github.com/authelia/authelia/v4/internal/logging" "github.com/authelia/authelia/v4/internal/logging"
"github.com/authelia/authelia/v4/internal/utils" "github.com/authelia/authelia/v4/internal/utils"
@ -20,6 +22,7 @@ type SMTPNotifier struct {
configuration *schema.SMTPNotifierConfiguration configuration *schema.SMTPNotifierConfiguration
client *smtp.Client client *smtp.Client
tlsConfig *tls.Config tlsConfig *tls.Config
log *logrus.Logger
} }
// NewSMTPNotifier creates a SMTPNotifier using the notifier configuration. // NewSMTPNotifier creates a SMTPNotifier using the notifier configuration.
@ -27,6 +30,7 @@ func NewSMTPNotifier(configuration *schema.SMTPNotifierConfiguration, certPool *
notifier := &SMTPNotifier{ notifier := &SMTPNotifier{
configuration: configuration, configuration: configuration,
tlsConfig: utils.NewTLSConfig(configuration.TLS, tls.VersionTLS12, certPool), tlsConfig: utils.NewTLSConfig(configuration.TLS, tls.VersionTLS12, certPool),
log: logging.Logger(),
} }
return notifier return notifier
@ -34,26 +38,25 @@ func NewSMTPNotifier(configuration *schema.SMTPNotifierConfiguration, certPool *
// Do startTLS if available (some servers only provide the auth extension after, and encryption is preferred). // Do startTLS if available (some servers only provide the auth extension after, and encryption is preferred).
func (n *SMTPNotifier) startTLS() error { func (n *SMTPNotifier) startTLS() error {
logger := logging.Logger()
// Only start if not already encrypted // Only start if not already encrypted
if _, ok := n.client.TLSConnectionState(); ok { if _, ok := n.client.TLSConnectionState(); ok {
logger.Debugf("Notifier SMTP connection is already encrypted, skipping STARTTLS") n.log.Debugf("Notifier SMTP connection is already encrypted, skipping STARTTLS")
return nil return nil
} }
switch ok, _ := n.client.Extension("STARTTLS"); ok { switch ok, _ := n.client.Extension("STARTTLS"); ok {
case true: case true:
logger.Debugf("Notifier SMTP server supports STARTTLS (disableVerifyCert: %t, ServerName: %s), attempting", n.tlsConfig.InsecureSkipVerify, n.tlsConfig.ServerName) 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 { if err := n.client.StartTLS(n.tlsConfig); err != nil {
return err return err
} }
logger.Debug("Notifier SMTP STARTTLS completed without error") n.log.Debug("Notifier SMTP STARTTLS completed without error")
default: default:
switch n.configuration.DisableRequireTLS { switch n.configuration.DisableRequireTLS {
case true: case true:
logger.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)") 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: default:
return errors.New("Notifier SMTP server does not support TLS and it is required by default (see documentation if you want to disable this highly recommended requirement)") return errors.New("Notifier SMTP server does not support TLS and it is required by default (see documentation if you want to disable this highly recommended requirement)")
} }
@ -64,7 +67,6 @@ func (n *SMTPNotifier) startTLS() error {
// Attempt Authentication. // Attempt Authentication.
func (n *SMTPNotifier) auth() error { func (n *SMTPNotifier) auth() error {
logger := logging.Logger()
// Attempt AUTH if password is specified only. // Attempt AUTH if password is specified only.
if n.configuration.Password != "" { if n.configuration.Password != "" {
_, ok := n.client.TLSConnectionState() _, ok := n.client.TLSConnectionState()
@ -77,18 +79,18 @@ func (n *SMTPNotifier) auth() error {
if ok { if ok {
var auth smtp.Auth var auth smtp.Auth
logger.Debugf("Notifier SMTP server supports authentication with the following mechanisms: %s", m) n.log.Debugf("Notifier SMTP server supports authentication with the following mechanisms: %s", m)
mechanisms := strings.Split(m, " ") mechanisms := strings.Split(m, " ")
// Adaptively select the AUTH mechanism to use based on what the server advertised. // Adaptively select the AUTH mechanism to use based on what the server advertised.
if utils.IsStringInSlice("PLAIN", mechanisms) { if utils.IsStringInSlice("PLAIN", mechanisms) {
auth = smtp.PlainAuth("", n.configuration.Username, n.configuration.Password, n.configuration.Host) auth = smtp.PlainAuth("", n.configuration.Username, n.configuration.Password, n.configuration.Host)
logger.Debug("Notifier SMTP client attempting AUTH PLAIN with server") n.log.Debug("Notifier SMTP client attempting AUTH PLAIN with server")
} else if utils.IsStringInSlice("LOGIN", mechanisms) { } else if utils.IsStringInSlice("LOGIN", mechanisms) {
auth = newLoginAuth(n.configuration.Username, n.configuration.Password, n.configuration.Host) auth = newLoginAuth(n.configuration.Username, n.configuration.Password, n.configuration.Host)
logger.Debug("Notifier SMTP client attempting AUTH LOGIN with server") n.log.Debug("Notifier SMTP client attempting AUTH LOGIN with server")
} }
// Throw error since AUTH extension is not supported. // Throw error since AUTH extension is not supported.
@ -101,7 +103,7 @@ func (n *SMTPNotifier) auth() error {
return err return err
} }
logger.Debug("Notifier SMTP client authenticated successfully with the server") n.log.Debug("Notifier SMTP client authenticated successfully with the server")
return nil return nil
} }
@ -109,14 +111,13 @@ func (n *SMTPNotifier) auth() error {
return errors.New("Notifier SMTP 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") return errors.New("Notifier SMTP 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")
} }
logger.Debug("Notifier SMTP config has no password specified so authentication is being skipped") n.log.Debug("Notifier SMTP config has no password specified so authentication is being skipped")
return nil return nil
} }
func (n *SMTPNotifier) compose(recipient, subject, body, htmlBody string) error { func (n *SMTPNotifier) compose(recipient, subject, body, htmlBody string) error {
logger := logging.Logger() n.log.Debugf("Notifier SMTP client attempting to send email body to %s", recipient)
logger.Debugf("Notifier SMTP client attempting to send email body to %s", recipient)
if !n.configuration.DisableRequireTLS { if !n.configuration.DisableRequireTLS {
_, ok := n.client.TLSConnectionState() _, ok := n.client.TLSConnectionState()
@ -127,7 +128,7 @@ func (n *SMTPNotifier) compose(recipient, subject, body, htmlBody string) error
wc, err := n.client.Data() wc, err := n.client.Data()
if err != nil { if err != nil {
logger.Debugf("Notifier SMTP client error while obtaining WriteCloser: %s", err) n.log.Debugf("Notifier SMTP client error while obtaining WriteCloser: %s", err)
return err return err
} }
@ -136,7 +137,7 @@ func (n *SMTPNotifier) compose(recipient, subject, body, htmlBody string) error
now := time.Now() now := time.Now()
msg := "Date:" + now.Format(rfc5322DateTimeLayout) + "\n" + msg := "Date:" + now.Format(rfc5322DateTimeLayout) + "\n" +
"From: " + n.configuration.Sender + "\n" + "From: " + n.configuration.Sender.String() + "\n" +
"To: " + recipient + "\n" + "To: " + recipient + "\n" +
"Subject: " + subject + "\n" + "Subject: " + subject + "\n" +
"MIME-version: 1.0\n" + "MIME-version: 1.0\n" +
@ -157,13 +158,13 @@ func (n *SMTPNotifier) compose(recipient, subject, body, htmlBody string) error
_, err = fmt.Fprint(wc, msg) _, err = fmt.Fprint(wc, msg)
if err != nil { if err != nil {
logger.Debugf("Notifier SMTP client error while sending email body over WriteCloser: %s", err) n.log.Debugf("Notifier SMTP client error while sending email body over WriteCloser: %s", err)
return err return err
} }
err = wc.Close() err = wc.Close()
if err != nil { if err != nil {
logger.Debugf("Notifier SMTP client error while closing the WriteCloser: %s", err) n.log.Debugf("Notifier SMTP client error while closing the WriteCloser: %s", err)
return err return err
} }
@ -172,20 +173,16 @@ func (n *SMTPNotifier) compose(recipient, subject, body, htmlBody string) error
// Dial the SMTP server with the SMTPNotifier config. // Dial the SMTP server with the SMTPNotifier config.
func (n *SMTPNotifier) dial() (err error) { func (n *SMTPNotifier) dial() (err error) {
logger := logging.Logger()
logger.Debugf("Notifier SMTP client attempting connection to %s:%d", n.configuration.Host, n.configuration.Port)
var ( var (
client *smtp.Client client *smtp.Client
conn net.Conn conn net.Conn
dialer = &net.Dialer{Timeout: n.configuration.Timeout}
) )
dialer := &net.Dialer{ n.log.Debugf("Notifier SMTP client attempting connection to %s:%d", n.configuration.Host, n.configuration.Port)
Timeout: n.configuration.Timeout,
}
if n.configuration.Port == 465 { if n.configuration.Port == 465 {
logger.Infof("Notifier SMTP client using submissions port 465. Make sure the mail server you are connecting to is configured for submissions and not SMTPS.") n.log.Infof("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.configuration.Host, n.configuration.Port), n.tlsConfig) conn, err = tls.DialWithDialer(dialer, "tcp", fmt.Sprintf("%s:%d", n.configuration.Host, n.configuration.Port), n.tlsConfig)
if err != nil { if err != nil {
@ -205,18 +202,16 @@ func (n *SMTPNotifier) dial() (err error) {
n.client = client n.client = client
logger.Debug("Notifier SMTP client connected successfully") n.log.Debug("Notifier SMTP client connected successfully")
return nil return nil
} }
// Closes the connection properly. // Closes the connection properly.
func (n *SMTPNotifier) cleanup() { func (n *SMTPNotifier) cleanup() {
logger := logging.Logger()
err := n.client.Quit() err := n.client.Quit()
if err != nil { if err != nil {
logger.Warnf("Notifier SMTP client encountered error during cleanup: %s", err) n.log.Warnf("Notifier SMTP client encountered error during cleanup: %s", err)
} }
} }
@ -240,7 +235,7 @@ func (n *SMTPNotifier) StartupCheck() (err error) {
return err return err
} }
if err := n.client.Mail(n.configuration.Sender); err != nil { if err := n.client.Mail(n.configuration.Sender.Address); err != nil {
return err return err
} }
@ -257,7 +252,6 @@ func (n *SMTPNotifier) StartupCheck() (err error) {
// Send is used to send an email to a recipient. // Send is used to send an email to a recipient.
func (n *SMTPNotifier) Send(recipient, title, body, htmlBody string) error { func (n *SMTPNotifier) Send(recipient, title, body, htmlBody string) error {
logger := logging.Logger()
subject := strings.ReplaceAll(n.configuration.Subject, "{title}", title) subject := strings.ReplaceAll(n.configuration.Subject, "{title}", title)
if err := n.dial(); err != nil { if err := n.dial(); err != nil {
@ -281,13 +275,13 @@ func (n *SMTPNotifier) Send(recipient, title, body, htmlBody string) error {
} }
// Set the sender and recipient first. // Set the sender and recipient first.
if err := n.client.Mail(n.configuration.Sender); err != nil { if err := n.client.Mail(n.configuration.Sender.Address); err != nil {
logger.Debugf("Notifier SMTP failed while sending MAIL FROM (using sender) with error: %s", err) n.log.Debugf("Notifier SMTP failed while sending MAIL FROM (using sender) with error: %s", err)
return err return err
} }
if err := n.client.Rcpt(recipient); err != nil { if err := n.client.Rcpt(recipient); err != nil {
logger.Debugf("Notifier SMTP failed while sending RCPT TO (using recipient) with error: %s", err) n.log.Debugf("Notifier SMTP failed while sending RCPT TO (using recipient) with error: %s", err)
return err return err
} }
@ -296,7 +290,7 @@ func (n *SMTPNotifier) Send(recipient, title, body, htmlBody string) error {
return err return err
} }
logger.Debug("Notifier SMTP client successfully sent email") n.log.Debug("Notifier SMTP client successfully sent email")
return nil return nil
} }

View File

@ -7,24 +7,21 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/authelia/authelia/v4/internal/configuration/schema" "github.com/authelia/authelia/v4/internal/configuration/schema"
"github.com/authelia/authelia/v4/internal/configuration/validator"
) )
func TestShouldConfigureSMTPNotifierWithTLS11AndDefaultHostname(t *testing.T) { func TestShouldConfigureSMTPNotifierWithTLS11(t *testing.T) {
config := &schema.NotifierConfiguration{ config := &schema.NotifierConfiguration{
DisableStartupCheck: true, DisableStartupCheck: true,
SMTP: &schema.SMTPNotifierConfiguration{ SMTP: &schema.SMTPNotifierConfiguration{
Host: "smtp.example.com", Host: "smtp.example.com",
Port: 25, Port: 25,
TLS: &schema.TLSConfig{ TLS: &schema.TLSConfig{
ServerName: "smtp.example.com",
MinimumVersion: "TLS1.1", MinimumVersion: "TLS1.1",
}, },
}, },
} }
sv := schema.NewStructValidator()
validator.ValidateNotifier(config, sv)
notifier := NewSMTPNotifier(config.SMTP, nil) notifier := NewSMTPNotifier(config.SMTP, nil)
assert.Equal(t, "smtp.example.com", notifier.tlsConfig.ServerName) assert.Equal(t, "smtp.example.com", notifier.tlsConfig.ServerName)
@ -44,9 +41,6 @@ func TestShouldConfigureSMTPNotifierWithServerNameOverrideAndDefaultTLS12(t *tes
}, },
} }
sv := schema.NewStructValidator()
validator.ValidateNotifier(config, sv)
notifier := NewSMTPNotifier(config.SMTP, nil) notifier := NewSMTPNotifier(config.SMTP, nil)
assert.Equal(t, "smtp.golang.org", notifier.tlsConfig.ServerName) assert.Equal(t, "smtp.golang.org", notifier.tlsConfig.ServerName)