fix(notification): incorrect date header format (#3684)
* fix(notification): incorrect date header format The date header in the email envelopes was incorrectly formatted missing a space between the `Date:` header and the value of this header. This also refactors the notification templates system allowing people to manually override the envelope itself. * test: fix tests and linting issues * fix: misc issues * refactor: misc refactoring * docs: add example for envelope with message id * refactor: organize smtp notifier * refactor: move subject interpolation * refactor: include additional placeholders * docs: fix missing link * docs: gravity * fix: rcpt to command * refactor: remove mid * refactor: apply suggestions Co-authored-by: Amir Zarrinkafsh <nightah@me.com> * refactor: include pid Co-authored-by: Amir Zarrinkafsh <nightah@me.com>pull/3718/head
parent
38e3f741b7
commit
df016be29e
|
@ -56,7 +56,9 @@ host: "[fd00:1111:2222:3333::1]"
|
|||
|
||||
{{< confkey type="integer" required="yes" >}}
|
||||
|
||||
The port the SMTP service is listening on.
|
||||
The port the SMTP service is listening on. Port 465 is treated as a special port where the entire connection is over
|
||||
TLS. This port was formerly known as the SMTPS port but is now known as the SUBMISSIONS port i.e. SUBMISSION Secure. All
|
||||
other ports expect to perform a STARTTLS negotiation.
|
||||
|
||||
### timeout
|
||||
|
||||
|
|
|
@ -115,7 +115,7 @@ This section documents the usage.
|
|||
{{< confkey type="string" required="no" >}}
|
||||
|
||||
The key `server_name` overrides the name checked against the certificate in the verification process. Useful if you
|
||||
require to use a direct IP address for the address of the backend service but want to verify a specific SNI.
|
||||
require an IP address for the host of the backend service but want to verify a specific certificate server name.
|
||||
|
||||
### skip_verify
|
||||
|
||||
|
|
|
@ -58,7 +58,38 @@ This is a basic example:
|
|||
Some Additional examples for specific purposes can be found in the
|
||||
[examples directory on GitHub](https://github.com/authelia/authelia/tree/master/examples/templates/notifications).
|
||||
|
||||
## Envelope Template
|
||||
|
||||
There is also a special envelope template. This is the email envelope which contains the content of the other templates
|
||||
when sent via the SMTP notifier. It's *__strongly recommended__* that you do not modify this template unless you know
|
||||
what you're doing. If you really want to modify it the name of the file must be `Envelope.tmpl`.
|
||||
|
||||
This template contains the following placeholders:
|
||||
|
||||
In template files, you can use the following placeholders which are automatically injected into the templates:
|
||||
|
||||
| Placeholder | Description |
|
||||
|:-----------------------:|:---------------------------------------------------------------------------:|
|
||||
| `{{ .ProcessID }}` | The Authelia Process ID. |
|
||||
| `{{ .UUID }}` | A string representation of a UUID v4 generated specifically for this email. |
|
||||
| `{{ .Host }}` | The configured [host]. |
|
||||
| `{{ .ServerName }}` | The configured TLS [server_name]. |
|
||||
| `{{ .SenderDomain }}` | The domain portion of the configured [sender]. |
|
||||
| `{{ .Identifier }}` | The configured [identifier]. |
|
||||
| `{{ .From }}` | The string representation of the configured [sender]. |
|
||||
| `{{ .To }}` | The string representation of the recipients email address. |
|
||||
| `{{ .Subject }}` | The email subject. |
|
||||
| `{{ .Date }}` | The time.Time of the email envelope being rendered. |
|
||||
| `{{ .Boundary }}` | The random alphanumeric 20 character multi-part email boundary. |
|
||||
| `{{ .Body.PlainText }}` | The plain text version of the email. |
|
||||
| `{{ .Body.HTML }}` | The HTML version of the email. |
|
||||
|
||||
## Original Templates
|
||||
|
||||
The original template content can be found on
|
||||
[GitHub](https://github.com/authelia/authelia/tree/master/internal/templates).
|
||||
[GitHub](https://github.com/authelia/authelia/tree/master/internal/templates/src/notification).
|
||||
|
||||
[host]: ../../configuration/notifications/smtp.md#host
|
||||
[server_name]: ../../configuration/notifications/smtp.md#tls
|
||||
[sender]: ../../configuration/notifications/smtp.md#sender
|
||||
[identifier]: ../../configuration/notifications/smtp.md#identifier
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
From: {{ .From }}
|
||||
To: {{ .To }}
|
||||
Subject: {{ .Subject }}
|
||||
Date: {{ .Date.Format "Mon, 2 Jan 2006 15:04:05 -0700" }}
|
||||
Message-ID: <{{ .UUID }}@{{ .Identifier }}>
|
||||
MIME-Version: 1.0
|
||||
Content-Type: multipart/alternative; boundary="{{ .Boundary }}"
|
||||
|
||||
--{{ .Boundary }}
|
||||
Content-Type: text/plain; charset="utf-8"
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
Content-Disposition: inline
|
||||
|
||||
{{ .Body.PlainText }}
|
||||
{{- if not (eq .Body.HTML "") }}
|
||||
--{{ .Boundary }}
|
||||
Content-Type: text/html; charset="utf-8"
|
||||
|
||||
{{ .Body.HTML }}
|
||||
{{- end }}
|
||||
--{{ .Boundary }}--
|
|
@ -2,6 +2,7 @@ package authentication
|
|||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net/mail"
|
||||
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"golang.org/x/text/encoding/unicode"
|
||||
|
@ -36,6 +37,24 @@ type UserDetails struct {
|
|||
Groups []string
|
||||
}
|
||||
|
||||
// Addresses returns the Emails []string as []mail.Address formatted with DisplayName as the Name attribute.
|
||||
func (d UserDetails) Addresses() (addresses []mail.Address) {
|
||||
if len(d.Emails) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
addresses = make([]mail.Address, len(d.Emails))
|
||||
|
||||
for i, email := range d.Emails {
|
||||
addresses[i] = mail.Address{
|
||||
Name: d.DisplayName,
|
||||
Address: email,
|
||||
}
|
||||
}
|
||||
|
||||
return addresses
|
||||
}
|
||||
|
||||
type ldapUserProfile struct {
|
||||
DN string
|
||||
Emails []string
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
"github.com/authelia/authelia/v4/internal/regulation"
|
||||
"github.com/authelia/authelia/v4/internal/session"
|
||||
"github.com/authelia/authelia/v4/internal/storage"
|
||||
"github.com/authelia/authelia/v4/internal/templates"
|
||||
"github.com/authelia/authelia/v4/internal/totp"
|
||||
"github.com/authelia/authelia/v4/internal/utils"
|
||||
)
|
||||
|
@ -49,11 +50,16 @@ func getProviders() (providers middlewares.Providers, warnings []error, errors [
|
|||
userProvider = authentication.NewLDAPUserProvider(config.AuthenticationBackend, autheliaCertPool)
|
||||
}
|
||||
|
||||
templatesProvider, err := templates.New(templates.Config{EmailTemplatesPath: config.Notifier.TemplatePath})
|
||||
if err != nil {
|
||||
errors = append(errors, err)
|
||||
}
|
||||
|
||||
var notifier notification.Notifier
|
||||
|
||||
switch {
|
||||
case config.Notifier.SMTP != nil:
|
||||
notifier = notification.NewSMTPNotifier(config.Notifier.SMTP, autheliaCertPool)
|
||||
notifier = notification.NewSMTPNotifier(config.Notifier.SMTP, autheliaCertPool, templatesProvider)
|
||||
case config.Notifier.FileSystem != nil:
|
||||
notifier = notification.NewFileNotifier(*config.Notifier.FileSystem)
|
||||
}
|
||||
|
@ -89,6 +95,7 @@ func getProviders() (providers middlewares.Providers, warnings []error, errors [
|
|||
NTP: ntpProvider,
|
||||
Notifier: notifier,
|
||||
SessionProvider: sessionProvider,
|
||||
Templates: templatesProvider,
|
||||
TOTP: totpProvider,
|
||||
PasswordPolicy: ppolicyProvider,
|
||||
}, warnings, errors
|
||||
|
|
|
@ -20,7 +20,7 @@ type SMTPNotifierConfiguration struct {
|
|||
Identifier string `koanf:"identifier"`
|
||||
Sender mail.Address `koanf:"sender"`
|
||||
Subject string `koanf:"subject"`
|
||||
StartupCheckAddress string `koanf:"startup_check_address"`
|
||||
StartupCheckAddress mail.Address `koanf:"startup_check_address"`
|
||||
DisableRequireTLS bool `koanf:"disable_require_tls"`
|
||||
DisableHTMLEmails bool `koanf:"disable_html_emails"`
|
||||
TLS *TLSConfig `koanf:"tls"`
|
||||
|
@ -39,7 +39,7 @@ var DefaultSMTPNotifierConfiguration = SMTPNotifierConfiguration{
|
|||
Timeout: time.Second * 5,
|
||||
Subject: "[Authelia] {title}",
|
||||
Identifier: "localhost",
|
||||
StartupCheckAddress: "test@authelia.com",
|
||||
StartupCheckAddress: mail.Address{Name: "Authelia Test", Address: "test@authelia.com"},
|
||||
TLS: &TLSConfig{
|
||||
MinimumVersion: "TLS1.2",
|
||||
},
|
||||
|
|
|
@ -54,7 +54,6 @@ const (
|
|||
"is configured"
|
||||
errFmtNotifierTemplatePathNotExist = "notifier: option 'template_path' refers to location '%s' which does not exist"
|
||||
errFmtNotifierTemplatePathUnknownError = "notifier: option 'template_path' refers to location '%s' which couldn't be opened: %w"
|
||||
errFmtNotifierTemplateLoad = "notifier: error loading template '%s': %w"
|
||||
errFmtNotifierFileSystemFileNameNotConfigured = "notifier: filesystem: option 'filename' is required"
|
||||
errFmtNotifierSMTPNotConfigured = "notifier: smtp: option '%s' is required"
|
||||
)
|
||||
|
|
|
@ -3,11 +3,8 @@ package validator
|
|||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"text/template"
|
||||
|
||||
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
||||
"github.com/authelia/authelia/v4/internal/templates"
|
||||
)
|
||||
|
||||
// ValidateNotifier validates and update notifier configuration.
|
||||
|
@ -42,7 +39,6 @@ func validateNotifierTemplates(config *schema.NotifierConfiguration, validator *
|
|||
|
||||
var (
|
||||
err error
|
||||
t *template.Template
|
||||
)
|
||||
|
||||
_, err = os.Stat(config.TemplatePath)
|
||||
|
@ -55,34 +51,10 @@ func validateNotifierTemplates(config *schema.NotifierConfiguration, validator *
|
|||
validator.Push(fmt.Errorf(errFmtNotifierTemplatePathUnknownError, config.TemplatePath, err))
|
||||
return
|
||||
}
|
||||
|
||||
if t, err = template.ParseFiles(filepath.Join(config.TemplatePath, templates.TemplateNameIdentityVerification+".html")); err == nil {
|
||||
templates.EmailIdentityVerificationHTML = t
|
||||
} else {
|
||||
validator.PushWarning(fmt.Errorf(errFmtNotifierTemplateLoad, templates.TemplateNameIdentityVerification+".html", err))
|
||||
}
|
||||
|
||||
if t, err = template.ParseFiles(filepath.Join(config.TemplatePath, templates.TemplateNameIdentityVerification+".txt")); err == nil {
|
||||
templates.EmailIdentityVerificationPlainText = t
|
||||
} else {
|
||||
validator.PushWarning(fmt.Errorf(errFmtNotifierTemplateLoad, templates.TemplateNameIdentityVerification+".txt", err))
|
||||
}
|
||||
|
||||
if t, err = template.ParseFiles(filepath.Join(config.TemplatePath, templates.TemplateNameBasic+".html")); err == nil {
|
||||
templates.EmailPasswordResetHTML = t
|
||||
} else {
|
||||
validator.PushWarning(fmt.Errorf(errFmtNotifierTemplateLoad, templates.TemplateNameBasic+".html", err))
|
||||
}
|
||||
|
||||
if t, err = template.ParseFiles(filepath.Join(config.TemplatePath, templates.TemplateNameBasic+".txt")); err == nil {
|
||||
templates.EmailPasswordResetPlainText = t
|
||||
} else {
|
||||
validator.PushWarning(fmt.Errorf(errFmtNotifierTemplateLoad, templates.TemplateNameBasic+".txt", err))
|
||||
}
|
||||
}
|
||||
|
||||
func validateSMTPNotifier(config *schema.SMTPNotifierConfiguration, validator *schema.StructValidator) {
|
||||
if config.StartupCheckAddress == "" {
|
||||
if config.StartupCheckAddress.Address == "" {
|
||||
config.StartupCheckAddress = schema.DefaultSMTPNotifierConfiguration.StartupCheckAddress
|
||||
}
|
||||
|
||||
|
|
|
@ -82,6 +82,17 @@ func (suite *NotifierSuite) TestSMTPShouldSetTLSDefaults() {
|
|||
suite.Assert().False(suite.config.SMTP.TLS.SkipVerify)
|
||||
}
|
||||
|
||||
func (suite *NotifierSuite) TestSMTPShouldDefaultStartupCheckAddress() {
|
||||
suite.Assert().Equal(mail.Address{Name: "", Address: ""}, suite.config.SMTP.StartupCheckAddress)
|
||||
|
||||
ValidateNotifier(&suite.config, suite.validator)
|
||||
|
||||
suite.Assert().Len(suite.validator.Warnings(), 0)
|
||||
suite.Assert().Len(suite.validator.Errors(), 0)
|
||||
|
||||
suite.Assert().Equal(mail.Address{Name: "Authelia Test", Address: "test@authelia.com"}, suite.config.SMTP.StartupCheckAddress)
|
||||
}
|
||||
|
||||
func (suite *NotifierSuite) TestSMTPShouldDefaultTLSServerNameToHost() {
|
||||
suite.config.SMTP.Host = "google.com"
|
||||
suite.config.SMTP.TLS = &schema.TLSConfig{
|
||||
|
|
|
@ -81,16 +81,14 @@ func ResetPasswordPOST(ctx *middlewares.AutheliaCtx) {
|
|||
disableHTML = ctx.Configuration.Notifier.SMTP.DisableHTMLEmails
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"Title": "Password changed successfully",
|
||||
"DisplayName": userInfo.DisplayName,
|
||||
"RemoteIP": ctx.RemoteIP().String(),
|
||||
values := templates.EmailPasswordResetValues{
|
||||
Title: "Password changed successfully",
|
||||
DisplayName: userInfo.DisplayName,
|
||||
RemoteIP: ctx.RemoteIP().String(),
|
||||
}
|
||||
|
||||
if !disableHTML {
|
||||
err = templates.EmailPasswordResetHTML.Execute(bufHTML, data)
|
||||
|
||||
if err != nil {
|
||||
if err = ctx.Providers.Templates.ExecuteEmailPasswordResetTemplate(bufHTML, values, templates.HTMLFormat); err != nil {
|
||||
ctx.Logger.Error(err)
|
||||
ctx.ReplyOK()
|
||||
|
||||
|
@ -100,17 +98,19 @@ func ResetPasswordPOST(ctx *middlewares.AutheliaCtx) {
|
|||
|
||||
bufText := new(bytes.Buffer)
|
||||
|
||||
if err = templates.EmailPasswordResetPlainText.Execute(bufText, data); err != nil {
|
||||
if err = ctx.Providers.Templates.ExecuteEmailPasswordResetTemplate(bufText, values, templates.PlainTextFormat); err != nil {
|
||||
ctx.Logger.Error(err)
|
||||
ctx.ReplyOK()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
addresses := userInfo.Addresses()
|
||||
|
||||
ctx.Logger.Debugf("Sending an email to user %s (%s) to inform that the password has changed.",
|
||||
username, userInfo.Emails[0])
|
||||
username, addresses[0])
|
||||
|
||||
if err = ctx.Providers.Notifier.Send(userInfo.Emails[0], "Password changed successfully", bufText.String(), bufHTML.String()); err != nil {
|
||||
if err = ctx.Providers.Notifier.Send(addresses[0], "Password changed successfully", bufText.String(), bufHTML.String()); err != nil {
|
||||
ctx.Logger.Error(err)
|
||||
ctx.ReplyOK()
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/mail"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
|
@ -76,16 +77,16 @@ func IdentityVerificationStart(args IdentityVerificationStartArgs, delayFunc Tim
|
|||
disableHTML = ctx.Configuration.Notifier.SMTP.DisableHTMLEmails
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"Title": args.MailTitle,
|
||||
"LinkURL": link,
|
||||
"LinkText": args.MailButtonContent,
|
||||
"DisplayName": identity.DisplayName,
|
||||
"RemoteIP": ctx.RemoteIP().String(),
|
||||
values := templates.EmailIdentityVerificationValues{
|
||||
Title: args.MailTitle,
|
||||
LinkURL: link,
|
||||
LinkText: args.MailButtonContent,
|
||||
DisplayName: identity.DisplayName,
|
||||
RemoteIP: ctx.RemoteIP().String(),
|
||||
}
|
||||
|
||||
if !disableHTML {
|
||||
if err = templates.EmailIdentityVerificationHTML.Execute(bufHTML, data); err != nil {
|
||||
if err = ctx.Providers.Templates.ExecuteEmailIdentityVerificationTemplate(bufHTML, values, templates.HTMLFormat); err != nil {
|
||||
ctx.Error(err, messageOperationFailed)
|
||||
return
|
||||
}
|
||||
|
@ -93,7 +94,7 @@ func IdentityVerificationStart(args IdentityVerificationStartArgs, delayFunc Tim
|
|||
|
||||
bufText := new(bytes.Buffer)
|
||||
|
||||
if err = templates.EmailIdentityVerificationPlainText.Execute(bufText, data); err != nil {
|
||||
if err = ctx.Providers.Templates.ExecuteEmailIdentityVerificationTemplate(bufText, values, templates.PlainTextFormat); err != nil {
|
||||
ctx.Error(err, messageOperationFailed)
|
||||
return
|
||||
}
|
||||
|
@ -101,7 +102,7 @@ func IdentityVerificationStart(args IdentityVerificationStartArgs, delayFunc Tim
|
|||
ctx.Logger.Debugf("Sending an email to user %s (%s) to confirm identity for registering a device.",
|
||||
identity.Username, identity.Email)
|
||||
|
||||
if err = ctx.Providers.Notifier.Send(identity.Email, args.MailTitle, bufText.String(), bufHTML.String()); err != nil {
|
||||
if err = ctx.Providers.Notifier.Send(mail.Address{Name: identity.DisplayName, Address: identity.Email}, args.MailTitle, bufText.String(), bufHTML.String()); err != nil {
|
||||
ctx.Error(err, messageOperationFailed)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package middlewares_test
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"net/mail"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
@ -80,7 +81,7 @@ func TestShouldFailSendingAnEmail(t *testing.T) {
|
|||
Return(nil)
|
||||
|
||||
mock.NotifierMock.EXPECT().
|
||||
Send(gomock.Eq("john@example.com"), gomock.Eq("Title"), gomock.Any(), gomock.Any()).
|
||||
Send(gomock.Eq(mail.Address{Address: "john@example.com"}), gomock.Eq("Title"), gomock.Any(), gomock.Any()).
|
||||
Return(fmt.Errorf("no notif"))
|
||||
|
||||
args := newArgs(defaultRetriever)
|
||||
|
@ -120,7 +121,7 @@ func TestShouldSucceedIdentityVerificationStartProcess(t *testing.T) {
|
|||
Return(nil)
|
||||
|
||||
mock.NotifierMock.EXPECT().
|
||||
Send(gomock.Eq("john@example.com"), gomock.Eq("Title"), gomock.Any(), gomock.Any()).
|
||||
Send(gomock.Eq(mail.Address{Address: "john@example.com"}), gomock.Eq("Title"), gomock.Any(), gomock.Any()).
|
||||
Return(nil)
|
||||
|
||||
args := newArgs(defaultRetriever)
|
||||
|
|
|
@ -14,6 +14,7 @@ import (
|
|||
"github.com/authelia/authelia/v4/internal/regulation"
|
||||
"github.com/authelia/authelia/v4/internal/session"
|
||||
"github.com/authelia/authelia/v4/internal/storage"
|
||||
"github.com/authelia/authelia/v4/internal/templates"
|
||||
"github.com/authelia/authelia/v4/internal/totp"
|
||||
"github.com/authelia/authelia/v4/internal/utils"
|
||||
)
|
||||
|
@ -40,6 +41,7 @@ type Providers struct {
|
|||
UserProvider authentication.UserProvider
|
||||
StorageProvider storage.Provider
|
||||
Notifier notification.Notifier
|
||||
Templates *templates.Provider
|
||||
TOTP totp.Provider
|
||||
PasswordPolicy PasswordPolicyProvider
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ import (
|
|||
"github.com/authelia/authelia/v4/internal/middlewares"
|
||||
"github.com/authelia/authelia/v4/internal/regulation"
|
||||
"github.com/authelia/authelia/v4/internal/session"
|
||||
"github.com/authelia/authelia/v4/internal/templates"
|
||||
)
|
||||
|
||||
// MockAutheliaCtx a mock of AutheliaCtx.
|
||||
|
@ -115,6 +116,12 @@ func NewMockAutheliaCtx(t *testing.T) *MockAutheliaCtx {
|
|||
mockAuthelia.TOTPMock = NewMockTOTP(mockAuthelia.Ctrl)
|
||||
providers.TOTP = mockAuthelia.TOTPMock
|
||||
|
||||
var err error
|
||||
|
||||
if providers.Templates, err = templates.New(templates.Config{}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
request := &fasthttp.RequestCtx{}
|
||||
// Set a cookie to identify this client throughout the test.
|
||||
// request.Request.Header.SetCookie("authelia_session", "client_cookie").
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
package mocks
|
||||
|
||||
import (
|
||||
mail "net/mail"
|
||||
reflect "reflect"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
|
@ -34,7 +35,7 @@ func (m *MockNotifier) EXPECT() *MockNotifierMockRecorder {
|
|||
}
|
||||
|
||||
// Send mocks base method.
|
||||
func (m *MockNotifier) Send(arg0, arg1, arg2, arg3 string) error {
|
||||
func (m *MockNotifier) Send(arg0 mail.Address, arg1, arg2, arg3 string) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Send", arg0, arg1, arg2, arg3)
|
||||
ret0, _ := ret[0].(error)
|
||||
|
|
|
@ -5,5 +5,20 @@ const (
|
|||
)
|
||||
|
||||
const (
|
||||
rfc5322DateTimeLayout = "Mon, 2 Jan 2006 15:04:05 -0700"
|
||||
smtpAUTHMechanismPlain = "PLAIN"
|
||||
smtpAUTHMechanismLogin = "LOGIN"
|
||||
|
||||
smtpPortSUBMISSIONS = 465
|
||||
|
||||
smtpCommandDATA = "DATA"
|
||||
smtpCommandHELLO = "EHLO/HELO"
|
||||
smtpCommandSTARTTLS = "STARTTLS"
|
||||
smtpCommandAUTH = "AUTH"
|
||||
smtpCommandMAIL = "MAIL"
|
||||
smtpCommandRCPT = "RCPT"
|
||||
)
|
||||
|
||||
const (
|
||||
fmtSMTPGenericError = "error performing %s with the SMTP server: %w"
|
||||
fmtSMTPDialError = "error dialing the SMTP server: %w"
|
||||
)
|
||||
|
|
|
@ -2,6 +2,7 @@ package notification
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"net/mail"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
@ -42,7 +43,7 @@ func (n *FileNotifier) StartupCheck() (err error) {
|
|||
}
|
||||
|
||||
// Send send a identity verification link to a user.
|
||||
func (n *FileNotifier) Send(recipient, subject, body, _ string) error {
|
||||
func (n *FileNotifier) Send(recipient mail.Address, subject, body, _ string) error {
|
||||
content := fmt.Sprintf("Date: %s\nRecipient: %s\nSubject: %s\nBody: %s", time.Now(), recipient, subject, body)
|
||||
|
||||
return os.WriteFile(n.path, []byte(content), fileNotifierMode)
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package notification
|
||||
|
||||
import (
|
||||
"net/mail"
|
||||
|
||||
"github.com/authelia/authelia/v4/internal/model"
|
||||
)
|
||||
|
||||
|
@ -8,5 +10,5 @@ import (
|
|||
type Notifier interface {
|
||||
model.StartupCheck
|
||||
|
||||
Send(recipient, subject, body, htmlBody string) (err error)
|
||||
Send(recipient mail.Address, subject, body, htmlBody string) (err error)
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
|
|||
return "", nil, errors.New("unexpected hostname from server")
|
||||
}
|
||||
|
||||
return "LOGIN", []byte{}, nil
|
||||
return smtpAUTHMechanismLogin, []byte{}, nil
|
||||
}
|
||||
|
||||
func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
|
||||
|
|
|
@ -20,7 +20,7 @@ func TestFullLoginAuth(t *testing.T) {
|
|||
auth := newLoginAuth(username, password, "mail.authelia.com")
|
||||
|
||||
proto, _, err := auth.Start(serverInfo)
|
||||
assert.Equal(t, "LOGIN", proto)
|
||||
assert.Equal(t, smtpAUTHMechanismLogin, proto)
|
||||
require.NoError(t, err)
|
||||
|
||||
toServer, err := auth.Next([]byte("Username:"), true)
|
||||
|
|
|
@ -7,36 +7,152 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"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"
|
||||
)
|
||||
|
||||
// SMTPNotifier a notifier to send emails to SMTP servers.
|
||||
type SMTPNotifier struct {
|
||||
configuration *schema.SMTPNotifierConfiguration
|
||||
client *smtp.Client
|
||||
tlsConfig *tls.Config
|
||||
log *logrus.Logger
|
||||
// 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,
|
||||
}
|
||||
|
||||
// NewSMTPNotifier creates a SMTPNotifier using the notifier configuration.
|
||||
func NewSMTPNotifier(configuration *schema.SMTPNotifierConfiguration, certPool *x509.CertPool) *SMTPNotifier {
|
||||
notifier := &SMTPNotifier{
|
||||
configuration: configuration,
|
||||
tlsConfig: utils.NewTLSConfig(configuration.TLS, tls.VersionTLS12, certPool),
|
||||
log: logging.Logger(),
|
||||
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, title, body, htmlBody string) (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, title, body, htmlBody); 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 {
|
||||
// Only start if not already encrypted.
|
||||
|
@ -55,11 +171,11 @@ func (n *SMTPNotifier) startTLS() error {
|
|||
|
||||
n.log.Debug("Notifier SMTP STARTTLS completed without error")
|
||||
default:
|
||||
switch n.configuration.DisableRequireTLS {
|
||||
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("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("server does not support TLS and it is required by default (see documentation if you want to disable this highly recommended requirement)")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -67,40 +183,44 @@ func (n *SMTPNotifier) startTLS() error {
|
|||
}
|
||||
|
||||
// Attempt Authentication.
|
||||
func (n *SMTPNotifier) auth() error {
|
||||
func (n *SMTPNotifier) auth() (err error) {
|
||||
// Attempt AUTH if password is specified only.
|
||||
if n.configuration.Password != "" {
|
||||
_, ok := n.client.TLSConnectionState()
|
||||
if !ok {
|
||||
return errors.New("Notifier SMTP client does not support authentication over plain text and the connection is currently plain text")
|
||||
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.
|
||||
ok, m := n.client.Extension("AUTH")
|
||||
if ok {
|
||||
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("PLAIN", mechanisms) {
|
||||
auth = smtp.PlainAuth("", n.configuration.Username, n.configuration.Password, n.configuration.Host)
|
||||
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("LOGIN", mechanisms) {
|
||||
auth = newLoginAuth(n.configuration.Username, n.configuration.Password, n.configuration.Host)
|
||||
} 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("notifier SMTP server does not advertise a AUTH mechanism that are supported by Authelia (PLAIN or LOGIN are supported, but server advertised %s mechanisms)", m)
|
||||
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 {
|
||||
if err = n.client.Auth(auth); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -109,7 +229,7 @@ func (n *SMTPNotifier) auth() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
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("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")
|
||||
|
@ -117,182 +237,67 @@ func (n *SMTPNotifier) auth() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (n *SMTPNotifier) compose(recipient, subject, body, htmlBody string) error {
|
||||
n.log.Debugf("Notifier SMTP client attempting to send email body to %s", recipient)
|
||||
func (n *SMTPNotifier) compose(recipient mail.Address, title, body, htmlBody string) (err error) {
|
||||
n.log.Debugf("Notifier SMTP client attempting to send email body to %s", recipient.String())
|
||||
|
||||
if !n.configuration.DisableRequireTLS {
|
||||
if !n.config.DisableRequireTLS {
|
||||
_, ok := n.client.TLSConnectionState()
|
||||
if !ok {
|
||||
return errors.New("Notifier SMTP client can't send an email over plain text connection")
|
||||
return errors.New("client can't send an email over plain text connection")
|
||||
}
|
||||
}
|
||||
|
||||
wc, err := n.client.Data()
|
||||
if err != nil {
|
||||
n.log.Debugf("Notifier SMTP client error while obtaining WriteCloser: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
boundary := utils.RandomString(30, utils.AlphaNumericCharacters, true)
|
||||
|
||||
now := time.Now()
|
||||
|
||||
msg := "Date:" + now.Format(rfc5322DateTimeLayout) + "\n" +
|
||||
"From: " + n.configuration.Sender.String() + "\n" +
|
||||
"To: " + recipient + "\n" +
|
||||
"Subject: " + subject + "\n" +
|
||||
"MIME-version: 1.0\n" +
|
||||
"Content-Type: multipart/alternative; boundary=" + boundary + "\n\n" +
|
||||
"--" + boundary + "\n" +
|
||||
"Content-Type: text/plain; charset=\"UTF-8\"\n" +
|
||||
"Content-Transfer-Encoding: quoted-printable\n" +
|
||||
"Content-Disposition: inline\n\n" +
|
||||
body + "\n"
|
||||
|
||||
if htmlBody != "" {
|
||||
msg += "--" + boundary + "\n" +
|
||||
"Content-Type: text/html; charset=\"UTF-8\"\n\n" +
|
||||
htmlBody + "\n"
|
||||
}
|
||||
|
||||
msg += "--" + boundary + "--"
|
||||
|
||||
_, err = fmt.Fprint(wc, msg)
|
||||
if err != nil {
|
||||
n.log.Debugf("Notifier SMTP client error while sending email body over WriteCloser: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
err = wc.Close()
|
||||
if err != nil {
|
||||
n.log.Debugf("Notifier SMTP client error while closing the WriteCloser: %s", err)
|
||||
return 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.configuration.Timeout}
|
||||
wc io.WriteCloser
|
||||
muuid uuid.UUID
|
||||
)
|
||||
|
||||
n.log.Debugf("Notifier SMTP client attempting connection to %s:%d", n.configuration.Host, n.configuration.Port)
|
||||
|
||||
if n.configuration.Port == 465 {
|
||||
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)
|
||||
} else {
|
||||
conn, err = dialer.Dial("tcp", fmt.Sprintf("%s:%d", n.configuration.Host, n.configuration.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:
|
||||
if wc, err = n.client.Data(); err != nil {
|
||||
n.log.Debugf("Notifier SMTP client error while obtaining WriteCloser: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
client, err = smtp.NewClient(conn, n.configuration.Host)
|
||||
if err != nil {
|
||||
if muuid, err = uuid.NewRandom(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
n.client = client
|
||||
values := 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}", title),
|
||||
Date: time.Now(),
|
||||
Boundary: utils.RandomString(30, utils.AlphaNumericCharacters, true),
|
||||
Body: templates.EmailEnvelopeBodyValues{
|
||||
PlainText: body,
|
||||
HTML: htmlBody,
|
||||
},
|
||||
}
|
||||
|
||||
n.log.Debug("Notifier SMTP client connected successfully")
|
||||
if err = n.templates.ExecuteEmailEnvelope(wc, values); err != nil {
|
||||
n.log.Debugf("Notifier SMTP client error while sending email body over WriteCloser: %v", err)
|
||||
|
||||
return 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() {
|
||||
err := n.client.Quit()
|
||||
if err != nil {
|
||||
n.log.Warnf("Notifier SMTP client encountered error during cleanup: %s", err)
|
||||
}
|
||||
if err := n.client.Quit(); err != nil {
|
||||
n.log.Warnf("Notifier SMTP client encountered error during cleanup: %v", err)
|
||||
}
|
||||
|
||||
// StartupCheck implements the startup check provider interface.
|
||||
func (n *SMTPNotifier) StartupCheck() (err error) {
|
||||
if err = n.dial(); err != nil {
|
||||
return fmt.Errorf("error dialing the smtp server: %w", err)
|
||||
}
|
||||
|
||||
defer n.cleanup()
|
||||
|
||||
if err = n.client.Hello(n.configuration.Identifier); err != nil {
|
||||
return fmt.Errorf("error performing HELO/EHLO with the smtp server: %w", err)
|
||||
}
|
||||
|
||||
if err = n.startTLS(); err != nil {
|
||||
return fmt.Errorf("error performing STARTTLS with the smtp server: %w", err)
|
||||
}
|
||||
|
||||
if err = n.auth(); err != nil {
|
||||
return fmt.Errorf("error performing AUTH with the smtp server: %w", err)
|
||||
}
|
||||
|
||||
if err = n.client.Mail(n.configuration.Sender.Address); err != nil {
|
||||
return fmt.Errorf("error performing MAIL FROM with the smtp server: %w", err)
|
||||
}
|
||||
|
||||
if err = n.client.Rcpt(n.configuration.StartupCheckAddress); err != nil {
|
||||
return fmt.Errorf("error performing RCPT with the smtp server: %w", err)
|
||||
}
|
||||
|
||||
return n.client.Reset()
|
||||
}
|
||||
|
||||
// Send is used to send an email to a recipient.
|
||||
func (n *SMTPNotifier) Send(recipient, title, body, htmlBody string) error {
|
||||
subject := strings.ReplaceAll(n.configuration.Subject, "{title}", title)
|
||||
|
||||
var err error
|
||||
|
||||
if err = n.dial(); err != nil {
|
||||
return fmt.Errorf("error dialing the smtp server: %w", err)
|
||||
}
|
||||
|
||||
// Always execute QUIT at the end once we're connected.
|
||||
defer n.cleanup()
|
||||
|
||||
if err = n.client.Hello(n.configuration.Identifier); err != nil {
|
||||
return fmt.Errorf("error performing HELO/EHLO with the smtp server: %w", err)
|
||||
}
|
||||
|
||||
if err = n.startTLS(); err != nil {
|
||||
return fmt.Errorf("error performing STARTTLS with the smtp server: %w", err)
|
||||
}
|
||||
|
||||
if err = n.auth(); err != nil {
|
||||
return fmt.Errorf("error performing AUTH with the smtp server: %w", err)
|
||||
}
|
||||
|
||||
if err = n.client.Mail(n.configuration.Sender.Address); err != nil {
|
||||
n.log.Debugf("Notifier SMTP failed while sending MAIL FROM (using sender) with error: %s", err)
|
||||
|
||||
return fmt.Errorf("error performing MAIL FROM with the smtp server: %w", err)
|
||||
}
|
||||
|
||||
if err = n.client.Rcpt(n.configuration.StartupCheckAddress); err != nil {
|
||||
n.log.Debugf("Notifier SMTP failed while sending RCPT TO (using recipient) with error: %s", err)
|
||||
|
||||
return fmt.Errorf("error performing RCPT with the smtp server: %w", err)
|
||||
}
|
||||
|
||||
// Compose and send the email body to the server.
|
||||
if err = n.compose(recipient, subject, body, htmlBody); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
n.log.Debug("Notifier SMTP client successfully sent email")
|
||||
|
||||
return nil
|
||||
n.client = nil
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
||||
"github.com/authelia/authelia/v4/internal/templates"
|
||||
)
|
||||
|
||||
func TestShouldConfigureSMTPNotifierWithTLS11(t *testing.T) {
|
||||
|
@ -22,7 +23,7 @@ func TestShouldConfigureSMTPNotifierWithTLS11(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
notifier := NewSMTPNotifier(config.SMTP, nil)
|
||||
notifier := NewSMTPNotifier(config.SMTP, nil, &templates.Provider{})
|
||||
|
||||
assert.Equal(t, "smtp.example.com", notifier.tlsConfig.ServerName)
|
||||
assert.Equal(t, uint16(tls.VersionTLS11), notifier.tlsConfig.MinVersion)
|
||||
|
@ -41,7 +42,7 @@ func TestShouldConfigureSMTPNotifierWithServerNameOverrideAndDefaultTLS12(t *tes
|
|||
},
|
||||
}
|
||||
|
||||
notifier := NewSMTPNotifier(config.SMTP, nil)
|
||||
notifier := NewSMTPNotifier(config.SMTP, nil, &templates.Provider{})
|
||||
|
||||
assert.Equal(t, "smtp.golang.org", notifier.tlsConfig.ServerName)
|
||||
assert.Equal(t, uint16(tls.VersionTLS12), notifier.tlsConfig.MinVersion)
|
||||
|
|
|
@ -2,6 +2,14 @@ package templates
|
|||
|
||||
// Template File Names.
|
||||
const (
|
||||
TemplateNameBasic = "Basic"
|
||||
TemplateNameIdentityVerification = "IdentityVerification"
|
||||
TemplateNameEmailEnvelope = "Envelope.tmpl"
|
||||
TemplateNameEmailIdentityVerificationHTML = "IdentityVerification.html"
|
||||
TemplateNameEmailIdentityVerificationTXT = "IdentityVerification.txt"
|
||||
TemplateNameEmailPasswordResetHTML = "PasswordReset.html"
|
||||
TemplateNameEmailPasswordResetTXT = "PasswordReset.txt"
|
||||
)
|
||||
|
||||
// Template Category Names.
|
||||
const (
|
||||
TemplateCategoryNotifications = "notification"
|
||||
)
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
package templates
|
||||
|
||||
import (
|
||||
"text/template"
|
||||
)
|
||||
|
||||
// EmailIdentityVerificationPlainText the template of email that the user will receive for identity verification.
|
||||
var EmailIdentityVerificationPlainText *template.Template
|
||||
|
||||
func init() {
|
||||
t, err := template.New("email_identity_verification_plain_text").Parse(emailContentIdentityVerificationPlainText)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
EmailIdentityVerificationPlainText = t
|
||||
}
|
||||
|
||||
const emailContentIdentityVerificationPlainText = `
|
||||
This email has been sent to you in order to validate your identity.
|
||||
If you did not initiate the process your credentials might have been compromised. You should reset your password and contact an administrator.
|
||||
|
||||
To setup your 2FA please visit the following URL: {{ .LinkURL }}
|
||||
|
||||
This email was generated by a user with the IP {{ .RemoteIP }}.
|
||||
|
||||
Please contact an administrator if you did not initiate this process.
|
||||
`
|
|
@ -1,26 +0,0 @@
|
|||
package templates
|
||||
|
||||
import (
|
||||
"text/template"
|
||||
)
|
||||
|
||||
// EmailPasswordResetPlainText the template of email that the user will receive for identity verification.
|
||||
var EmailPasswordResetPlainText *template.Template
|
||||
|
||||
func init() {
|
||||
t, err := template.New("email_password_reset_plain_text").Parse(emailContentBasicPlainText)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
EmailPasswordResetPlainText = t
|
||||
}
|
||||
|
||||
const emailContentBasicPlainText = `
|
||||
Your password has been successfully reset.
|
||||
If you did not initiate the process your credentials might have been compromised. You should reset your password and contact an administrator.
|
||||
|
||||
This email was generated by a user with the IP {{ .RemoteIP }}.
|
||||
|
||||
Please contact an administrator if you did not initiate this process.
|
||||
`
|
|
@ -0,0 +1,8 @@
|
|||
package templates
|
||||
|
||||
import (
|
||||
"embed"
|
||||
)
|
||||
|
||||
//go:embed src/*
|
||||
var embedFS embed.FS
|
|
@ -0,0 +1,23 @@
|
|||
package templates
|
||||
|
||||
import (
|
||||
"text/template"
|
||||
)
|
||||
|
||||
// HTMLPlainTextTemplate is the template type which contains both the html and txt versions of a template.
|
||||
type HTMLPlainTextTemplate struct {
|
||||
html *template.Template
|
||||
txt *template.Template
|
||||
}
|
||||
|
||||
// Get returns the appropriate template given the format.
|
||||
func (f HTMLPlainTextTemplate) Get(format Format) (t *template.Template) {
|
||||
switch format {
|
||||
case HTMLFormat:
|
||||
return f.html
|
||||
case PlainTextFormat:
|
||||
return f.txt
|
||||
default:
|
||||
return f.html
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
package templates
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
// New creates a new templates' provider.
|
||||
func New(config Config) (provider *Provider, err error) {
|
||||
provider = &Provider{
|
||||
config: config,
|
||||
}
|
||||
|
||||
if err = provider.load(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return provider, nil
|
||||
}
|
||||
|
||||
// Provider of templates.
|
||||
type Provider struct {
|
||||
config Config
|
||||
templates Templates
|
||||
}
|
||||
|
||||
// ExecuteEmailEnvelope writes the envelope template to the given io.Writer.
|
||||
func (p Provider) ExecuteEmailEnvelope(wr io.Writer, data EmailEnvelopeValues) (err error) {
|
||||
return p.templates.notification.envelope.Execute(wr, data)
|
||||
}
|
||||
|
||||
// ExecuteEmailPasswordResetTemplate writes the password reset template to the given io.Writer.
|
||||
func (p Provider) ExecuteEmailPasswordResetTemplate(wr io.Writer, data EmailPasswordResetValues, format Format) (err error) {
|
||||
return p.templates.notification.passwordReset.Get(format).Execute(wr, data)
|
||||
}
|
||||
|
||||
// ExecuteEmailIdentityVerificationTemplate writes the identity verification template to the given io.Writer.
|
||||
func (p Provider) ExecuteEmailIdentityVerificationTemplate(wr io.Writer, data EmailIdentityVerificationValues, format Format) (err error) {
|
||||
return p.templates.notification.identityVerification.Get(format).Execute(wr, data)
|
||||
}
|
||||
|
||||
func (p *Provider) load() (err error) {
|
||||
var errs []error
|
||||
|
||||
if p.templates.notification.envelope, err = loadTemplate(TemplateNameEmailEnvelope, TemplateCategoryNotifications, p.config.EmailTemplatesPath); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
if p.templates.notification.identityVerification.txt, err = loadTemplate(TemplateNameEmailIdentityVerificationTXT, TemplateCategoryNotifications, p.config.EmailTemplatesPath); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
if p.templates.notification.identityVerification.html, err = loadTemplate(TemplateNameEmailIdentityVerificationHTML, TemplateCategoryNotifications, p.config.EmailTemplatesPath); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
if p.templates.notification.passwordReset.txt, err = loadTemplate(TemplateNameEmailPasswordResetTXT, TemplateCategoryNotifications, p.config.EmailTemplatesPath); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
if p.templates.notification.passwordReset.html, err = loadTemplate(TemplateNameEmailPasswordResetHTML, TemplateCategoryNotifications, p.config.EmailTemplatesPath); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
if len(errs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for i, e := range errs {
|
||||
if i == 0 {
|
||||
err = e
|
||||
continue
|
||||
}
|
||||
|
||||
err = fmt.Errorf("%v, %w", err, e)
|
||||
}
|
||||
|
||||
return fmt.Errorf("one or more errors occurred loading templates: %w", err)
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
From: {{ .From }}
|
||||
To: {{ .To }}
|
||||
Subject: {{ .Subject }}
|
||||
Date: {{ .Date.Format "Mon, 2 Jan 2006 15:04:05 -0700" }}
|
||||
MIME-Version: 1.0
|
||||
Content-Type: multipart/alternative; boundary="{{ .Boundary }}"
|
||||
|
||||
--{{ .Boundary }}
|
||||
Content-Type: text/plain; charset="utf-8"
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
Content-Disposition: inline
|
||||
|
||||
{{ .Body.PlainText }}
|
||||
{{- if not (eq .Body.HTML "") }}
|
||||
--{{ .Boundary }}
|
||||
Content-Type: text/html; charset="utf-8"
|
||||
|
||||
{{ .Body.HTML }}
|
||||
{{- end }}
|
||||
--{{ .Boundary }}--
|
|
@ -1,22 +1,3 @@
|
|||
package templates
|
||||
|
||||
import (
|
||||
"text/template"
|
||||
)
|
||||
|
||||
// EmailIdentityVerificationHTML the template of email that the user will receive for identity verification.
|
||||
var EmailIdentityVerificationHTML *template.Template
|
||||
|
||||
func init() {
|
||||
t, err := template.New("email_identity_verification_html").Parse(emailContentIdentityVerificationHTML)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
EmailIdentityVerificationHTML = t
|
||||
}
|
||||
|
||||
const emailContentIdentityVerificationHTML = `
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
|
||||
|
@ -435,4 +416,3 @@ const emailContentIdentityVerificationHTML = `
|
|||
</body>
|
||||
|
||||
</html>
|
||||
`
|
|
@ -0,0 +1,9 @@
|
|||
This email has been sent to you in order to validate your identity.
|
||||
|
||||
If you did not initiate the process your credentials might have been compromised and you should reset your password and contact an administrator.
|
||||
|
||||
To setup your 2FA please visit the following URL: {{ .LinkURL }}
|
||||
|
||||
This email was generated by a user with the IP {{ .RemoteIP }}.
|
||||
|
||||
Please contact an administrator if you did not initiate this process.
|
|
@ -1,23 +1,3 @@
|
|||
package templates
|
||||
|
||||
import (
|
||||
"text/template"
|
||||
)
|
||||
|
||||
// EmailPasswordResetHTML the template of email that the user will receive for identity verification.
|
||||
var EmailPasswordResetHTML *template.Template
|
||||
|
||||
func init() {
|
||||
t, err := template.New("email_password_reset_html").Parse(emailContentPasswordResetHTML)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
EmailPasswordResetHTML = t
|
||||
}
|
||||
|
||||
//nolint:gosec // This is a template not hardcoded credentials.
|
||||
const emailContentPasswordResetHTML = `
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
|
||||
|
@ -421,4 +401,3 @@ const emailContentPasswordResetHTML = `
|
|||
</body>
|
||||
|
||||
</html>
|
||||
`
|
|
@ -0,0 +1,7 @@
|
|||
Your password has been successfully reset.
|
||||
|
||||
If you did not initiate the process your credentials might have been compromised and you should reset your password and contact an administrator.
|
||||
|
||||
This email was generated by a user with the IP {{ .RemoteIP }}.
|
||||
|
||||
Please contact an administrator if you did not initiate this process.
|
|
@ -0,0 +1,73 @@
|
|||
package templates
|
||||
|
||||
import (
|
||||
"text/template"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Templates is the struct which holds all the *template.Template values.
|
||||
type Templates struct {
|
||||
notification NotificationTemplates
|
||||
}
|
||||
|
||||
// NotificationTemplates are the templates for the notification system.
|
||||
type NotificationTemplates struct {
|
||||
envelope *template.Template
|
||||
passwordReset HTMLPlainTextTemplate
|
||||
identityVerification HTMLPlainTextTemplate
|
||||
}
|
||||
|
||||
// Format of a template.
|
||||
type Format int
|
||||
|
||||
// Formats.
|
||||
const (
|
||||
DefaultFormat Format = iota
|
||||
HTMLFormat
|
||||
PlainTextFormat
|
||||
)
|
||||
|
||||
// Config for the Provider.
|
||||
type Config struct {
|
||||
EmailTemplatesPath string
|
||||
}
|
||||
|
||||
// EmailPasswordResetValues are the values used for password reset templates.
|
||||
type EmailPasswordResetValues struct {
|
||||
UUID string
|
||||
Title string
|
||||
DisplayName string
|
||||
RemoteIP string
|
||||
}
|
||||
|
||||
// EmailIdentityVerificationValues are the values used for the identity verification templates.
|
||||
type EmailIdentityVerificationValues struct {
|
||||
UUID string
|
||||
Title string
|
||||
DisplayName string
|
||||
RemoteIP string
|
||||
LinkURL string
|
||||
LinkText string
|
||||
}
|
||||
|
||||
// EmailEnvelopeValues are the values used for the email envelopes.
|
||||
type EmailEnvelopeValues struct {
|
||||
ProcessID int
|
||||
UUID string
|
||||
Host string
|
||||
ServerName string
|
||||
SenderDomain string
|
||||
Identifier string
|
||||
From string
|
||||
To string
|
||||
Subject string
|
||||
Date time.Time
|
||||
Boundary string
|
||||
Body EmailEnvelopeBodyValues
|
||||
}
|
||||
|
||||
// EmailEnvelopeBodyValues are the values used for the email envelopes bodies.
|
||||
type EmailEnvelopeBodyValues struct {
|
||||
PlainText string
|
||||
HTML string
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
package templates
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
func templateExists(path string) (exists bool) {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
//nolint:unparam
|
||||
func loadTemplate(name, category, overridePath string) (t *template.Template, err error) {
|
||||
if overridePath != "" {
|
||||
tPath := filepath.Join(overridePath, name)
|
||||
|
||||
if templateExists(tPath) {
|
||||
if t, err = template.ParseFiles(tPath); err != nil {
|
||||
return nil, fmt.Errorf("could not parse template at path '%s': %w", tPath, err)
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
||||
}
|
||||
|
||||
data, err := embedFS.ReadFile(path.Join("src", category, name))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if t, err = template.New(name).Parse(string(data)); err != nil {
|
||||
panic(fmt.Errorf("failed to parse internal template: %w", err))
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
Loading…
Reference in New Issue