diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 000000000..a8a206ad2
--- /dev/null
+++ b/.gitattributes
@@ -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
diff --git a/docs/content/en/reference/guides/notification-templates.md b/docs/content/en/reference/guides/notification-templates.md
index fe11a1326..42ab8f871 100644
--- a/docs/content/en/reference/guides/notification-templates.md
+++ b/docs/content/en/reference/guides/notification-templates.md
@@ -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
diff --git a/examples/templates/notifications/envelope_with_message_id.tmpl b/examples/templates/notifications/envelope_with_message_id.tmpl
index 3a05ad8ba..774bac8ca 100644
--- a/examples/templates/notifications/envelope_with_message_id.tmpl
+++ b/examples/templates/notifications/envelope_with_message_id.tmpl
@@ -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 }}--
diff --git a/internal/duo/duo.go b/internal/duo/duo.go
index bcfaa15a5..345190904 100644
--- a/internal/duo/duo.go
+++ b/internal/duo/duo.go
@@ -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.
diff --git a/internal/handlers/handler_reset_password_step2.go b/internal/handlers/handler_reset_password_step2.go
index 4cdf0c976..213a7305e 100644
--- a/internal/handlers/handler_reset_password_step2.go
+++ b/internal/handlers/handler_reset_password_step2.go
@@ -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()
diff --git a/internal/middlewares/identity_verification.go b/internal/middlewares/identity_verification.go
index 1e93f223a..6189331e4 100644
--- a/internal/middlewares/identity_verification.go
+++ b/internal/middlewares/identity_verification.go
@@ -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
}
diff --git a/internal/mocks/notifier.go b/internal/mocks/notifier.go
index 829176b9b..208fe2eb6 100644
--- a/internal/mocks/notifier.go
+++ b/internal/mocks/notifier.go
@@ -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)
diff --git a/internal/notification/const.go b/internal/notification/const.go
index f215c4d91..aee69c680 100644
--- a/internal/notification/const.go
+++ b/internal/notification/const.go
@@ -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")
+)
diff --git a/internal/notification/file_notifier.go b/internal/notification/file_notifier.go
index fe6305ae4..af2dc91e0 100644
--- a/internal/notification/file_notifier.go
+++ b/internal/notification/file_notifier.go
@@ -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)
}
diff --git a/internal/notification/notifier.go b/internal/notification/notifier.go
index d10509ee4..87c39a9b0 100644
--- a/internal/notification/notifier.go
+++ b/internal/notification/notifier.go
@@ -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)
}
diff --git a/internal/notification/smtp_notifier.go b/internal/notification/smtp_notifier.go
index 8dceb73e6..de58dcba9 100644
--- a/internal/notification/smtp_notifier.go
+++ b/internal/notification/smtp_notifier.go
@@ -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
diff --git a/internal/notification/smtp_util.go b/internal/notification/smtp_util.go
new file mode 100644
index 000000000..4a1258e12
--- /dev/null
+++ b/internal/notification/smtp_util.go
@@ -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
+}
diff --git a/internal/templates/embed_test.go b/internal/templates/embed_test.go
new file mode 100644
index 000000000..75080ef6e
--- /dev/null
+++ b/internal/templates/embed_test.go
@@ -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))
+}
diff --git a/internal/templates/provider.go b/internal/templates/provider.go
index 7d2b2172e..500be22e9 100644
--- a/internal/templates/provider.go
+++ b/internal/templates/provider.go
@@ -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)
}
diff --git a/internal/templates/src/notification/Envelope.tmpl b/internal/templates/src/notification/Envelope.tmpl
index 007bfc821..813cbef71 100644
--- a/internal/templates/src/notification/Envelope.tmpl
+++ b/internal/templates/src/notification/Envelope.tmpl
@@ -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 }}--
diff --git a/internal/templates/types.go b/internal/templates/types.go
index 2bcf8c179..daa7fc0c8 100644
--- a/internal/templates/types.go
+++ b/internal/templates/types.go
@@ -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
}
diff --git a/internal/templates/util.go b/internal/templates/util.go
index 8ff073584..f747b478a 100644
--- a/internal/templates/util.go
+++ b/internal/templates/util.go
@@ -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)
+}
diff --git a/internal/utils/io.go b/internal/utils/io.go
new file mode 100644
index 000000000..eacaf0386
--- /dev/null
+++ b/internal/utils/io.go
@@ -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
+}
diff --git a/web/src/assets/images/applestore-badge.svg b/web/src/assets/images/applestore-badge.svg
index ac111e597..0fe477c56 100644
--- a/web/src/assets/images/applestore-badge.svg
+++ b/web/src/assets/images/applestore-badge.svg
@@ -1,129 +1,129 @@
-
-
-
-
+
+
+
+
diff --git a/web/src/assets/images/user.svg b/web/src/assets/images/user.svg
index 8a8ddd725..ddb47bfd6 100644
--- a/web/src/assets/images/user.svg
+++ b/web/src/assets/images/user.svg
@@ -1,21 +1,21 @@
-
-
-