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>
pull/37266/head
Djairho Geuens 4 years ago committed by GitHub
parent cec12676e7
commit 4cadbba686
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      conf/defaults.ini
  2. 3
      conf/sample.ini
  3. 6
      docs/sources/administration/configuration.md
  4. 5
      docs/sources/http_api/admin.md
  5. 3
      emails/README.md
  6. 14
      emails/grunt/assemble.js
  7. 17
      emails/grunt/premailer.js
  8. 2
      emails/grunt/replace.js
  9. 1
      emails/grunt/watch.js
  10. 26
      emails/templates/alert_notification.txt
  11. 9
      emails/templates/invited_to_org.txt
  12. 3
      emails/templates/layouts/default.txt
  13. 2
      emails/templates/new_user_invite.html
  14. 7
      emails/templates/new_user_invite.txt
  15. 38
      emails/templates/ng_alert_notification.txt
  16. 6
      emails/templates/reset_password.txt
  17. 9
      emails/templates/signup_started.txt
  18. 2
      emails/templates/welcome_on_signup.html
  19. 11
      emails/templates/welcome_on_signup.txt
  20. 4
      pkg/api/org_invite.go
  21. 4
      pkg/models/notifications.go
  22. 2
      pkg/services/alerting/notifiers/email.go
  23. 2
      pkg/services/ngalert/notifier/channels/email.go
  24. 2
      pkg/services/ngalert/notifier/channels/email_test.go
  25. 2
      pkg/services/notifications/email.go
  26. 64
      pkg/services/notifications/mailer.go
  27. 41
      pkg/services/notifications/mailer_test.go
  28. 10
      pkg/services/notifications/notifications.go
  29. 9
      pkg/services/notifications/notifications_test.go
  30. 9
      pkg/services/notifications/send_email_integration_test.go
  31. 8
      pkg/setting/setting_smtp.go
  32. 2
      pkg/tests/api/alerting/api_notification_channel_test.go
  33. 28
      public/emails/alert_notification.txt
  34. 12
      public/emails/invited_to_org.txt
  35. 2
      public/emails/new_user_invite.html
  36. 11
      public/emails/new_user_invite.txt
  37. 41
      public/emails/ng_alert_notification.txt
  38. 9
      public/emails/reset_password.txt
  39. 11
      public/emails/signup_started.txt
  40. 2
      public/emails/welcome_on_signup.html
  41. 14
      public/emails/welcome_on_signup.txt

@ -595,7 +595,8 @@ startTLS_policy =
[emails] [emails]
welcome_email_on_sign_up = false welcome_email_on_sign_up = false
templates_pattern = emails/*.html templates_pattern = emails/*.html, emails/*.txt
content_types = text/html
#################################### Logging ########################## #################################### Logging ##########################
[log] [log]

@ -578,7 +578,8 @@
[emails] [emails]
;welcome_email_on_sign_up = false ;welcome_email_on_sign_up = false
;templates_pattern = emails/*.html ;templates_pattern = emails/*.html, emails/*.txt
;content_types = text/html
#################################### Logging ########################## #################################### Logging ##########################
[log] [log]

@ -909,7 +909,11 @@ Default is `false`.
### templates_pattern ### 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`.
<hr> <hr>

@ -97,8 +97,9 @@ Content-Type: application/json
"user":"root" "user":"root"
}, },
"emails":{ "emails":{
"templates_pattern":"emails/*.html", "templates_pattern":"emails/*.html, emails/*.txt",
"welcome_email_on_sign_up":"false" "welcome_email_on_sign_up":"false",
"content_types":"text/html"
}, },
"log":{ "log":{
"buffer_len":"10000", "buffer_len":"10000",

@ -6,10 +6,9 @@
## Tasks ## Tasks
- npm run build (default task will build new inlines email templates) - 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 ## Result
Assembled email templates will be in `dist/` and final Assembled email templates will be in `dist/` and final
inlined templates will be in `../public/emails/` inlined templates will be in `../public/emails/`

@ -2,15 +2,25 @@ module.exports = function () {
'use strict'; 'use strict';
return { return {
options: { options: {
layout: 'templates/layouts/default.html',
partials: ['templates/partials/*.hbs'], partials: ['templates/partials/*.hbs'],
helpers: ['templates/helpers/**/*.js'], helpers: ['templates/helpers/**/*.js'],
data: [], data: [],
flatten: true, flatten: true,
}, },
pages: { html: {
options: {
layout: 'templates/layouts/default.html',
},
src: ['templates/*.html'], src: ['templates/*.html'],
dest: 'dist/', dest: 'dist/',
}, },
txt: {
options: {
layout: 'templates/layouts/default.txt',
ext: '.txt',
},
src: ['templates/*.txt'],
dest: 'dist/',
},
}; };
}; };

@ -1,5 +1,5 @@
module.exports = { module.exports = {
main: { html: {
options: { options: {
verbose: true, verbose: true,
removeComments: 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.
},
],
},
}; };

@ -1,7 +1,7 @@
module.exports = { module.exports = {
dist: { dist: {
overwrite: true, overwrite: true,
src: ['dist/*.html'], src: ['dist/*.html', 'dist/*.txt'],
replacements: [ replacements: [
{ {
from: '[[', from: '[[',

@ -4,6 +4,7 @@ module.exports = {
//what are the files that we want to watch //what are the files that we want to watch
'assets/css/*.css', 'assets/css/*.css',
'templates/**/*.html', 'templates/**/*.html',
'templates/**/*.txt',
'grunt/*.js', 'grunt/*.js',
], ],
tasks: ['default'], tasks: ['default'],

@ -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]]

@ -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]]

@ -0,0 +1,3 @@
{{> body }}
Sent by Grafana v[[.BuildVersion]] (c) 2021 Grafana Labs

@ -39,7 +39,7 @@
</tr> </tr>
<tr> <tr>
<td class="center"> <td class="center">
<p>You can also copy/paste this link into your browser directly: <a href="[[.LinkUrl]]">[[.LinkUrl]]</a></p> <p>You can also copy and paste this link into your browser directly: <a href="[[.LinkUrl]]">[[.LinkUrl]]</a></p>
</td> </td>
<td class="expander"></td> <td class="expander"></td>
</tr> </tr>

@ -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]]

@ -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]]

@ -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]]

@ -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]]

@ -29,7 +29,7 @@
<tr> <tr>
<td class="center"> <td class="center">
<p> <p>
If you are new to Grafana please read the <a href="https://grafana.com/docs/grafana/latest/getting-started/getting-started/">Getting Started</a> guide. If you are new to Grafana, refer to the <a href="https://grafana.com/docs/grafana/latest/getting-started/getting-started/">Getting started with Grafana</a> guide.
</p> </p>
</td> </td>
<td class="expander"></td> <td class="expander"></td>

@ -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

@ -69,7 +69,7 @@ func AddOrgInvite(c *models.ReqContext, inviteDto dtos.AddInviteForm) response.R
if inviteDto.SendEmail && util.IsEmail(inviteDto.LoginOrEmail) { if inviteDto.SendEmail && util.IsEmail(inviteDto.LoginOrEmail) {
emailCmd := models.SendEmailCommand{ emailCmd := models.SendEmailCommand{
To: []string{inviteDto.LoginOrEmail}, To: []string{inviteDto.LoginOrEmail},
Template: "new_user_invite.html", Template: "new_user_invite",
Data: map[string]interface{}{ Data: map[string]interface{}{
"Name": util.StringsFallback2(cmd.Name, cmd.Email), "Name": util.StringsFallback2(cmd.Name, cmd.Email),
"OrgName": c.OrgName, "OrgName": c.OrgName,
@ -111,7 +111,7 @@ func inviteExistingUserToOrg(c *models.ReqContext, user *models.User, inviteDto
if inviteDto.SendEmail && util.IsEmail(user.Email) { if inviteDto.SendEmail && util.IsEmail(user.Email) {
emailCmd := models.SendEmailCommand{ emailCmd := models.SendEmailCommand{
To: []string{user.Email}, To: []string{user.Email},
Template: "invited_to_org.html", Template: "invited_to_org",
Data: map[string]interface{}{ Data: map[string]interface{}{
"Name": user.NameOrFallback(), "Name": user.NameOrFallback(),
"OrgName": c.OrgName, "OrgName": c.OrgName,

@ -11,7 +11,7 @@ type SendEmailAttachFile struct {
Content []byte Content []byte
} }
// SendEmailCommand is command for sending emails // SendEmailCommand is the command for sending emails
type SendEmailCommand struct { type SendEmailCommand struct {
To []string To []string
SingleEmail bool SingleEmail bool
@ -24,7 +24,7 @@ type SendEmailCommand struct {
AttachedFiles []*SendEmailAttachFile AttachedFiles []*SendEmailAttachFile
} }
// SendEmailCommandSync is command for sending emails in sync // SendEmailCommandSync is the command for sending emails synchronously
type SendEmailCommandSync struct { type SendEmailCommandSync struct {
SendEmailCommand SendEmailCommand
} }

@ -100,7 +100,7 @@ func (en *EmailNotifier) Notify(evalContext *alerting.EvalContext) error {
}, },
To: en.Addresses, To: en.Addresses,
SingleEmail: en.SingleEmail, SingleEmail: en.SingleEmail,
Template: "alert_notification.html", Template: "alert_notification",
EmbeddedFiles: []string{}, EmbeddedFiles: []string{},
}, },
} }

@ -96,7 +96,7 @@ func (en *EmailNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool,
}, },
To: en.Addresses, To: en.Addresses,
SingleEmail: en.SingleEmail, 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)", "subject": "[FIRING:1] (AlwaysFiring warning)",
"to": []string{"someops@example.com", "somedev@example.com"}, "to": []string{"someops@example.com", "somedev@example.com"},
"single_email": false, "single_email": false,
"template": "ng_alert_notification.html", "template": "ng_alert_notification",
"data": map[string]interface{}{ "data": map[string]interface{}{
"Title": "[FIRING:1] (AlwaysFiring warning)", "Title": "[FIRING:1] (AlwaysFiring warning)",
"Message": "[FIRING:1] (AlwaysFiring warning)", "Message": "[FIRING:1] (AlwaysFiring warning)",

@ -17,7 +17,7 @@ type Message struct {
SingleEmail bool SingleEmail bool
From string From string
Subject string Subject string
Body string Body map[string]string
Info string Info string
ReplyTo []string ReplyTo []string
EmbeddedFiles []string EmbeddedFiles []string

@ -67,18 +67,7 @@ func (ns *NotificationService) dialAndSend(messages ...*Message) (int, error) {
} }
for _, msg := range messages { for _, msg := range messages {
m := gomail.NewMessage() m := ns.buildEmail(msg)
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)
innerError := dialer.DialAndSend(m) innerError := dialer.DialAndSend(m)
emailsSentTotal.Inc() emailsSentTotal.Inc()
@ -100,6 +89,28 @@ func (ns *NotificationService) dialAndSend(messages ...*Message) (int, error) {
return sentEmailsCount, err 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 // setFiles attaches files in various forms
func (ns *NotificationService) setFiles( func (ns *NotificationService) setFiles(
m *gomail.Message, m *gomail.Message,
@ -169,19 +180,27 @@ func (ns *NotificationService) buildEmailMessage(cmd *models.SendEmailCommand) (
return nil, models.ErrSmtpNotEnabled return nil, models.ErrSmtpNotEnabled
} }
var buffer bytes.Buffer
var err error
data := cmd.Data data := cmd.Data
if data == nil { if data == nil {
data = make(map[string]interface{}, 10) data = make(map[string]interface{}, 10)
} }
setDefaultTemplateData(data, nil) setDefaultTemplateData(data, nil)
err = mailTemplates.ExecuteTemplate(&buffer, cmd.Template, data)
body := make(map[string]string)
for _, contentType := range ns.Cfg.Smtp.ContentTypes {
fileExtension, err := getFileExtensionByContentType(contentType)
if err != nil { if err != nil {
return nil, err 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 subject := cmd.Subject
if cmd.Subject == "" { if cmd.Subject == "" {
@ -213,7 +232,7 @@ func (ns *NotificationService) buildEmailMessage(cmd *models.SendEmailCommand) (
SingleEmail: cmd.SingleEmail, SingleEmail: cmd.SingleEmail,
From: addr.String(), From: addr.String(),
Subject: subject, Subject: subject,
Body: buffer.String(), Body: body,
EmbeddedFiles: cmd.EmbeddedFiles, EmbeddedFiles: cmd.EmbeddedFiles,
AttachedFiles: buildAttachedFiles(cmd.AttachedFiles), AttachedFiles: buildAttachedFiles(cmd.AttachedFiles),
ReplyTo: cmd.ReplyTo, ReplyTo: cmd.ReplyTo,
@ -235,3 +254,14 @@ func buildAttachedFiles(
return result 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 mailTemplates *template.Template
var tmplResetPassword = "reset_password.html" var tmplResetPassword = "reset_password"
var tmplSignUpStarted = "signup_started.html" var tmplSignUpStarted = "signup_started"
var tmplWelcomeOnSignUp = "welcome_on_signup.html" var tmplWelcomeOnSignUp = "welcome_on_signup"
func init() { func init() {
registry.RegisterService(&NotificationService{}) registry.RegisterService(&NotificationService{})
@ -56,11 +56,13 @@ func (ns *NotificationService) Init() error {
"Subject": subjectTemplateFunc, "Subject": subjectTemplateFunc,
}) })
templatePattern := filepath.Join(ns.Cfg.StaticRootPath, ns.Cfg.Smtp.TemplatesPattern) for _, pattern := range ns.Cfg.Smtp.TemplatesPatterns {
templatePattern := filepath.Join(ns.Cfg.StaticRootPath, pattern)
_, err := mailTemplates.ParseGlob(templatePattern) _, err := mailTemplates.ParseGlob(templatePattern)
if err != nil { if err != nil {
return err return err
} }
}
if !util.IsEmail(ns.Cfg.Smtp.FromAddress) { if !util.IsEmail(ns.Cfg.Smtp.FromAddress) {
return errors.New("invalid email address for SMTP from_address config") return errors.New("invalid email address for SMTP from_address config")

@ -16,9 +16,10 @@ func TestNotificationService(t *testing.T) {
} }
ns.Cfg.StaticRootPath = "../../../public/" ns.Cfg.StaticRootPath = "../../../public/"
ns.Cfg.Smtp.Enabled = true 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.FromAddress = "from@address.com"
ns.Cfg.Smtp.FromName = "Grafana Admin" ns.Cfg.Smtp.FromName = "Grafana Admin"
ns.Cfg.Smtp.ContentTypes = []string{"text/html", "text/plain"}
ns.Bus = bus.New() ns.Bus = bus.New()
err := ns.Init() err := ns.Init()
@ -29,8 +30,10 @@ func TestNotificationService(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
sentMsg := <-ns.mailQueue 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.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.Bus = bus.New()
ns.Cfg = setting.NewCfg() ns.Cfg = setting.NewCfg()
ns.Cfg.Smtp.Enabled = true 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.FromAddress = "from@address.com"
ns.Cfg.Smtp.FromName = "Grafana Admin" ns.Cfg.Smtp.FromName = "Grafana Admin"
ns.Cfg.Smtp.ContentTypes = []string{"text/html", "text/plain"}
err := ns.Init() err := ns.Init()
So(err, ShouldBeNil) So(err, ShouldBeNil)
@ -52,7 +53,7 @@ func TestEmailIntegrationTest(t *testing.T) {
}, },
}, },
To: []string{"asdf@asdf.com"}, To: []string{"asdf@asdf.com"},
Template: "alert_notification.html", Template: "alert_notification",
} }
err := ns.sendEmailCommandHandler(cmd) err := ns.sendEmailCommandHandler(cmd)
@ -61,7 +62,9 @@ func TestEmailIntegrationTest(t *testing.T) {
sentMsg := <-ns.mailQueue sentMsg := <-ns.mailQueue
So(sentMsg.From, ShouldEqual, "Grafana Admin <from@address.com>") So(sentMsg.From, ShouldEqual, "Grafana Admin <from@address.com>")
So(sentMsg.To[0], ShouldEqual, "asdf@asdf.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) So(err, ShouldBeNil)
}) })
}) })

@ -1,5 +1,7 @@
package setting package setting
import "github.com/grafana/grafana/pkg/util"
type SmtpSettings struct { type SmtpSettings struct {
Enabled bool Enabled bool
Host string Host string
@ -14,7 +16,8 @@ type SmtpSettings struct {
SkipVerify bool SkipVerify bool
SendWelcomeEmailOnSignUp bool SendWelcomeEmailOnSignUp bool
TemplatesPattern string TemplatesPatterns []string
ContentTypes []string
} }
func (cfg *Cfg) readSmtpSettings() { func (cfg *Cfg) readSmtpSettings() {
@ -33,5 +36,6 @@ func (cfg *Cfg) readSmtpSettings() {
emails := cfg.Raw.Section("emails") emails := cfg.Raw.Section("emails")
cfg.Smtp.SendWelcomeEmailOnSignUp = emails.Key("welcome_email_on_sign_up").MustBool(false) 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{ SendEmailCommand: models.SendEmailCommand{
To: []string{"test@email.com"}, To: []string{"test@email.com"},
SingleEmail: true, SingleEmail: true,
Template: "ng_alert_notification.html", Template: "ng_alert_notification",
Subject: "[FIRING:1] EmailAlert ", Subject: "[FIRING:1] EmailAlert ",
Data: map[string]interface{}{ Data: map[string]interface{}{
"Title": "[FIRING:1] EmailAlert ", "Title": "[FIRING:1] EmailAlert ",

@ -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

@ -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

@ -245,7 +245,7 @@ text-decoration: underline;
</tr> </tr>
<tr style="vertical-align: top; padding: 0;" align="left"> <tr style="vertical-align: top; padding: 0;" align="left">
<td class="center" style="word-break: break-word; -webkit-hyphens: auto; -moz-hyphens: auto; hyphens: auto; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-weight: normal; line-height: 19px; font-size: 14px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; margin: 0; padding: 0px 0px 10px;" align="center" valign="top"> <td class="center" style="word-break: break-word; -webkit-hyphens: auto; -moz-hyphens: auto; hyphens: auto; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-weight: normal; line-height: 19px; font-size: 14px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; margin: 0; padding: 0px 0px 10px;" align="center" valign="top">
<p style="color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-weight: normal; line-height: 19px; font-size: 14px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; margin: 0 0 10px; padding: 0;" align="left">You can also copy/paste this link into your browser directly: <a href="{{.LinkUrl}}" style="color: #E67612; text-decoration: none;">{{.LinkUrl}}</a></p> <p style="color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-weight: normal; line-height: 19px; font-size: 14px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; margin: 0 0 10px; padding: 0;" align="left">You can also copy and paste this link into your browser directly: <a href="{{.LinkUrl}}" style="color: #E67612; text-decoration: none;">{{.LinkUrl}}</a></p>
</td> </td>
<td class="expander" style="word-break: break-word; -webkit-hyphens: auto; -moz-hyphens: auto; hyphens: auto; border-collapse: collapse !important; visibility: hidden; width: 0px; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-weight: normal; line-height: 19px; font-size: 14px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; margin: 0; padding: 0;" align="left" valign="top"></td> <td class="expander" style="word-break: break-word; -webkit-hyphens: auto; -moz-hyphens: auto; hyphens: auto; border-collapse: collapse !important; visibility: hidden; width: 0px; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-weight: normal; line-height: 19px; font-size: 14px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; margin: 0; padding: 0;" align="left" valign="top"></td>
</tr> </tr>

@ -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

@ -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

@ -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

@ -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

@ -235,7 +235,7 @@ text-decoration: underline;
<tr style="vertical-align: top; padding: 0;" align="left"> <tr style="vertical-align: top; padding: 0;" align="left">
<td class="center" style="word-break: break-word; -webkit-hyphens: auto; -moz-hyphens: auto; hyphens: auto; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-weight: normal; line-height: 19px; font-size: 14px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; margin: 0; padding: 0px 0px 10px;" align="center" valign="top"> <td class="center" style="word-break: break-word; -webkit-hyphens: auto; -moz-hyphens: auto; hyphens: auto; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-weight: normal; line-height: 19px; font-size: 14px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; margin: 0; padding: 0px 0px 10px;" align="center" valign="top">
<p style="color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-weight: normal; line-height: 19px; font-size: 14px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; margin: 0 0 10px; padding: 0;" align="left"> <p style="color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-weight: normal; line-height: 19px; font-size: 14px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; margin: 0 0 10px; padding: 0;" align="left">
If you are new to Grafana please read the <a href="https://grafana.com/docs/grafana/latest/getting-started/getting-started/" style="color: #E67612; text-decoration: none;">Getting Started</a> guide. If you are new to Grafana, refer to the <a href="https://grafana.com/docs/grafana/latest/getting-started/getting-started/" style="color: #E67612; text-decoration: none;">Getting started with Grafana</a> guide.
</p> </p>
</td> </td>
<td class="expander" style="word-break: break-word; -webkit-hyphens: auto; -moz-hyphens: auto; hyphens: auto; border-collapse: collapse !important; visibility: hidden; width: 0px; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-weight: normal; line-height: 19px; font-size: 14px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; margin: 0; padding: 0;" align="left" valign="top"></td> <td class="expander" style="word-break: break-word; -webkit-hyphens: auto; -moz-hyphens: auto; hyphens: auto; border-collapse: collapse !important; visibility: hidden; width: 0px; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-weight: normal; line-height: 19px; font-size: 14px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; margin: 0; padding: 0;" align="left" valign="top"></td>

@ -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
Loading…
Cancel
Save