Email: Allow configuration of content types for email notifications (#34530)

* Alerting: Allow configuration of content types for email notifications

* Fix lint error

* Improves email templates

* Improve configuration documentation

Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com>

* Improve code comments

Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com>

* Improve configuration documentation

Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com>

* Improve email template

* Remove unnecessary predeclaration

Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>

* Adds handling for unrecognized content type

Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>

* Move utility function outside of util package

* Fixes syntax

* Remove unused package

* Fix lint error

* improve email templates

* Fix test

* Alerting: Allow configuration of content types for email notifications

* Fix lint error

* Improves email templates

* Improve configuration documentation

Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com>

* Improve code comments

Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com>

* Improve configuration documentation

Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com>

* Improve email template

* Remove unnecessary predeclaration

Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>

* Adds handling for unrecognized content type

Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>

* Move utility function outside of util package

* Fixes syntax

* Remove unused package

* Fix lint error

* improve email templates

* Fix test

* Fix comment style

Co-authored-by: Ganesh Vernekar <15064823+codesome@users.noreply.github.com>

* Fix template formatting

* Add test and improve error handling

* Fix test

* Fix formatting

* Fix formatting

* Improve documentation and regenerates txt template

* Update docs/sources/administration/configuration.md

Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>

Co-authored-by: Djairho Geuens <djairho.geuens@ae.be>
Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com>
Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>
Co-authored-by: Ganesh Vernekar <15064823+codesome@users.noreply.github.com>
Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>
This commit is contained in:
Djairho Geuens 2021-07-19 12:31:51 +02:00 committed by GitHub
parent cec12676e7
commit 4cadbba686
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 408 additions and 58 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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: '[[',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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{},
}, },
} }

View File

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

View File

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

View File

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

View File

@ -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,18 +180,26 @@ 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)
if err != nil { body := make(map[string]string)
return nil, err 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 subject := 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)
}
}

View File

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

View File

@ -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,10 +56,12 @@ 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 {
_, err := mailTemplates.ParseGlob(templatePattern) templatePattern := filepath.Join(ns.Cfg.StaticRootPath, pattern)
if err != nil { _, err := mailTemplates.ParseGlob(templatePattern)
return err if err != nil {
return err
}
} }
if !util.IsEmail(ns.Cfg.Smtp.FromAddress) { if !util.IsEmail(ns.Cfg.Smtp.FromAddress) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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