mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Config: Can add static headers to email messages (#79365)
* Can add allowed custom headers to an email Message. WIP. * adds slug as a custom email header to all outgoing emails * Headers are static - declared as key/value pairs in config. All static headers get added to emails. * updates comment * adds tests for parsing smtp static headers * updates test to assert static headers are included when building email * updates test to use multiple static headers * updates test names * fixes linting issue with error * ignore gocyclo for loading config * updates email headers in tests to be formatted properly * add static headers first * updates tests to assert that regular headers like From cant be overwritten * ensures only the header is in a valid format for smtp and not the value * updates comment and error message wording * adds to docs and ini sample files * updates smtp.static_headers docs examples formatting * removes lines commented with semi colons * prettier:write * renames var
This commit is contained in:
parent
8bcd40a186
commit
d5b9602a79
@ -938,6 +938,9 @@ from_name = Grafana
|
||||
ehlo_identity =
|
||||
startTLS_policy =
|
||||
|
||||
[smtp.static_headers]
|
||||
# Include custom static headers in all outgoing emails
|
||||
|
||||
[emails]
|
||||
welcome_email_on_sign_up = false
|
||||
templates_pattern = emails/*.html, emails/*.txt
|
||||
|
@ -891,6 +891,11 @@
|
||||
# SMTP startTLS policy (defaults to 'OpportunisticStartTLS')
|
||||
;startTLS_policy = NoStartTLS
|
||||
|
||||
[smtp.static_headers]
|
||||
# Include custom static headers in all outgoing emails
|
||||
;Foo-Header = bar
|
||||
;Foo = bar
|
||||
|
||||
[emails]
|
||||
;welcome_email_on_sign_up = false
|
||||
;templates_pattern = emails/*.html, emails/*.txt
|
||||
|
@ -1283,6 +1283,13 @@ Either "OpportunisticStartTLS", "MandatoryStartTLS", "NoStartTLS". Default is `e
|
||||
|
||||
<hr>
|
||||
|
||||
## [smtp.static_headers]
|
||||
|
||||
Enter key-value pairs on their own lines to be included as headers on outgoing emails. All keys must be in canonical mail header format.
|
||||
Examples: `Foo=bar`, `Foo-Header=bar`.
|
||||
|
||||
<hr>
|
||||
|
||||
## [emails]
|
||||
|
||||
### welcome_email_on_sign_up
|
||||
|
@ -62,6 +62,10 @@ func (sc *SmtpClient) Send(messages ...*Message) (int, error) {
|
||||
// buildEmail converts the Message DTO to a gomail message.
|
||||
func (sc *SmtpClient) buildEmail(msg *Message) *gomail.Message {
|
||||
m := gomail.NewMessage()
|
||||
// add all static headers to the email message
|
||||
for h, val := range sc.cfg.StaticHeaders {
|
||||
m.SetHeader(h, val)
|
||||
}
|
||||
m.SetHeader("From", msg.From)
|
||||
m.SetHeader("To", msg.To...)
|
||||
m.SetHeader("Subject", msg.Subject)
|
||||
|
@ -14,6 +14,7 @@ import (
|
||||
func TestBuildMail(t *testing.T) {
|
||||
cfg := setting.NewCfg()
|
||||
cfg.Smtp.ContentTypes = []string{"text/html", "text/plain"}
|
||||
cfg.Smtp.StaticHeaders = map[string]string{"Foo-Header": "foo_value", "From": "malicious_value"}
|
||||
|
||||
sc, err := NewSmtpClient(cfg.Smtp)
|
||||
require.NoError(t, err)
|
||||
@ -29,13 +30,17 @@ func TestBuildMail(t *testing.T) {
|
||||
ReplyTo: []string{"from@address.com"},
|
||||
}
|
||||
|
||||
t.Run("When building email", func(t *testing.T) {
|
||||
t.Run("Can successfully build mail", func(t *testing.T) {
|
||||
email := sc.buildEmail(message)
|
||||
staticHeader := email.GetHeader("Foo-Header")[0]
|
||||
assert.Equal(t, staticHeader, "foo_value")
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
_, err := email.WriteTo(buf)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Contains(t, buf.String(), "Foo-Header: foo_value")
|
||||
assert.Contains(t, buf.String(), "From: from@address.com")
|
||||
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"))
|
||||
|
@ -1026,6 +1026,7 @@ func (cfg *Cfg) validateStaticRootPath() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// nolint:gocyclo
|
||||
func (cfg *Cfg) Load(args CommandLineArgs) error {
|
||||
cfg.setHomePath(args)
|
||||
|
||||
@ -1197,7 +1198,9 @@ func (cfg *Cfg) Load(args CommandLineArgs) error {
|
||||
cfg.handleAWSConfig()
|
||||
cfg.readAzureSettings()
|
||||
cfg.readSessionConfig()
|
||||
cfg.readSmtpSettings()
|
||||
if err := cfg.readSmtpSettings(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cfg.readAnnotationSettings(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -1,6 +1,11 @@
|
||||
package setting
|
||||
|
||||
import "github.com/grafana/grafana/pkg/util"
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
type SmtpSettings struct {
|
||||
Enabled bool
|
||||
@ -14,13 +19,17 @@ type SmtpSettings struct {
|
||||
EhloIdentity string
|
||||
StartTLSPolicy string
|
||||
SkipVerify bool
|
||||
StaticHeaders map[string]string
|
||||
|
||||
SendWelcomeEmailOnSignUp bool
|
||||
TemplatesPatterns []string
|
||||
ContentTypes []string
|
||||
}
|
||||
|
||||
func (cfg *Cfg) readSmtpSettings() {
|
||||
// validates mail headers
|
||||
var mailHeaderRegex = regexp.MustCompile(`^[A-Z][A-Za-z0-9]*(-[A-Z][A-Za-z0-9]*)*$`)
|
||||
|
||||
func (cfg *Cfg) readSmtpSettings() error {
|
||||
sec := cfg.Raw.Section("smtp")
|
||||
cfg.Smtp.Enabled = sec.Key("enabled").MustBool(false)
|
||||
cfg.Smtp.Host = sec.Key("host").String()
|
||||
@ -38,4 +47,31 @@ func (cfg *Cfg) readSmtpSettings() {
|
||||
cfg.Smtp.SendWelcomeEmailOnSignUp = emails.Key("welcome_email_on_sign_up").MustBool(false)
|
||||
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"))
|
||||
|
||||
// populate static headers
|
||||
if err := cfg.readGrafanaSmtpStaticHeaders(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validHeader(header string) bool {
|
||||
return mailHeaderRegex.MatchString(header)
|
||||
}
|
||||
|
||||
func (cfg *Cfg) readGrafanaSmtpStaticHeaders() error {
|
||||
staticHeadersSection := cfg.Raw.Section("smtp.static_headers")
|
||||
keys := staticHeadersSection.Keys()
|
||||
cfg.Smtp.StaticHeaders = make(map[string]string, len(keys))
|
||||
|
||||
for _, key := range keys {
|
||||
if !validHeader(key.Name()) {
|
||||
return fmt.Errorf("header %q in [smtp.static_headers] configuration: must follow canonical MIME form", key.Name())
|
||||
}
|
||||
|
||||
cfg.Smtp.StaticHeaders[key.Name()] = key.Value()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
93
pkg/setting/setting_smtp_test.go
Normal file
93
pkg/setting/setting_smtp_test.go
Normal file
@ -0,0 +1,93 @@
|
||||
package setting
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
func TestLoadSmtpStaticHeaders(t *testing.T) {
|
||||
t.Run("will load valid headers", func(t *testing.T) {
|
||||
f := ini.Empty()
|
||||
cfg := NewCfg()
|
||||
s, err := f.NewSection("smtp.static_headers")
|
||||
require.NoError(t, err)
|
||||
cfg.Raw = f
|
||||
_, err = s.NewKey("Foo-Header", "foo_val")
|
||||
require.NoError(t, err)
|
||||
_, err = s.NewKey("Bar", "bar_val")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = cfg.readGrafanaSmtpStaticHeaders()
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "foo_val", cfg.Smtp.StaticHeaders["Foo-Header"])
|
||||
assert.Equal(t, "bar_val", cfg.Smtp.StaticHeaders["Bar"])
|
||||
})
|
||||
|
||||
t.Run("will load no static headers into smtp config when section is defined but has no keys", func(t *testing.T) {
|
||||
f := ini.Empty()
|
||||
cfg := NewCfg()
|
||||
_, err := f.NewSection("smtp.static_headers")
|
||||
require.NoError(t, err)
|
||||
cfg.Raw = f
|
||||
|
||||
err = cfg.readGrafanaSmtpStaticHeaders()
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Empty(t, cfg.Smtp.StaticHeaders)
|
||||
})
|
||||
|
||||
t.Run("will load no static headers into smtp config when section is not defined", func(t *testing.T) {
|
||||
f := ini.Empty()
|
||||
cfg := NewCfg()
|
||||
cfg.Raw = f
|
||||
|
||||
err := cfg.readGrafanaSmtpStaticHeaders()
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Empty(t, cfg.Smtp.StaticHeaders)
|
||||
})
|
||||
|
||||
t.Run("will return error when header label is not in valid format", func(t *testing.T) {
|
||||
f := ini.Empty()
|
||||
cfg := NewCfg()
|
||||
s, err := f.NewSection("smtp.static_headers")
|
||||
require.NoError(t, err)
|
||||
_, err = s.NewKey("header with spaces", "value")
|
||||
require.NoError(t, err)
|
||||
cfg.Raw = f
|
||||
|
||||
err = cfg.readGrafanaSmtpStaticHeaders()
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSmtpHeaderValidation(t *testing.T) {
|
||||
testCases := []struct {
|
||||
input string
|
||||
expected bool
|
||||
}{
|
||||
//valid
|
||||
{"Foo", true},
|
||||
{"Foo-Bar", true},
|
||||
{"Foo123-Bar123", true},
|
||||
|
||||
//invalid
|
||||
{"foo", false},
|
||||
{"Foo Bar", false},
|
||||
{"123Foo", false},
|
||||
{"Foo.Bar", false},
|
||||
{"foo-bar", false},
|
||||
{"foo-Bar", false},
|
||||
{"Foo-bar", false},
|
||||
{"-Bar", false},
|
||||
{"Foo--", false},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
assert.Equal(t, validHeader(tc.input), tc.expected)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user