113 lines
4.6 KiB
Go
113 lines
4.6 KiB
Go
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)
|
||
}
|