package alert import ( "crypto/tls" "errors" "fmt" "mime" "net" "net/smtp" "strings" "time" "nasmon/internal/config" ) // SendMail delivers a plain-text message using the configured SMTP account. func SendMail(cfg config.SMTP, subject, body string) error { if cfg.Host == "" { return errors.New("SMTP host is not configured") } if len(cfg.To) == 0 { return errors.New("no recipients configured") } from := cfg.From if from == "" { from = cfg.User } if from == "" { return errors.New("sender address (from) is not configured") } addr := net.JoinHostPort(cfg.Host, fmt.Sprint(cfg.Port)) dialer := net.Dialer{Timeout: 20 * time.Second} var c *smtp.Client switch cfg.Security { case "tls": conn, err := tls.DialWithDialer(&dialer, "tcp", addr, &tls.Config{ServerName: cfg.Host}) if err != nil { return fmt.Errorf("tls connect: %w", err) } c, err = smtp.NewClient(conn, cfg.Host) if err != nil { conn.Close() return err } default: conn, err := dialer.Dial("tcp", addr) if err != nil { return fmt.Errorf("connect: %w", err) } conn.SetDeadline(time.Now().Add(60 * time.Second)) c, err = smtp.NewClient(conn, cfg.Host) if err != nil { conn.Close() return err } if cfg.Security == "starttls" { if ok, _ := c.Extension("STARTTLS"); !ok { c.Close() return errors.New("server does not support STARTTLS") } if err := c.StartTLS(&tls.Config{ServerName: cfg.Host}); err != nil { c.Close() return fmt.Errorf("starttls: %w", err) } } } defer c.Close() if cfg.User != "" { ok, mechs := c.Extension("AUTH") var auth smtp.Auth switch { case ok && strings.Contains(mechs, "PLAIN"): auth = smtp.PlainAuth("", cfg.User, cfg.Pass, cfg.Host) case ok && strings.Contains(mechs, "LOGIN"): auth = loginAuth{cfg.User, cfg.Pass} case ok && strings.Contains(mechs, "CRAM-MD5"): auth = smtp.CRAMMD5Auth(cfg.User, cfg.Pass) default: auth = smtp.PlainAuth("", cfg.User, cfg.Pass, cfg.Host) } if err := c.Auth(auth); err != nil { return fmt.Errorf("auth: %w", err) } } if err := c.Mail(from); err != nil { return fmt.Errorf("MAIL FROM: %w", err) } for _, to := range cfg.To { if err := c.Rcpt(to); err != nil { return fmt.Errorf("RCPT TO %s: %w", to, err) } } w, err := c.Data() if err != nil { return err } msg := fmt.Sprintf("From: %s\r\nTo: %s\r\nSubject: %s\r\nDate: %s\r\nMIME-Version: 1.0\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Transfer-Encoding: 8bit\r\n\r\n%s", from, strings.Join(cfg.To, ", "), mime.QEncoding.Encode("utf-8", subject), time.Now().Format(time.RFC1123Z), strings.ReplaceAll(body, "\n", "\r\n")) if _, err := w.Write([]byte(msg)); err != nil { return err } if err := w.Close(); err != nil { return err } return c.Quit() } // loginAuth implements the legacy AUTH LOGIN mechanism (Yandex, Mail.ru, Exchange). type loginAuth struct{ user, pass string } func (a loginAuth) Start(*smtp.ServerInfo) (string, []byte, error) { return "LOGIN", nil, nil } func (a loginAuth) Next(fromServer []byte, more bool) ([]byte, error) { if !more { return nil, nil } switch strings.ToLower(strings.TrimSpace(string(fromServer))) { case "username:": return []byte(a.user), nil case "password:": return []byte(a.pass), nil } return nil, fmt.Errorf("unexpected LOGIN challenge %q", fromServer) }