smtp_notifier.go 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. package smtp
  2. import (
  3. "crypto/tls"
  4. "fmt"
  5. "log/slog"
  6. "net"
  7. "net/smtp"
  8. "net/url"
  9. "strconv"
  10. "strings"
  11. "time"
  12. "github.com/ncarlier/webhookd/pkg/helper"
  13. "github.com/ncarlier/webhookd/pkg/notification"
  14. )
  15. // smtpNotifier is able to send notification to a email destination.
  16. type smtpNotifier struct {
  17. Host string
  18. Username string
  19. Password string
  20. Conn string
  21. From string
  22. To string
  23. Subject string
  24. PrefixFilter string
  25. }
  26. func newSMTPNotifier(uri *url.URL) (notification.Notifier, error) {
  27. slog.Info("using SMTP notification system", "uri", uri.Redacted())
  28. q := uri.Query()
  29. return &smtpNotifier{
  30. Host: helper.GetValueOrAlt(q, "smtp", "localhost:25"),
  31. Username: helper.GetValueOrAlt(q, "username", ""),
  32. Password: helper.GetValueOrAlt(q, "password", ""),
  33. Conn: helper.GetValueOrAlt(q, "conn", "plain"),
  34. From: helper.GetValueOrAlt(q, "from", "noreply@nunux.org"),
  35. To: uri.Opaque,
  36. Subject: helper.GetValueOrAlt(uri.Query(), "subject", "[whd-notification] {name}#{id} {status}"),
  37. PrefixFilter: helper.GetValueOrAlt(uri.Query(), "prefix", "notify:"),
  38. }, nil
  39. }
  40. func (n *smtpNotifier) buildEmailPayload(result notification.HookResult) string {
  41. // Get email body
  42. body := result.Logs(n.PrefixFilter)
  43. if strings.TrimSpace(body) == "" {
  44. return ""
  45. }
  46. // Build email subject
  47. subject := buildSubject(n.Subject, result)
  48. // Build email headers
  49. headers := make(map[string]string)
  50. headers["From"] = n.From
  51. headers["To"] = n.To
  52. headers["Subject"] = subject
  53. // Build email payload
  54. payload := ""
  55. for k, v := range headers {
  56. payload += fmt.Sprintf("%s: %s\r\n", k, v)
  57. }
  58. payload += "\r\n" + body
  59. return payload
  60. }
  61. // Notify send a notification to a email destination.
  62. func (n *smtpNotifier) Notify(result notification.HookResult) error {
  63. hostname, _, _ := net.SplitHostPort(n.Host)
  64. payload := n.buildEmailPayload(result)
  65. if payload == "" {
  66. // Nothing to notify, abort
  67. return nil
  68. }
  69. // Dial connection
  70. conn, err := net.DialTimeout("tcp", n.Host, 5*time.Second)
  71. if err != nil {
  72. return err
  73. }
  74. // Connect to SMTP server
  75. client, err := smtp.NewClient(conn, hostname)
  76. if err != nil {
  77. return err
  78. }
  79. if n.Conn == "tls" || n.Conn == "tls-insecure" {
  80. // TLS config
  81. tlsConfig := &tls.Config{
  82. InsecureSkipVerify: n.Conn == "tls-insecure",
  83. ServerName: hostname,
  84. }
  85. if err := client.StartTLS(tlsConfig); err != nil {
  86. return err
  87. }
  88. }
  89. // Set auth if needed
  90. if n.Username != "" {
  91. if err := client.Auth(smtp.PlainAuth("", n.Username, n.Password, hostname)); err != nil {
  92. return err
  93. }
  94. }
  95. // Set the sender and recipient first
  96. if err := client.Mail(n.From); err != nil {
  97. return err
  98. }
  99. if err := client.Rcpt(n.To); err != nil {
  100. return err
  101. }
  102. // Send the email body.
  103. wc, err := client.Data()
  104. if err != nil {
  105. return err
  106. }
  107. _, err = wc.Write([]byte(payload))
  108. if err != nil {
  109. return err
  110. }
  111. err = wc.Close()
  112. if err != nil {
  113. return err
  114. }
  115. slog.Info("notification sent", "hook", result.Name(), "id", result.ID(), "to", n.To)
  116. // Send the QUIT command and close the connection.
  117. return client.Quit()
  118. }
  119. func buildSubject(template string, result notification.HookResult) string {
  120. subject := strings.ReplaceAll(template, "{name}", result.Name())
  121. subject = strings.ReplaceAll(subject, "{id}", strconv.FormatUint(uint64(result.ID()), 10))
  122. subject = strings.ReplaceAll(subject, "{status}", result.StatusLabel())
  123. return subject
  124. }
  125. func init() {
  126. notification.Register("mailto", newSMTPNotifier)
  127. }