diff --git a/conf/defaults.ini b/conf/defaults.ini index 1c5ffce9621..6a837319248 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -595,7 +595,8 @@ startTLS_policy = [emails] welcome_email_on_sign_up = false -templates_pattern = emails/*.html +templates_pattern = emails/*.html, emails/*.txt +content_types = text/html #################################### Logging ########################## [log] diff --git a/conf/sample.ini b/conf/sample.ini index 02dea2e49e0..0cb73f926f8 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -578,7 +578,8 @@ [emails] ;welcome_email_on_sign_up = false -;templates_pattern = emails/*.html +;templates_pattern = emails/*.html, emails/*.txt +;content_types = text/html #################################### Logging ########################## [log] diff --git a/docs/sources/administration/configuration.md b/docs/sources/administration/configuration.md index f7fdcd4fcbf..c2548cdbd4f 100644 --- a/docs/sources/administration/configuration.md +++ b/docs/sources/administration/configuration.md @@ -909,7 +909,11 @@ Default is `false`. ### templates_pattern -Default is `emails/*.html`. +Enter a comma separated list of template patterns. Default is `emails/*.html, emails/*.txt`. + +### content_types + +Enter a comma-separated list of content types that should be included in the emails that are sent. List the content types according descending preference, e.g. `text/html, text/plain` for HTML as the most preferred. The order of the parts is significant as the mail clients will use the content type that is supported and most preferred by the sender. Supported content types are `text/html` and `text/plain`. Default is `text/html`.
diff --git a/docs/sources/http_api/admin.md b/docs/sources/http_api/admin.md index 76e27002ba7..cfd34b27e73 100644 --- a/docs/sources/http_api/admin.md +++ b/docs/sources/http_api/admin.md @@ -97,8 +97,9 @@ Content-Type: application/json "user":"root" }, "emails":{ - "templates_pattern":"emails/*.html", - "welcome_email_on_sign_up":"false" + "templates_pattern":"emails/*.html, emails/*.txt", + "welcome_email_on_sign_up":"false", + "content_types":"text/html" }, "log":{ "buffer_len":"10000", diff --git a/emails/README.md b/emails/README.md index 562d5241bc7..4e46c280637 100644 --- a/emails/README.md +++ b/emails/README.md @@ -6,10 +6,9 @@ ## Tasks - npm run build (default task will build new inlines email templates) -- npm start (will build on source html or css change) +- npm start (builds on source HTML, text, or CSS change) ## Result Assembled email templates will be in `dist/` and final inlined templates will be in `../public/emails/` - diff --git a/emails/grunt/assemble.js b/emails/grunt/assemble.js index 40686af05f0..6ef46860267 100644 --- a/emails/grunt/assemble.js +++ b/emails/grunt/assemble.js @@ -2,15 +2,25 @@ module.exports = function () { 'use strict'; return { options: { - layout: 'templates/layouts/default.html', partials: ['templates/partials/*.hbs'], helpers: ['templates/helpers/**/*.js'], data: [], flatten: true, }, - pages: { + html: { + options: { + layout: 'templates/layouts/default.html', + }, src: ['templates/*.html'], dest: 'dist/', }, + txt: { + options: { + layout: 'templates/layouts/default.txt', + ext: '.txt', + }, + src: ['templates/*.txt'], + dest: 'dist/', + }, }; }; diff --git a/emails/grunt/premailer.js b/emails/grunt/premailer.js index 34671a76a13..f587095c256 100644 --- a/emails/grunt/premailer.js +++ b/emails/grunt/premailer.js @@ -1,5 +1,5 @@ module.exports = { - main: { + html: { options: { verbose: true, removeComments: true, @@ -13,4 +13,19 @@ module.exports = { }, ], }, + txt: { + options: { + verbose: true, + mode: 'txt', + lineLength: 90, + }, + files: [ + { + expand: true, // Enable dynamic expansion. + cwd: 'dist', // Src matches are relative to this path. + src: ['*.txt'], // Actual patterns to match. + dest: '../public/emails/', // Destination path prefix. + }, + ], + }, }; diff --git a/emails/grunt/replace.js b/emails/grunt/replace.js index dff1639794f..0d8c030d2f3 100644 --- a/emails/grunt/replace.js +++ b/emails/grunt/replace.js @@ -1,7 +1,7 @@ module.exports = { dist: { overwrite: true, - src: ['dist/*.html'], + src: ['dist/*.html', 'dist/*.txt'], replacements: [ { from: '[[', diff --git a/emails/grunt/watch.js b/emails/grunt/watch.js index 8596d0b5b5a..b071320b3e6 100644 --- a/emails/grunt/watch.js +++ b/emails/grunt/watch.js @@ -4,6 +4,7 @@ module.exports = { //what are the files that we want to watch 'assets/css/*.css', 'templates/**/*.html', + 'templates/**/*.txt', 'grunt/*.js', ], tasks: ['default'], diff --git a/emails/templates/alert_notification.txt b/emails/templates/alert_notification.txt new file mode 100644 index 00000000000..92b8d91386e --- /dev/null +++ b/emails/templates/alert_notification.txt @@ -0,0 +1,26 @@ +[[Subject .Subject "[[.Title]]"]] + +[[.Title]] +---------------- + +[[.Message]] + +[[if ne .Error "" ]] +Error message: +[[.Error]] +[[end]] + +[[if ne .State "ok" ]] +[[range .EvalMatches]] +Metric name: +[[.Metric]] +Value: +[[.Value]] +[[end]] +[[end]] + +View your Alert rule: +[[.RuleUrl]]" + +Go to the Alerts page: +[[.AlertPageUrl]] diff --git a/emails/templates/invited_to_org.txt b/emails/templates/invited_to_org.txt new file mode 100644 index 00000000000..322119aa942 --- /dev/null +++ b/emails/templates/invited_to_org.txt @@ -0,0 +1,9 @@ +[[Subject .Subject "[[.InvitedBy]] has added you to the [[.OrgName]] organization"]] + +You have been added to [[.OrgName]] + +[[.InvitedBy]] has added you to the [[.OrgName]] organization in Grafana. +Once logged in, [[.OrgName]] will be available in the left side menu, in the dropdown below your username. + +Log in now: +[[.AppUrl]] \ No newline at end of file diff --git a/emails/templates/layouts/default.txt b/emails/templates/layouts/default.txt new file mode 100644 index 00000000000..1a323c772d3 --- /dev/null +++ b/emails/templates/layouts/default.txt @@ -0,0 +1,3 @@ +{{> body }} + +Sent by Grafana v[[.BuildVersion]] (c) 2021 Grafana Labs \ No newline at end of file diff --git a/emails/templates/new_user_invite.html b/emails/templates/new_user_invite.html index 6e75a203953..cafbf4a4af8 100644 --- a/emails/templates/new_user_invite.html +++ b/emails/templates/new_user_invite.html @@ -39,7 +39,7 @@ -

You can also copy/paste this link into your browser directly: [[.LinkUrl]]

+

You can also copy and paste this link into your browser directly: [[.LinkUrl]]

diff --git a/emails/templates/new_user_invite.txt b/emails/templates/new_user_invite.txt new file mode 100644 index 00000000000..bfb0cb15cee --- /dev/null +++ b/emails/templates/new_user_invite.txt @@ -0,0 +1,7 @@ +[[Subject .Subject "[[.InvitedBy]] has invited you to join Grafana"]] + +You're invited to join [[.OrgName]] + +You've been invited to join the [[.OrgName]] organization by [[.InvitedBy]]. To accept your invitation and join the team, copy and paste the link below into your browser directly: + +[[.LinkUrl]] \ No newline at end of file diff --git a/emails/templates/ng_alert_notification.txt b/emails/templates/ng_alert_notification.txt new file mode 100644 index 00000000000..b387748b3f7 --- /dev/null +++ b/emails/templates/ng_alert_notification.txt @@ -0,0 +1,38 @@ +[[Subject .Subject "[[.Title]]"]] + +[[.Title]] +---------------- + +[[ .Alerts | len ]] alert[[ if gt (len .Alerts) 1 ]]s[[ end ]] for +[[ range .GroupLabels.SortedPairs ]] +[[ .Name ]] = [[ .Value ]] +[[ end ]] +[[ if gt (len .Alerts.Firing) 0 ]]([[ .Alerts.Firing | len ]]) Firing[[ end ]] +[[ range .Alerts.Firing ]] +Labels: +[[ range .Labels.SortedPairs ]] +[[ .Name ]] = [[ .Value ]] +[[ end ]] +[[ if gt (len .Annotations) 0 ]] +Annotations: +[[ end ]] +[[ range .Annotations.SortedPairs ]] +[[ .Name ]] = [[ .Value ]] +[[ end ]] +[[ end ]][[ if gt (len .Alerts.Resolved) 0 ]]([[ .Alerts.Resolved | len ]]) Resolved[[ end ]] +[[ range .Alerts.Resolved ]] +Labels: +[[ range .Labels.SortedPairs ]] +[[ .Name ]] = [[ .Value ]] +[[ end ]] +[[ if gt (len .Annotations) 0 ]] +Annotations: +[[ end ]] +[[ range .Annotations.SortedPairs ]] +[[ .Name ]] = [[ .Value ]] +[[ end ]] +[[ end ]]View your Alert rule: +[[.RuleUrl]] + +Go to the Alerts page: +[[.AlertPageUrl]] diff --git a/emails/templates/reset_password.txt b/emails/templates/reset_password.txt new file mode 100644 index 00000000000..a983a44c825 --- /dev/null +++ b/emails/templates/reset_password.txt @@ -0,0 +1,6 @@ +[[Subject .Subject "Reset your Grafana password - [[.Name]]"]] + +Hi [[.Name]], + +Copy and paste the following link directly in your browser to reset your password within [[.EmailCodeValidHours]] hours. +[[.AppUrl]]user/password/reset?code=[[.Code]] diff --git a/emails/templates/signup_started.txt b/emails/templates/signup_started.txt new file mode 100644 index 00000000000..12aa0d87fd7 --- /dev/null +++ b/emails/templates/signup_started.txt @@ -0,0 +1,9 @@ +[[Subject .Subject "Welcome to Grafana, please complete your sign up!"]] + +Complete the signup + +Copy and paste the email verification code: +[[.Code]] +in the sign up form or use the link below. + +[[.SignUpUrl]] diff --git a/emails/templates/welcome_on_signup.html b/emails/templates/welcome_on_signup.html index c75b68f6021..7e2e004342a 100644 --- a/emails/templates/welcome_on_signup.html +++ b/emails/templates/welcome_on_signup.html @@ -29,7 +29,7 @@

- If you are new to Grafana please read the Getting Started guide. + If you are new to Grafana, refer to the Getting started with Grafana guide.

diff --git a/emails/templates/welcome_on_signup.txt b/emails/templates/welcome_on_signup.txt new file mode 100644 index 00000000000..6df3c15d205 --- /dev/null +++ b/emails/templates/welcome_on_signup.txt @@ -0,0 +1,11 @@ +[[Subject .Subject "Welcome to Grafana"]] + +Hi [[.Name]], + +Welcome! Ready to start building some beautiful metric and analytic dashboards? + +If you are new to Grafana, refer to the Getting started with Grafana guide on https://grafana.com/docs/grafana/latest/getting-started/getting-started/. + +Thank you for joining our community. + +The Grafana team diff --git a/pkg/api/org_invite.go b/pkg/api/org_invite.go index b10ba84ee04..d6d0a9099e3 100644 --- a/pkg/api/org_invite.go +++ b/pkg/api/org_invite.go @@ -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, diff --git a/pkg/models/notifications.go b/pkg/models/notifications.go index 4ac0a473823..41dafa15614 100644 --- a/pkg/models/notifications.go +++ b/pkg/models/notifications.go @@ -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 } diff --git a/pkg/services/alerting/notifiers/email.go b/pkg/services/alerting/notifiers/email.go index bc5444fd739..c7f1149f605 100644 --- a/pkg/services/alerting/notifiers/email.go +++ b/pkg/services/alerting/notifiers/email.go @@ -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{}, }, } diff --git a/pkg/services/ngalert/notifier/channels/email.go b/pkg/services/ngalert/notifier/channels/email.go index c37ed29c857..5deb62ea829 100644 --- a/pkg/services/ngalert/notifier/channels/email.go +++ b/pkg/services/ngalert/notifier/channels/email.go @@ -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", }, } diff --git a/pkg/services/ngalert/notifier/channels/email_test.go b/pkg/services/ngalert/notifier/channels/email_test.go index 907a52283e9..b003335c721 100644 --- a/pkg/services/ngalert/notifier/channels/email_test.go +++ b/pkg/services/ngalert/notifier/channels/email_test.go @@ -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)", diff --git a/pkg/services/notifications/email.go b/pkg/services/notifications/email.go index 5b2379bef3f..08cdd6b445a 100644 --- a/pkg/services/notifications/email.go +++ b/pkg/services/notifications/email.go @@ -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 diff --git a/pkg/services/notifications/mailer.go b/pkg/services/notifications/mailer.go index 949f5ec2760..070230f0718 100644 --- a/pkg/services/notifications/mailer.go +++ b/pkg/services/notifications/mailer.go @@ -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) + } +} diff --git a/pkg/services/notifications/mailer_test.go b/pkg/services/notifications/mailer_test.go new file mode 100644 index 00000000000..263ec75abb6 --- /dev/null +++ b/pkg/services/notifications/mailer_test.go @@ -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")) + }) +} diff --git a/pkg/services/notifications/notifications.go b/pkg/services/notifications/notifications.go index 797424c5e4d..a62e381e307 100644 --- a/pkg/services/notifications/notifications.go +++ b/pkg/services/notifications/notifications.go @@ -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) { diff --git a/pkg/services/notifications/notifications_test.go b/pkg/services/notifications/notifications_test.go index 5f8744b6ec6..566dbee2017 100644 --- a/pkg/services/notifications/notifications_test.go +++ b/pkg/services/notifications/notifications_test.go @@ -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") }) } diff --git a/pkg/services/notifications/send_email_integration_test.go b/pkg/services/notifications/send_email_integration_test.go index 8331503cd69..cf819c7427f 100644 --- a/pkg/services/notifications/send_email_integration_test.go +++ b/pkg/services/notifications/send_email_integration_test.go @@ -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 ") 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) }) }) diff --git a/pkg/setting/setting_smtp.go b/pkg/setting/setting_smtp.go index 2e56b12e419..a2a9a11d27a 100644 --- a/pkg/setting/setting_smtp.go +++ b/pkg/setting/setting_smtp.go @@ -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")) } diff --git a/pkg/tests/api/alerting/api_notification_channel_test.go b/pkg/tests/api/alerting/api_notification_channel_test.go index 438f1d3eb01..c7d9e59dc35 100644 --- a/pkg/tests/api/alerting/api_notification_channel_test.go +++ b/pkg/tests/api/alerting/api_notification_channel_test.go @@ -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 ", diff --git a/public/emails/alert_notification.txt b/public/emails/alert_notification.txt new file mode 100644 index 00000000000..f595ce7196f --- /dev/null +++ b/public/emails/alert_notification.txt @@ -0,0 +1,28 @@ +{{Subject .Subject "{{.Title}}"}} + +{{.Title}} +---------------- + +{{.Message}} + +{{if ne .Error "" }} +Error message: +{{.Error}} +{{end}} + +{{if ne .State "ok" }} +{{range .EvalMatches}} +Metric name: +{{.Metric}} +Value: +{{.Value}} +{{end}} +{{end}} + +View your Alert rule: +{{.RuleUrl}}" + +Go to the Alerts page: +{{.AlertPageUrl}} + +Sent by Grafana v{{.BuildVersion}} (c) 2021 Grafana Labs diff --git a/public/emails/invited_to_org.txt b/public/emails/invited_to_org.txt new file mode 100644 index 00000000000..f8d5119dd91 --- /dev/null +++ b/public/emails/invited_to_org.txt @@ -0,0 +1,12 @@ +{{Subject .Subject "{{.InvitedBy}} has added you to the {{.OrgName}} organization"}} + +You have been added to {{.OrgName}} + +{{.InvitedBy}} has added you to the {{.OrgName}} organization in Grafana. +Once logged in, {{.OrgName}} will be available in the left side menu, in the dropdown +below your username. + +Log in now: +{{.AppUrl}} + +Sent by Grafana v{{.BuildVersion}} (c) 2021 Grafana Labs diff --git a/public/emails/new_user_invite.html b/public/emails/new_user_invite.html index 9679738e6fa..89f465a8446 100644 --- a/public/emails/new_user_invite.html +++ b/public/emails/new_user_invite.html @@ -245,7 +245,7 @@ text-decoration: underline; -

You can also copy/paste this link into your browser directly: {{.LinkUrl}}

+

You can also copy and paste this link into your browser directly: {{.LinkUrl}}

diff --git a/public/emails/new_user_invite.txt b/public/emails/new_user_invite.txt new file mode 100644 index 00000000000..dbf4f9801ce --- /dev/null +++ b/public/emails/new_user_invite.txt @@ -0,0 +1,11 @@ +{{Subject .Subject "{{.InvitedBy}} has invited you to join Grafana"}} + +You're invited to join {{.OrgName}} + +You've been invited to join the {{.OrgName}} organization by {{.InvitedBy}}. To accept +your invitation and join the team, copy and paste the link below into your browser +directly: + +{{.LinkUrl}} + +Sent by Grafana v{{.BuildVersion}} (c) 2021 Grafana Labs diff --git a/public/emails/ng_alert_notification.txt b/public/emails/ng_alert_notification.txt new file mode 100644 index 00000000000..12ae1fbe6b8 --- /dev/null +++ b/public/emails/ng_alert_notification.txt @@ -0,0 +1,41 @@ +{{Subject .Subject "{{.Title}}"}} + +{{.Title}} +---------------- + +{{ .Alerts | len }} alert{{ if gt (len .Alerts) 1 }}s{{ end }} for +{{ range .GroupLabels.SortedPairs }} +{{ .Name }} = {{ .Value }} +{{ end }} +{{ if gt (len .Alerts.Firing) 0 }}({{ .Alerts.Firing | len }}) Firing{{ end }} +{{ range .Alerts.Firing }} +Labels: +{{ range .Labels.SortedPairs }} +{{ .Name }} = {{ .Value }} +{{ end }} +{{ if gt (len .Annotations) 0 }} +Annotations: +{{ end }} +{{ range .Annotations.SortedPairs }} +{{ .Name }} = {{ .Value }} +{{ end }} +{{ end }}{{ if gt (len .Alerts.Resolved) 0 }}({{ .Alerts.Resolved | len }}) Resolved{{ end +}} +{{ range .Alerts.Resolved }} +Labels: +{{ range .Labels.SortedPairs }} +{{ .Name }} = {{ .Value }} +{{ end }} +{{ if gt (len .Annotations) 0 }} +Annotations: +{{ end }} +{{ range .Annotations.SortedPairs }} +{{ .Name }} = {{ .Value }} +{{ end }} +{{ end }}View your Alert rule: +{{.RuleUrl}} + +Go to the Alerts page: +{{.AlertPageUrl}} + +Sent by Grafana v{{.BuildVersion}} (c) 2021 Grafana Labs diff --git a/public/emails/reset_password.txt b/public/emails/reset_password.txt new file mode 100644 index 00000000000..857b9989a6e --- /dev/null +++ b/public/emails/reset_password.txt @@ -0,0 +1,9 @@ +{{Subject .Subject "Reset your Grafana password - {{.Name}}"}} + +Hi {{.Name}}, + +Copy and paste the following link directly in your browser to reset your password within +{{.EmailCodeValidHours}} hours. +{{.AppUrl}}user/password/reset?code={{.Code}} + +Sent by Grafana v{{.BuildVersion}} (c) 2021 Grafana Labs diff --git a/public/emails/signup_started.txt b/public/emails/signup_started.txt new file mode 100644 index 00000000000..63100473eae --- /dev/null +++ b/public/emails/signup_started.txt @@ -0,0 +1,11 @@ +{{Subject .Subject "Welcome to Grafana, please complete your sign up!"}} + +Complete the signup + +Copy and paste the email verification code: +{{.Code}} +in the sign up form or use the link below. + +{{.SignUpUrl}} + +Sent by Grafana v{{.BuildVersion}} (c) 2021 Grafana Labs diff --git a/public/emails/welcome_on_signup.html b/public/emails/welcome_on_signup.html index e874cc8dd30..921c96faae3 100644 --- a/public/emails/welcome_on_signup.html +++ b/public/emails/welcome_on_signup.html @@ -235,7 +235,7 @@ text-decoration: underline;

- If you are new to Grafana please read the Getting Started guide. + If you are new to Grafana, refer to the Getting started with Grafana guide.

diff --git a/public/emails/welcome_on_signup.txt b/public/emails/welcome_on_signup.txt new file mode 100644 index 00000000000..bb05c04f9b5 --- /dev/null +++ b/public/emails/welcome_on_signup.txt @@ -0,0 +1,14 @@ +{{Subject .Subject "Welcome to Grafana"}} + +Hi {{.Name}}, + +Welcome! Ready to start building some beautiful metric and analytic dashboards? + +If you are new to Grafana, refer to the Getting started with Grafana guide on +https://grafana.com/docs/grafana/latest/getting-started/getting-started/. + +Thank you for joining our community. + +The Grafana team + +Sent by Grafana v{{.BuildVersion}} (c) 2021 Grafana Labs