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()) }