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]
welcome_email_on_sign_up = false
templates_pattern = emails/*.html
templates_pattern = emails/*.html, emails/*.txt
content_types = text/html
#################################### Logging ##########################
[log]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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) {
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,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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 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) {

View File

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

View File

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

View File

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

View File

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

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

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

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