fix(notification): text emails not encoded properly (#3854)
This fixes an issue where the plain text portion of emails is not encoded with quoted printable encoding.pull/3912/head
parent
4d3ac31051
commit
319a8cf9d4
|
@ -0,0 +1,31 @@
|
||||||
|
* text=auto
|
||||||
|
|
||||||
|
*.go text
|
||||||
|
*.sh text
|
||||||
|
*.html text
|
||||||
|
*.js text
|
||||||
|
*.jsx text
|
||||||
|
*.ts text
|
||||||
|
*.tsx text
|
||||||
|
*.md text
|
||||||
|
*.json text
|
||||||
|
*.svg text
|
||||||
|
*.css text
|
||||||
|
*.yaml text
|
||||||
|
*.yml text
|
||||||
|
*.txt text
|
||||||
|
*.tmpl text
|
||||||
|
*.service text
|
||||||
|
Dockerfile text
|
||||||
|
Dockerfile.* text
|
||||||
|
LICENSE text
|
||||||
|
|
||||||
|
*.jpg binary
|
||||||
|
*.png binary
|
||||||
|
*.gif binary
|
||||||
|
*.ico binary
|
||||||
|
|
||||||
|
# RFC2822 requires us to save the email templates with CRLF line endings.
|
||||||
|
internal/templates/src/notification/* text eol=crlf
|
||||||
|
examples/templates/notifications/*.tmpl text eol=crlf
|
||||||
|
examples/templates/notifications/*.html text eol=crlf
|
|
@ -18,6 +18,17 @@ two extensions; `.html` for HTML templates, and `.txt` for plaintext templates.
|
||||||
This guide effectively documents the usage of the
|
This guide effectively documents the usage of the
|
||||||
[template_path](../../configuration/notifications/introduction.md#template_path) notification configuration option.
|
[template_path](../../configuration/notifications/introduction.md#template_path) notification configuration option.
|
||||||
|
|
||||||
|
## Important Notes
|
||||||
|
|
||||||
|
1. The templates are not covered by our stability guarantees. While we aim to avoid changes to the templates which
|
||||||
|
would cause users to have to manually change them changes may be necessary in order to facilitate bug fixes or
|
||||||
|
generally improve the templates.
|
||||||
|
1. This is especially important for the [Envelope Template](#envelope-template).
|
||||||
|
2. It is your responsibility to ensure your templates are up to date. We make no efforts in facilitating this.
|
||||||
|
2. We may not be able to offer any direct support in debugging these templates. We only offer support and fixes to
|
||||||
|
the official templates.
|
||||||
|
3. All templates __*MUST*__ be encoded in UTF-8 with CRLF line endings. The line endings __*MUST NOT*__ be a simple LF.
|
||||||
|
|
||||||
## Template Names
|
## Template Names
|
||||||
|
|
||||||
| Template | Description |
|
| Template | Description |
|
||||||
|
@ -60,13 +71,14 @@ Some Additional examples for specific purposes can be found in the
|
||||||
|
|
||||||
## Envelope Template
|
## Envelope Template
|
||||||
|
|
||||||
|
*__Important Note:__ This template must end with a CRLF newline. Failure to include this newline will result in
|
||||||
|
malformed emails.*
|
||||||
|
|
||||||
There is also a special envelope template. This is the email envelope which contains the content of the other templates
|
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
|
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`.
|
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:
|
This template contains the following placeholders which are automatically injected into the template:
|
||||||
|
|
||||||
In template files, you can use the following placeholders which are automatically injected into the templates:
|
|
||||||
|
|
||||||
| Placeholder | Description |
|
| Placeholder | Description |
|
||||||
|:-----------------------:|:---------------------------------------------------------------------------:|
|
|:-----------------------:|:---------------------------------------------------------------------------:|
|
||||||
|
@ -80,9 +92,6 @@ In template files, you can use the following placeholders which are automaticall
|
||||||
| `{{ .To }}` | The string representation of the recipients email address. |
|
| `{{ .To }}` | The string representation of the recipients email address. |
|
||||||
| `{{ .Subject }}` | The email subject. |
|
| `{{ .Subject }}` | The email subject. |
|
||||||
| `{{ .Date }}` | The time.Time of the email envelope being rendered. |
|
| `{{ .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
|
## Original Templates
|
||||||
|
|
||||||
|
|
|
@ -4,18 +4,3 @@ Subject: {{ .Subject }}
|
||||||
Date: {{ .Date.Format "Mon, 2 Jan 2006 15:04:05 -0700" }}
|
Date: {{ .Date.Format "Mon, 2 Jan 2006 15:04:05 -0700" }}
|
||||||
Message-ID: <{{ .UUID }}@{{ .Identifier }}>
|
Message-ID: <{{ .UUID }}@{{ .Identifier }}>
|
||||||
MIME-Version: 1.0
|
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 }}--
|
|
||||||
|
|
|
@ -11,10 +11,9 @@ import (
|
||||||
|
|
||||||
// NewDuoAPI create duo API instance.
|
// NewDuoAPI create duo API instance.
|
||||||
func NewDuoAPI(duoAPI *duoapi.DuoApi) *APIImpl {
|
func NewDuoAPI(duoAPI *duoapi.DuoApi) *APIImpl {
|
||||||
api := new(APIImpl)
|
return &APIImpl{
|
||||||
api.DuoApi = duoAPI
|
DuoApi: duoAPI,
|
||||||
|
}
|
||||||
return api
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call call to the DuoAPI.
|
// Call call to the DuoAPI.
|
||||||
|
|
|
@ -74,8 +74,6 @@ func ResetPasswordPOST(ctx *middlewares.AutheliaCtx) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
bufHTML := new(bytes.Buffer)
|
|
||||||
|
|
||||||
disableHTML := false
|
disableHTML := false
|
||||||
if ctx.Configuration.Notifier.SMTP != nil {
|
if ctx.Configuration.Notifier.SMTP != nil {
|
||||||
disableHTML = ctx.Configuration.Notifier.SMTP.DisableHTMLEmails
|
disableHTML = ctx.Configuration.Notifier.SMTP.DisableHTMLEmails
|
||||||
|
@ -87,6 +85,8 @@ func ResetPasswordPOST(ctx *middlewares.AutheliaCtx) {
|
||||||
RemoteIP: ctx.RemoteIP().String(),
|
RemoteIP: ctx.RemoteIP().String(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bufHTML, bufText := &bytes.Buffer{}, &bytes.Buffer{}
|
||||||
|
|
||||||
if !disableHTML {
|
if !disableHTML {
|
||||||
if err = ctx.Providers.Templates.ExecuteEmailPasswordResetTemplate(bufHTML, values, templates.HTMLFormat); err != nil {
|
if err = ctx.Providers.Templates.ExecuteEmailPasswordResetTemplate(bufHTML, values, templates.HTMLFormat); err != nil {
|
||||||
ctx.Logger.Error(err)
|
ctx.Logger.Error(err)
|
||||||
|
@ -96,8 +96,6 @@ func ResetPasswordPOST(ctx *middlewares.AutheliaCtx) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bufText := new(bytes.Buffer)
|
|
||||||
|
|
||||||
if err = ctx.Providers.Templates.ExecuteEmailPasswordResetTemplate(bufText, values, templates.PlainTextFormat); err != nil {
|
if err = ctx.Providers.Templates.ExecuteEmailPasswordResetTemplate(bufText, values, templates.PlainTextFormat); err != nil {
|
||||||
ctx.Logger.Error(err)
|
ctx.Logger.Error(err)
|
||||||
ctx.ReplyOK()
|
ctx.ReplyOK()
|
||||||
|
@ -110,7 +108,7 @@ func ResetPasswordPOST(ctx *middlewares.AutheliaCtx) {
|
||||||
ctx.Logger.Debugf("Sending an email to user %s (%s) to inform that the password has changed.",
|
ctx.Logger.Debugf("Sending an email to user %s (%s) to inform that the password has changed.",
|
||||||
username, addresses[0])
|
username, addresses[0])
|
||||||
|
|
||||||
if err = ctx.Providers.Notifier.Send(addresses[0], "Password changed successfully", bufText.String(), bufHTML.String()); err != nil {
|
if err = ctx.Providers.Notifier.Send(addresses[0], "Password changed successfully", bufText.Bytes(), bufHTML.Bytes()); err != nil {
|
||||||
ctx.Logger.Error(err)
|
ctx.Logger.Error(err)
|
||||||
ctx.ReplyOK()
|
ctx.ReplyOK()
|
||||||
|
|
||||||
|
|
|
@ -62,16 +62,15 @@ func IdentityVerificationStart(args IdentityVerificationStartArgs, delayFunc Tim
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
uri, err := ctx.ExternalRootURL()
|
var (
|
||||||
if err != nil {
|
uri string
|
||||||
|
)
|
||||||
|
|
||||||
|
if uri, err = ctx.ExternalRootURL(); err != nil {
|
||||||
ctx.Error(err, messageOperationFailed)
|
ctx.Error(err, messageOperationFailed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
link := fmt.Sprintf("%s%s?token=%s", uri, args.TargetEndpoint, ss)
|
|
||||||
|
|
||||||
bufHTML := new(bytes.Buffer)
|
|
||||||
|
|
||||||
disableHTML := false
|
disableHTML := false
|
||||||
if ctx.Configuration.Notifier.SMTP != nil {
|
if ctx.Configuration.Notifier.SMTP != nil {
|
||||||
disableHTML = ctx.Configuration.Notifier.SMTP.DisableHTMLEmails
|
disableHTML = ctx.Configuration.Notifier.SMTP.DisableHTMLEmails
|
||||||
|
@ -79,12 +78,14 @@ func IdentityVerificationStart(args IdentityVerificationStartArgs, delayFunc Tim
|
||||||
|
|
||||||
values := templates.EmailIdentityVerificationValues{
|
values := templates.EmailIdentityVerificationValues{
|
||||||
Title: args.MailTitle,
|
Title: args.MailTitle,
|
||||||
LinkURL: link,
|
LinkURL: fmt.Sprintf("%s%s?token=%s", uri, args.TargetEndpoint, ss),
|
||||||
LinkText: args.MailButtonContent,
|
LinkText: args.MailButtonContent,
|
||||||
DisplayName: identity.DisplayName,
|
DisplayName: identity.DisplayName,
|
||||||
RemoteIP: ctx.RemoteIP().String(),
|
RemoteIP: ctx.RemoteIP().String(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bufHTML, bufText := &bytes.Buffer{}, &bytes.Buffer{}
|
||||||
|
|
||||||
if !disableHTML {
|
if !disableHTML {
|
||||||
if err = ctx.Providers.Templates.ExecuteEmailIdentityVerificationTemplate(bufHTML, values, templates.HTMLFormat); err != nil {
|
if err = ctx.Providers.Templates.ExecuteEmailIdentityVerificationTemplate(bufHTML, values, templates.HTMLFormat); err != nil {
|
||||||
ctx.Error(err, messageOperationFailed)
|
ctx.Error(err, messageOperationFailed)
|
||||||
|
@ -92,8 +93,6 @@ func IdentityVerificationStart(args IdentityVerificationStartArgs, delayFunc Tim
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bufText := new(bytes.Buffer)
|
|
||||||
|
|
||||||
if err = ctx.Providers.Templates.ExecuteEmailIdentityVerificationTemplate(bufText, values, templates.PlainTextFormat); err != nil {
|
if err = ctx.Providers.Templates.ExecuteEmailIdentityVerificationTemplate(bufText, values, templates.PlainTextFormat); err != nil {
|
||||||
ctx.Error(err, messageOperationFailed)
|
ctx.Error(err, messageOperationFailed)
|
||||||
return
|
return
|
||||||
|
@ -102,7 +101,7 @@ func IdentityVerificationStart(args IdentityVerificationStartArgs, delayFunc Tim
|
||||||
ctx.Logger.Debugf("Sending an email to user %s (%s) to confirm identity for registering a device.",
|
ctx.Logger.Debugf("Sending an email to user %s (%s) to confirm identity for registering a device.",
|
||||||
identity.Username, identity.Email)
|
identity.Username, identity.Email)
|
||||||
|
|
||||||
if err = ctx.Providers.Notifier.Send(mail.Address{Name: identity.DisplayName, Address: 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.Bytes(), bufHTML.Bytes()); err != nil {
|
||||||
ctx.Error(err, messageOperationFailed)
|
ctx.Error(err, messageOperationFailed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,7 +35,7 @@ func (m *MockNotifier) EXPECT() *MockNotifierMockRecorder {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send mocks base method.
|
// Send mocks base method.
|
||||||
func (m *MockNotifier) Send(arg0 mail.Address, arg1, arg2, arg3 string) error {
|
func (m *MockNotifier) Send(arg0 mail.Address, arg1 string, arg2, arg3 []byte) error {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "Send", arg0, arg1, arg2, arg3)
|
ret := m.ctrl.Call(m, "Send", arg0, arg1, arg2, arg3)
|
||||||
ret0, _ := ret[0].(error)
|
ret0, _ := ret[0].(error)
|
||||||
|
|
|
@ -16,9 +16,30 @@ const (
|
||||||
smtpCommandAUTH = "AUTH"
|
smtpCommandAUTH = "AUTH"
|
||||||
smtpCommandMAIL = "MAIL"
|
smtpCommandMAIL = "MAIL"
|
||||||
smtpCommandRCPT = "RCPT"
|
smtpCommandRCPT = "RCPT"
|
||||||
|
|
||||||
|
smtpEncodingQuotedPrintable = "quoted-printable"
|
||||||
|
smtpEncoding8bit = "8bit"
|
||||||
|
|
||||||
|
smtpContentTypeTextPlain = "text/plain"
|
||||||
|
smtpContentTypeTextHTML = "text/html"
|
||||||
|
smtpFmtContentType = `%s; charset="UTF-8"`
|
||||||
|
smtpFmtContentDispositionInline = "inline"
|
||||||
|
|
||||||
|
smtpExtSTARTTLS = smtpCommandSTARTTLS
|
||||||
|
smtpExt8BITMIME = "8BITMIME"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
headerContentType = "Content-Type"
|
||||||
|
headerContentDisposition = "Content-Disposition"
|
||||||
|
headerContentTransferEncoding = "Content-Transfer-Encoding"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
fmtSMTPGenericError = "error performing %s with the SMTP server: %w"
|
fmtSMTPGenericError = "error performing %s with the SMTP server: %w"
|
||||||
fmtSMTPDialError = "error dialing the SMTP server: %w"
|
fmtSMTPDialError = "error dialing the SMTP server: %w"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
rfc2822DoubleNewLine = []byte("\r\n\r\n")
|
||||||
|
)
|
||||||
|
|
|
@ -43,8 +43,8 @@ func (n *FileNotifier) StartupCheck() (err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send send a identity verification link to a user.
|
// Send send a identity verification link to a user.
|
||||||
func (n *FileNotifier) Send(recipient mail.Address, subject, body, _ string) error {
|
func (n *FileNotifier) Send(recipient mail.Address, subject string, bodyText, _ []byte) error {
|
||||||
content := fmt.Sprintf("Date: %s\nRecipient: %s\nSubject: %s\nBody: %s", time.Now(), recipient, subject, body)
|
content := fmt.Sprintf("Date: %s\nRecipient: %s\nSubject: %s\nBody: %s", time.Now(), recipient, subject, bodyText)
|
||||||
|
|
||||||
return os.WriteFile(n.path, []byte(content), fileNotifierMode)
|
return os.WriteFile(n.path, []byte(content), fileNotifierMode)
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,5 +10,5 @@ import (
|
||||||
type Notifier interface {
|
type Notifier interface {
|
||||||
model.StartupCheck
|
model.StartupCheck
|
||||||
|
|
||||||
Send(recipient mail.Address, subject, body, htmlBody string) (err error)
|
Send(recipient mail.Address, subject string, bodyText, bodyHTML []byte) (err error)
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"mime/multipart"
|
||||||
"net"
|
"net"
|
||||||
"net/mail"
|
"net/mail"
|
||||||
"net/smtp"
|
"net/smtp"
|
||||||
|
@ -52,7 +53,7 @@ type SMTPNotifier struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send is used to email a recipient.
|
// Send is used to email a recipient.
|
||||||
func (n *SMTPNotifier) Send(recipient mail.Address, title, body, htmlBody string) (err error) {
|
func (n *SMTPNotifier) Send(recipient mail.Address, subject string, bodyText, bodyHTML []byte) (err error) {
|
||||||
if err = n.dial(); err != nil {
|
if err = n.dial(); err != nil {
|
||||||
return fmt.Errorf(fmtSMTPDialError, err)
|
return fmt.Errorf(fmtSMTPDialError, err)
|
||||||
}
|
}
|
||||||
|
@ -65,7 +66,7 @@ func (n *SMTPNotifier) Send(recipient mail.Address, title, body, htmlBody string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compose and send the email body to the server.
|
// Compose and send the email body to the server.
|
||||||
if err = n.compose(recipient, title, body, htmlBody); err != nil {
|
if err = n.compose(recipient, subject, bodyText, bodyHTML); err != nil {
|
||||||
return fmt.Errorf(fmtSMTPGenericError, smtpCommandDATA, err)
|
return fmt.Errorf(fmtSMTPGenericError, smtpCommandDATA, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -161,7 +162,7 @@ func (n *SMTPNotifier) startTLS() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
switch ok, _ := n.client.Extension("STARTTLS"); ok {
|
switch ok, _ := n.client.Extension(smtpExtSTARTTLS); ok {
|
||||||
case true:
|
case true:
|
||||||
n.log.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)
|
||||||
|
|
||||||
|
@ -237,7 +238,7 @@ func (n *SMTPNotifier) auth() (err error) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *SMTPNotifier) compose(recipient mail.Address, title, body, htmlBody string) (err error) {
|
func (n *SMTPNotifier) compose(recipient mail.Address, subject string, bodyText, bodyHTML []byte) (err error) {
|
||||||
n.log.Debugf("Notifier SMTP client attempting to send email body to %s", recipient.String())
|
n.log.Debugf("Notifier SMTP client attempting to send email body to %s", recipient.String())
|
||||||
|
|
||||||
if !n.config.DisableRequireTLS {
|
if !n.config.DisableRequireTLS {
|
||||||
|
@ -261,7 +262,7 @@ func (n *SMTPNotifier) compose(recipient mail.Address, title, body, htmlBody str
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
values := templates.EmailEnvelopeValues{
|
data := templates.EmailEnvelopeValues{
|
||||||
ProcessID: os.Getpid(),
|
ProcessID: os.Getpid(),
|
||||||
UUID: muuid.String(),
|
UUID: muuid.String(),
|
||||||
Host: n.config.Host,
|
Host: n.config.Host,
|
||||||
|
@ -270,21 +271,43 @@ func (n *SMTPNotifier) compose(recipient mail.Address, title, body, htmlBody str
|
||||||
Identifier: n.config.Identifier,
|
Identifier: n.config.Identifier,
|
||||||
From: n.config.Sender.String(),
|
From: n.config.Sender.String(),
|
||||||
To: recipient.String(),
|
To: recipient.String(),
|
||||||
Subject: strings.ReplaceAll(n.config.Subject, "{title}", title),
|
Subject: strings.ReplaceAll(n.config.Subject, "{title}", subject),
|
||||||
Date: time.Now(),
|
Date: time.Now(),
|
||||||
Boundary: utils.RandomString(30, utils.AlphaNumericCharacters, true),
|
|
||||||
Body: templates.EmailEnvelopeBodyValues{
|
|
||||||
PlainText: body,
|
|
||||||
HTML: htmlBody,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = n.templates.ExecuteEmailEnvelope(wc, values); err != nil {
|
if err = n.templates.ExecuteEmailEnvelope(wc, data); err != nil {
|
||||||
n.log.Debugf("Notifier SMTP client error while sending email body over WriteCloser: %v", err)
|
n.log.Debugf("Notifier SMTP client error while sending email body over WriteCloser: %v", err)
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mwr := multipart.NewWriter(wc)
|
||||||
|
|
||||||
|
if _, err = wc.Write([]byte(fmt.Sprintf(`Content-Type: multipart/alternative; boundary="%s"`, mwr.Boundary()))); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = wc.Write(rfc2822DoubleNewLine); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ext8BITMIME, _ := n.client.Extension(smtpExt8BITMIME)
|
||||||
|
|
||||||
|
if err = multipartWrite(mwr, smtpMIMEHeaders(ext8BITMIME, smtpContentTypeTextPlain, bodyText), bodyText); err != nil {
|
||||||
|
return fmt.Errorf("failed to write text/plain part: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(bodyHTML) != 0 {
|
||||||
|
if err = multipartWrite(mwr,
|
||||||
|
smtpMIMEHeaders(ext8BITMIME, smtpContentTypeTextHTML, bodyText), bodyHTML); err != nil {
|
||||||
|
return fmt.Errorf("failed to write text/html part: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = mwr.Close(); err != nil {
|
||||||
|
return fmt.Errorf("failed to finalize the multipart content: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
if err = wc.Close(); err != nil {
|
if err = wc.Close(); err != nil {
|
||||||
n.log.Debugf("Notifier SMTP client error while closing the WriteCloser: %v", err)
|
n.log.Debugf("Notifier SMTP client error while closing the WriteCloser: %v", err)
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -0,0 +1,96 @@
|
||||||
|
package notification
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"mime/multipart"
|
||||||
|
"mime/quotedprintable"
|
||||||
|
"net/textproto"
|
||||||
|
|
||||||
|
"github.com/authelia/authelia/v4/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func smtpMIMEHeaders(ext8BITMIME bool, contentType string, data []byte) textproto.MIMEHeader {
|
||||||
|
headers := textproto.MIMEHeader{
|
||||||
|
headerContentType: []string{fmt.Sprintf(smtpFmtContentType, contentType)},
|
||||||
|
headerContentDisposition: []string{smtpFmtContentDispositionInline},
|
||||||
|
}
|
||||||
|
|
||||||
|
characteristics := NewMIMECharacteristics(data)
|
||||||
|
|
||||||
|
if !ext8BITMIME || characteristics.LongLines || characteristics.Characters8BIT {
|
||||||
|
headers.Set(headerContentTransferEncoding, smtpEncodingQuotedPrintable)
|
||||||
|
} else {
|
||||||
|
headers.Set(headerContentTransferEncoding, smtpEncoding8bit)
|
||||||
|
}
|
||||||
|
|
||||||
|
return headers
|
||||||
|
}
|
||||||
|
|
||||||
|
func multipartWrite(mwr *multipart.Writer, header textproto.MIMEHeader, data []byte) (err error) {
|
||||||
|
var (
|
||||||
|
wc io.WriteCloser
|
||||||
|
wr io.Writer
|
||||||
|
)
|
||||||
|
|
||||||
|
if wr, err = mwr.CreatePart(header); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch header.Get(headerContentTransferEncoding) {
|
||||||
|
case smtpEncodingQuotedPrintable:
|
||||||
|
wc = quotedprintable.NewWriter(wr)
|
||||||
|
case smtpEncoding8bit, "":
|
||||||
|
wc = utils.NewWriteCloser(wr)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unknown encoding: %s", header.Get(headerContentTransferEncoding))
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = wc.Write(data); err != nil {
|
||||||
|
_ = wc.Close()
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = wc.Close()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMIMECharacteristics detects the SMTP MIMECharacteristics for the given data bytes.
|
||||||
|
func NewMIMECharacteristics(data []byte) MIMECharacteristics {
|
||||||
|
characteristics := MIMECharacteristics{}
|
||||||
|
|
||||||
|
cl := 0
|
||||||
|
|
||||||
|
n := len(data)
|
||||||
|
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
cl++
|
||||||
|
|
||||||
|
if cl > 1000 {
|
||||||
|
characteristics.LongLines = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if data[i] == 10 {
|
||||||
|
cl = 0
|
||||||
|
|
||||||
|
if i == 0 || data[i-1] != 13 {
|
||||||
|
characteristics.LineFeeds = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if data[i] >= 128 {
|
||||||
|
characteristics.Characters8BIT = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return characteristics
|
||||||
|
}
|
||||||
|
|
||||||
|
// MIMECharacteristics represents specific MIME related characteristics.
|
||||||
|
type MIMECharacteristics struct {
|
||||||
|
LongLines bool
|
||||||
|
LineFeeds bool
|
||||||
|
Characters8BIT bool
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
package templates
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestShouldNotHaveDeprecatedPlaceholders(t *testing.T) {
|
||||||
|
data, err := embedFS.ReadFile(path.Join("src", TemplateCategoryNotifications, TemplateNameEmailEnvelope))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.False(t, tmplEnvelopeHasDeprecatedPlaceholders(data))
|
||||||
|
}
|
|
@ -25,23 +25,33 @@ type Provider struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExecuteEmailEnvelope writes the envelope template to the given io.Writer.
|
// ExecuteEmailEnvelope writes the envelope template to the given io.Writer.
|
||||||
func (p Provider) ExecuteEmailEnvelope(wr io.Writer, data EmailEnvelopeValues) (err error) {
|
func (p *Provider) ExecuteEmailEnvelope(wr io.Writer, data EmailEnvelopeValues) (err error) {
|
||||||
return p.templates.notification.envelope.Execute(wr, data)
|
return p.templates.notification.envelope.Execute(wr, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExecuteEmailPasswordResetTemplate writes the password reset template to the given io.Writer.
|
// ExecuteEmailPasswordResetTemplate writes the password reset template to the given io.Writer.
|
||||||
func (p Provider) ExecuteEmailPasswordResetTemplate(wr io.Writer, data EmailPasswordResetValues, format Format) (err error) {
|
func (p *Provider) ExecuteEmailPasswordResetTemplate(wr io.Writer, data EmailPasswordResetValues, format Format) (err error) {
|
||||||
return p.templates.notification.passwordReset.Get(format).Execute(wr, data)
|
return p.templates.notification.passwordReset.Get(format).Execute(wr, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExecuteEmailIdentityVerificationTemplate writes the identity verification template to the given io.Writer.
|
// ExecuteEmailIdentityVerificationTemplate writes the identity verification template to the given io.Writer.
|
||||||
func (p Provider) ExecuteEmailIdentityVerificationTemplate(wr io.Writer, data EmailIdentityVerificationValues, format Format) (err error) {
|
func (p *Provider) ExecuteEmailIdentityVerificationTemplate(wr io.Writer, data EmailIdentityVerificationValues, format Format) (err error) {
|
||||||
return p.templates.notification.identityVerification.Get(format).Execute(wr, data)
|
return p.templates.notification.identityVerification.Get(format).Execute(wr, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) load() (err error) {
|
func (p *Provider) load() (err error) {
|
||||||
var errs []error
|
var errs []error
|
||||||
|
|
||||||
|
if tPath, embed, data, err := readTemplate(TemplateNameEmailEnvelope, TemplateCategoryNotifications, p.config.EmailTemplatesPath); err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
} else {
|
||||||
|
if !embed && tmplEnvelopeHasDeprecatedPlaceholders(data) {
|
||||||
|
errs = append(errs, fmt.Errorf("the evelope template override appears to contain removed placeholders"))
|
||||||
|
} else if p.templates.notification.envelope, err = parseTemplate(TemplateNameEmailEnvelope, tPath, embed, data); err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if p.templates.notification.envelope, err = loadTemplate(TemplateNameEmailEnvelope, TemplateCategoryNotifications, p.config.EmailTemplatesPath); err != nil {
|
if p.templates.notification.envelope, err = loadTemplate(TemplateNameEmailEnvelope, TemplateCategoryNotifications, p.config.EmailTemplatesPath); err != nil {
|
||||||
errs = append(errs, err)
|
errs = append(errs, err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,18 +3,3 @@ To: {{ .To }}
|
||||||
Subject: {{ .Subject }}
|
Subject: {{ .Subject }}
|
||||||
Date: {{ .Date.Format "Mon, 2 Jan 2006 15:04:05 -0700" }}
|
Date: {{ .Date.Format "Mon, 2 Jan 2006 15:04:05 -0700" }}
|
||||||
MIME-Version: 1.0
|
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 }}--
|
|
||||||
|
|
|
@ -62,12 +62,4 @@ type EmailEnvelopeValues struct {
|
||||||
To string
|
To string
|
||||||
Subject string
|
Subject string
|
||||||
Date time.Time
|
Date time.Time
|
||||||
Boundary string
|
|
||||||
Body EmailEnvelopeBodyValues
|
|
||||||
}
|
|
||||||
|
|
||||||
// EmailEnvelopeBodyValues are the values used for the email envelopes bodies.
|
|
||||||
type EmailEnvelopeBodyValues struct {
|
|
||||||
PlainText string
|
|
||||||
HTML string
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package templates
|
package templates
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
@ -8,6 +9,12 @@ import (
|
||||||
"text/template"
|
"text/template"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func tmplEnvelopeHasDeprecatedPlaceholders(data []byte) bool {
|
||||||
|
return bytes.Contains(data, []byte("{{ .Boundary }}")) ||
|
||||||
|
bytes.Contains(data, []byte("{{ .Body.PlainText }}")) ||
|
||||||
|
bytes.Contains(data, []byte("{{ .Body.HTML }}"))
|
||||||
|
}
|
||||||
|
|
||||||
func templateExists(path string) (exists bool) {
|
func templateExists(path string) (exists bool) {
|
||||||
info, err := os.Stat(path)
|
info, err := os.Stat(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -21,28 +28,51 @@ func templateExists(path string) (exists bool) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
//nolint:unparam
|
func readTemplate(name, category, overridePath string) (tPath string, embed bool, data []byte, err error) {
|
||||||
func loadTemplate(name, category, overridePath string) (t *template.Template, err error) {
|
|
||||||
if overridePath != "" {
|
if overridePath != "" {
|
||||||
tPath := filepath.Join(overridePath, name)
|
tPath = filepath.Join(overridePath, name)
|
||||||
|
|
||||||
if templateExists(tPath) {
|
if templateExists(tPath) {
|
||||||
if t, err = template.ParseFiles(tPath); err != nil {
|
if data, err = os.ReadFile(tPath); err != nil {
|
||||||
return nil, fmt.Errorf("could not parse template at path '%s': %w", tPath, err)
|
return tPath, false, nil, fmt.Errorf("failed to read template override at path '%s': %w", tPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tPath, false, data, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tPath = path.Join("src", category, name)
|
||||||
|
|
||||||
|
if data, err = embedFS.ReadFile(tPath); err != nil {
|
||||||
|
return tPath, true, nil, fmt.Errorf("failed to read embedded template '%s': %w", tPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tPath, true, data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseTemplate(name, tPath string, embed bool, data []byte) (t *template.Template, err error) {
|
||||||
|
if t, err = template.New(name).Parse(string(data)); err != nil {
|
||||||
|
if embed {
|
||||||
|
return nil, fmt.Errorf("failed to parse embedded template '%s': %w", tPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("failed to parse template override at path '%s': %w", tPath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return t, nil
|
return t, nil
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
data, err := embedFS.ReadFile(path.Join("src", category, name))
|
//nolint:unparam
|
||||||
if err != nil {
|
func loadTemplate(name, category, overridePath string) (t *template.Template, err error) {
|
||||||
|
var (
|
||||||
|
embed bool
|
||||||
|
tPath string
|
||||||
|
data []byte
|
||||||
|
)
|
||||||
|
|
||||||
|
if tPath, embed, data, err = readTemplate(name, category, overridePath); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if t, err = template.New(name).Parse(string(data)); err != nil {
|
return parseTemplate(name, tPath, embed, data)
|
||||||
panic(fmt.Errorf("failed to parse internal template: %w", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
return t, nil
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewWriteCloser creates a new io.WriteCloser from an io.Writer.
|
||||||
|
func NewWriteCloser(wr io.Writer) io.WriteCloser {
|
||||||
|
return &WriteCloser{wr: wr}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteCloser is a io.Writer with an io.Closer.
|
||||||
|
type WriteCloser struct {
|
||||||
|
wr io.Writer
|
||||||
|
|
||||||
|
closed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write to the io.Writer.
|
||||||
|
func (w *WriteCloser) Write(p []byte) (n int, err error) {
|
||||||
|
if w.closed {
|
||||||
|
return -1, errors.New("already closed")
|
||||||
|
}
|
||||||
|
|
||||||
|
return w.wr.Write(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the io.Closer.
|
||||||
|
func (w *WriteCloser) Close() error {
|
||||||
|
w.closed = true
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
Loading…
Reference in New Issue