Files
ProxmoxDash/email.go

194 lines
6.9 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"crypto/tls"
"fmt"
"html"
"log"
"mime"
"net"
"net/smtp"
"sort"
"strings"
"time"
)
func filterEmailAlerts(alerts []Alert, settings EmailSettings) []Alert {
allowedSeverity, allowedSource := map[string]bool{}, map[string]bool{}
for _, value := range settings.Severities {
allowedSeverity[value] = true
}
for _, value := range settings.Sources {
allowedSource[value] = true
}
var result []Alert
for _, alert := range alerts {
if allowedSeverity[alert.Severity] && allowedSource[alert.Source] {
result = append(result, alert)
}
}
return result
}
func sendAlertEmail(settings EmailSettings, alerts []Alert, test bool) error {
subject := "Proxmox Dashboard: сводка уведомлений"
body := `<div style="padding:32px;background:#0b1117;color:#edf3f7;font-family:Arial,sans-serif"><div style="max-width:680px;margin:auto"><div style="color:#55d58a;font-size:12px;letter-spacing:2px">PROXMOX DASHBOARD</div><h1 style="margin:10px 0 24px">Сводка событий</h1>`
if test {
subject = "Proxmox Dashboard: тест почты"
body += `<div style="padding:22px;border:1px solid #2b3944;border-radius:14px;background:#111a22"><b style="font-size:18px">Почта настроена</b><p style="color:#9aabb7;margin-bottom:0">Тестовое сообщение успешно отправлено вашим сервером.</p></div>`
}
if len(alerts) > 0 {
groups := map[string][]Alert{}
for _, alert := range alerts {
groups[alert.Severity] = append(groups[alert.Severity], alert)
}
var sections []string
for _, severity := range []string{"critical", "warning", "info"} {
items := groups[severity]
if len(items) == 0 {
continue
}
title := map[string]string{"critical": "Критические", "warning": "Предупреждения", "info": "Информация"}[severity]
color := map[string]string{"critical": "#ff665c", "warning": "#ffb35c", "info": "#6ba8ff"}[severity]
sort.Slice(items, func(i, j int) bool { return items[i].Title < items[j].Title })
cards := ""
for _, item := range items {
cards += fmt.Sprintf(`<div style="margin:10px 0;padding:16px 18px;border:1px solid #2b3944;border-left:4px solid %s;border-radius:10px;background:#111a22"><div style="color:#8fa0ac;font-size:11px;text-transform:uppercase">%s</div><div style="font-weight:bold;font-size:16px;margin:5px 0">%s</div><div style="color:#aab7c0;font-size:14px">%s</div></div>`, color, html.EscapeString(item.Source), html.EscapeString(item.Title), html.EscapeString(item.Message))
}
sections = append(sections, fmt.Sprintf(`<h2 style="font-size:14px;color:%s;margin:24px 0 8px">%s · %d</h2>%s`, color, title, len(items), cards))
}
body += strings.Join(sections, "")
subject = fmt.Sprintf("Proxmox Dashboard: событий — %d", len(alerts))
}
body += `<div style="color:#687985;font-size:12px;margin-top:28px">События собраны напрямую с Proxmox-хоста. Подтверждённые события повторно не отправляются.</div></div></div>`
message := []byte("From: " + settings.From + "\r\nTo: " + settings.To + "\r\nSubject: " + mime.QEncoding.Encode("UTF-8", subject) + "\r\nMIME-Version: 1.0\r\nContent-Type: text/html; charset=UTF-8\r\nContent-Transfer-Encoding: 8bit\r\n\r\n" + body + "\r\n")
address := net.JoinHostPort(settings.Host, fmt.Sprint(settings.Port))
tlsConfig := &tls.Config{ServerName: settings.Host, MinVersion: tls.VersionTLS12, InsecureSkipVerify: settings.SkipTLSVerify} //nolint:gosec -- explicit private-network option
var client *smtp.Client
if settings.Port == 465 {
connection, err := tls.Dial("tcp", address, tlsConfig)
if err != nil {
return err
}
client, err = smtp.NewClient(connection, settings.Host)
if err != nil {
return err
}
} else {
var err error
client, err = smtp.Dial(address)
if err != nil {
return err
}
if ok, _ := client.Extension("STARTTLS"); ok {
if err = client.StartTLS(tlsConfig); err != nil {
return err
}
}
}
defer client.Close()
if settings.Username != "" {
if err := client.Auth(smtp.PlainAuth("", settings.Username, settings.Password, settings.Host)); err != nil {
return err
}
}
if err := client.Mail(settings.From); err != nil {
return err
}
if err := client.Rcpt(settings.To); err != nil {
return err
}
writer, err := client.Data()
if err != nil {
return err
}
if _, err = writer.Write(message); err != nil {
return err
}
if err = writer.Close(); err != nil {
return err
}
return client.Quit()
}
func sendRecoveryEmail(settings EmailSettings, items []RecoveryEvent) error {
alerts := make([]Alert, 0, len(items))
for _, v := range items {
alerts = append(alerts, Alert{ID: v.ID, Severity: "info", Source: v.Source, Title: "Восстановлено: " + v.Title, Message: "Проблема больше не наблюдается."})
}
return sendAlertEmail(settings, alerts, false)
}
func runImmediateNotifier(collect func() (dashboardMetrics, error), store *Store) {
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for range ticker.C {
settings := store.EmailSettings(true)
if !settings.Enabled {
continue
}
if _, err := collect(); err != nil {
continue
}
urgent, err := store.PendingImmediateAlerts()
if err == nil && len(urgent) > 0 {
urgent = filterEmailAlerts(urgent, settings)
if len(urgent) > 0 && sendAlertEmail(settings, urgent, false) == nil {
_ = store.MarkAlertsEmailed(urgent)
}
}
recoveries, err := store.PendingRecoveries()
if err == nil && len(recoveries) > 0 {
var filtered []RecoveryEvent
for _, v := range recoveries {
if contains(settings.Sources, v.Source) {
filtered = append(filtered, v)
}
}
if len(filtered) > 0 && sendRecoveryEmail(settings, filtered) == nil {
_ = store.MarkRecoveriesEmailed(filtered)
}
}
}
}
func contains(items []string, want string) bool {
for _, v := range items {
if v == want {
return true
}
}
return false
}
func runEmailNotifier(collect func() (dashboardMetrics, error), store *Store) {
ticker := time.NewTicker(time.Minute)
defer ticker.Stop()
for range ticker.C {
settings := store.EmailSettings(true)
if !settings.Enabled || time.Since(store.LastEmailAt()) < time.Duration(settings.IntervalMinutes)*time.Minute {
continue
}
_, err := collect()
if err != nil {
continue
}
pending, err := store.PendingEmailAlerts()
if err != nil {
continue
}
alerts := filterEmailAlerts(pending, settings)
if len(alerts) == 0 {
continue
}
if err := sendAlertEmail(settings, alerts, false); err != nil {
log.Printf("Не удалось отправить почтовую сводку: %v", err)
continue
}
if err := store.MarkAlertsEmailed(alerts); err != nil {
log.Printf("Не удалось отметить отправленные события: %v", err)
continue
}
store.SetLastEmailAt(time.Now())
}
}