package users_service import ( "fmt" "html" "strings" ) // Шаблоны писем сервиса. Структура по базе знаний копирайтера (email.md): // приветствие → контекст («почему это письмо») → ценность (данные для входа) // → CTA → подпись → P.S. Письма транзакционные, но по форме — «человек // человеку»: Gmail и другие почтовики агрессивнее фильтруют короткие // «роботные» письма без структуры. // // Каждое письмо формируется в двух версиях: text/plain (для старых клиентов // и фильтров) и text/html (для отображения); sender собирает их в // multipart/alternative. const ( // welcomeSubject — тема письма о регистрации: конкретика без КАПСА и // кликбейта, до 50 символов (видно на мобильных целиком). welcomeSubject = "Твой доступ к «Вечернему детективу»" // resetSubject — тема письма о сбросе пароля. resetSubject = "Новый пароль для «Вечернего детектива»" ) // welcomePlain возвращает текстовую версию письма о регистрации. func welcomePlain(username, email, password, loginURL string) string { var b strings.Builder b.WriteString(greeting(username)) b.WriteString("\n\n") b.WriteString("Твоя регистрация в «Вечернем детективе» прошла. Данные для входа:\n\n") b.WriteString(" Логин: ") b.WriteString(email) b.WriteString("\n") b.WriteString(" Пароль: ") b.WriteString(password) b.WriteString("\n\n") if loginURL != "" { b.WriteString("Войти в игру: ") b.WriteString(loginURL) b.WriteString("\n\n") } b.WriteString("Если ты не регистрировался — просто проигнорируй это письмо.\n\n") b.WriteString("— Команда «Вечернего детектива»\n") b.WriteString("P.S. Сохрани это письмо: пароль понадобится для входа.") return sanitizeLine(b.String()) } // welcomeHTML возвращает HTML-версию письма о регистрации. func welcomeHTML(username, email, password, loginURL string) string { var content strings.Builder content.WriteString("

Привет, ") content.WriteString(html.EscapeString(username)) content.WriteString("!

\n") content.WriteString("

Твоя регистрация в «Вечернем детективе» прошла. Данные для входа:

\n") content.WriteString(credentialsBlockHTML(email, password)) if loginURL != "" { content.WriteString(buttonHTML(loginURL, "Войти в игру")) } content.WriteString("

Если ты не регистрировался — просто проигнорируй это письмо.

") return wrapHTML(content.String()) } // resetPlain возвращает текстовую версию письма о сбросе пароля. func resetPlain(username, email, password, loginURL string) string { var b strings.Builder b.WriteString(greeting(username)) b.WriteString("\n\n") b.WriteString("Ты запросил сброс пароля в «Вечернем детективе». Новый пароль для входа:\n\n") b.WriteString(" Логин: ") b.WriteString(email) b.WriteString("\n") b.WriteString(" Пароль: ") b.WriteString(password) b.WriteString("\n\n") if loginURL != "" { b.WriteString("Войти в игру: ") b.WriteString(loginURL) b.WriteString("\n\n") } b.WriteString("Никому не сообщай пароль и не пересылай это письмо.\n") b.WriteString("Если это был не ты — ответь на это письмо: мы поможем защитить аккаунт.\n\n") b.WriteString("— Команда «Вечернего детектива»\n") return sanitizeLine(b.String()) } // resetHTML возвращает HTML-версию письма о сбросе пароля. func resetHTML(username, email, password, loginURL string) string { var content strings.Builder content.WriteString("

Привет, ") content.WriteString(html.EscapeString(username)) content.WriteString("!

\n") content.WriteString("

Ты запросил сброс пароля в «Вечернем детективе». Новый пароль для входа:

\n") content.WriteString(credentialsBlockHTML(email, password)) if loginURL != "" { content.WriteString(buttonHTML(loginURL, "Войти в игру")) } content.WriteString("

Никому не сообщай пароль и не пересылай это письмо.

\n") content.WriteString("

Если это был не ты — ответь на это письмо: мы поможем защитить аккаунт.

") return wrapHTML(content.String()) } // greeting возвращает приветствие с именем пользователя; при пустом имени — // без обращения, чтобы не было «Привет, !». func greeting(username string) string { username = sanitizeLine(strings.TrimSpace(username)) if username == "" { return "Привет!" } return fmt.Sprintf("Привет, %s!", username) } // credentialsBlockHTML — блок «Логин / Пароль» в табличной вёрстке, // совместимой с Gmail. func credentialsBlockHTML(email, password string) string { return `
Логин ` + html.EscapeString(email) + `
Пароль ` + html.EscapeString(password) + `
` } // buttonHTML — CTA-кнопка табличной вёрсткой (Gmail не поддерживает // margin/padding на , поэтому отступы — на ячейке ). func buttonHTML(link, label string) string { return `
` + html.EscapeString(label) + `
` } // wrapHTML — каркас письма: тёмная шапка с названием сервиса, контент, // подвал с дисклеймером. Только табличная вёрстка и inline-стили: внешние // таблицы стилей Gmail вырезает. func wrapHTML(content string) string { return `
ВЕЧЕРНИЙ ДЕТЕКТИВ
` + content + `
Это автоматическое письмо сервиса «Вечерний детектив».
Если ты получил его по ошибке — просто ответь на это письмо.
` } // sanitizeLine убирает переносы строк из пользовательского ввода, // вставляемого в тело письма (защита от инъекции строк в DATA-фазу SMTP). func sanitizeLine(s string) string { return strings.NewReplacer("\r", "", "\n", "").Replace(s) } // loginURL нормализует адрес фронтенда для ссылки в письме: срезает // завершающий слэш и допускает только http(s). Пустое или некорректное // значение — ссылка в письма не добавляется. func loginURL(baseURL string) string { baseURL = strings.TrimSpace(baseURL) baseURL = strings.TrimRight(baseURL, "/") if baseURL == "" { return "" } if !strings.HasPrefix(baseURL, "http://") && !strings.HasPrefix(baseURL, "https://") { return "" } return baseURL }