Go service (nasmon/) plus nfpm packaging (packaging/), carried over from the home_nas monorepo. Adds rpm packaging alongside the existing deb build (Fedora/RHEL/Rocky dependency names via nfpm overrides, per-format systemd unit path, dpkg+rpm-compatible maintainer scripts) and a Gitea Actions workflow that builds both formats on every push. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
129 lines
3.3 KiB
Go
129 lines
3.3 KiB
Go
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)
|
|
}
|