Files
josh 9bb4b09a04
CI / Lint + build + test (push) Has been cancelled
Initial commit: full Phases 1-6 implementation
Post-repair hardware validation pipeline for Proxmox cluster hosts.
Go orchestrator + in-image agent + mkosi live image + bundled dnsmasq
PXE + SQLite + HTMX/SSE UI + notify registry + janitor + full docs.
2026-04-17 21:32:10 -04:00

82 lines
2.3 KiB
Go

package notify
import (
"context"
"fmt"
"net/smtp"
"strconv"
"strings"
)
// SMTPNotifier sends a plaintext email. Authentication is left at zero
// (LAN-only relay assumed); if the configured server requires auth the
// Send call will return an error and the Registry will log it.
//
// SendMailFn is overridable so tests can capture the outgoing message
// without needing a live SMTP server.
type SMTPNotifier struct {
NameStr string
Host string
Port int
From string
To []string
SendMailFn func(addr string, a smtp.Auth, from string, to []string, msg []byte) error
}
func NewSMTP(name, host string, port int, from string, to []string) *SMTPNotifier {
if port == 0 {
port = 25
}
return &SMTPNotifier{
NameStr: name,
Host: host,
Port: port,
From: from,
To: to,
SendMailFn: smtp.SendMail,
}
}
func (s *SMTPNotifier) Name() string { return s.NameStr }
func (s *SMTPNotifier) Send(ctx context.Context, ev Event) error {
if s.Host == "" || s.From == "" || len(s.To) == 0 {
return fmt.Errorf("smtp: incomplete config (host/from/to required)")
}
// We intentionally don't honour ctx here — net/smtp.SendMail doesn't
// accept a context; for a LAN relay with a short TCP timeout the
// Registry's goroutine will outlive the timeout but only by seconds.
addr := s.Host + ":" + strconv.Itoa(s.Port)
msg := buildEmail(s.From, s.To, ev)
return s.SendMailFn(addr, nil, s.From, s.To, msg)
}
// buildEmail produces an RFC 5322 minimal message. Body is plaintext;
// the URL is appended so the recipient can click through from a text
// mail client. No MIME for now — keeps it robust.
func buildEmail(from string, to []string, ev Event) []byte {
var b strings.Builder
b.WriteString("From: ")
b.WriteString(from)
b.WriteString("\r\n")
b.WriteString("To: ")
b.WriteString(strings.Join(to, ", "))
b.WriteString("\r\n")
subject := ev.Title
if subject == "" {
subject = "[vetting] " + string(ev.Kind)
}
b.WriteString("Subject: ")
b.WriteString(subject)
b.WriteString("\r\n")
b.WriteString("Content-Type: text/plain; charset=UTF-8\r\n")
b.WriteString("\r\n")
b.WriteString(ev.Body)
if ev.URL != "" {
b.WriteString("\r\n\r\nLink: ")
b.WriteString(ev.URL)
}
b.WriteString("\r\n")
return []byte(b.String())
}