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
|
||||
[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 | Description |
|
||||
|
@ -60,13 +71,14 @@ Some Additional examples for specific purposes can be found in the
|
|||
|
||||
## 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
|
||||
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:
|
||||
This template contains the following placeholders which are automatically injected into the template:
|
||||
|
||||
| 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. |
|
||||
| `{{ .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
|
||||
|
||||
|
|
|
@ -4,18 +4,3 @@ 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 }}--
|
||||
|
|
|
@ -11,10 +11,9 @@ import (
|
|||
|
||||
// NewDuoAPI create duo API instance.
|
||||
func NewDuoAPI(duoAPI *duoapi.DuoApi) *APIImpl {
|
||||
api := new(APIImpl)
|
||||
api.DuoApi = duoAPI
|
||||
|
||||
return api
|
||||
return &APIImpl{
|
||||
DuoApi: duoAPI,
|
||||
}
|
||||
}
|
||||
|
||||
// Call call to the DuoAPI.
|
||||
|
|
|
@ -74,8 +74,6 @@ func ResetPasswordPOST(ctx *middlewares.AutheliaCtx) {
|
|||
return
|
||||
}
|
||||
|
||||
bufHTML := new(bytes.Buffer)
|
||||
|
||||
disableHTML := false
|
||||
if ctx.Configuration.Notifier.SMTP != nil {
|
||||
disableHTML = ctx.Configuration.Notifier.SMTP.DisableHTMLEmails
|
||||
|
@ -87,6 +85,8 @@ func ResetPasswordPOST(ctx *middlewares.AutheliaCtx) {
|
|||
RemoteIP: ctx.RemoteIP().String(),
|
||||
}
|
||||
|
||||
bufHTML, bufText := &bytes.Buffer{}, &bytes.Buffer{}
|
||||
|
||||
if !disableHTML {
|
||||
if err = ctx.Providers.Templates.ExecuteEmailPasswordResetTemplate(bufHTML, values, templates.HTMLFormat); err != nil {
|
||||
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 {
|
||||
ctx.Logger.Error(err)
|
||||
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.",
|
||||
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.ReplyOK()
|
||||
|
||||
|
|
|
@ -62,16 +62,15 @@ func IdentityVerificationStart(args IdentityVerificationStartArgs, delayFunc Tim
|
|||
return
|
||||
}
|
||||
|
||||
uri, err := ctx.ExternalRootURL()
|
||||
if err != nil {
|
||||
var (
|
||||
uri string
|
||||
)
|
||||
|
||||
if uri, err = ctx.ExternalRootURL(); err != nil {
|
||||
ctx.Error(err, messageOperationFailed)
|
||||
return
|
||||
}
|
||||
|
||||
link := fmt.Sprintf("%s%s?token=%s", uri, args.TargetEndpoint, ss)
|
||||
|
||||
bufHTML := new(bytes.Buffer)
|
||||
|
||||
disableHTML := false
|
||||
if ctx.Configuration.Notifier.SMTP != nil {
|
||||
disableHTML = ctx.Configuration.Notifier.SMTP.DisableHTMLEmails
|
||||
|
@ -79,12 +78,14 @@ func IdentityVerificationStart(args IdentityVerificationStartArgs, delayFunc Tim
|
|||
|
||||
values := templates.EmailIdentityVerificationValues{
|
||||
Title: args.MailTitle,
|
||||
LinkURL: link,
|
||||
LinkURL: fmt.Sprintf("%s%s?token=%s", uri, args.TargetEndpoint, ss),
|
||||
LinkText: args.MailButtonContent,
|
||||
DisplayName: identity.DisplayName,
|
||||
RemoteIP: ctx.RemoteIP().String(),
|
||||
}
|
||||
|
||||
bufHTML, bufText := &bytes.Buffer{}, &bytes.Buffer{}
|
||||
|
||||
if !disableHTML {
|
||||
if err = ctx.Providers.Templates.ExecuteEmailIdentityVerificationTemplate(bufHTML, values, templates.HTMLFormat); err != nil {
|
||||
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 {
|
||||
ctx.Error(err, messageOperationFailed)
|
||||
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.",
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ func (m *MockNotifier) EXPECT() *MockNotifierMockRecorder {
|
|||
}
|
||||
|
||||
// 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()
|
||||
ret := m.ctrl.Call(m, "Send", arg0, arg1, arg2, arg3)
|
||||
ret0, _ := ret[0].(error)
|
||||
|
|
|
@ -16,9 +16,30 @@ const (
|
|||
smtpCommandAUTH = "AUTH"
|
||||
smtpCommandMAIL = "MAIL"
|
||||
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 (
|
||||
fmtSMTPGenericError = "error performing %s with 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.
|
||||
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)
|
||||
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, bodyText)
|
||||
|
||||
return os.WriteFile(n.path, []byte(content), fileNotifierMode)
|
||||
}
|
||||
|
|
|
@ -10,5 +10,5 @@ import (
|
|||
type Notifier interface {
|
||||
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"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net"
|
||||
"net/mail"
|
||||
"net/smtp"
|
||||
|
@ -52,7 +53,7 @@ type SMTPNotifier struct {
|
|||
}
|
||||
|
||||
// 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 {
|
||||
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.
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -161,7 +162,7 @@ func (n *SMTPNotifier) startTLS() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
switch ok, _ := n.client.Extension("STARTTLS"); ok {
|
||||
switch ok, _ := n.client.Extension(smtpExtSTARTTLS); ok {
|
||||
case true:
|
||||
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
|
||||
}
|
||||
|
||||
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())
|
||||
|
||||
if !n.config.DisableRequireTLS {
|
||||
|
@ -261,7 +262,7 @@ func (n *SMTPNotifier) compose(recipient mail.Address, title, body, htmlBody str
|
|||
return err
|
||||
}
|
||||
|
||||
values := templates.EmailEnvelopeValues{
|
||||
data := templates.EmailEnvelopeValues{
|
||||
ProcessID: os.Getpid(),
|
||||
UUID: muuid.String(),
|
||||
Host: n.config.Host,
|
||||
|
@ -270,21 +271,43 @@ func (n *SMTPNotifier) compose(recipient mail.Address, title, body, htmlBody str
|
|||
Identifier: n.config.Identifier,
|
||||
From: n.config.Sender.String(),
|
||||
To: recipient.String(),
|
||||
Subject: strings.ReplaceAll(n.config.Subject, "{title}", title),
|
||||
Subject: strings.ReplaceAll(n.config.Subject, "{title}", subject),
|
||||
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)
|
||||
|
||||
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 {
|
||||
n.log.Debugf("Notifier SMTP client error while closing the WriteCloser: %v", 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.
|
||||
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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
func (p *Provider) load() (err 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 {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
|
|
@ -3,18 +3,3 @@ 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 }}--
|
||||
|
|
|
@ -62,12 +62,4 @@ type EmailEnvelopeValues struct {
|
|||
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
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package templates
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
|
@ -8,6 +9,12 @@ import (
|
|||
"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) {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
|
@ -21,28 +28,51 @@ func templateExists(path string) (exists bool) {
|
|||
return true
|
||||
}
|
||||
|
||||
//nolint:unparam
|
||||
func loadTemplate(name, category, overridePath string) (t *template.Template, err error) {
|
||||
func readTemplate(name, category, overridePath string) (tPath string, embed bool, data []byte, err error) {
|
||||
if overridePath != "" {
|
||||
tPath := filepath.Join(overridePath, name)
|
||||
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)
|
||||
if data, err = os.ReadFile(tPath); err != nil {
|
||||
return tPath, false, nil, fmt.Errorf("failed to read template override at path '%s': %w", tPath, err)
|
||||
}
|
||||
|
||||
return t, nil
|
||||
return tPath, false, data, nil
|
||||
}
|
||||
}
|
||||
|
||||
data, err := embedFS.ReadFile(path.Join("src", category, name))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
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 {
|
||||
panic(fmt.Errorf("failed to parse internal template: %w", err))
|
||||
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
|
||||
}
|
||||
|
||||
//nolint:unparam
|
||||
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 parseTemplate(name, tPath, embed, data)
|
||||
}
|
||||
|
|
|
@ -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