This commit is contained in:
@@ -1,90 +1,38 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/smtp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mrhid6/vantage/server/internal/models"
|
||||
"github.com/mrhid6/vantage/shared/mail"
|
||||
)
|
||||
|
||||
const smtpTimeout = 15 * time.Second
|
||||
|
||||
// dispatchSMTP delivers a state change over one channel's own SMTP settings.
|
||||
//
|
||||
// The transport, the envelope and the look of the message all live in
|
||||
// shared/mail, which admin and sitesvc use too — a Vantage alert and a Vantage
|
||||
// licence email should not look like they came from different products. This
|
||||
// function only turns a channel document into a Sender.
|
||||
func dispatchSMTP(ch models.NotificationChannel, ev Event) error {
|
||||
host := ch.Config["host"]
|
||||
port := ch.Config["port"]
|
||||
from := ch.Config["from"]
|
||||
to := ch.Config["to"]
|
||||
if host == "" || port == "" || from == "" || to == "" {
|
||||
sender := mail.Sender{
|
||||
Host: ch.Config["host"],
|
||||
Port: ch.Config["port"],
|
||||
From: ch.Config["from"],
|
||||
Username: ch.Config["username"],
|
||||
Password: ch.Config["password"],
|
||||
}
|
||||
if !sender.Enabled() || sender.Port == "" || to == "" {
|
||||
return fmt.Errorf("smtp: missing host/port/from/to")
|
||||
}
|
||||
|
||||
addr := net.JoinHostPort(host, port)
|
||||
conn, err := net.DialTimeout("tcp", addr, smtpTimeout)
|
||||
if err != nil {
|
||||
return fmt.Errorf("smtp: dial %s: %w", addr, err)
|
||||
}
|
||||
_ = conn.SetDeadline(time.Now().Add(smtpTimeout))
|
||||
|
||||
if port == "465" {
|
||||
conn = tls.Client(conn, &tls.Config{ServerName: host})
|
||||
}
|
||||
|
||||
c, err := smtp.NewClient(conn, host)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return fmt.Errorf("smtp: client: %w", err)
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
if port != "465" {
|
||||
if ok, _ := c.Extension("STARTTLS"); ok {
|
||||
if err := c.StartTLS(&tls.Config{ServerName: host}); err != nil {
|
||||
return fmt.Errorf("smtp: starttls: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if user := ch.Config["username"]; user != "" {
|
||||
if err := c.Auth(smtp.PlainAuth("", user, ch.Config["password"], host)); err != nil {
|
||||
return fmt.Errorf("smtp: auth: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
recipients := strings.Split(to, ",")
|
||||
for i := range recipients {
|
||||
recipients[i] = strings.TrimSpace(recipients[i])
|
||||
}
|
||||
|
||||
if err := c.Mail(from); err != nil {
|
||||
return fmt.Errorf("smtp: mail from: %w", err)
|
||||
}
|
||||
for _, rcpt := range recipients {
|
||||
if rcpt == "" {
|
||||
continue
|
||||
}
|
||||
if err := c.Rcpt(rcpt); err != nil {
|
||||
return fmt.Errorf("smtp: rcpt %s: %w", rcpt, err)
|
||||
}
|
||||
}
|
||||
|
||||
msg, err := buildMIME(from, to, ev.title(), textEmail(ev), htmlEmail(ev))
|
||||
if err != nil {
|
||||
return fmt.Errorf("smtp: build message: %w", err)
|
||||
}
|
||||
|
||||
w, err := c.Data()
|
||||
if err != nil {
|
||||
return fmt.Errorf("smtp: data: %w", err)
|
||||
}
|
||||
if _, err := w.Write(msg); err != nil {
|
||||
return fmt.Errorf("smtp: write: %w", err)
|
||||
}
|
||||
if err := w.Close(); err != nil {
|
||||
return fmt.Errorf("smtp: close data: %w", err)
|
||||
}
|
||||
return c.Quit()
|
||||
return sender.SendMonitorAlert(to, mail.MonitorEvent{
|
||||
MonitorName: ev.MonitorName,
|
||||
Type: ev.Type,
|
||||
OldStatus: ev.OldStatus,
|
||||
NewStatus: ev.NewStatus,
|
||||
Message: ev.Message,
|
||||
Time: ev.Time,
|
||||
Down: ev.NewStatus == models.StatusDown,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,161 +0,0 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html"
|
||||
"mime/multipart"
|
||||
"net/textproto"
|
||||
"strings"
|
||||
|
||||
"github.com/mrhid6/vantage/server/internal/models"
|
||||
)
|
||||
|
||||
const (
|
||||
colBg = "#0f1117"
|
||||
colSurface = "#1a1d27"
|
||||
colSurface2 = "#232635"
|
||||
colBorder = "#2e3147"
|
||||
colText = "#e8eaf0"
|
||||
colTextMuted = "#9095a8"
|
||||
colAccent = "#6366f1"
|
||||
colSuccess = "#22c55e"
|
||||
colDanger = "#ef4444"
|
||||
)
|
||||
|
||||
func statusColor(status string) string {
|
||||
switch status {
|
||||
case models.StatusUp:
|
||||
return colSuccess
|
||||
case models.StatusDown:
|
||||
return colDanger
|
||||
default:
|
||||
return colTextMuted
|
||||
}
|
||||
}
|
||||
|
||||
func buildMIME(from, to, subject, text, htmlBody string) ([]byte, error) {
|
||||
var buf strings.Builder
|
||||
w := multipart.NewWriter(&buf)
|
||||
|
||||
var head strings.Builder
|
||||
head.WriteString("From: " + from + "\r\n")
|
||||
head.WriteString("To: " + to + "\r\n")
|
||||
head.WriteString("Subject: " + subject + "\r\n")
|
||||
head.WriteString("MIME-Version: 1.0\r\n")
|
||||
head.WriteString(fmt.Sprintf("Content-Type: multipart/alternative; boundary=%s\r\n\r\n", w.Boundary()))
|
||||
|
||||
textPart, err := w.CreatePart(textproto.MIMEHeader{"Content-Type": {"text/plain; charset=UTF-8"}})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
textPart.Write([]byte(text))
|
||||
|
||||
htmlPart, err := w.CreatePart(textproto.MIMEHeader{"Content-Type": {"text/html; charset=UTF-8"}})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
htmlPart.Write([]byte(htmlBody))
|
||||
|
||||
if err := w.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []byte(head.String() + buf.String()), nil
|
||||
}
|
||||
|
||||
func htmlEmail(ev Event) string {
|
||||
accent := statusColor(ev.NewStatus)
|
||||
label := "Recovered"
|
||||
if ev.NewStatus == models.StatusDown {
|
||||
label = "Down"
|
||||
}
|
||||
|
||||
esc := html.EscapeString
|
||||
row := func(k, v string) string {
|
||||
if v == "" {
|
||||
v = ""
|
||||
}
|
||||
return fmt.Sprintf(
|
||||
`<tr>`+
|
||||
`<td style="padding:8px 0;color:%s;font-size:13px;width:120px;">%s</td>`+
|
||||
`<td style="padding:8px 0;color:%s;font-size:13px;font-weight:500;">%s</td>`+
|
||||
`</tr>`,
|
||||
colTextMuted, k, colText, esc(v))
|
||||
}
|
||||
|
||||
message := ""
|
||||
if ev.Message != "" {
|
||||
message = fmt.Sprintf(
|
||||
`<p style="margin:0 0 20px;padding:12px 14px;background:%s;border:1px solid %s;border-radius:8px;color:%s;font-size:13px;">%s</p>`,
|
||||
colSurface2, colBorder, colText, esc(ev.Message))
|
||||
}
|
||||
|
||||
transition := esc(ev.OldStatus) + " → " + esc(ev.NewStatus)
|
||||
|
||||
return fmt.Sprintf(`<!DOCTYPE html>
|
||||
<html>
|
||||
<body style="margin:0;padding:0;background:%s;">
|
||||
<table role="presentation" width="100%%" cellpadding="0" cellspacing="0" style="background:%s;padding:32px 0;">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table role="presentation" width="480" cellpadding="0" cellspacing="0" style="max-width:480px;background:%s;border:1px solid %s;border-radius:12px;overflow:hidden;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;">
|
||||
<tr><td style="height:4px;background:%s;"></td></tr>
|
||||
<tr>
|
||||
<td style="padding:28px 28px 20px;">
|
||||
<table role="presentation" cellpadding="0" cellspacing="0" style="margin-bottom:18px;">
|
||||
<tr>
|
||||
<td style="font-size:18px;font-weight:700;color:%s;">Vantage</td>
|
||||
</tr>
|
||||
</table>
|
||||
<span style="display:inline-block;padding:4px 12px;border-radius:9999px;background:%s22;color:%s;font-size:12px;font-weight:600;letter-spacing:.02em;">%s</span>
|
||||
<h1 style="margin:14px 0 6px;font-size:20px;font-weight:700;color:%s;">%s</h1>
|
||||
<p style="margin:0 0 20px;color:%s;font-size:13px;">%s check</p>
|
||||
%s
|
||||
<table role="presentation" width="100%%" cellpadding="0" cellspacing="0" style="border-top:1px solid %s;">
|
||||
%s
|
||||
%s
|
||||
%s
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:16px 28px;border-top:1px solid %s;">
|
||||
<p style="margin:0;color:%s;font-size:12px;">Sent by Vantage · self-hosted service monitoring</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>`,
|
||||
colBg,
|
||||
colBg,
|
||||
colSurface, colBorder,
|
||||
accent,
|
||||
colText,
|
||||
accent, accent, label,
|
||||
colText, esc(ev.MonitorName),
|
||||
colTextMuted, esc(ev.Type),
|
||||
message,
|
||||
colBorder,
|
||||
row("Status", transition),
|
||||
row("Type", ev.Type),
|
||||
row("Time", ev.Time.Format("2006-01-02 15:04:05 MST")),
|
||||
colBorder,
|
||||
colTextMuted,
|
||||
)
|
||||
}
|
||||
|
||||
func textEmail(ev Event) string {
|
||||
return strings.Join([]string{
|
||||
ev.title(),
|
||||
"",
|
||||
"Monitor: " + ev.MonitorName,
|
||||
"Type: " + ev.Type,
|
||||
"Status: " + ev.OldStatus + " -> " + ev.NewStatus,
|
||||
"Message: " + ev.Message,
|
||||
"Time: " + ev.Time.Format("2006-01-02 15:04:05 MST"),
|
||||
"",
|
||||
"Sent by Vantage · self-hosted service monitoring",
|
||||
}, "\r\n")
|
||||
}
|
||||
Reference in New Issue
Block a user