1
2
3
4
5
6
7
8
9
10
11
12
13
14 package email
15
16 import (
17 "bytes"
18 "context"
19 "crypto/tls"
20 "fmt"
21 "math/rand"
22 "mime"
23 "mime/multipart"
24 "mime/quotedprintable"
25 "net"
26 "net/mail"
27 "net/smtp"
28 "net/textproto"
29 "os"
30 "strings"
31 "time"
32
33 "github.com/go-kit/log"
34 "github.com/go-kit/log/level"
35 "github.com/pkg/errors"
36 commoncfg "github.com/prometheus/common/config"
37
38 "github.com/prometheus/alertmanager/config"
39 "github.com/prometheus/alertmanager/notify"
40 "github.com/prometheus/alertmanager/template"
41 "github.com/prometheus/alertmanager/types"
42 )
43
44
45 type Email struct {
46 conf *config.EmailConfig
47 tmpl *template.Template
48 logger log.Logger
49 hostname string
50 }
51
52
53 func New(c *config.EmailConfig, t *template.Template, l log.Logger) *Email {
54 if _, ok := c.Headers["Subject"]; !ok {
55 c.Headers["Subject"] = config.DefaultEmailSubject
56 }
57 if _, ok := c.Headers["To"]; !ok {
58 c.Headers["To"] = c.To
59 }
60 if _, ok := c.Headers["From"]; !ok {
61 c.Headers["From"] = c.From
62 }
63
64 h, err := os.Hostname()
65
66 if err != nil {
67 h = "localhost.localdomain"
68 }
69 return &Email{conf: c, tmpl: t, logger: l, hostname: h}
70 }
71
72
73 func (n *Email) auth(mechs string) (smtp.Auth, error) {
74 username := n.conf.AuthUsername
75
76
77 if n.conf.AuthUsername == "" {
78 level.Debug(n.logger).Log("msg", "smtp_auth_username is not configured. Attempting to send email without authenticating")
79 return nil, nil
80 }
81
82 err := &types.MultiError{}
83 for _, mech := range strings.Split(mechs, " ") {
84 switch mech {
85 case "CRAM-MD5":
86 secret := string(n.conf.AuthSecret)
87 if secret == "" {
88 err.Add(errors.New("missing secret for CRAM-MD5 auth mechanism"))
89 continue
90 }
91 return smtp.CRAMMD5Auth(username, secret), nil
92
93 case "PLAIN":
94 password, passwordErr := n.getPassword()
95 if passwordErr != nil {
96 err.Add(passwordErr)
97 continue
98 }
99 if password == "" {
100 err.Add(errors.New("missing password for PLAIN auth mechanism"))
101 continue
102 }
103 identity := n.conf.AuthIdentity
104
105 return smtp.PlainAuth(identity, username, password, n.conf.Smarthost.Host), nil
106 case "LOGIN":
107 password, passwordErr := n.getPassword()
108 if passwordErr != nil {
109 err.Add(passwordErr)
110 continue
111 }
112 if password == "" {
113 err.Add(errors.New("missing password for LOGIN auth mechanism"))
114 continue
115 }
116 return LoginAuth(username, password), nil
117 }
118 }
119 if err.Len() == 0 {
120 err.Add(errors.New("unknown auth mechanism: " + mechs))
121 }
122 return nil, err
123 }
124
125
126 func (n *Email) Notify(ctx context.Context, as ...*types.Alert) (bool, error) {
127 var (
128 c *smtp.Client
129 conn net.Conn
130 err error
131 success = false
132 )
133 if n.conf.Smarthost.Port == "465" {
134 tlsConfig, err := commoncfg.NewTLSConfig(&n.conf.TLSConfig)
135 if err != nil {
136 return false, errors.Wrap(err, "parse TLS configuration")
137 }
138 if tlsConfig.ServerName == "" {
139 tlsConfig.ServerName = n.conf.Smarthost.Host
140 }
141
142 conn, err = tls.Dial("tcp", n.conf.Smarthost.String(), tlsConfig)
143 if err != nil {
144 return true, errors.Wrap(err, "establish TLS connection to server")
145 }
146 } else {
147 var (
148 d = net.Dialer{}
149 err error
150 )
151 conn, err = d.DialContext(ctx, "tcp", n.conf.Smarthost.String())
152 if err != nil {
153 return true, errors.Wrap(err, "establish connection to server")
154 }
155 }
156 c, err = smtp.NewClient(conn, n.conf.Smarthost.Host)
157 if err != nil {
158 conn.Close()
159 return true, errors.Wrap(err, "create SMTP client")
160 }
161 defer func() {
162
163 if err := c.Quit(); success && err != nil {
164 level.Warn(n.logger).Log("msg", "failed to close SMTP connection", "err", err)
165 }
166 }()
167
168 if n.conf.Hello != "" {
169 err = c.Hello(n.conf.Hello)
170 if err != nil {
171 return true, errors.Wrap(err, "send EHLO command")
172 }
173 }
174
175
176 if *n.conf.RequireTLS {
177 if ok, _ := c.Extension("STARTTLS"); !ok {
178 return true, errors.Errorf("'require_tls' is true (default) but %q does not advertise the STARTTLS extension", n.conf.Smarthost)
179 }
180
181 tlsConf, err := commoncfg.NewTLSConfig(&n.conf.TLSConfig)
182 if err != nil {
183 return false, errors.Wrap(err, "parse TLS configuration")
184 }
185 if tlsConf.ServerName == "" {
186 tlsConf.ServerName = n.conf.Smarthost.Host
187 }
188
189 if err := c.StartTLS(tlsConf); err != nil {
190 return true, errors.Wrap(err, "send STARTTLS command")
191 }
192 }
193
194 if ok, mech := c.Extension("AUTH"); ok {
195 auth, err := n.auth(mech)
196 if err != nil {
197 return true, errors.Wrap(err, "find auth mechanism")
198 }
199 if auth != nil {
200 if err := c.Auth(auth); err != nil {
201 return true, errors.Wrapf(err, "%T auth", auth)
202 }
203 }
204 }
205
206 var (
207 tmplErr error
208 data = notify.GetTemplateData(ctx, n.tmpl, as, n.logger)
209 tmpl = notify.TmplText(n.tmpl, data, &tmplErr)
210 )
211 from := tmpl(n.conf.From)
212 if tmplErr != nil {
213 return false, errors.Wrap(tmplErr, "execute 'from' template")
214 }
215 to := tmpl(n.conf.To)
216 if tmplErr != nil {
217 return false, errors.Wrap(tmplErr, "execute 'to' template")
218 }
219
220 addrs, err := mail.ParseAddressList(from)
221 if err != nil {
222 return false, errors.Wrap(err, "parse 'from' addresses")
223 }
224 if len(addrs) != 1 {
225 return false, errors.Errorf("must be exactly one 'from' address (got: %d)", len(addrs))
226 }
227 if err = c.Mail(addrs[0].Address); err != nil {
228 return true, errors.Wrap(err, "send MAIL command")
229 }
230 addrs, err = mail.ParseAddressList(to)
231 if err != nil {
232 return false, errors.Wrapf(err, "parse 'to' addresses")
233 }
234 for _, addr := range addrs {
235 if err = c.Rcpt(addr.Address); err != nil {
236 return true, errors.Wrapf(err, "send RCPT command")
237 }
238 }
239
240
241 message, err := c.Data()
242 if err != nil {
243 return true, errors.Wrapf(err, "send DATA command")
244 }
245 defer message.Close()
246
247 buffer := &bytes.Buffer{}
248 for header, t := range n.conf.Headers {
249 value, err := n.tmpl.ExecuteTextString(t, data)
250 if err != nil {
251 return false, errors.Wrapf(err, "execute %q header template", header)
252 }
253 fmt.Fprintf(buffer, "%s: %s\r\n", header, mime.QEncoding.Encode("utf-8", value))
254 }
255
256 if _, ok := n.conf.Headers["Message-Id"]; !ok {
257 fmt.Fprintf(buffer, "Message-Id: %s\r\n", fmt.Sprintf("<%d.%d@%s>", time.Now().UnixNano(), rand.Uint64(), n.hostname))
258 }
259
260 multipartBuffer := &bytes.Buffer{}
261 multipartWriter := multipart.NewWriter(multipartBuffer)
262
263 fmt.Fprintf(buffer, "Date: %s\r\n", time.Now().Format(time.RFC1123Z))
264 fmt.Fprintf(buffer, "Content-Type: multipart/alternative; boundary=%s\r\n", multipartWriter.Boundary())
265 fmt.Fprintf(buffer, "MIME-Version: 1.0\r\n\r\n")
266
267
268
269 _, err = message.Write(buffer.Bytes())
270 if err != nil {
271 return false, errors.Wrap(err, "write headers")
272 }
273
274 if len(n.conf.Text) > 0 {
275
276 w, err := multipartWriter.CreatePart(textproto.MIMEHeader{
277 "Content-Transfer-Encoding": {"quoted-printable"},
278 "Content-Type": {"text/plain; charset=UTF-8"},
279 })
280 if err != nil {
281 return false, errors.Wrap(err, "create part for text template")
282 }
283 body, err := n.tmpl.ExecuteTextString(n.conf.Text, data)
284 if err != nil {
285 return false, errors.Wrap(err, "execute text template")
286 }
287 qw := quotedprintable.NewWriter(w)
288 _, err = qw.Write([]byte(body))
289 if err != nil {
290 return true, errors.Wrap(err, "write text part")
291 }
292 err = qw.Close()
293 if err != nil {
294 return true, errors.Wrap(err, "close text part")
295 }
296 }
297
298 if len(n.conf.HTML) > 0 {
299
300
301
302 w, err := multipartWriter.CreatePart(textproto.MIMEHeader{
303 "Content-Transfer-Encoding": {"quoted-printable"},
304 "Content-Type": {"text/html; charset=UTF-8"},
305 })
306 if err != nil {
307 return false, errors.Wrap(err, "create part for html template")
308 }
309 body, err := n.tmpl.ExecuteHTMLString(n.conf.HTML, data)
310 if err != nil {
311 return false, errors.Wrap(err, "execute html template")
312 }
313 qw := quotedprintable.NewWriter(w)
314 _, err = qw.Write([]byte(body))
315 if err != nil {
316 return true, errors.Wrap(err, "write HTML part")
317 }
318 err = qw.Close()
319 if err != nil {
320 return true, errors.Wrap(err, "close HTML part")
321 }
322 }
323
324 err = multipartWriter.Close()
325 if err != nil {
326 return false, errors.Wrap(err, "close multipartWriter")
327 }
328
329 _, err = message.Write(multipartBuffer.Bytes())
330 if err != nil {
331 return false, errors.Wrap(err, "write body buffer")
332 }
333
334 success = true
335 return false, nil
336 }
337
338 type loginAuth struct {
339 username, password string
340 }
341
342 func LoginAuth(username, password string) smtp.Auth {
343 return &loginAuth{username, password}
344 }
345
346 func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
347 return "LOGIN", []byte{}, nil
348 }
349
350
351 func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
352 if more {
353 switch strings.ToLower(string(fromServer)) {
354 case "username:":
355 return []byte(a.username), nil
356 case "password:":
357 return []byte(a.password), nil
358 default:
359 return nil, errors.New("unexpected server challenge")
360 }
361 }
362 return nil, nil
363 }
364
365 func (n *Email) getPassword() (string, error) {
366 if len(n.conf.AuthPasswordFile) > 0 {
367 content, err := os.ReadFile(n.conf.AuthPasswordFile)
368 if err != nil {
369 return "", fmt.Errorf("could not read %s: %w", n.conf.AuthPasswordFile, err)
370 }
371 return string(content), nil
372 }
373 return string(n.conf.AuthPassword), nil
374 }
375
View as plain text