diff --git a/pkg/services/ngalert/notifier/channels/email.go b/pkg/services/ngalert/notifier/channels/email.go
index d2542c81238..2680f82c0e4 100644
--- a/pkg/services/ngalert/notifier/channels/email.go
+++ b/pkg/services/ngalert/notifier/channels/email.go
@@ -2,37 +2,33 @@ package channels
import (
"context"
+ "encoding/json"
"errors"
+ "fmt"
"net/url"
"os"
"path"
"path/filepath"
+ "strings"
"github.com/prometheus/alertmanager/template"
"github.com/prometheus/alertmanager/types"
"github.com/grafana/alerting/alerting/notifier/channels"
-
- "github.com/grafana/grafana/pkg/components/simplejson"
- "github.com/grafana/grafana/pkg/util"
)
// EmailNotifier is responsible for sending
// alert notifications over email.
type EmailNotifier struct {
*channels.Base
- Addresses []string
- SingleEmail bool
- Message string
- Subject string
- log channels.Logger
- ns channels.EmailSender
- images channels.ImageStore
- tmpl *template.Template
+ log channels.Logger
+ ns channels.EmailSender
+ images channels.ImageStore
+ tmpl *template.Template
+ settings *emailSettings
}
-type EmailConfig struct {
- *channels.NotificationChannelConfig
+type emailSettings struct {
SingleEmail bool
Addresses []string
Message string
@@ -40,50 +36,60 @@ type EmailConfig struct {
}
func EmailFactory(fc channels.FactoryConfig) (channels.NotificationChannel, error) {
- cfg, err := NewEmailConfig(fc.Config)
+ notifier, err := buildEmailNotifier(fc)
if err != nil {
return nil, receiverInitError{
Reason: err.Error(),
Cfg: *fc.Config,
}
}
- return NewEmailNotifier(cfg, fc.Logger, fc.NotificationService, fc.ImageStore, fc.Template), nil
+ return notifier, nil
}
-func NewEmailConfig(config *channels.NotificationChannelConfig) (*EmailConfig, error) {
- settings, err := simplejson.NewJson(config.Settings)
- if err != nil {
- return nil, err
+func buildEmailSettings(fc channels.FactoryConfig) (*emailSettings, error) {
+ type emailSettingsRaw struct {
+ SingleEmail bool `json:"singleEmail,omitempty"`
+ Addresses string `json:"addresses,omitempty"`
+ Message string `json:"message,omitempty"`
+ Subject string `json:"subject,omitempty"`
}
- addressesString := settings.Get("addresses").MustString()
- if addressesString == "" {
+
+ var settings emailSettingsRaw
+ err := json.Unmarshal(fc.Config.Settings, &settings)
+ if err != nil {
+ return nil, fmt.Errorf("failed to unmarshal settings: %w", err)
+ }
+ if settings.Addresses == "" {
return nil, errors.New("could not find addresses in settings")
}
// split addresses with a few different ways
- addresses := util.SplitEmails(addressesString)
- return &EmailConfig{
- NotificationChannelConfig: config,
- SingleEmail: settings.Get("singleEmail").MustBool(false),
- Message: settings.Get("message").MustString(),
- Subject: settings.Get("subject").MustString(channels.DefaultMessageTitleEmbed),
- Addresses: addresses,
+ addresses := splitEmails(settings.Addresses)
+
+ if settings.Subject == "" {
+ settings.Subject = channels.DefaultMessageTitleEmbed
+ }
+
+ return &emailSettings{
+ SingleEmail: settings.SingleEmail,
+ Message: settings.Message,
+ Subject: settings.Subject,
+ Addresses: addresses,
}, nil
}
-// NewEmailNotifier is the constructor function
-// for the EmailNotifier.
-func NewEmailNotifier(config *EmailConfig, l channels.Logger, ns channels.EmailSender, images channels.ImageStore, t *template.Template) *EmailNotifier {
- return &EmailNotifier{
- Base: channels.NewBase(config.NotificationChannelConfig),
- Addresses: config.Addresses,
- SingleEmail: config.SingleEmail,
- Message: config.Message,
- Subject: config.Subject,
- log: l,
- ns: ns,
- images: images,
- tmpl: t,
+func buildEmailNotifier(fc channels.FactoryConfig) (*EmailNotifier, error) {
+ settings, err := buildEmailSettings(fc)
+ if err != nil {
+ return nil, err
}
+ return &EmailNotifier{
+ Base: channels.NewBase(fc.Config),
+ log: fc.Logger,
+ ns: fc.NotificationService,
+ images: fc.ImageStore,
+ tmpl: fc.Template,
+ settings: settings,
+ }, nil
}
// Notify sends the alert notification.
@@ -91,7 +97,7 @@ func (en *EmailNotifier) Notify(ctx context.Context, alerts ...*types.Alert) (bo
var tmplErr error
tmpl, data := channels.TmplText(ctx, en.tmpl, alerts, en.log, &tmplErr)
- subject := tmpl(en.Subject)
+ subject := tmpl(en.settings.Subject)
alertPageURL := en.tmpl.ExternalURL.String()
ruleURL := en.tmpl.ExternalURL.String()
u, err := url.Parse(en.tmpl.ExternalURL.String())
@@ -127,7 +133,7 @@ func (en *EmailNotifier) Notify(ctx context.Context, alerts ...*types.Alert) (bo
Subject: subject,
Data: map[string]interface{}{
"Title": subject,
- "Message": tmpl(en.Message),
+ "Message": tmpl(en.settings.Message),
"Status": data.Status,
"Alerts": data.Alerts,
"GroupLabels": data.GroupLabels,
@@ -138,8 +144,8 @@ func (en *EmailNotifier) Notify(ctx context.Context, alerts ...*types.Alert) (bo
"AlertPageUrl": alertPageURL,
},
EmbeddedFiles: embeddedFiles,
- To: en.Addresses,
- SingleEmail: en.SingleEmail,
+ To: en.settings.Addresses,
+ SingleEmail: en.settings.SingleEmail,
Template: "ng_alert_notification",
}
@@ -157,3 +163,13 @@ func (en *EmailNotifier) Notify(ctx context.Context, alerts ...*types.Alert) (bo
func (en *EmailNotifier) SendResolved() bool {
return !en.GetDisableResolveMessage()
}
+
+func splitEmails(emails string) []string {
+ return strings.FieldsFunc(emails, func(r rune) bool {
+ switch r {
+ case ',', ';', '\n':
+ return true
+ }
+ return false
+ })
+}
diff --git a/pkg/services/ngalert/notifier/channels/email_test.go b/pkg/services/ngalert/notifier/channels/email_test.go
index 8e641d4e657..7136dd75b23 100644
--- a/pkg/services/ngalert/notifier/channels/email_test.go
+++ b/pkg/services/ngalert/notifier/channels/email_test.go
@@ -11,30 +11,142 @@ import (
"github.com/prometheus/alertmanager/types"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/require"
-
- "github.com/grafana/grafana/pkg/services/notifications"
)
-func TestEmailNotifier(t *testing.T) {
+func TestEmailNotifier_Init(t *testing.T) {
+ testCase := []struct {
+ Name string
+ Config json.RawMessage
+ Expected *emailSettings
+ ExpectedError string
+ }{
+ {
+ Name: "error if JSON is empty",
+ Config: json.RawMessage(`{}`),
+ ExpectedError: "could not find addresses in settings",
+ },
+ {
+ Name: "should split addresses separated by semicolon",
+ Config: json.RawMessage(`{
+ "addresses": "someops@example.com;somedev@example.com"
+ }`),
+ Expected: &emailSettings{
+ SingleEmail: false,
+ Addresses: []string{
+ "someops@example.com",
+ "somedev@example.com",
+ },
+ Message: "",
+ Subject: channels.DefaultMessageTitleEmbed,
+ },
+ },
+ {
+ Name: "should split addresses separated by comma",
+ Config: json.RawMessage(`{
+ "addresses": "someops@example.com,somedev@example.com"
+ }`),
+ Expected: &emailSettings{
+ SingleEmail: false,
+ Addresses: []string{
+ "someops@example.com",
+ "somedev@example.com",
+ },
+ Message: "",
+ Subject: channels.DefaultMessageTitleEmbed,
+ },
+ },
+ {
+ Name: "should split addresses separated by new-line",
+ Config: json.RawMessage(`{
+ "addresses": "someops@example.com\nsomedev@example.com"
+ }`),
+ Expected: &emailSettings{
+ SingleEmail: false,
+ Addresses: []string{
+ "someops@example.com",
+ "somedev@example.com",
+ },
+ Message: "",
+ Subject: channels.DefaultMessageTitleEmbed,
+ },
+ },
+ {
+ Name: "should split addresses separated by mixed separators",
+ Config: json.RawMessage(`{
+ "addresses": "someops@example.com\nsomedev@example.com;somedev2@example.com,somedev3@example.com"
+ }`),
+ Expected: &emailSettings{
+ SingleEmail: false,
+ Addresses: []string{
+ "someops@example.com",
+ "somedev@example.com",
+ "somedev2@example.com",
+ "somedev3@example.com",
+ },
+ Message: "",
+ Subject: channels.DefaultMessageTitleEmbed,
+ },
+ },
+ {
+ Name: "should split addresses separated by mixed separators",
+ Config: json.RawMessage(`{
+ "addresses": "someops@example.com\nsomedev@example.com;somedev2@example.com,somedev3@example.com"
+ }`),
+ Expected: &emailSettings{
+ SingleEmail: false,
+ Addresses: []string{
+ "someops@example.com",
+ "somedev@example.com",
+ "somedev2@example.com",
+ "somedev3@example.com",
+ },
+ Message: "",
+ Subject: channels.DefaultMessageTitleEmbed,
+ },
+ },
+ {
+ Name: "should parse all settings",
+ Config: json.RawMessage(`{
+ "singleEmail": true,
+ "addresses": "someops@example.com",
+ "message": "test-message",
+ "subject": "test-subject"
+ }`),
+ Expected: &emailSettings{
+ SingleEmail: true,
+ Addresses: []string{
+ "someops@example.com",
+ },
+ Message: "test-message",
+ Subject: "test-subject",
+ },
+ },
+ }
+
+ for _, test := range testCase {
+ t.Run(test.Name, func(t *testing.T) {
+ cfg := &channels.NotificationChannelConfig{
+ Name: "ops",
+ Type: "email",
+ Settings: test.Config,
+ }
+ settings, err := buildEmailSettings(channels.FactoryConfig{Config: cfg})
+ if test.ExpectedError != "" {
+ require.ErrorContains(t, err, test.ExpectedError)
+ } else {
+ require.Equal(t, *test.Expected, *settings)
+ }
+ })
+ }
+}
+
+func TestEmailNotifier_Notify(t *testing.T) {
tmpl := templateForTests(t)
externalURL, err := url.Parse("http://localhost/base")
require.NoError(t, err)
tmpl.ExternalURL = externalURL
- t.Run("empty settings should return error", func(t *testing.T) {
- jsonData := `{ }`
- settingsJSON := json.RawMessage(jsonData)
- model := &channels.NotificationChannelConfig{
- Name: "ops",
- Type: "email",
- Settings: settingsJSON,
- }
-
- _, err := NewEmailConfig(model)
- require.Error(t, err)
- })
-
t.Run("with the correct settings it should not fail and produce the expected command", func(t *testing.T) {
jsonData := `{
"addresses": "someops@example.com;somedev@example.com",
@@ -42,13 +154,24 @@ func TestEmailNotifier(t *testing.T) {
}`
emailSender := mockNotificationService()
- cfg, err := NewEmailConfig(&channels.NotificationChannelConfig{
- Name: "ops",
- Type: "email",
- Settings: json.RawMessage(jsonData),
- })
+
+ fc := channels.FactoryConfig{
+ Config: &channels.NotificationChannelConfig{
+ Name: "ops",
+ Type: "email",
+ Settings: json.RawMessage(jsonData),
+ },
+ NotificationService: emailSender,
+ DecryptFunc: func(ctx context.Context, sjd map[string][]byte, key string, fallback string) string {
+ return fallback
+ },
+ ImageStore: &channels.UnavailableImageStore{},
+ Template: tmpl,
+ Logger: &channels.FakeLogger{},
+ }
+
+ emailNotifier, err := EmailFactory(fc)
require.NoError(t, err)
- emailNotifier := NewEmailNotifier(cfg, &channels.FakeLogger{}, emailSender, &channels.UnavailableImageStore{}, tmpl)
alerts := []*types.Alert{
{
@@ -100,204 +223,3 @@ func TestEmailNotifier(t *testing.T) {
}, expected)
})
}
-
-func TestEmailNotifierIntegration(t *testing.T) {
- ns := createEmailSender(t)
-
- emailTmpl := templateForTests(t)
- externalURL, err := url.Parse("http://localhost/base")
- require.NoError(t, err)
- emailTmpl.ExternalURL = externalURL
-
- cases := []struct {
- name string
- alerts []*types.Alert
- messageTmpl string
- subjectTmpl string
- expSubject string
- expSnippets []string
- }{
- {
- name: "single alert with templated message",
- alerts: []*types.Alert{
- {
- Alert: model.Alert{
- Labels: model.LabelSet{"alertname": "AlwaysFiring", "severity": "warning"},
- Annotations: model.LabelSet{"runbook_url": "http://fix.me", "__dashboardUid__": "abc", "__panelId__": "5"},
- },
- },
- },
- messageTmpl: `Hi, this is a custom template.
- {{ if gt (len .Alerts.Firing) 0 }}
- You have {{ len .Alerts.Firing }} alerts firing.
- {{ range .Alerts.Firing }} Firing: {{ .Labels.alertname }} at {{ .Labels.severity }} {{ end }}
- {{ end }}`,
- expSubject: "[FIRING:1] (AlwaysFiring warning)",
- expSnippets: []string{
- "Hi, this is a custom template.",
- "You have 1 alerts firing.",
- "Firing: AlwaysFiring at warning",
- },
- },
- {
- name: "multiple alerts with templated message",
- alerts: []*types.Alert{
- {
- Alert: model.Alert{
- Labels: model.LabelSet{"alertname": "FiringOne", "severity": "warning"},
- Annotations: model.LabelSet{"runbook_url": "http://fix.me", "__dashboardUid__": "abc", "__panelId__": "5"},
- },
- },
- {
- Alert: model.Alert{
- Labels: model.LabelSet{"alertname": "FiringTwo", "severity": "critical"},
- Annotations: model.LabelSet{"runbook_url": "http://fix.me", "__dashboardUid__": "abc", "__panelId__": "5"},
- },
- },
- },
- messageTmpl: `Hi, this is a custom template.
- {{ if gt (len .Alerts.Firing) 0 }}
- You have {{ len .Alerts.Firing }} alerts firing.
- {{ range .Alerts.Firing }} Firing: {{ .Labels.alertname }} at {{ .Labels.severity }} {{ end }}
- {{ end }}`,
- expSubject: "[FIRING:2] ",
- expSnippets: []string{
- "Hi, this is a custom template.",
- "You have 2 alerts firing.",
- "Firing: FiringOne at warning",
- "Firing: FiringTwo at critical",
- },
- },
- {
- name: "empty message with alerts uses default template content",
- alerts: []*types.Alert{
- {
- Alert: model.Alert{
- Labels: model.LabelSet{"alertname": "FiringOne", "severity": "warning"},
- Annotations: model.LabelSet{"runbook_url": "http://fix.me", "__dashboardUid__": "abc", "__panelId__": "5"},
- },
- },
- {
- Alert: model.Alert{
- Labels: model.LabelSet{"alertname": "FiringTwo", "severity": "critical"},
- Annotations: model.LabelSet{"runbook_url": "http://fix.me", "__dashboardUid__": "abc", "__panelId__": "5"},
- },
- },
- },
- messageTmpl: "",
- expSubject: "[FIRING:2] ",
- expSnippets: []string{
- "2 firing instances",
- "severity",
- "warning\n",
- "critical\n",
- "alertname",
- "FiringTwo\n",
- "FiringOne\n",
- "Hi, this is a custom template.
- {{ if gt (len .Alerts.Firing) 0 }}
-
- {{range .Alerts.Firing }}- Firing: {{ .Labels.alertname }} at {{ .Labels.severity }}
{{ end }}
-
- {{ end }}`,
- expSubject: "[FIRING:1] (AlwaysFiring warning)",
- expSnippets: []string{
- "<marquee>Hi, this is a custom template.</marquee>",
- "<li>Firing: AlwaysFiring at warning </li>",
- },
- },
- {
- name: "single alert with templated subject",
- alerts: []*types.Alert{
- {
- Alert: model.Alert{
- Labels: model.LabelSet{"alertname": "AlwaysFiring", "severity": "warning"},
- Annotations: model.LabelSet{"runbook_url": "http://fix.me", "__dashboardUid__": "abc", "__panelId__": "5"},
- },
- },
- },
- subjectTmpl: `This notification is {{ .Status }}!`,
- expSubject: "This notification is firing!",
- expSnippets: []string{},
- },
- }
-
- for _, c := range cases {
- t.Run(c.name, func(t *testing.T) {
- emailNotifier := createSut(t, c.messageTmpl, c.subjectTmpl, emailTmpl, ns)
-
- ok, err := emailNotifier.Notify(context.Background(), c.alerts...)
- require.NoError(t, err)
- require.True(t, ok)
-
- sentMsg := getSingleSentMessage(t, ns)
-
- require.NotNil(t, sentMsg)
-
- require.Equal(t, "\"Grafana Admin\" ", sentMsg.From)
- require.Equal(t, sentMsg.To[0], "someops@example.com")
-
- require.Equal(t, c.expSubject, sentMsg.Subject)
-
- require.Contains(t, sentMsg.Body, "text/html")
- html := sentMsg.Body["text/html"]
- require.NotNil(t, html)
-
- for _, s := range c.expSnippets {
- require.Contains(t, html, s)
- }
- })
- }
-}
-
-func createSut(t *testing.T, messageTmpl string, subjectTmpl string, emailTmpl *template.Template, ns *emailSender) *EmailNotifier {
- t.Helper()
-
- jsonData := map[string]interface{}{
- "addresses": "someops@example.com;somedev@example.com",
- "singleEmail": true,
- }
- if messageTmpl != "" {
- jsonData["message"] = messageTmpl
- }
-
- if subjectTmpl != "" {
- jsonData["subject"] = subjectTmpl
- }
- bytes, err := json.Marshal(jsonData)
- require.NoError(t, err)
- cfg, err := NewEmailConfig(&channels.NotificationChannelConfig{
- Name: "ops",
- Type: "email",
- Settings: bytes,
- })
- require.NoError(t, err)
- emailNotifier := NewEmailNotifier(cfg, &channels.FakeLogger{}, ns, &channels.UnavailableImageStore{}, emailTmpl)
-
- return emailNotifier
-}
-
-func getSingleSentMessage(t *testing.T, ns *emailSender) *notifications.Message {
- t.Helper()
-
- mailer := ns.ns.GetMailer().(*notifications.FakeMailer)
- require.Len(t, mailer.Sent, 1)
- sent := mailer.Sent[0]
- mailer.Sent = []*notifications.Message{}
- return sent
-}
diff --git a/pkg/services/ngalert/notifier/channels/testing.go b/pkg/services/ngalert/notifier/channels/testing.go
index 1247058db23..441a3328939 100644
--- a/pkg/services/ngalert/notifier/channels/testing.go
+++ b/pkg/services/ngalert/notifier/channels/testing.go
@@ -3,17 +3,9 @@ package channels
import (
"context"
"fmt"
- "testing"
"time"
"github.com/grafana/alerting/alerting/notifier/channels"
- "github.com/stretchr/testify/require"
-
- "github.com/grafana/grafana/pkg/bus"
- "github.com/grafana/grafana/pkg/infra/tracing"
- "github.com/grafana/grafana/pkg/models"
- "github.com/grafana/grafana/pkg/services/notifications"
- "github.com/grafana/grafana/pkg/setting"
)
type fakeImageStore struct {
@@ -81,53 +73,3 @@ func (ns *notificationServiceMock) SendEmail(ctx context.Context, cmd *channels.
}
func mockNotificationService() *notificationServiceMock { return ¬ificationServiceMock{} }
-
-type emailSender struct {
- ns *notifications.NotificationService
-}
-
-func (e emailSender) SendEmail(ctx context.Context, cmd *channels.SendEmailSettings) error {
- attached := make([]*models.SendEmailAttachFile, 0, len(cmd.AttachedFiles))
- for _, file := range cmd.AttachedFiles {
- attached = append(attached, &models.SendEmailAttachFile{
- Name: file.Name,
- Content: file.Content,
- })
- }
- return e.ns.SendEmailCommandHandlerSync(ctx, &models.SendEmailCommandSync{
- SendEmailCommand: models.SendEmailCommand{
- To: cmd.To,
- SingleEmail: cmd.SingleEmail,
- Template: cmd.Template,
- Subject: cmd.Subject,
- Data: cmd.Data,
- Info: cmd.Info,
- ReplyTo: cmd.ReplyTo,
- EmbeddedFiles: cmd.EmbeddedFiles,
- AttachedFiles: attached,
- },
- })
-}
-
-func createEmailSender(t *testing.T) *emailSender {
- t.Helper()
-
- tracer := tracing.InitializeTracerForTest()
- bus := bus.ProvideBus(tracer)
-
- cfg := setting.NewCfg()
- cfg.StaticRootPath = "../../../../../public/"
- cfg.BuildVersion = "4.0.0"
- cfg.Smtp.Enabled = true
- cfg.Smtp.TemplatesPatterns = []string{"emails/*.html", "emails/*.txt"}
- cfg.Smtp.FromAddress = "from@address.com"
- cfg.Smtp.FromName = "Grafana Admin"
- cfg.Smtp.ContentTypes = []string{"text/html", "text/plain"}
- cfg.Smtp.Host = "localhost:1234"
- mailer := notifications.NewFakeMailer()
-
- ns, err := notifications.ProvideService(bus, cfg, mailer, nil)
- require.NoError(t, err)
-
- return &emailSender{ns: ns}
-}
diff --git a/pkg/services/ngalert/notifier/email_test.go b/pkg/services/ngalert/notifier/email_test.go
new file mode 100644
index 00000000000..217b4a037f2
--- /dev/null
+++ b/pkg/services/ngalert/notifier/email_test.go
@@ -0,0 +1,307 @@
+package notifier
+
+import (
+ "context"
+ "encoding/json"
+ "net/url"
+ "os"
+ "testing"
+
+ "github.com/grafana/alerting/alerting/notifier/channels"
+ "github.com/prometheus/alertmanager/template"
+ "github.com/prometheus/alertmanager/types"
+ "github.com/prometheus/common/model"
+ "github.com/stretchr/testify/require"
+
+ "github.com/grafana/grafana/pkg/bus"
+ "github.com/grafana/grafana/pkg/infra/tracing"
+ "github.com/grafana/grafana/pkg/models"
+ ngchannels "github.com/grafana/grafana/pkg/services/ngalert/notifier/channels"
+ "github.com/grafana/grafana/pkg/services/notifications"
+ "github.com/grafana/grafana/pkg/setting"
+)
+
+// TestEmailNotifierIntegration tests channels.EmailNotifier in conjunction with Grafana notifications.EmailSender and two staged expansion of the email body
+func TestEmailNotifierIntegration(t *testing.T) {
+ ns := createEmailSender(t)
+
+ emailTmpl := templateForTests(t)
+ externalURL, err := url.Parse("http://localhost/base")
+ require.NoError(t, err)
+ emailTmpl.ExternalURL = externalURL
+
+ cases := []struct {
+ name string
+ alerts []*types.Alert
+ messageTmpl string
+ subjectTmpl string
+ expSubject string
+ expSnippets []string
+ }{
+ {
+ name: "single alert with templated message",
+ alerts: []*types.Alert{
+ {
+ Alert: model.Alert{
+ Labels: model.LabelSet{"alertname": "AlwaysFiring", "severity": "warning"},
+ Annotations: model.LabelSet{"runbook_url": "http://fix.me", "__dashboardUid__": "abc", "__panelId__": "5"},
+ },
+ },
+ },
+ messageTmpl: `Hi, this is a custom template.
+ {{ if gt (len .Alerts.Firing) 0 }}
+ You have {{ len .Alerts.Firing }} alerts firing.
+ {{ range .Alerts.Firing }} Firing: {{ .Labels.alertname }} at {{ .Labels.severity }} {{ end }}
+ {{ end }}`,
+ expSubject: "[FIRING:1] (AlwaysFiring warning)",
+ expSnippets: []string{
+ "Hi, this is a custom template.",
+ "You have 1 alerts firing.",
+ "Firing: AlwaysFiring at warning",
+ },
+ },
+ {
+ name: "multiple alerts with templated message",
+ alerts: []*types.Alert{
+ {
+ Alert: model.Alert{
+ Labels: model.LabelSet{"alertname": "FiringOne", "severity": "warning"},
+ Annotations: model.LabelSet{"runbook_url": "http://fix.me", "__dashboardUid__": "abc", "__panelId__": "5"},
+ },
+ },
+ {
+ Alert: model.Alert{
+ Labels: model.LabelSet{"alertname": "FiringTwo", "severity": "critical"},
+ Annotations: model.LabelSet{"runbook_url": "http://fix.me", "__dashboardUid__": "abc", "__panelId__": "5"},
+ },
+ },
+ },
+ messageTmpl: `Hi, this is a custom template.
+ {{ if gt (len .Alerts.Firing) 0 }}
+ You have {{ len .Alerts.Firing }} alerts firing.
+ {{ range .Alerts.Firing }} Firing: {{ .Labels.alertname }} at {{ .Labels.severity }} {{ end }}
+ {{ end }}`,
+ expSubject: "[FIRING:2] ",
+ expSnippets: []string{
+ "Hi, this is a custom template.",
+ "You have 2 alerts firing.",
+ "Firing: FiringOne at warning",
+ "Firing: FiringTwo at critical",
+ },
+ },
+ {
+ name: "empty message with alerts uses default template content",
+ alerts: []*types.Alert{
+ {
+ Alert: model.Alert{
+ Labels: model.LabelSet{"alertname": "FiringOne", "severity": "warning"},
+ Annotations: model.LabelSet{"runbook_url": "http://fix.me", "__dashboardUid__": "abc", "__panelId__": "5"},
+ },
+ },
+ {
+ Alert: model.Alert{
+ Labels: model.LabelSet{"alertname": "FiringTwo", "severity": "critical"},
+ Annotations: model.LabelSet{"runbook_url": "http://fix.me", "__dashboardUid__": "abc", "__panelId__": "5"},
+ },
+ },
+ },
+ messageTmpl: "",
+ expSubject: "[FIRING:2] ",
+ expSnippets: []string{
+ "2 firing instances",
+ "severity",
+ "warning\n",
+ "critical\n",
+ "alertname",
+ "FiringTwo\n",
+ "FiringOne\n",
+ "Hi, this is a custom template.
+ {{ if gt (len .Alerts.Firing) 0 }}
+
+ {{range .Alerts.Firing }}- Firing: {{ .Labels.alertname }} at {{ .Labels.severity }}
{{ end }}
+
+ {{ end }}`,
+ expSubject: "[FIRING:1] (AlwaysFiring warning)",
+ expSnippets: []string{
+ "<marquee>Hi, this is a custom template.</marquee>",
+ "<li>Firing: AlwaysFiring at warning </li>",
+ },
+ },
+ {
+ name: "single alert with templated subject",
+ alerts: []*types.Alert{
+ {
+ Alert: model.Alert{
+ Labels: model.LabelSet{"alertname": "AlwaysFiring", "severity": "warning"},
+ Annotations: model.LabelSet{"runbook_url": "http://fix.me", "__dashboardUid__": "abc", "__panelId__": "5"},
+ },
+ },
+ },
+ subjectTmpl: `This notification is {{ .Status }}!`,
+ expSubject: "This notification is firing!",
+ expSnippets: []string{},
+ },
+ }
+
+ for _, c := range cases {
+ t.Run(c.name, func(t *testing.T) {
+ emailNotifier := createSut(t, c.messageTmpl, c.subjectTmpl, emailTmpl, ns)
+
+ ok, err := emailNotifier.Notify(context.Background(), c.alerts...)
+ require.NoError(t, err)
+ require.True(t, ok)
+
+ sentMsg := getSingleSentMessage(t, ns)
+
+ require.NotNil(t, sentMsg)
+
+ require.Equal(t, "\"Grafana Admin\" ", sentMsg.From)
+ require.Equal(t, sentMsg.To[0], "someops@example.com")
+
+ require.Equal(t, c.expSubject, sentMsg.Subject)
+
+ require.Contains(t, sentMsg.Body, "text/html")
+ html := sentMsg.Body["text/html"]
+ require.NotNil(t, html)
+
+ for _, s := range c.expSnippets {
+ require.Contains(t, html, s)
+ }
+ })
+ }
+}
+
+func createSut(t *testing.T, messageTmpl string, subjectTmpl string, emailTmpl *template.Template, ns channels.NotificationSender) channels.NotificationChannel {
+ t.Helper()
+
+ jsonData := map[string]interface{}{
+ "addresses": "someops@example.com;somedev@example.com",
+ "singleEmail": true,
+ }
+ if messageTmpl != "" {
+ jsonData["message"] = messageTmpl
+ }
+
+ if subjectTmpl != "" {
+ jsonData["subject"] = subjectTmpl
+ }
+ bytes, err := json.Marshal(jsonData)
+ require.NoError(t, err)
+
+ fc := channels.FactoryConfig{
+ Config: &channels.NotificationChannelConfig{
+ Name: "ops",
+ Type: "email",
+ Settings: json.RawMessage(bytes),
+ },
+ NotificationService: ns,
+ DecryptFunc: func(ctx context.Context, sjd map[string][]byte, key string, fallback string) string {
+ return fallback
+ },
+ ImageStore: &channels.UnavailableImageStore{},
+ Template: emailTmpl,
+ Logger: &channels.FakeLogger{},
+ }
+ emailNotifier, err := ngchannels.EmailFactory(fc)
+ require.NoError(t, err)
+ return emailNotifier
+}
+
+func getSingleSentMessage(t *testing.T, ns *emailSender) *notifications.Message {
+ t.Helper()
+
+ mailer := ns.ns.GetMailer().(*notifications.FakeMailer)
+ require.Len(t, mailer.Sent, 1)
+ sent := mailer.Sent[0]
+ mailer.Sent = []*notifications.Message{}
+ return sent
+}
+
+type emailSender struct {
+ ns *notifications.NotificationService
+}
+
+func (e emailSender) SendWebhook(ctx context.Context, cmd *channels.SendWebhookSettings) error {
+ panic("not implemented")
+}
+
+func (e emailSender) SendEmail(ctx context.Context, cmd *channels.SendEmailSettings) error {
+ attached := make([]*models.SendEmailAttachFile, 0, len(cmd.AttachedFiles))
+ for _, file := range cmd.AttachedFiles {
+ attached = append(attached, &models.SendEmailAttachFile{
+ Name: file.Name,
+ Content: file.Content,
+ })
+ }
+ return e.ns.SendEmailCommandHandlerSync(ctx, &models.SendEmailCommandSync{
+ SendEmailCommand: models.SendEmailCommand{
+ To: cmd.To,
+ SingleEmail: cmd.SingleEmail,
+ Template: cmd.Template,
+ Subject: cmd.Subject,
+ Data: cmd.Data,
+ Info: cmd.Info,
+ ReplyTo: cmd.ReplyTo,
+ EmbeddedFiles: cmd.EmbeddedFiles,
+ AttachedFiles: attached,
+ },
+ })
+}
+
+func createEmailSender(t *testing.T) *emailSender {
+ t.Helper()
+
+ tracer := tracing.InitializeTracerForTest()
+ bus := bus.ProvideBus(tracer)
+
+ cfg := setting.NewCfg()
+ cfg.StaticRootPath = "../../../../public/"
+ cfg.BuildVersion = "4.0.0"
+ cfg.Smtp.Enabled = true
+ cfg.Smtp.TemplatesPatterns = []string{"emails/*.html", "emails/*.txt"}
+ cfg.Smtp.FromAddress = "from@address.com"
+ cfg.Smtp.FromName = "Grafana Admin"
+ cfg.Smtp.ContentTypes = []string{"text/html", "text/plain"}
+ cfg.Smtp.Host = "localhost:1234"
+ mailer := notifications.NewFakeMailer()
+
+ ns, err := notifications.ProvideService(bus, cfg, mailer, nil)
+ require.NoError(t, err)
+
+ return &emailSender{ns: ns}
+}
+
+func templateForTests(t *testing.T) *template.Template {
+ f, err := os.CreateTemp("/tmp", "template")
+ require.NoError(t, err)
+ defer func(f *os.File) {
+ _ = f.Close()
+ }(f)
+
+ t.Cleanup(func() {
+ require.NoError(t, os.RemoveAll(f.Name()))
+ })
+
+ _, err = f.WriteString(channels.TemplateForTestsString)
+ require.NoError(t, err)
+
+ tmpl, err := template.FromGlobs(f.Name())
+ require.NoError(t, err)
+
+ return tmpl
+}