SMTP: Update email templates to include populated <title> tag (#61430)

* add .TemplateData property to data in order to populate template <title> tags with the compiled subject value

* update all templates

* re-enable integration test and update implementation to check changes

* chore: fmt

* add HiddenSubject template func and update text templates

* slight performance improvement, only execute subject template once

* update template I missed

---------

Co-authored-by: Gilles De Mey <gilles.de.mey@gmail.com>
This commit is contained in:
Michael Mandrus 2023-01-30 16:56:23 -05:00 committed by GitHub
parent a92c081a33
commit 8dab3bf36c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 107 additions and 70 deletions

View File

@ -2,7 +2,7 @@
<mj-head>
<!-- ⬇ Don't forget to specifify an email subject! Use the HTML comment below ⬇ -->
<mj-title>
{{ Subject .Subject "{{ .InvitedBy }} has added you to the {{ .OrgName }} organization" }}
{{ Subject .Subject .TemplateData "{{ .InvitedBy }} has added you to the {{ .OrgName }} organization" }}
</mj-title>
<mj-include path="./partials/layout/head.mjml" />
</mj-head>

View File

@ -1,4 +1,4 @@
[[Subject .Subject "[[.InvitedBy]] has added you to the [[.OrgName]] organization"]]
[[HiddenSubject .Subject "[[.InvitedBy]] has added you to the [[.OrgName]] organization"]]
You have been added to [[.OrgName]]

View File

@ -2,7 +2,7 @@
<mj-head>
<!-- ⬇ Don't forget to specifify an email subject below! ⬇ -->
<mj-title>
{{ Subject .Subject "{{ .InvitedBy }} has invited you to join Grafana" }}
{{ Subject .Subject .TemplateData "{{ .InvitedBy }} has invited you to join Grafana" }}
</mj-title>
<mj-include path="./partials/layout/head.mjml" />
</mj-head>

View File

@ -1,4 +1,4 @@
[[Subject .Subject "[[.InvitedBy]] has invited you to join Grafana"]]
[[HiddenSubject .Subject "[[.InvitedBy]] has invited you to join Grafana"]]
You're invited to join [[.OrgName]]

View File

@ -2,7 +2,7 @@
<mj-head>
<!-- ⬇ Don't forget to specifify an email subject below! ⬇ -->
<mj-title>
{{ Subject .Subject "{{ .Title }}" }}
{{ Subject .Subject .TemplateData "{{ .Title }}" }}
</mj-title>
<mj-include path="./partials/layout/head.mjml" />
<!-- Summary of the email contents, this will go in the email preview -->

View File

@ -1,4 +1,4 @@
[[Subject .Subject "[[.Title]]"]]
[[HiddenSubject .Subject "[[.Title]]"]]
[[.Title]]
----------------

View File

@ -2,7 +2,7 @@
<mj-head>
<!-- ⬇ Don't forget to specifify an email subject below! ⬇ -->
<mj-title>
{{ Subject .Subject "Reset your Grafana password - {{.Name}}" }}
{{ Subject .Subject .TemplateData "Reset your Grafana password - {{.Name}}" }}
</mj-title>
<mj-include path="./partials/layout/head.mjml" />
</mj-head>

View File

@ -1,4 +1,4 @@
[[Subject .Subject "Reset your Grafana password - [[.Name]]"]]
[[HiddenSubject .Subject "Reset your Grafana password - [[.Name]]"]]
Hi [[.Name]],

View File

@ -2,7 +2,7 @@
<mj-head>
<!-- ⬇ Don't forget to specifify an email subject below! ⬇ -->
<mj-title>
{{ Subject .Subject "Welcome to Grafana, please complete your sign up!" }}
{{ Subject .Subject .TemplateData "Welcome to Grafana, please complete your sign up!" }}
</mj-title>
<mj-include path="./partials/layout/head.mjml" />
</mj-head>

View File

@ -1,4 +1,4 @@
[[Subject .Subject "Welcome to Grafana, please complete your sign up!"]]
[[HiddenSubject .Subject "Welcome to Grafana, please complete your sign up!"]]
Complete the signup

View File

@ -2,7 +2,7 @@
<mj-head>
<!-- ⬇ Don't forget to specifify an email subject below! ⬇ -->
<mj-title>
{{ Subject .Subject "Welcome to Grafana" }}
{{ Subject .Subject .TemplateData "Welcome to Grafana" }}
</mj-title>
<mj-include path="./partials/layout/head.mjml" />
</mj-head>

View File

@ -1,4 +1,4 @@
[[Subject .Subject "Welcome to Grafana"]]
[[HiddenSubject .Subject "Welcome to Grafana"]]
Hi [[.Name]],

View File

@ -33,4 +33,9 @@ func setDefaultTemplateData(cfg *setting.Cfg, data map[string]interface{}, u *us
if u != nil {
data["Name"] = u.NameOrFallback()
}
dataCopy := map[string]interface{}{}
for k, v := range data {
dataCopy[k] = v
}
data["TemplateData"] = dataCopy
}

View File

@ -82,26 +82,31 @@ func (ns *NotificationService) buildEmailMessage(cmd *SendEmailCommand) (*Messag
subject := cmd.Subject
if cmd.Subject == "" {
var subjectText interface{}
subjectData := data["Subject"].(map[string]interface{})
subjectText, hasSubject := subjectData["value"]
subjectText, hasSubject := subjectData["executed_template"].(string)
if hasSubject {
// first check to see if the template has already been executed in a template func
subject = subjectText
} else {
subjectTemplate, hasSubject := subjectData["value"]
if !hasSubject {
return nil, fmt.Errorf("missing subject in template %s", cmd.Template)
if !hasSubject {
return nil, fmt.Errorf("missing subject in template %s", cmd.Template)
}
subjectTmpl, err := template.New("subject").Parse(subjectTemplate.(string))
if err != nil {
return nil, err
}
var subjectBuffer bytes.Buffer
err = subjectTmpl.ExecuteTemplate(&subjectBuffer, "subject", data)
if err != nil {
return nil, err
}
subject = subjectBuffer.String()
}
subjectTmpl, err := template.New("subject").Parse(subjectText.(string))
if err != nil {
return nil, err
}
var subjectBuffer bytes.Buffer
err = subjectTmpl.ExecuteTemplate(&subjectBuffer, "subject", data)
if err != nil {
return nil, err
}
subject = subjectBuffer.String()
}
addr := mail.Address{Name: ns.Cfg.Smtp.FromName, Address: ns.Cfg.Smtp.FromAddress}

View File

@ -1,6 +1,7 @@
package notifications
import (
"bytes"
"context"
"errors"
"fmt"
@ -53,7 +54,8 @@ func ProvideService(bus bus.Bus, cfg *setting.Cfg, mailer Mailer, store TempUser
mailTemplates = template.New("name")
mailTemplates.Funcs(template.FuncMap{
"Subject": subjectTemplateFunc,
"Subject": subjectTemplateFunc,
"HiddenSubject": hiddenSubjectTemplateFunc,
})
mailTemplates.Funcs(sprig.FuncMap())
@ -143,11 +145,35 @@ func (ns *NotificationService) SendWebhookSync(ctx context.Context, cmd *SendWeb
})
}
func subjectTemplateFunc(obj map[string]interface{}, value string) string {
// hiddenSubjectTemplateFunc sets the subject template (value) on the map represented by `.Subject.` (obj) so that it can be compiled and executed later.
// It returns a blank string, so there will be no resulting value left in place of the template.
func hiddenSubjectTemplateFunc(obj map[string]interface{}, value string) string {
obj["value"] = value
return ""
}
// subjectTemplateFunc does the same thing has hiddenSubjectTemplateFunc, but in addition it executes and returns the subject template using the data represented in `.TemplateData` (data)
// This results in the template being replaced by the subject string.
func subjectTemplateFunc(obj map[string]interface{}, data map[string]interface{}, value string) string {
obj["value"] = value
titleTmpl, err := template.New("title").Parse(value)
if err != nil {
return ""
}
var buf bytes.Buffer
err = titleTmpl.ExecuteTemplate(&buf, "title", data)
if err != nil {
return ""
}
subj := buf.String()
// Since we have already executed the template, save it to subject data so we don't have to do it again later on
obj["executed_template"] = subj
return subj
}
func (ns *NotificationService) SendEmailCommandHandlerSync(ctx context.Context, cmd *SendEmailCommandSync) error {
message, err := ns.buildEmailMessage(&SendEmailCommand{
Data: cmd.Data,

View File

@ -12,19 +12,17 @@ import (
func TestEmailIntegrationTest(t *testing.T) {
t.Run("Given the notifications service", func(t *testing.T) {
t.Skip()
setting.StaticRootPath = "../../../public/"
setting.BuildVersion = "4.0.0"
ns := &NotificationService{}
ns.Bus = newBus(t)
ns.Cfg = setting.NewCfg()
ns.Cfg.Smtp.Enabled = true
ns.Cfg.Smtp.TemplatesPatterns = []string{"emails/*.html", "emails/*.txt"}
ns.Cfg.Smtp.FromAddress = "from@address.com"
ns.Cfg.Smtp.FromName = "Grafana Admin"
ns.Cfg.Smtp.ContentTypes = []string{"text/html", "text/plain"}
cfg := setting.NewCfg()
cfg.Smtp.Enabled = true
cfg.StaticRootPath = "../../../public/"
cfg.Smtp.TemplatesPatterns = []string{"emails/*.html", "emails/*.txt"}
cfg.Smtp.FromAddress = "from@address.com"
cfg.Smtp.FromName = "Grafana Admin"
cfg.Smtp.ContentTypes = []string{"text/html", "text/plain"}
ns, err := ProvideService(newBus(t), cfg, NewFakeMailer(), nil)
require.NoError(t, err)
t.Run("When sending reset email password", func(t *testing.T) {
cmd := &SendEmailCommand{
@ -59,11 +57,19 @@ func TestEmailIntegrationTest(t *testing.T) {
require.NoError(t, err)
sentMsg := <-ns.mailQueue
require.Equal(t, sentMsg.From, "Grafana Admin <from@address.com>")
require.Equal(t, sentMsg.To[0], "asdf@asdf.com")
err = os.WriteFile("../../../tmp/test_email.html", []byte(sentMsg.Body["text/html"]), 0777)
require.Equal(t, "\"Grafana Admin\" <from@address.com>", sentMsg.From)
require.Equal(t, "asdf@asdf.com", sentMsg.To[0])
require.Equal(t, "[CRITICAL] Imaginary timeseries alert", sentMsg.Subject)
require.Contains(t, sentMsg.Body["text/html"], "<title>[CRITICAL] Imaginary timeseries alert</title>")
path, err := os.MkdirTemp("../../..", "tmp")
require.NoError(t, err)
err = os.WriteFile("../../../tmp/test_email.txt", []byte(sentMsg.Body["text/plain"]), 0777)
t.Cleanup(func() {
_ = os.RemoveAll(path)
})
err = os.WriteFile(path+"/test_email.html", []byte(sentMsg.Body["text/html"]), 0777)
require.NoError(t, err)
err = os.WriteFile(path+"/test_email.txt", []byte(sentMsg.Body["text/plain"]), 0777)
require.NoError(t, err)
})
})

View File

@ -1,6 +1,7 @@
<!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">
<head>
<title>{{Subject .Subject .TemplateData "{{.Title}}"}}</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width" />
@ -204,7 +205,6 @@ text-decoration: underline;
</tr>
<tr style="vertical-align: top; padding: 0;" align="left">
<td class="mini-centered-text" style="color: #343b41; mso-table-lspace: 0pt; mso-table-rspace: 0pt; word-break: break-word; -webkit-hyphens: auto; -moz-hyphens: auto; hyphens: auto; border-collapse: collapse !important; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; margin: 0; padding: 25px 35px; font: 400 16px/27px 'Helvetica Neue', Helvetica, Arial, sans-serif;" align="center" valign="top">
{{Subject .Subject "{{.Title}}"}}
<table class="row" style="border-spacing: 0; border-collapse: collapse; vertical-align: top; text-align: left; width: 100%; position: relative; display: block; padding: 0px;">
<tr style="vertical-align: top; padding: 0;" align="left">

View File

@ -1,4 +1,4 @@
{{Subject .Subject "{{.Title}}"}}
{{HiddenSubject .Subject "{{.Title}}"}}
{{.Title}}
----------------

View File

@ -3,7 +3,7 @@
<head>
<title>
{{ Subject .Subject "{{ .InvitedBy }} has added you to the {{ .OrgName }} organization" }}
{{ Subject .Subject .TemplateData "{{ .InvitedBy }} has added you to the {{ .OrgName }} organization" }}
</title>
<!--[if !mso]><!-->
<meta http-equiv="X-UA-Compatible" content="IE=edge">

View File

@ -1,4 +1,4 @@
{{Subject .Subject "{{.InvitedBy}} has added you to the {{.OrgName}} organization"}}
{{HiddenSubject .Subject "{{.InvitedBy}} has added you to the {{.OrgName}} organization"}}
You have been added to {{.OrgName}}

View File

@ -3,7 +3,7 @@
<head>
<title>
{{ Subject .Subject "{{ .InvitedBy }} has invited you to join Grafana" }}
{{ Subject .Subject .TemplateData "{{ .InvitedBy }} has invited you to join Grafana" }}
</title>
<!--[if !mso]><!-->
<meta http-equiv="X-UA-Compatible" content="IE=edge">

View File

@ -1,4 +1,4 @@
{{Subject .Subject "{{.InvitedBy}} has invited you to join Grafana"}}
{{HiddenSubject .Subject "{{.InvitedBy}} has invited you to join Grafana"}}
You're invited to join {{.OrgName}}

View File

@ -3,7 +3,7 @@
<head>
<title>
{{ Subject .Subject "{{ .Title }}" }}
{{ Subject .Subject .TemplateData "{{ .Title }}" }}
</title>
<!--[if !mso]><!-->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
@ -238,8 +238,7 @@
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:1.5;text-align:left;color:#FFFFFF;">
<mj-raw>
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:1.5;text-align:left;color:#FFFFFF;"><mj-raw>
{{ range $line := (splitList "\n" .Message) }}
</mj-raw>
{{ $line }}<br>
@ -469,8 +468,7 @@
</tr>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:1.5;text-align:left;color:#FFFFFF;">
<mj-raw>
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:1.5;text-align:left;color:#FFFFFF;"><mj-raw>
{{ range $line := (splitList "\n" .Annotations.description) }}
</mj-raw>
{{ $line }}<br>
@ -532,8 +530,7 @@
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:1.5;text-align:left;color:#FFFFFF;">
<mj-raw>
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:1.5;text-align:left;color:#FFFFFF;"><mj-raw>
{{ range $refID, $value := .Values }}
</mj-raw>
{{ $refID }}={{ $value }}&nbsp; <mj-raw>
@ -978,8 +975,7 @@
</tr>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:1.5;text-align:left;color:#FFFFFF;">
<mj-raw>
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:1.5;text-align:left;color:#FFFFFF;"><mj-raw>
{{ range $line := (splitList "\n" .Annotations.description) }}
</mj-raw>
{{ $line }}<br>
@ -1041,8 +1037,7 @@
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:1.5;text-align:left;color:#FFFFFF;">
<mj-raw>
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:1.5;text-align:left;color:#FFFFFF;"><mj-raw>
{{ range $refID, $value := .Values }}
</mj-raw>
{{ $refID }}={{ $value }}&nbsp; <mj-raw>

View File

@ -1,4 +1,4 @@
{{Subject .Subject "{{.Title}}"}}
{{HiddenSubject .Subject "{{.Title}}"}}
{{.Title}}
----------------

View File

@ -3,7 +3,7 @@
<head>
<title>
{{ Subject .Subject "Reset your Grafana password - {{.Name}}" }}
{{ Subject .Subject .TemplateData "Reset your Grafana password - {{.Name}}" }}
</title>
<!--[if !mso]><!-->
<meta http-equiv="X-UA-Compatible" content="IE=edge">

View File

@ -1,4 +1,4 @@
{{Subject .Subject "Reset your Grafana password - {{.Name}}"}}
{{HiddenSubject .Subject "Reset your Grafana password - {{.Name}}"}}
Hi {{.Name}},

View File

@ -3,7 +3,7 @@
<head>
<title>
{{ Subject .Subject "Welcome to Grafana, please complete your sign up!" }}
{{ Subject .Subject .TemplateData "Welcome to Grafana, please complete your sign up!" }}
</title>
<!--[if !mso]><!-->
<meta http-equiv="X-UA-Compatible" content="IE=edge">

View File

@ -1,4 +1,4 @@
{{Subject .Subject "Welcome to Grafana, please complete your sign up!"}}
{{HiddenSubject .Subject "Welcome to Grafana, please complete your sign up!"}}
Complete the signup

View File

@ -3,7 +3,7 @@
<head>
<title>
{{ Subject .Subject "Welcome to Grafana" }}
{{ Subject .Subject .TemplateData "Welcome to Grafana" }}
</title>
<!--[if !mso]><!-->
<meta http-equiv="X-UA-Compatible" content="IE=edge">

View File

@ -1,4 +1,4 @@
{{Subject .Subject "Welcome to Grafana"}}
{{HiddenSubject .Subject "Welcome to Grafana"}}
Hi {{.Name}},