The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/pkg/services/notifications/smtp_test.go

347 lines
9.6 KiB

package notifications
import (
"bufio"
"bytes"
"context"
"errors"
"fmt"
"io"
"net/textproto"
"sort"
"strings"
"testing"
"time"
smtpmock "github.com/mocktools/go-smtp-mock/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/setting"
)
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)
message := &Message{
To: []string{"to@address.com"},
From: "Mr. Foo <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"},
}
ctx := context.Background()
t.Run("Can successfully build mail", func(t *testing.T) {
email := sc.buildEmail(ctx, 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: Mr. Foo <from@address.com>")
assert.Regexp(t, "Message-ID: <.*@address.com>", buf.String())
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"))
})
t.Run("Skips trace headers when context has no span", func(t *testing.T) {
cfg.Smtp.EnableTracing = true
sc, err := NewSmtpClient(cfg.Smtp)
require.NoError(t, err)
email := sc.buildEmail(ctx, message)
assert.Empty(t, email.GetHeader("traceparent"))
})
t.Run("Adds trace headers when context has span", func(t *testing.T) {
cfg.Smtp.EnableTracing = true
sc, err := NewSmtpClient(cfg.Smtp)
require.NoError(t, err)
tracer := tracing.InitializeTracerForTest()
ctx, span := tracer.Start(ctx, "notifications.SmtpClient.SendContext")
defer span.End()
email := sc.buildEmail(ctx, message)
assert.NotEmpty(t, email.GetHeader("traceparent"))
})
}
func TestSmtpDialer(t *testing.T) {
ctx := context.Background()
t.Run("When SMTP hostname is invalid", func(t *testing.T) {
cfg := createSmtpConfig()
cfg.Smtp.Host = "invalid%hostname:123:456"
client, err := ProvideSmtpService(cfg)
require.NoError(t, err)
message := &Message{
To: []string{"asdf@grafana.com"},
SingleEmail: true,
Subject: "subject",
Body: map[string]string{
"text/html": "body",
"text/plain": "body",
},
}
count, err := client.Send(ctx, message)
require.Equal(t, 0, count)
require.EqualError(t, err, "address invalid%hostname:123:456: too many colons in address")
})
t.Run("When SMTP port is invalid", func(t *testing.T) {
cfg := createSmtpConfig()
cfg.Smtp.Host = "invalid%hostname:123a"
client, err := ProvideSmtpService(cfg)
require.NoError(t, err)
message := &Message{
To: []string{"asdf@grafana.com"},
SingleEmail: true,
Subject: "subject",
Body: map[string]string{
"text/html": "body",
"text/plain": "body",
},
}
count, err := client.Send(ctx, message)
require.Equal(t, 0, count)
require.EqualError(t, err, "strconv.Atoi: parsing \"123a\": invalid syntax")
})
t.Run("When TLS certificate does not exist", func(t *testing.T) {
cfg := createSmtpConfig()
cfg.Smtp.Host = "localhost:1234"
cfg.Smtp.CertFile = "/var/certs/does-not-exist.pem"
client, err := ProvideSmtpService(cfg)
require.NoError(t, err)
message := &Message{
To: []string{"asdf@grafana.com"},
SingleEmail: true,
Subject: "subject",
Body: map[string]string{
"text/html": "body",
"text/plain": "body",
},
}
count, err := client.Send(ctx, message)
require.Equal(t, 0, count)
require.EqualError(t, err, "could not load cert or key file: open /var/certs/does-not-exist.pem: no such file or directory")
})
}
func TestSmtpSend(t *testing.T) {
srv := smtpmock.New(smtpmock.ConfigurationAttr{
MultipleRcptto: true,
HostAddress: "127.0.0.1",
})
require.NoError(t, srv.Start())
defer func() { _ = srv.Stop() }()
cfg := createSmtpConfig()
cfg.Smtp.Host = fmt.Sprintf("127.0.0.1:%d", srv.PortNumber())
cfg.Smtp.EnableTracing = true
client, err := NewSmtpClient(cfg.Smtp)
require.NoError(t, err)
ctx := context.Background()
t.Run("single message sends", func(t *testing.T) {
tracer := tracing.InitializeTracerForTest()
ctx, span := tracer.Start(ctx, "notifications.SmtpClient.SendContext")
defer span.End()
message := &Message{
From: "from@example.com",
To: []string{"rcpt@example.com"},
Subject: "subject",
Body: map[string]string{"text/plain": "hello world"},
}
count, err := client.Send(ctx, message)
require.NoError(t, err)
require.Equal(t, 1, count)
// workaround for https://github.com/mocktools/go-smtp-mock/issues/181
time.Sleep(1 * time.Millisecond)
messages := srv.MessagesAndPurge()
require.Len(t, messages, 1)
sentMsg := messages[0]
// read the headers
r := bufio.NewReader(strings.NewReader(sentMsg.MsgRequest()))
mimeReader := textproto.NewReader(r)
hdr, err := mimeReader.ReadMIMEHeader()
require.NoError(t, err)
// make sure the trace is propagated
traceId := span.SpanContext().TraceID().String()
hasPrefix := strings.HasPrefix(hdr.Get("traceparent"), "00-"+traceId+"-")
require.True(t, hasPrefix)
// one of the lines should be the body we expect!
found := false
for {
line, err := mimeReader.ReadLine()
if errors.Is(err, io.EOF) {
break
}
require.NoError(t, err)
t.Logf("line: %q", line)
if strings.Contains(line, "hello world") {
found = true
break
}
}
require.True(t, found)
})
t.Run("multiple recipients, single message", func(t *testing.T) {
tracer := tracing.InitializeTracerForTest()
ctx, span := tracer.Start(ctx, "notifications.SmtpClient.SendContext")
defer span.End()
message := &Message{
From: "from@example.com",
To: []string{"rcpt1@example.com", "rcpt2@example.com", "rcpt3@example.com"},
Subject: "subject",
Body: map[string]string{"text/plain": "hello world"},
}
count, err := client.Send(ctx, message)
require.NoError(t, err)
require.Equal(t, 1, count)
// workaround for https://github.com/mocktools/go-smtp-mock/issues/181
time.Sleep(1 * time.Millisecond)
messages := srv.MessagesAndPurge()
require.Len(t, messages, 1)
sentMsg := messages[0]
rcpts := sentMsg.RcpttoRequestResponse()
require.EqualValues(t, [][]string{
{"RCPT TO:<rcpt1@example.com>", "250 Received"},
{"RCPT TO:<rcpt2@example.com>", "250 Received"},
{"RCPT TO:<rcpt3@example.com>", "250 Received"},
}, rcpts)
// read the headers
r := bufio.NewReader(strings.NewReader(sentMsg.MsgRequest()))
mimeReader := textproto.NewReader(r)
hdr, err := mimeReader.ReadMIMEHeader()
require.NoError(t, err)
// make sure the trace is propagated
traceId := span.SpanContext().TraceID().String()
hasPrefix := strings.HasPrefix(hdr.Get("traceparent"), "00-"+traceId+"-")
require.True(t, hasPrefix)
// one of the lines should be the body we expect!
found := false
for {
line, err := mimeReader.ReadLine()
if errors.Is(err, io.EOF) {
break
}
require.NoError(t, err)
t.Logf("line: %q", line)
if strings.Contains(line, "hello world") {
found = true
break
}
}
require.True(t, found)
})
t.Run("multiple recipients, multiple messages", func(t *testing.T) {
tracer := tracing.InitializeTracerForTest()
ctx, span := tracer.Start(ctx, "notifications.SmtpClient.SendContext")
defer span.End()
msgs := []*Message{
{From: "from@example.com", To: []string{"rcpt1@example.com"},
Subject: "subject", Body: map[string]string{"text/plain": "hello world"}},
{From: "from@example.com", To: []string{"rcpt2@example.com"},
Subject: "subject", Body: map[string]string{"text/plain": "hello world"}},
{From: "from@example.com", To: []string{"rcpt3@example.com"},
Subject: "subject", Body: map[string]string{"text/plain": "hello world"}},
}
count, err := client.Send(ctx, msgs...)
require.NoError(t, err)
assert.Equal(t, 3, count)
// workaround for https://github.com/mocktools/go-smtp-mock/issues/181
time.Sleep(1 * time.Millisecond)
messages := srv.MessagesAndPurge()
assert.Len(t, messages, 3)
// sort for test consistency
sort.Slice(messages, func(i, j int) bool {
return messages[i].RcpttoRequestResponse()[0][0] < messages[j].RcpttoRequestResponse()[0][0]
})
for i, sentMsg := range messages {
rcpts := sentMsg.RcpttoRequestResponse()
assert.EqualValues(t, [][]string{
{fmt.Sprintf("RCPT TO:<rcpt%d@example.com>", i+1), "250 Received"},
}, rcpts)
// read the headers
r := bufio.NewReader(strings.NewReader(sentMsg.MsgRequest()))
mimeReader := textproto.NewReader(r)
hdr, err := mimeReader.ReadMIMEHeader()
require.NoError(t, err)
// make sure the trace is propagated
traceId := span.SpanContext().TraceID().String()
hasPrefix := strings.HasPrefix(hdr.Get("traceparent"), "00-"+traceId+"-")
assert.True(t, hasPrefix)
// one of the lines should be the body we expect!
found := false
for {
line, err := mimeReader.ReadLine()
if errors.Is(err, io.EOF) {
break
}
require.NoError(t, err)
t.Logf("line: %q", line)
if strings.Contains(line, "hello world") {
found = true
break
}
}
assert.True(t, found)
}
})
}