Email: Allow configuration of content types for email notifications (#34530)
* Alerting: Allow configuration of content types for email notifications * Fix lint error * Improves email templates * Improve configuration documentation Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com> * Improve code comments Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com> * Improve configuration documentation Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com> * Improve email template * Remove unnecessary predeclaration Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com> * Adds handling for unrecognized content type Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com> * Move utility function outside of util package * Fixes syntax * Remove unused package * Fix lint error * improve email templates * Fix test * Alerting: Allow configuration of content types for email notifications * Fix lint error * Improves email templates * Improve configuration documentation Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com> * Improve code comments Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com> * Improve configuration documentation Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com> * Improve email template * Remove unnecessary predeclaration Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com> * Adds handling for unrecognized content type Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com> * Move utility function outside of util package * Fixes syntax * Remove unused package * Fix lint error * improve email templates * Fix test * Fix comment style Co-authored-by: Ganesh Vernekar <15064823+codesome@users.noreply.github.com> * Fix template formatting * Add test and improve error handling * Fix test * Fix formatting * Fix formatting * Improve documentation and regenerates txt template * Update docs/sources/administration/configuration.md Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> Co-authored-by: Djairho Geuens <djairho.geuens@ae.be> Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com> Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com> Co-authored-by: Ganesh Vernekar <15064823+codesome@users.noreply.github.com> Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>
This commit is contained in:
co-authored by
Diana Payton
Arve Knudsen
Ganesh Vernekar
achatterjee-grafana
Djairho Geuens
parent
cec12676e7
commit
4cadbba686
@@ -69,7 +69,7 @@ func AddOrgInvite(c *models.ReqContext, inviteDto dtos.AddInviteForm) response.R
|
||||
if inviteDto.SendEmail && util.IsEmail(inviteDto.LoginOrEmail) {
|
||||
emailCmd := models.SendEmailCommand{
|
||||
To: []string{inviteDto.LoginOrEmail},
|
||||
Template: "new_user_invite.html",
|
||||
Template: "new_user_invite",
|
||||
Data: map[string]interface{}{
|
||||
"Name": util.StringsFallback2(cmd.Name, cmd.Email),
|
||||
"OrgName": c.OrgName,
|
||||
@@ -111,7 +111,7 @@ func inviteExistingUserToOrg(c *models.ReqContext, user *models.User, inviteDto
|
||||
if inviteDto.SendEmail && util.IsEmail(user.Email) {
|
||||
emailCmd := models.SendEmailCommand{
|
||||
To: []string{user.Email},
|
||||
Template: "invited_to_org.html",
|
||||
Template: "invited_to_org",
|
||||
Data: map[string]interface{}{
|
||||
"Name": user.NameOrFallback(),
|
||||
"OrgName": c.OrgName,
|
||||
|
||||
@@ -11,7 +11,7 @@ type SendEmailAttachFile struct {
|
||||
Content []byte
|
||||
}
|
||||
|
||||
// SendEmailCommand is command for sending emails
|
||||
// SendEmailCommand is the command for sending emails
|
||||
type SendEmailCommand struct {
|
||||
To []string
|
||||
SingleEmail bool
|
||||
@@ -24,7 +24,7 @@ type SendEmailCommand struct {
|
||||
AttachedFiles []*SendEmailAttachFile
|
||||
}
|
||||
|
||||
// SendEmailCommandSync is command for sending emails in sync
|
||||
// SendEmailCommandSync is the command for sending emails synchronously
|
||||
type SendEmailCommandSync struct {
|
||||
SendEmailCommand
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ func (en *EmailNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
},
|
||||
To: en.Addresses,
|
||||
SingleEmail: en.SingleEmail,
|
||||
Template: "alert_notification.html",
|
||||
Template: "alert_notification",
|
||||
EmbeddedFiles: []string{},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ func (en *EmailNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool,
|
||||
},
|
||||
To: en.Addresses,
|
||||
SingleEmail: en.SingleEmail,
|
||||
Template: "ng_alert_notification.html",
|
||||
Template: "ng_alert_notification",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@ func TestEmailNotifier(t *testing.T) {
|
||||
"subject": "[FIRING:1] (AlwaysFiring warning)",
|
||||
"to": []string{"someops@example.com", "somedev@example.com"},
|
||||
"single_email": false,
|
||||
"template": "ng_alert_notification.html",
|
||||
"template": "ng_alert_notification",
|
||||
"data": map[string]interface{}{
|
||||
"Title": "[FIRING:1] (AlwaysFiring warning)",
|
||||
"Message": "[FIRING:1] (AlwaysFiring warning)",
|
||||
|
||||
@@ -17,7 +17,7 @@ type Message struct {
|
||||
SingleEmail bool
|
||||
From string
|
||||
Subject string
|
||||
Body string
|
||||
Body map[string]string
|
||||
Info string
|
||||
ReplyTo []string
|
||||
EmbeddedFiles []string
|
||||
|
||||
@@ -67,18 +67,7 @@ func (ns *NotificationService) dialAndSend(messages ...*Message) (int, error) {
|
||||
}
|
||||
|
||||
for _, msg := range messages {
|
||||
m := gomail.NewMessage()
|
||||
m.SetHeader("From", msg.From)
|
||||
m.SetHeader("To", msg.To...)
|
||||
m.SetHeader("Subject", msg.Subject)
|
||||
|
||||
ns.setFiles(m, msg)
|
||||
|
||||
for _, replyTo := range msg.ReplyTo {
|
||||
m.SetAddressHeader("Reply-To", replyTo, "")
|
||||
}
|
||||
|
||||
m.SetBody("text/html", msg.Body)
|
||||
m := ns.buildEmail(msg)
|
||||
|
||||
innerError := dialer.DialAndSend(m)
|
||||
emailsSentTotal.Inc()
|
||||
@@ -100,6 +89,28 @@ func (ns *NotificationService) dialAndSend(messages ...*Message) (int, error) {
|
||||
return sentEmailsCount, err
|
||||
}
|
||||
|
||||
func (ns *NotificationService) buildEmail(msg *Message) *gomail.Message {
|
||||
m := gomail.NewMessage()
|
||||
m.SetHeader("From", msg.From)
|
||||
m.SetHeader("To", msg.To...)
|
||||
m.SetHeader("Subject", msg.Subject)
|
||||
ns.setFiles(m, msg)
|
||||
for _, replyTo := range msg.ReplyTo {
|
||||
m.SetAddressHeader("Reply-To", replyTo, "")
|
||||
}
|
||||
// loop over content types from settings in reverse order as they are ordered in according to descending
|
||||
// preference while the alternatives should be ordered according to ascending preference
|
||||
for i := len(ns.Cfg.Smtp.ContentTypes) - 1; i >= 0; i-- {
|
||||
if i == len(ns.Cfg.Smtp.ContentTypes)-1 {
|
||||
m.SetBody(ns.Cfg.Smtp.ContentTypes[i], msg.Body[ns.Cfg.Smtp.ContentTypes[i]])
|
||||
} else {
|
||||
m.AddAlternative(ns.Cfg.Smtp.ContentTypes[i], msg.Body[ns.Cfg.Smtp.ContentTypes[i]])
|
||||
}
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// setFiles attaches files in various forms
|
||||
func (ns *NotificationService) setFiles(
|
||||
m *gomail.Message,
|
||||
@@ -169,18 +180,26 @@ func (ns *NotificationService) buildEmailMessage(cmd *models.SendEmailCommand) (
|
||||
return nil, models.ErrSmtpNotEnabled
|
||||
}
|
||||
|
||||
var buffer bytes.Buffer
|
||||
var err error
|
||||
|
||||
data := cmd.Data
|
||||
if data == nil {
|
||||
data = make(map[string]interface{}, 10)
|
||||
}
|
||||
|
||||
setDefaultTemplateData(data, nil)
|
||||
err = mailTemplates.ExecuteTemplate(&buffer, cmd.Template, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
body := make(map[string]string)
|
||||
for _, contentType := range ns.Cfg.Smtp.ContentTypes {
|
||||
fileExtension, err := getFileExtensionByContentType(contentType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var buffer bytes.Buffer
|
||||
err = mailTemplates.ExecuteTemplate(&buffer, cmd.Template+fileExtension, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
body[contentType] = buffer.String()
|
||||
}
|
||||
|
||||
subject := cmd.Subject
|
||||
@@ -213,7 +232,7 @@ func (ns *NotificationService) buildEmailMessage(cmd *models.SendEmailCommand) (
|
||||
SingleEmail: cmd.SingleEmail,
|
||||
From: addr.String(),
|
||||
Subject: subject,
|
||||
Body: buffer.String(),
|
||||
Body: body,
|
||||
EmbeddedFiles: cmd.EmbeddedFiles,
|
||||
AttachedFiles: buildAttachedFiles(cmd.AttachedFiles),
|
||||
ReplyTo: cmd.ReplyTo,
|
||||
@@ -235,3 +254,14 @@ func buildAttachedFiles(
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func getFileExtensionByContentType(contentType string) (string, error) {
|
||||
switch contentType {
|
||||
case "text/html":
|
||||
return ".html", nil
|
||||
case "text/plain":
|
||||
return ".txt", nil
|
||||
default:
|
||||
return "", fmt.Errorf("unrecognized content type %q", contentType)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package notifications
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestBuildMail(t *testing.T) {
|
||||
ns := &NotificationService{
|
||||
Cfg: setting.NewCfg(),
|
||||
}
|
||||
ns.Cfg.Smtp.ContentTypes = []string{"text/html", "text/plain"}
|
||||
|
||||
message := &Message{
|
||||
To: []string{"to@address.com"},
|
||||
From: "from@address.com",
|
||||
Subject: "Some subject",
|
||||
Body: map[string]string{
|
||||
"text/html": "Some HTML body",
|
||||
"text/plain": "Some plain text body",
|
||||
},
|
||||
ReplyTo: []string{"from@address.com"},
|
||||
}
|
||||
|
||||
t.Run("When building email", func(t *testing.T) {
|
||||
email := ns.buildEmail(message)
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
_, err := email.WriteTo(buf)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Contains(t, buf.String(), "Some HTML body")
|
||||
assert.Contains(t, buf.String(), "Some plain text body")
|
||||
assert.Less(t, strings.Index(buf.String(), "Some plain text body"), strings.Index(buf.String(), "Some HTML body"))
|
||||
})
|
||||
}
|
||||
@@ -19,9 +19,9 @@ import (
|
||||
)
|
||||
|
||||
var mailTemplates *template.Template
|
||||
var tmplResetPassword = "reset_password.html"
|
||||
var tmplSignUpStarted = "signup_started.html"
|
||||
var tmplWelcomeOnSignUp = "welcome_on_signup.html"
|
||||
var tmplResetPassword = "reset_password"
|
||||
var tmplSignUpStarted = "signup_started"
|
||||
var tmplWelcomeOnSignUp = "welcome_on_signup"
|
||||
|
||||
func init() {
|
||||
registry.RegisterService(&NotificationService{})
|
||||
@@ -56,10 +56,12 @@ func (ns *NotificationService) Init() error {
|
||||
"Subject": subjectTemplateFunc,
|
||||
})
|
||||
|
||||
templatePattern := filepath.Join(ns.Cfg.StaticRootPath, ns.Cfg.Smtp.TemplatesPattern)
|
||||
_, err := mailTemplates.ParseGlob(templatePattern)
|
||||
if err != nil {
|
||||
return err
|
||||
for _, pattern := range ns.Cfg.Smtp.TemplatesPatterns {
|
||||
templatePattern := filepath.Join(ns.Cfg.StaticRootPath, pattern)
|
||||
_, err := mailTemplates.ParseGlob(templatePattern)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if !util.IsEmail(ns.Cfg.Smtp.FromAddress) {
|
||||
|
||||
@@ -16,9 +16,10 @@ func TestNotificationService(t *testing.T) {
|
||||
}
|
||||
ns.Cfg.StaticRootPath = "../../../public/"
|
||||
ns.Cfg.Smtp.Enabled = true
|
||||
ns.Cfg.Smtp.TemplatesPattern = "emails/*.html"
|
||||
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"}
|
||||
ns.Bus = bus.New()
|
||||
|
||||
err := ns.Init()
|
||||
@@ -29,8 +30,10 @@ func TestNotificationService(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
sentMsg := <-ns.mailQueue
|
||||
assert.Contains(t, sentMsg.Body, "body")
|
||||
assert.Contains(t, sentMsg.Body["text/html"], "body")
|
||||
assert.NotContains(t, sentMsg.Body["text/plain"], "body")
|
||||
assert.Equal(t, "Reset your Grafana password - asd@asd.com", sentMsg.Subject)
|
||||
assert.NotContains(t, sentMsg.Body, "Subject")
|
||||
assert.NotContains(t, sentMsg.Body["text/html"], "Subject")
|
||||
assert.NotContains(t, sentMsg.Body["text/plain"], "Subject")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -19,9 +19,10 @@ func TestEmailIntegrationTest(t *testing.T) {
|
||||
ns.Bus = bus.New()
|
||||
ns.Cfg = setting.NewCfg()
|
||||
ns.Cfg.Smtp.Enabled = true
|
||||
ns.Cfg.Smtp.TemplatesPattern = "emails/*.html"
|
||||
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"}
|
||||
|
||||
err := ns.Init()
|
||||
So(err, ShouldBeNil)
|
||||
@@ -52,7 +53,7 @@ func TestEmailIntegrationTest(t *testing.T) {
|
||||
},
|
||||
},
|
||||
To: []string{"asdf@asdf.com"},
|
||||
Template: "alert_notification.html",
|
||||
Template: "alert_notification",
|
||||
}
|
||||
|
||||
err := ns.sendEmailCommandHandler(cmd)
|
||||
@@ -61,7 +62,9 @@ func TestEmailIntegrationTest(t *testing.T) {
|
||||
sentMsg := <-ns.mailQueue
|
||||
So(sentMsg.From, ShouldEqual, "Grafana Admin <from@address.com>")
|
||||
So(sentMsg.To[0], ShouldEqual, "asdf@asdf.com")
|
||||
err = ioutil.WriteFile("../../../tmp/test_email.html", []byte(sentMsg.Body), 0777)
|
||||
err = ioutil.WriteFile("../../../tmp/test_email.html", []byte(sentMsg.Body["text/html"]), 0777)
|
||||
So(err, ShouldBeNil)
|
||||
err = ioutil.WriteFile("../../../tmp/test_email.txt", []byte(sentMsg.Body["text/plain"]), 0777)
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package setting
|
||||
|
||||
import "github.com/grafana/grafana/pkg/util"
|
||||
|
||||
type SmtpSettings struct {
|
||||
Enabled bool
|
||||
Host string
|
||||
@@ -14,7 +16,8 @@ type SmtpSettings struct {
|
||||
SkipVerify bool
|
||||
|
||||
SendWelcomeEmailOnSignUp bool
|
||||
TemplatesPattern string
|
||||
TemplatesPatterns []string
|
||||
ContentTypes []string
|
||||
}
|
||||
|
||||
func (cfg *Cfg) readSmtpSettings() {
|
||||
@@ -33,5 +36,6 @@ func (cfg *Cfg) readSmtpSettings() {
|
||||
|
||||
emails := cfg.Raw.Section("emails")
|
||||
cfg.Smtp.SendWelcomeEmailOnSignUp = emails.Key("welcome_email_on_sign_up").MustBool(false)
|
||||
cfg.Smtp.TemplatesPattern = emails.Key("templates_pattern").MustString("emails/*.html")
|
||||
cfg.Smtp.TemplatesPatterns = util.SplitString(emails.Key("templates_pattern").MustString("emails/*.html, emails/*.txt"))
|
||||
cfg.Smtp.ContentTypes = util.SplitString(emails.Key("content_types").MustString("text/html"))
|
||||
}
|
||||
|
||||
@@ -1369,7 +1369,7 @@ var expEmailNotifications = []*models.SendEmailCommandSync{
|
||||
SendEmailCommand: models.SendEmailCommand{
|
||||
To: []string{"test@email.com"},
|
||||
SingleEmail: true,
|
||||
Template: "ng_alert_notification.html",
|
||||
Template: "ng_alert_notification",
|
||||
Subject: "[FIRING:1] EmailAlert ",
|
||||
Data: map[string]interface{}{
|
||||
"Title": "[FIRING:1] EmailAlert ",
|
||||
|
||||
Reference in New Issue
Block a user