Initial import: nasmon health monitor, split out of home_nas
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>
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user