Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Config: Can add static headers to email messages #79365

Merged
merged 22 commits into from
Dec 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
b61e9ef
Can add allowed custom headers to an email Message. WIP.
owensmallwood Dec 11, 2023
edf0908
adds slug as a custom email header to all outgoing emails
owensmallwood Dec 11, 2023
29921ea
Headers are static - declared as key/value pairs in config. All stati…
owensmallwood Dec 12, 2023
4f914da
updates comment
owensmallwood Dec 12, 2023
188e739
adds tests for parsing smtp static headers
owensmallwood Dec 12, 2023
174190d
updates test to assert static headers are included when building email
owensmallwood Dec 12, 2023
14eff96
updates test to use multiple static headers
owensmallwood Dec 12, 2023
5d28dbf
updates test names
owensmallwood Dec 12, 2023
bf0ed0b
Merge branch 'main' into owensmallwood/can-configure-email-custom-hea…
owensmallwood Dec 12, 2023
db6c199
fixes linting issue with error
owensmallwood Dec 12, 2023
17d47cf
ignore gocyclo for loading config
owensmallwood Dec 12, 2023
12096b7
updates email headers in tests to be formatted properly
owensmallwood Dec 13, 2023
15c6d92
add static headers first
owensmallwood Dec 13, 2023
aa44c21
updates tests to assert that regular headers like From cant be overwr…
owensmallwood Dec 13, 2023
fbe2153
ensures only the header is in a valid format for smtp and not the value
owensmallwood Dec 13, 2023
2073062
updates comment and error message wording
owensmallwood Dec 14, 2023
5469247
adds to docs and ini sample files
owensmallwood Dec 14, 2023
4885c0e
updates smtp.static_headers docs examples formatting
owensmallwood Dec 14, 2023
88ebd4f
removes lines commented with semi colons
owensmallwood Dec 14, 2023
5910e71
prettier:write
owensmallwood Dec 14, 2023
ea61552
Merge branch 'main' into owensmallwood/can-configure-email-custom-hea…
owensmallwood Dec 14, 2023
3c5d944
renames var
owensmallwood Dec 14, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions conf/defaults.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions conf/sample.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions docs/sources/setup-grafana/configure-grafana/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions pkg/services/notifications/smtp.go
Original file line number Diff line number Diff line change
Expand 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 {
hairyhenderson marked this conversation as resolved.
Show resolved Hide resolved
m.SetHeader(h, val)
}
m.SetHeader("From", msg.From)
m.SetHeader("To", msg.To...)
m.SetHeader("Subject", msg.Subject)
Expand Down
7 changes: 6 additions & 1 deletion pkg/services/notifications/smtp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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"))
Expand Down
5 changes: 4 additions & 1 deletion pkg/setting/setting.go
Original file line number Diff line number Diff line change
Expand Up @@ -1026,6 +1026,7 @@ func (cfg *Cfg) validateStaticRootPath() error {
return nil
}

// nolint:gocyclo
func (cfg *Cfg) Load(args CommandLineArgs) error {
cfg.setHomePath(args)

Expand Down Expand Up @@ -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
}
Expand Down
40 changes: 38 additions & 2 deletions pkg/setting/setting_smtp.go
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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()
Expand All @@ -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 changes: 93 additions & 0 deletions pkg/setting/setting_smtp_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}