Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19b4aef95b |
@@ -79,24 +79,16 @@ func dispatchSMTP(ch models.NotificationChannel, ev Event) error {
|
||||
}
|
||||
}
|
||||
|
||||
title := ev.title()
|
||||
msg := strings.Join([]string{
|
||||
"From: " + from,
|
||||
"To: " + to,
|
||||
"Subject: " + title,
|
||||
"",
|
||||
title,
|
||||
"",
|
||||
"Monitor: " + ev.MonitorName,
|
||||
"Status: " + ev.OldStatus + " -> " + ev.NewStatus,
|
||||
"Time: " + ev.Time.String(),
|
||||
}, "\r\n")
|
||||
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([]byte(msg)); err != nil {
|
||||
if _, err := w.Write(msg); err != nil {
|
||||
return fmt.Errorf("smtp: write: %w", err)
|
||||
}
|
||||
if err := w.Close(); err != nil {
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html"
|
||||
"mime/multipart"
|
||||
"net/textproto"
|
||||
"strings"
|
||||
|
||||
"github.com/mrhid6/vantage/server/internal/models"
|
||||
)
|
||||
|
||||
// App theme colors (mirrors web/tailwind.config.ts).
|
||||
const (
|
||||
colBg = "#0f1117"
|
||||
colSurface = "#1a1d27"
|
||||
colSurface2 = "#232635"
|
||||
colBorder = "#2e3147"
|
||||
colText = "#e8eaf0"
|
||||
colTextMuted = "#9095a8"
|
||||
colAccent = "#6366f1"
|
||||
colSuccess = "#22c55e"
|
||||
colDanger = "#ef4444"
|
||||
)
|
||||
|
||||
// statusColor returns the accent color for a monitor status.
|
||||
func statusColor(status string) string {
|
||||
switch status {
|
||||
case models.StatusUp:
|
||||
return colSuccess
|
||||
case models.StatusDown:
|
||||
return colDanger
|
||||
default:
|
||||
return colTextMuted
|
||||
}
|
||||
}
|
||||
|
||||
// buildMIME assembles a multipart/alternative message (plain + HTML) with the
|
||||
// standard email headers, ready to hand to the SMTP DATA command.
|
||||
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
|
||||
}
|
||||
|
||||
// htmlEmail renders the alert as a dark-themed HTML email matching the app.
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
||||
// textEmail renders the plain-text fallback.
|
||||
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