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. 68
      pkg/services/notifications/mailer.go
  27. 41
      pkg/services/notifications/mailer_test.go
  28. 16
      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]
welcome_email_on_sign_up = false
templates_pattern = emails/*.html
templates_pattern = emails/*.html, emails/*.txt
content_types = text/html
#################################### Logging ##########################
[log]

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

@ -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`.
<hr>

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

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

@ -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/',
},
};
};

@ -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.
},
],
},
};

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

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

@ -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>
<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 class="expander"></td>
</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>
<td class="center">
<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>
</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) {
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 ",

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

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