auspex/email.go
2026-06-09 17:12:18 +03:00

113 lines
4.6 KiB
Go
Executable file
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 (
"fmt"
"log"
"net/smtp"
"strings"
)
// emailSendHTML отправляет HTML-письмо через SMTP.
// Telegram-совместимые теги (<b>, <code>, <i>) корректно отображаются в HTML-письме.
// Если email не настроен или произошла ошибка — пишет в лог и не паникует.
func emailSendHTML(subject, message string) error {
// Проверяем минимально необходимые параметры
if !isEmailConfigured() {
log.Printf("[Email] Не настроен (нужны EMAIL_SMTP_HOST, EMAIL_FROM, EMAIL_TO) — пропускаем")
return fmt.Errorf("email не настроен")
}
// Предупреждаем если авторизация неполная — попытку всё равно делаем,
// некоторые SMTP-серверы работают без auth (например внутренние relay)
if cfgEmailSMTPUser == "" && cfgEmailSMTPPassword == "" {
log.Printf("[Email] ⚠️ EMAIL_SMTP_USER и EMAIL_SMTP_PASSWORD не заданы — попытка без авторизации")
} else if cfgEmailSMTPUser == "" {
log.Printf("[Email] ⚠️ EMAIL_SMTP_USER не задан — авторизация невозможна, попытка без неё")
} else if cfgEmailSMTPPassword == "" {
log.Printf("[Email] ⚠️ EMAIL_SMTP_PASSWORD не задан — авторизация невозможна, попытка без неё")
}
body := emailBuildHTML(subject, message)
addr := fmt.Sprintf("%s:%d", cfgEmailSMTPHost, cfgEmailSMTPPort)
var auth smtp.Auth
if cfgEmailSMTPUser != "" && cfgEmailSMTPPassword != "" {
auth = &emailLoginAuth{user: cfgEmailSMTPUser, password: cfgEmailSMTPPassword}
}
header := strings.Join([]string{
"From: " + cfgEmailFrom,
"To: " + strings.Join(cfgEmailTo, ", "),
"Subject: " + subject,
"MIME-Version: 1.0",
"Content-Type: text/html; charset=UTF-8",
"",
}, "\r\n")
msg := []byte(header + "\r\n" + body)
log.Printf("[Email] Отправка: %s → %s (smtp: %s)", cfgEmailFrom, strings.Join(cfgEmailTo, ", "), addr)
if err := smtp.SendMail(addr, auth, cfgEmailFrom, cfgEmailTo, msg); err != nil {
return fmt.Errorf("ошибка отправки письма (smtp: %s): %w", addr, err)
}
return nil
}
// ── LOGIN Auth ───────────────────────────────────────────────────────────────
// Реализация AUTH LOGIN для SMTP-серверов которые не поддерживают AUTH PLAIN.
// Стандартный smtp.PlainAuth использует механизм PLAIN — некоторые серверы
// (например Microsoft Exchange) принимают только LOGIN.
type emailLoginAuth struct {
user string
password string
}
func (a *emailLoginAuth) Start(_ *smtp.ServerInfo) (string, []byte, error) {
return "LOGIN", nil, nil
}
func (a *emailLoginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
if !more {
return nil, nil
}
switch strings.ToLower(strings.TrimSpace(string(fromServer))) {
case "username:":
return []byte(a.user), nil
case "password:":
return []byte(a.password), nil
default:
return nil, fmt.Errorf("неожиданный запрос от SMTP-сервера: %q", string(fromServer))
}
}
// emailBuildHTML оборачивает тело сообщения в HTML-шаблон письма.
// Telegram-теги уже являются валидным HTML, поэтому конвертация не нужна.
func emailBuildHTML(subject, body string) string {
// Заменяем переносы строк на <br> для корректного отображения в письме
htmlBody := strings.ReplaceAll(body, "\n", "<br>\n")
return fmt.Sprintf(`<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>%s</title>
<style>
body { font-family: Arial, sans-serif; font-size: 14px; color: #222; background: #f9f9f9; padding: 20px; }
.card { background: #fff; border: 1px solid #ddd; border-radius: 6px; padding: 20px; max-width: 640px; }
b { color: #c0392b; }
code { background: #f0f0f0; padding: 2px 5px; border-radius: 3px; font-family: monospace; font-size: 13px; }
i { color: #555; }
.footer { margin-top: 16px; font-size: 12px; color: #999; }
</style>
</head>
<body>
<div class="card">
%s
<div class="footer">Auspex v%s — мониторинг инфраструктуры</div>
</div>
</body>
</html>`, subject, htmlBody, appVersion)
}