dispatch returns a DeliveryFunc capable of delivering a notification via SMTP. Our requirements are too complex to be implemented using smtp.SendMail: - we require custom TLS settings - dynamic determination of available AUTH mechanisms NOTE: this is inspired by Alertmanager's email notifier: http
(subject, htmlBody, plainBody, to string)
| 97 | // NOTE: this is inspired by Alertmanager's email notifier: |
| 98 | // https://github.com/prometheus/alertmanager/blob/342f6a599ce16c138663f18ed0b880e777c3017d/notify/email/email.go |
| 99 | func (s *SMTPHandler) dispatch(subject, htmlBody, plainBody, to string) DeliveryFunc { |
| 100 | return func(ctx context.Context, msgID uuid.UUID) (bool, error) { |
| 101 | select { |
| 102 | case <-ctx.Done(): |
| 103 | return false, ctx.Err() |
| 104 | default: |
| 105 | } |
| 106 | |
| 107 | s.log.Debug(ctx, "dispatching via SMTP", slog.F("msg_id", msgID)) |
| 108 | |
| 109 | // Dial the smarthost to establish a connection. |
| 110 | smarthost, smarthostPort, err := s.smarthost() |
| 111 | if err != nil { |
| 112 | return false, xerrors.Errorf("'smarthost' validation: %w", err) |
| 113 | } |
| 114 | |
| 115 | // Outer context has a deadline (see CODER_NOTIFICATIONS_DISPATCH_TIMEOUT). |
| 116 | if _, ok := ctx.Deadline(); !ok { |
| 117 | return false, xerrors.Errorf("context has no deadline") |
| 118 | } |
| 119 | |
| 120 | // TODO: reuse client across dispatches (if possible). |
| 121 | // Create an SMTP client for communication with the smarthost. |
| 122 | c, err := s.client(ctx, smarthost, smarthostPort) |
| 123 | if err != nil { |
| 124 | return true, xerrors.Errorf("SMTP client creation: %w", err) |
| 125 | } |
| 126 | |
| 127 | // Cleanup. |
| 128 | defer func() { |
| 129 | if err := c.Quit(); err != nil { |
| 130 | s.log.Warn(ctx, "failed to close SMTP connection", slog.Error(err)) |
| 131 | } |
| 132 | }() |
| 133 | |
| 134 | // Check for authentication capabilities. |
| 135 | if ok, avail := c.Extension("AUTH"); ok { |
| 136 | // Ensure the auth mechanisms available are ones we can use, and create a SASL client. |
| 137 | auth, err := s.auth(ctx, avail) |
| 138 | if err != nil { |
| 139 | return true, xerrors.Errorf("determine auth mechanism: %w", err) |
| 140 | } |
| 141 | |
| 142 | if auth == nil { |
| 143 | // If we get here, no SASL client (which handles authentication) was returned. |
| 144 | // This is expected if auth is supported by the smarthost BUT no authentication details were configured. |
| 145 | s.noAuthWarnOnce.Do(func() { |
| 146 | s.log.Warn(ctx, "skipping auth; no authentication client created") |
| 147 | }) |
| 148 | } else { |
| 149 | // We have a SASL client, use it to authenticate. |
| 150 | if err := c.Auth(auth); err != nil { |
| 151 | return true, xerrors.Errorf("%T auth: %w", auth, err) |
| 152 | } |
| 153 | } |
| 154 | } else if !s.cfg.Auth.Empty() { |
| 155 | return false, xerrors.New("no authentication mechanisms supported by server") |
| 156 | } |
no test coverage detected