194 lines
6.9 KiB
Go
194 lines
6.9 KiB
Go
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())
|
||
}
|
||
}
|