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
co-authored by Gilles De Mey
parent a92c081a33
commit 8dab3bf36c
30 changed files with 107 additions and 70 deletions
+5
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
}
+22 -17
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}
+28 -2
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,
@@ -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)
})
})