notify/email: wrap all errors for easier debugging (#1953)

* notify/email: wrap all errors for easier debugging

In addition, this commit passes the current context to the TCP dialer
and it doesn't log any QUIT errors if the email delivery wasn't
successful.

Signed-off-by: Simon Pasquier <spasquie@redhat.com>

* Fix typo

Signed-off-by: Simon Pasquier <spasquie@redhat.com>
This commit is contained in:
Simon Pasquier 2019-07-10 11:24:51 +02:00 committed by GitHub
parent 97d979848b
commit 9b0ecaa0fe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 246 additions and 72 deletions

1
.gitignore vendored
View File

@ -11,6 +11,7 @@
!/cli/testdata/*.yml !/cli/testdata/*.yml
!/cli/config/testdata/*.yml !/cli/config/testdata/*.yml
!/config/testdata/*.yml !/config/testdata/*.yml
!/notify/email/testdata/*.yml
!/doc/examples/simple.yml !/doc/examples/simple.yml
!/circle.yml !/circle.yml
!/.travis.yml !/.travis.yml

View File

@ -17,7 +17,6 @@ import (
"bytes" "bytes"
"context" "context"
"crypto/tls" "crypto/tls"
"errors"
"fmt" "fmt"
"mime" "mime"
"mime/multipart" "mime/multipart"
@ -31,6 +30,7 @@ import (
"github.com/go-kit/kit/log" "github.com/go-kit/kit/log"
"github.com/go-kit/kit/log/level" "github.com/go-kit/kit/log/level"
"github.com/pkg/errors"
commoncfg "github.com/prometheus/common/config" commoncfg "github.com/prometheus/common/config"
"github.com/prometheus/alertmanager/config" "github.com/prometheus/alertmanager/config"
@ -92,7 +92,7 @@ func (n *Email) auth(mechs string) (smtp.Auth, error) {
// We need to know the hostname for both auth and TLS. // We need to know the hostname for both auth and TLS.
host, _, err := net.SplitHostPort(n.conf.Smarthost) host, _, err := net.SplitHostPort(n.conf.Smarthost)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid address: %s", err) return nil, errors.Wrap(err, "split address")
} }
return smtp.PlainAuth(identity, username, password, host), nil return smtp.PlainAuth(identity, username, password, host), nil
case "LOGIN": case "LOGIN":
@ -112,77 +112,87 @@ func (n *Email) auth(mechs string) (smtp.Auth, error) {
// Notify implements the Notifier interface. // Notify implements the Notifier interface.
func (n *Email) Notify(ctx context.Context, as ...*types.Alert) (bool, error) { func (n *Email) Notify(ctx context.Context, as ...*types.Alert) (bool, error) {
// TODO: move the check to the config package.
// We need to know the hostname for both auth and TLS. // We need to know the hostname for both auth and TLS.
var c *smtp.Client
host, port, err := net.SplitHostPort(n.conf.Smarthost) host, port, err := net.SplitHostPort(n.conf.Smarthost)
if err != nil { if err != nil {
return false, fmt.Errorf("invalid address: %s", err) return false, errors.Wrap(err, "split address")
} }
var (
c *smtp.Client
conn net.Conn
success = false
)
if port == "465" { if port == "465" {
tlsConfig, err := commoncfg.NewTLSConfig(&n.conf.TLSConfig) tlsConfig, err := commoncfg.NewTLSConfig(&n.conf.TLSConfig)
if err != nil { if err != nil {
return false, err return false, errors.Wrap(err, "parse TLS configuration")
} }
if tlsConfig.ServerName == "" { if tlsConfig.ServerName == "" {
tlsConfig.ServerName = host tlsConfig.ServerName = host
} }
conn, err := tls.Dial("tcp", n.conf.Smarthost, tlsConfig) conn, err = tls.Dial("tcp", n.conf.Smarthost, tlsConfig)
if err != nil { if err != nil {
return true, err return true, errors.Wrap(err, "establish TLS connection to server")
}
} else {
var (
d = net.Dialer{}
err error
)
conn, err = d.DialContext(ctx, "tcp", n.conf.Smarthost)
if err != nil {
return true, errors.Wrap(err, "establish connection to server")
}
} }
c, err = smtp.NewClient(conn, host) c, err = smtp.NewClient(conn, host)
if err != nil { if err != nil {
return true, err conn.Close()
} return true, errors.Wrap(err, "create SMTP client")
} else {
// Connect to the SMTP smarthost.
c, err = smtp.Dial(n.conf.Smarthost)
if err != nil {
return true, err
}
} }
defer func() { defer func() {
if err := c.Quit(); err != nil { // Try to clean up after ourselves but don't log anything if something has failed.
level.Error(n.logger).Log("msg", "failed to close SMTP connection", "err", err) if err := c.Quit(); success && err != nil {
level.Warn(n.logger).Log("msg", "failed to close SMTP connection", "err", err)
} }
}() }()
if n.conf.Hello != "" { if n.conf.Hello != "" {
err := c.Hello(n.conf.Hello) err = c.Hello(n.conf.Hello)
if err != nil { if err != nil {
return true, err return true, errors.Wrap(err, "send EHLO command")
} }
} }
// Global Config guarantees RequireTLS is not nil. // Global Config guarantees RequireTLS is not nil.
if *n.conf.RequireTLS { if *n.conf.RequireTLS {
if ok, _ := c.Extension("STARTTLS"); !ok { if ok, _ := c.Extension("STARTTLS"); !ok {
return true, fmt.Errorf("require_tls: true (default), but %q does not advertise the STARTTLS extension", n.conf.Smarthost) return true, errors.Errorf("'require_tls' is true (default) but %q does not advertise the STARTTLS extension", n.conf.Smarthost)
} }
tlsConf, err := commoncfg.NewTLSConfig(&n.conf.TLSConfig) tlsConf, err := commoncfg.NewTLSConfig(&n.conf.TLSConfig)
if err != nil { if err != nil {
return false, err return false, errors.Wrap(err, "parse TLS configuration")
} }
if tlsConf.ServerName == "" { if tlsConf.ServerName == "" {
tlsConf.ServerName = host tlsConf.ServerName = host
} }
if err := c.StartTLS(tlsConf); err != nil { if err := c.StartTLS(tlsConf); err != nil {
return true, fmt.Errorf("starttls failed: %s", err) return true, errors.Wrap(err, "send STARTTLS command")
} }
} }
if ok, mech := c.Extension("AUTH"); ok { if ok, mech := c.Extension("AUTH"); ok {
auth, err := n.auth(mech) auth, err := n.auth(mech)
if err != nil { if err != nil {
return true, err return true, errors.Wrap(err, "find auth mechanism")
} }
if auth != nil { if auth != nil {
if err := c.Auth(auth); err != nil { if err := c.Auth(auth); err != nil {
return true, fmt.Errorf("%T failed: %s", auth, err) return true, errors.Wrapf(err, "%T auth", auth)
} }
} }
} }
@ -191,45 +201,48 @@ func (n *Email) Notify(ctx context.Context, as ...*types.Alert) (bool, error) {
tmplErr error tmplErr error
data = notify.GetTemplateData(ctx, n.tmpl, as, n.logger) data = notify.GetTemplateData(ctx, n.tmpl, as, n.logger)
tmpl = notify.TmplText(n.tmpl, data, &tmplErr) tmpl = notify.TmplText(n.tmpl, data, &tmplErr)
from = tmpl(n.conf.From)
to = tmpl(n.conf.To)
) )
from := tmpl(n.conf.From)
if tmplErr != nil { if tmplErr != nil {
return false, fmt.Errorf("failed to template 'from' or 'to': %v", tmplErr) return false, errors.Wrap(tmplErr, "execute 'from' template")
}
to := tmpl(n.conf.To)
if tmplErr != nil {
return false, errors.Wrap(tmplErr, "execute 'to' template")
} }
addrs, err := mail.ParseAddressList(from) addrs, err := mail.ParseAddressList(from)
if err != nil { if err != nil {
return false, fmt.Errorf("parsing from addresses: %s", err) return false, errors.Wrap(err, "parse 'from' addresses")
} }
if len(addrs) != 1 { if len(addrs) != 1 {
return false, fmt.Errorf("must be exactly one from address") return false, errors.Errorf("must be exactly one 'from' address (got: %d)", len(addrs))
} }
if err := c.Mail(addrs[0].Address); err != nil { if err = c.Mail(addrs[0].Address); err != nil {
return true, fmt.Errorf("sending mail from: %s", err) return true, errors.Wrap(err, "send MAIL command")
} }
addrs, err = mail.ParseAddressList(to) addrs, err = mail.ParseAddressList(to)
if err != nil { if err != nil {
return false, fmt.Errorf("parsing to addresses: %s", err) return false, errors.Wrapf(err, "parse 'to' addresses")
} }
for _, addr := range addrs { for _, addr := range addrs {
if err := c.Rcpt(addr.Address); err != nil { if err = c.Rcpt(addr.Address); err != nil {
return true, fmt.Errorf("sending rcpt to: %s", err) return true, errors.Wrapf(err, "send RCPT command")
} }
} }
// Send the email body. // Send the email headers and body.
wc, err := c.Data() message, err := c.Data()
if err != nil { if err != nil {
return true, err return true, errors.Wrapf(err, "send DATA command")
} }
defer wc.Close() defer message.Close()
buffer := &bytes.Buffer{} buffer := &bytes.Buffer{}
for header, t := range n.conf.Headers { for header, t := range n.conf.Headers {
value, err := n.tmpl.ExecuteTextString(t, data) value, err := n.tmpl.ExecuteTextString(t, data)
if err != nil { if err != nil {
return false, fmt.Errorf("executing %q header template: %s", header, err) return false, errors.Wrapf(err, "execute %q header template", header)
} }
fmt.Fprintf(buffer, "%s: %s\r\n", header, mime.QEncoding.Encode("utf-8", value)) fmt.Fprintf(buffer, "%s: %s\r\n", header, mime.QEncoding.Encode("utf-8", value))
} }
@ -243,9 +256,9 @@ func (n *Email) Notify(ctx context.Context, as ...*types.Alert) (bool, error) {
// TODO: Add some useful headers here, such as URL of the alertmanager // TODO: Add some useful headers here, such as URL of the alertmanager
// and active/resolved. // and active/resolved.
_, err = wc.Write(buffer.Bytes()) _, err = message.Write(buffer.Bytes())
if err != nil { if err != nil {
return false, fmt.Errorf("failed to write header buffer: %v", err) return false, errors.Wrap(err, "write headers")
} }
if len(n.conf.Text) > 0 { if len(n.conf.Text) > 0 {
@ -255,20 +268,20 @@ func (n *Email) Notify(ctx context.Context, as ...*types.Alert) (bool, error) {
"Content-Type": {"text/plain; charset=UTF-8"}, "Content-Type": {"text/plain; charset=UTF-8"},
}) })
if err != nil { if err != nil {
return false, fmt.Errorf("creating part for text template: %s", err) return false, errors.Wrap(err, "create part for text template")
} }
body, err := n.tmpl.ExecuteTextString(n.conf.Text, data) body, err := n.tmpl.ExecuteTextString(n.conf.Text, data)
if err != nil { if err != nil {
return false, fmt.Errorf("executing email text template: %s", err) return false, errors.Wrap(err, "execute text template")
} }
qw := quotedprintable.NewWriter(w) qw := quotedprintable.NewWriter(w)
_, err = qw.Write([]byte(body)) _, err = qw.Write([]byte(body))
if err != nil { if err != nil {
return true, err return true, errors.Wrap(err, "write text part")
} }
err = qw.Close() err = qw.Close()
if err != nil { if err != nil {
return true, err return true, errors.Wrap(err, "close text part")
} }
} }
@ -281,33 +294,34 @@ func (n *Email) Notify(ctx context.Context, as ...*types.Alert) (bool, error) {
"Content-Type": {"text/html; charset=UTF-8"}, "Content-Type": {"text/html; charset=UTF-8"},
}) })
if err != nil { if err != nil {
return false, fmt.Errorf("creating part for html template: %s", err) return false, errors.Wrap(err, "create part for html template")
} }
body, err := n.tmpl.ExecuteHTMLString(n.conf.HTML, data) body, err := n.tmpl.ExecuteHTMLString(n.conf.HTML, data)
if err != nil { if err != nil {
return false, fmt.Errorf("executing email html template: %s", err) return false, errors.Wrap(err, "execute html template")
} }
qw := quotedprintable.NewWriter(w) qw := quotedprintable.NewWriter(w)
_, err = qw.Write([]byte(body)) _, err = qw.Write([]byte(body))
if err != nil { if err != nil {
return true, err return true, errors.Wrap(err, "write HTML part")
} }
err = qw.Close() err = qw.Close()
if err != nil { if err != nil {
return true, err return true, errors.Wrap(err, "close HTML part")
} }
} }
err = multipartWriter.Close() err = multipartWriter.Close()
if err != nil { if err != nil {
return false, fmt.Errorf("failed to close multipartWriter: %v", err) return false, errors.Wrap(err, "close multipartWriter")
} }
_, err = wc.Write(multipartBuffer.Bytes()) _, err = message.Write(multipartBuffer.Bytes())
if err != nil { if err != nil {
return false, fmt.Errorf("failed to write body buffer: %v", err) return false, errors.Wrap(err, "write body buffer")
} }
success = true
return false, nil return false, nil
} }

View File

@ -23,7 +23,7 @@
// $ docker run --rm -p 1080:1080 -p 1025:1025 --entrypoint bin/maildev djfarrelly/maildev@sha256:624e0ec781e11c3531da83d9448f5861f258ee008c1b2da63b3248bfd680acfa -v // $ docker run --rm -p 1080:1080 -p 1025:1025 --entrypoint bin/maildev djfarrelly/maildev@sha256:624e0ec781e11c3531da83d9448f5861f258ee008c1b2da63b3248bfd680acfa -v
// $ docker run --rm -p 1081:1080 -p 1026:1025 --entrypoint bin/maildev djfarrelly/maildev@sha256:624e0ec781e11c3531da83d9448f5861f258ee008c1b2da63b3248bfd680acfa --incoming-user user --incoming-pass pass -v // $ docker run --rm -p 1081:1080 -p 1026:1025 --entrypoint bin/maildev djfarrelly/maildev@sha256:624e0ec781e11c3531da83d9448f5861f258ee008c1b2da63b3248bfd680acfa --incoming-user user --incoming-pass pass -v
// //
// $ EMAIL_NO_AUTH_CONFIG=testdata/email_noauth.yml EMAIL_AUTH_CONFIG=testdata/email_auth.yml make // $ EMAIL_NO_AUTH_CONFIG=testdata/noauth.yml EMAIL_AUTH_CONFIG=testdata/auth.yml make
// //
// See also https://github.com/djfarrelly/MailDev for more details. // See also https://github.com/djfarrelly/MailDev for more details.
package email package email
@ -103,7 +103,7 @@ func (m *mailDev) getLastEmail() (*email, error) {
return nil, err return nil, err
} }
if len(emails) == 0 { if len(emails) == 0 {
return nil, fmt.Errorf("expected non-empty list of emails") return nil, nil
} }
return &emails[len(emails)-1], nil return &emails[len(emails)-1], nil
} }
@ -158,9 +158,13 @@ func loadEmailTestConfiguration(f string) (emailTestConfig, error) {
return c, nil return c, nil
} }
// notifyEmail sends a notification with one firing alert and retrieves the
// email from the SMTP server.
func notifyEmail(cfg *config.EmailConfig, server *mailDev) (*email, bool, error) { func notifyEmail(cfg *config.EmailConfig, server *mailDev) (*email, bool, error) {
return notifyEmailWithContext(context.Background(), cfg, server)
}
// notifyEmailWithContext sends a notification with one firing alert and retrieves the
// email from the SMTP server if the notification has been successfully delivered.
func notifyEmailWithContext(ctx context.Context, cfg *config.EmailConfig, server *mailDev) (*email, bool, error) {
if cfg.RequireTLS == nil { if cfg.RequireTLS == nil {
cfg.RequireTLS = new(bool) cfg.RequireTLS = new(bool)
} }
@ -186,14 +190,163 @@ func notifyEmail(cfg *config.EmailConfig, server *mailDev) (*email, bool, error)
tmpl.ExternalURL, _ = url.Parse("http://am") tmpl.ExternalURL, _ = url.Parse("http://am")
email := New(cfg, tmpl, log.NewNopLogger()) email := New(cfg, tmpl, log.NewNopLogger())
ctx := context.Background()
retry, err := email.Notify(ctx, firingAlert) retry, err := email.Notify(ctx, firingAlert)
if err != nil { if err != nil {
return nil, retry, err return nil, retry, err
} }
e, err := server.getLastEmail() e, err := server.getLastEmail()
return e, retry, err if err != nil {
return nil, retry, err
} else if e == nil {
return nil, retry, fmt.Errorf("email not found")
}
return e, retry, nil
}
// TestEmailNotifyWithErrors tries to send emails with buggy inputs.
func TestEmailNotifyWithErrors(t *testing.T) {
cfgFile := os.Getenv(emailNoAuthConfigVar)
if len(cfgFile) == 0 {
t.Skipf("%s not set", emailNoAuthConfigVar)
}
c, err := loadEmailTestConfiguration(cfgFile)
if err != nil {
t.Fatal(err)
}
for _, tc := range []struct {
title string
updateCfg func(*config.EmailConfig)
errMsg string
hasEmail bool
}{
{
title: "invalid address",
updateCfg: func(cfg *config.EmailConfig) {
cfg.Smarthost = "example.com"
},
errMsg: "split address:",
},
{
title: "invalid 'from' template",
updateCfg: func(cfg *config.EmailConfig) {
cfg.From = `{{ template "invalid" }}`
},
errMsg: "execute 'from' template:",
},
{
title: "invalid 'from' address",
updateCfg: func(cfg *config.EmailConfig) {
cfg.From = `xxx`
},
errMsg: "parse 'from' addresses:",
},
{
title: "invalid 'to' template",
updateCfg: func(cfg *config.EmailConfig) {
cfg.To = `{{ template "invalid" }}`
},
errMsg: "execute 'to' template:",
},
{
title: "invalid 'to' address",
updateCfg: func(cfg *config.EmailConfig) {
cfg.To = `xxx`
},
errMsg: "parse 'to' addresses:",
},
{
title: "invalid 'subject' template",
updateCfg: func(cfg *config.EmailConfig) {
cfg.Headers["subject"] = `{{ template "invalid" }}`
},
errMsg: `execute "subject" header template:`,
hasEmail: true,
},
{
title: "invalid 'text' template",
updateCfg: func(cfg *config.EmailConfig) {
cfg.Text = `{{ template "invalid" }}`
},
errMsg: `execute text template:`,
hasEmail: true,
},
{
title: "invalid 'html' template",
updateCfg: func(cfg *config.EmailConfig) {
cfg.HTML = `{{ template "invalid" }}`
},
errMsg: `execute html template:`,
hasEmail: true,
},
} {
tc := tc
t.Run(tc.title, func(t *testing.T) {
if len(tc.errMsg) == 0 {
t.Fatal("please define the expected error message")
return
}
emailCfg := &config.EmailConfig{
Smarthost: c.Smarthost,
To: emailTo,
From: emailFrom,
HTML: "HTML body",
Text: "Text body",
Headers: map[string]string{
"Subject": "{{ len .Alerts }} {{ .Status }} alert(s)",
},
}
if tc.updateCfg != nil {
tc.updateCfg(emailCfg)
}
_, retry, err := notifyEmail(emailCfg, c.Server)
require.Error(t, err)
require.Contains(t, err.Error(), tc.errMsg)
require.Equal(t, false, retry)
e, err := c.Server.getLastEmail()
require.NoError(t, err)
if tc.hasEmail {
require.NotNil(t, e)
} else {
require.Nil(t, e)
}
})
}
}
// TestEmailNotifyWithDoneContext tries to send an email with a context that is done.
func TestEmailNotifyWithDoneContext(t *testing.T) {
cfgFile := os.Getenv(emailNoAuthConfigVar)
if len(cfgFile) == 0 {
t.Skipf("%s not set", emailNoAuthConfigVar)
}
c, err := loadEmailTestConfiguration(cfgFile)
if err != nil {
t.Fatal(err)
}
ctx, cancel := context.WithCancel(context.Background())
cancel()
_, _, err = notifyEmailWithContext(
ctx,
&config.EmailConfig{
Smarthost: c.Smarthost,
To: emailTo,
From: emailFrom,
HTML: "HTML body",
Text: "Text body",
},
c.Server,
)
require.Error(t, err)
require.Contains(t, err.Error(), "establish connection to server")
} }
// TestEmailNotifyWithoutAuthentication sends an email to an instance of // TestEmailNotifyWithoutAuthentication sends an email to an instance of
@ -269,20 +422,22 @@ func TestEmailNotifyWithAuthentication(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
testCases := []struct { for _, tc := range []struct {
title string
updateCfg func(*config.EmailConfig) updateCfg func(*config.EmailConfig)
errMsg string errMsg string
retry bool retry bool
}{ }{
{ {
title: "email with authentication",
updateCfg: func(cfg *config.EmailConfig) { updateCfg: func(cfg *config.EmailConfig) {
cfg.AuthUsername = c.Username cfg.AuthUsername = c.Username
cfg.AuthPassword = config.Secret(c.Password) cfg.AuthPassword = config.Secret(c.Password)
}, },
}, },
{ {
// HTML-only email. title: "HTML-only email",
updateCfg: func(cfg *config.EmailConfig) { updateCfg: func(cfg *config.EmailConfig) {
cfg.AuthUsername = c.Username cfg.AuthUsername = c.Username
cfg.AuthPassword = config.Secret(c.Password) cfg.AuthPassword = config.Secret(c.Password)
@ -290,7 +445,7 @@ func TestEmailNotifyWithAuthentication(t *testing.T) {
}, },
}, },
{ {
// text-only email. title: "text-only email",
updateCfg: func(cfg *config.EmailConfig) { updateCfg: func(cfg *config.EmailConfig) {
cfg.AuthUsername = c.Username cfg.AuthUsername = c.Username
cfg.AuthPassword = config.Secret(c.Password) cfg.AuthPassword = config.Secret(c.Password)
@ -298,7 +453,7 @@ func TestEmailNotifyWithAuthentication(t *testing.T) {
}, },
}, },
{ {
// Multiple To addresses. title: "multiple To addresses",
updateCfg: func(cfg *config.EmailConfig) { updateCfg: func(cfg *config.EmailConfig) {
cfg.AuthUsername = c.Username cfg.AuthUsername = c.Username
cfg.AuthPassword = config.Secret(c.Password) cfg.AuthPassword = config.Secret(c.Password)
@ -306,18 +461,18 @@ func TestEmailNotifyWithAuthentication(t *testing.T) {
}, },
}, },
{ {
// No more than one From address. title: "no more than one From address",
updateCfg: func(cfg *config.EmailConfig) { updateCfg: func(cfg *config.EmailConfig) {
cfg.AuthUsername = c.Username cfg.AuthUsername = c.Username
cfg.AuthPassword = config.Secret(c.Password) cfg.AuthPassword = config.Secret(c.Password)
cfg.From = strings.Join([]string{emailFrom, emailTo}, ",") cfg.From = strings.Join([]string{emailFrom, emailTo}, ",")
}, },
errMsg: "must be exactly one from address", errMsg: "must be exactly one 'from' address",
retry: false, retry: false,
}, },
{ {
// Wrong credentials. title: "wrong credentials",
updateCfg: func(cfg *config.EmailConfig) { updateCfg: func(cfg *config.EmailConfig) {
cfg.AuthUsername = c.Username cfg.AuthUsername = c.Username
cfg.AuthPassword = config.Secret(c.Password + "wrong") cfg.AuthPassword = config.Secret(c.Password + "wrong")
@ -327,12 +482,12 @@ func TestEmailNotifyWithAuthentication(t *testing.T) {
retry: true, retry: true,
}, },
{ {
// No credentials. title: "no credentials",
errMsg: "authentication Required", errMsg: "authentication Required",
retry: true, retry: true,
}, },
{ {
// Fail to enable STARTTLS. title: "try to enable STARTTLS",
updateCfg: func(cfg *config.EmailConfig) { updateCfg: func(cfg *config.EmailConfig) {
cfg.RequireTLS = new(bool) cfg.RequireTLS = new(bool)
*cfg.RequireTLS = true *cfg.RequireTLS = true
@ -342,7 +497,7 @@ func TestEmailNotifyWithAuthentication(t *testing.T) {
retry: true, retry: true,
}, },
{ {
// Invalid Hello string. title: "invalid Hello string",
updateCfg: func(cfg *config.EmailConfig) { updateCfg: func(cfg *config.EmailConfig) {
cfg.AuthUsername = c.Username cfg.AuthUsername = c.Username
cfg.AuthPassword = config.Secret(c.Password) cfg.AuthPassword = config.Secret(c.Password)
@ -352,11 +507,9 @@ func TestEmailNotifyWithAuthentication(t *testing.T) {
errMsg: "501 Error", errMsg: "501 Error",
retry: true, retry: true,
}, },
} } {
for _, tc := range testCases {
tc := tc tc := tc
t.Run("", func(t *testing.T) { t.Run(tc.title, func(t *testing.T) {
emailCfg := &config.EmailConfig{ emailCfg := &config.EmailConfig{
Smarthost: c.Smarthost, Smarthost: c.Smarthost,
To: emailTo, To: emailTo,

4
notify/email/testdata/auth.yml vendored Normal file
View File

@ -0,0 +1,4 @@
smarthost: localhost:1026
server: http://localhost:1081/
username: user
password: pass

2
notify/email/testdata/noauth.yml vendored Normal file
View File

@ -0,0 +1,2 @@
smarthost: localhost:1025
server: http://localhost:1080/

View File

@ -364,7 +364,7 @@ func (fs FanoutStage) Exec(ctx context.Context, l log.Logger, alerts ...*types.A
// message should only be logged at the debug level. // message should only be logged at the debug level.
lvl = level.Debug(l) lvl = level.Debug(l)
} }
lvl.Log("msg", "Error on notify", "err", err) lvl.Log("msg", "Error on notify", "err", err, "context_err", ctx.Err())
} }
wg.Done() wg.Done()
}(s) }(s)