Files
evening_detective_server/internal/services/users_service/emails.go
T
2026-09-15 05:01:51 +07:00

231 lines
12 KiB
Go

package users_service
import (
"fmt"
"html"
"strings"
)
// Письма содержат текстовую и 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("<p class=\"paper-text\" style=\"color:#211a14; -webkit-text-fill-color:#211a14;\">Привет, <strong>")
content.WriteString(html.EscapeString(username))
content.WriteString("</strong>!</p>\n")
content.WriteString("<p class=\"paper-text\" style=\"color:#211a14; -webkit-text-fill-color:#211a14;\">Твоя регистрация в «Вечернем детективе» прошла. Данные для входа:</p>\n")
content.WriteString(credentialsBlockHTML(email, password))
if loginURL != "" {
content.WriteString(buttonHTML(loginURL, "Войти в игру"))
}
content.WriteString("<p class=\"paper-text\" style=\"color:#211a14; -webkit-text-fill-color:#211a14; font-size:13px;\">Если ты не регистрировался — просто проигнорируй это письмо.</p>")
return wrapHTML(content.String(), loginURL)
}
// 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("<p class=\"paper-text\" style=\"color:#211a14; -webkit-text-fill-color:#211a14;\">Привет, <strong>")
content.WriteString(html.EscapeString(username))
content.WriteString("</strong>!</p>\n")
content.WriteString("<p class=\"paper-text\" style=\"color:#211a14; -webkit-text-fill-color:#211a14;\">Ты запросил сброс пароля в «Вечернем детективе». Новый пароль для входа:</p>\n")
content.WriteString(credentialsBlockHTML(email, password))
if loginURL != "" {
content.WriteString(buttonHTML(loginURL, "Войти в игру"))
}
content.WriteString("<p class=\"paper-text\" style=\"color:#211a14; -webkit-text-fill-color:#211a14;\">Никому не сообщай пароль и не пересылай это письмо.</p>\n")
content.WriteString("<p class=\"paper-text\" style=\"color:#211a14; -webkit-text-fill-color:#211a14; font-size:13px;\">Если это был не ты — ответь на это письмо: мы поможем защитить аккаунт.</p>")
return wrapHTML(content.String(), loginURL)
}
// 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 `<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin:24px 0; border-top:1px dashed black; border-bottom:1px dashed black; table-layout:fixed;">
<tr><td class="paper-text" style="padding:16px 0 4px; font-family:Arial,Helvetica,sans-serif; font-size:12px; color:#211a14; -webkit-text-fill-color:#211a14;">Логин</td></tr>
<tr><td class="paper-text" style="padding:0 0 16px; font-size:16px; color:#211a14; -webkit-text-fill-color:#211a14; overflow-wrap:anywhere; word-break:break-all;">` + html.EscapeString(email) + `</td></tr>
<tr><td class="paper-text" style="padding:16px 0 4px; border-top:1px dashed black; font-family:Arial,Helvetica,sans-serif; font-size:12px; color:#211a14; -webkit-text-fill-color:#211a14;">Пароль</td></tr>
<tr><td class="paper-text" style="padding:0 0 16px; font-size:20px; color:#211a14; -webkit-text-fill-color:#211a14; overflow-wrap:anywhere; word-break:break-all;"><strong>` + html.EscapeString(password) + `</strong></td></tr>
</table>`
}
// buttonHTML — кнопка входа с inline-стилями и табличным фоном.
func buttonHTML(link, label string) string {
return `<table role="presentation" cellpadding="0" cellspacing="0" style="margin:20px 0;">
<tr>
<td style="border-radius:5px; background-color:#222; background-image:url('` + emailAssetURL(link, "belt.png") + `'); background-size:cover;">
<a href="` + html.EscapeString(link) + `" style="display:inline-block; padding:8px 16px; color:#bfa07d; -webkit-text-fill-color:#bfa07d; font-family:'font_old_typer','Courier New',serif; font-weight:400; text-decoration:none; font-size:18px; line-height:24px; border-radius:5px;">` + html.EscapeString(label) + `</a>
</td>
</tr>
</table>`
}
// wrapHTML — каркас письма: тёмная шапка с названием сервиса, контент,
// подвал. Основное оформление — inline; при блокировке текстур и шрифта
// остаются светлый фон и системный шрифт.
func wrapHTML(content, baseURL string) string {
return `<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="color-scheme" content="light">
<meta name="supported-color-schemes" content="light">
<style>
:root { color-scheme:light; supported-color-schemes:light; }
@font-face { font-family:'font_old_typer'; src:url('` + emailAssetURL(baseURL, "a_OldTyper.ttf") + `') format('truetype'); }
/* Apple Mail, Samsung Email and other clients that expose the theme. */
@media (prefers-color-scheme:dark) {
.paper { background-color:#f4e4cc !important; }
.paper-text, .paper-text strong, .paper-text a { color:#211a14 !important; -webkit-text-fill-color:#211a14 !important; }
}
/* Outlook web/mobile: attributes may be on the element itself or a parent. */
[data-ogsb] .paper, .paper[data-ogsb] { background-color:#f4e4cc !important; }
[data-ogsc] .paper-text, .paper-text[data-ogsc], [data-ogsc] .paper-text strong, [data-ogsc] .paper-text a {
color:#211a14 !important; -webkit-text-fill-color:#211a14 !important;
}
/* Gmail iOS: paint letters with a gradient that survives color inversion.
https://emailmatrix.ru/blog/gmail-ios-dark-mode/ */
@media only screen and (max-width:480px) {
u + .email-body .paper-text {
background-image:linear-gradient(#211a14,#211a14);
background-clip:text;
color:transparent !important;
-webkit-text-fill-color:transparent !important;
}
u + .email-body .paper-text strong, u + .email-body .paper-text a {
color:inherit !important; -webkit-text-fill-color:inherit !important;
}
/* Gmail Android cannot clip the gradient to text. Keep background and
text together so its automatic recoloring preserves contrast. */
div > u + .email-body .paper-text {
background-image:none;
background-clip:border-box;
background-color:#f4e4cc;
color:#211a14 !important;
-webkit-text-fill-color:currentcolor !important;
}
}
</style>
</head>
<body class="email-body" style="margin:0; padding:0; background-color:#111; font-family:Arial, Helvetica, sans-serif;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#111; padding:24px 12px;">
<tr><td align="center">
<table role="presentation" width="600" cellpadding="0" cellspacing="0" style="max-width:600px; width:100%;">
<tr>
<td style="padding:16px 0 24px;">
<span style="color:#bfa07d; -webkit-text-fill-color:#bfa07d; font-family:'font_old_typer','Courier New',serif; font-size:22px; line-height:1.4; font-weight:normal;">ВЕЧЕРНИЙ ДЕТЕКТИВ</span>
</td>
</tr>
<tr>
<td class="paper" bgcolor="#f4e4cc" style="padding:16px; background-color:#f4e4cc; background-image:url('` + emailAssetURL(baseURL, "paper.jpg") + `'); background-image:url('` + emailAssetURL(baseURL, "paper.jpg") + `'),linear-gradient(#f4e4cc,#f4e4cc); background-size:cover; border-radius:5px; box-shadow:0 0 5px black; color:#211a14; font-family:'font_old_typer','Courier New',serif; font-size:18px; line-height:1.5; font-weight:400;">
` + content + `
</td>
</tr>
<tr>
<td style="padding:24px 0; color:#aaa; -webkit-text-fill-color:#aaa; font-size:12px; line-height:1.6;">
Это автоматическое письмо сервиса «Вечерний детектив».<br>
Если ты получил его по ошибке — просто ответь на это письмо.
</td>
</tr>
</table>
</td></tr>
</table>
</body>
</html>`
}
// Статические копии игровых текстур и шрифта находятся в public/email фронтенда.
func emailAssetURL(baseURL, filename string) string {
if baseURL == "" {
return ""
}
return html.EscapeString(strings.TrimRight(baseURL, "/") + "/email/" + filename)
}
// 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
}