webhookd/pkg/notification/smtp/smtp_notifier.go

147 lines
3.5 KiB
Go

package smtp
import (
"crypto/tls"
"fmt"
"log/slog"
"net"
"net/smtp"
"net/url"
"strconv"
"strings"
"time"
"github.com/ncarlier/webhookd/pkg/helper"
"github.com/ncarlier/webhookd/pkg/notification"
)
// smtpNotifier is able to send notification to a email destination.
type smtpNotifier struct {
Host string
Username string
Password string
Conn string
From string
To string
Subject string
PrefixFilter string
}
func newSMTPNotifier(uri *url.URL) (notification.Notifier, error) {
slog.Info("using SMTP notification system", "uri", uri.Opaque)
q := uri.Query()
return &smtpNotifier{
Host: helper.GetValueOrAlt(q, "smtp", "localhost:25"),
Username: helper.GetValueOrAlt(q, "username", ""),
Password: helper.GetValueOrAlt(q, "password", ""),
Conn: helper.GetValueOrAlt(q, "conn", "plain"),
From: helper.GetValueOrAlt(q, "from", "noreply@nunux.org"),
To: uri.Opaque,
Subject: helper.GetValueOrAlt(uri.Query(), "subject", "[whd-notification] {name}#{id} {status}"),
PrefixFilter: helper.GetValueOrAlt(uri.Query(), "prefix", "notify:"),
}, nil
}
func (n *smtpNotifier) buildEmailPayload(result notification.HookResult) string {
// Get email body
body := result.Logs(n.PrefixFilter)
if strings.TrimSpace(body) == "" {
return ""
}
// Build email subject
subject := buildSubject(n.Subject, result)
// Build email headers
headers := make(map[string]string)
headers["From"] = n.From
headers["To"] = n.To
headers["Subject"] = subject
// Build email payload
payload := ""
for k, v := range headers {
payload += fmt.Sprintf("%s: %s\r\n", k, v)
}
payload += "\r\n" + body
return payload
}
// Notify send a notification to a email destination.
func (n *smtpNotifier) Notify(result notification.HookResult) error {
hostname, _, _ := net.SplitHostPort(n.Host)
payload := n.buildEmailPayload(result)
if payload == "" {
// Nothing to notify, abort
return nil
}
// Dial connection
conn, err := net.DialTimeout("tcp", n.Host, 5*time.Second)
if err != nil {
return err
}
// Connect to SMTP server
client, err := smtp.NewClient(conn, hostname)
if err != nil {
return err
}
if n.Conn == "tls" || n.Conn == "tls-insecure" {
// TLS config
tlsConfig := &tls.Config{
InsecureSkipVerify: n.Conn == "tls-insecure",
ServerName: hostname,
}
if err := client.StartTLS(tlsConfig); err != nil {
return err
}
}
// Set auth if needed
if n.Username != "" {
if err := client.Auth(smtp.PlainAuth("", n.Username, n.Password, hostname)); err != nil {
return err
}
}
// Set the sender and recipient first
if err := client.Mail(n.From); err != nil {
return err
}
if err := client.Rcpt(n.To); err != nil {
return err
}
// Send the email body.
wc, err := client.Data()
if err != nil {
return err
}
_, err = wc.Write([]byte(payload))
if err != nil {
return err
}
err = wc.Close()
if err != nil {
return err
}
slog.Info("notification sent", "hook", result.Name(), "id", result.ID(), "to", n.To)
// Send the QUIT command and close the connection.
return client.Quit()
}
func buildSubject(template string, result notification.HookResult) string {
subject := strings.ReplaceAll(template, "{name}", result.Name())
subject = strings.ReplaceAll(subject, "{id}", strconv.FormatUint(uint64(result.ID()), 10))
subject = strings.ReplaceAll(subject, "{status}", result.StatusLabel())
return subject
}
func init() {
notification.Register("mailto", newSMTPNotifier)
}