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 := `
PROXMOX DASHBOARD

Сводка событий

` if test { subject = "Proxmox Dashboard: тест почты" body += `
Почта настроена

Тестовое сообщение успешно отправлено вашим сервером.

` } 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(`
%s
%s
%s
`, color, html.EscapeString(item.Source), html.EscapeString(item.Title), html.EscapeString(item.Message)) } sections = append(sections, fmt.Sprintf(`

%s · %d

%s`, color, title, len(items), cards)) } body += strings.Join(sections, "") subject = fmt.Sprintf("Proxmox Dashboard: событий — %d", len(alerts)) } body += `
События собраны напрямую с Proxmox-хоста. Подтверждённые события повторно не отправляются.
` 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()) } }