package main import ( "fmt" "log" "net/smtp" "strings" ) // emailSendHTML отправляет HTML-письмо через SMTP. // Telegram-совместимые теги (, , ) корректно отображаются в 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 { // Заменяем переносы строк на
для корректного отображения в письме htmlBody := strings.ReplaceAll(body, "\n", "
\n") return fmt.Sprintf(` %s
%s
`, subject, htmlBody, appVersion) }