Files
hotelsync/backend/src/email.ts
HotelSync bda0cbb7d5 Improve email deliverability: plain text, contrast fix, Content-Language, List-Unsubscribe
- Add plain text (text:) version to all emails — helps SpamAssassin scoring
- Add lang="ru" to <html> and Content-Language: ru header — signals intentional Cyrillic
- Fix HTML_FONT_LOW_CONTRAST: change #94a3b8 to #475569 / #334155 for note text
- Add List-Unsubscribe header (reduces spam score)
- Refactor: extract baseHtml() helper to avoid duplication

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 20:01:25 +03:00

128 lines
6.8 KiB
TypeScript
Raw 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.
import nodemailer from 'nodemailer'
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST ?? 'smtp.timeweb.ru',
port: parseInt(process.env.SMTP_PORT ?? '465', 10),
secure: true,
auth: {
user: process.env.SMTP_USER ?? 'noreply@hotelsync.ru',
pass: process.env.SMTP_PASS ?? '08K28150zBwii32c85',
},
})
const fromAddr = () => process.env.SMTP_USER ?? 'noreply@hotelsync.ru'
const appUrl = () => process.env.APP_URL ?? 'https://app.hotelsync.ru'
const apiUrl = () => process.env.API_URL ?? 'https://api.hotelsync.ru'
const unsubscribeHeader = `<mailto:noreply@hotelsync.ru?subject=unsubscribe>`
function baseHtml(headerColor: string, icon: string, title: string, body: string): string {
return `<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
</head>
<body style="font-family:Arial,sans-serif;background:#f1f5f9;margin:0;padding:40px 16px;">
<div style="max-width:520px;margin:0 auto;background:#ffffff;border-radius:16px;overflow:hidden;box-shadow:0 4px 24px rgba(0,0,0,0.08);">
<div style="background:${headerColor};padding:32px 40px;text-align:center;">
<div style="display:inline-block;width:48px;height:48px;background:rgba(255,255,255,0.2);border-radius:12px;line-height:48px;font-size:24px;margin-bottom:12px;">${icon}</div>
<h1 style="color:#ffffff;margin:0;font-size:26px;font-weight:700;letter-spacing:-0.5px;">HotelSync</h1>
<p style="color:#c7d2fe;margin:6px 0 0;font-size:14px;">Современная PMS-система</p>
</div>
<div style="padding:40px;">
${body}
<hr style="border:none;border-top:1px solid #e2e8f0;margin:20px 0;">
<p style="color:#64748b;font-size:11px;margin:0;line-height:1.6;text-align:center;">
© 2026 HotelSync · <a href="${appUrl()}" style="color:#6366f1;text-decoration:none;">app.hotelsync.ru</a>
</p>
</div>
</div>
</body>
</html>`
}
export async function sendPasswordResetEmail(to: string, name: string, token: string): Promise<void> {
const resetUrl = `${appUrl()}/reset-password?token=${token}`
const html = baseHtml(
'linear-gradient(135deg,#4f46e5,#6366f1)',
'🔑',
'Сброс пароля',
`<h2 style="color:#1e293b;font-size:20px;margin:0 0 12px;font-weight:600;">Сброс пароля</h2>
<p style="color:#334155;line-height:1.7;margin:0 0 8px;font-size:15px;">Привет, ${name}!</p>
<p style="color:#334155;line-height:1.7;margin:0 0 28px;font-size:15px;">
Мы получили запрос на сброс пароля для вашего аккаунта HotelSync. Нажмите на кнопку ниже, чтобы создать новый пароль.
</p>
<div style="text-align:center;margin:0 0 28px;">
<a href="${resetUrl}" style="display:inline-block;background:#4f46e5;color:#ffffff;text-decoration:none;padding:14px 36px;border-radius:10px;font-weight:600;font-size:16px;letter-spacing:0.2px;">
Создать новый пароль
</a>
</div>
<p style="color:#475569;font-size:13px;margin:0 0 16px;line-height:1.6;background:#f8fafc;padding:12px 16px;border-radius:8px;border-left:3px solid #e2e8f0;">
Ссылка действительна 1 час.<br>
Если вы не запрашивали сброс пароля — просто проигнорируйте это письмо.
</p>
<p style="color:#475569;font-size:12px;margin:0;line-height:1.6;">
Кнопка не работает? Скопируйте ссылку в браузер:<br>
<a href="${resetUrl}" style="color:#6366f1;word-break:break-all;font-size:11px;">${resetUrl}</a>
</p>`,
)
const text = `Сброс пароля — HotelSync\n\ривет, ${name}!\n\nМы получили запрос на сброс пароля для вашего аккаунта HotelSync.\n\nСсылка для сброса пароля (действительна 1 час):\n${resetUrl}\n\nЕсли вы не запрашивали сброс пароля — проигнорируйте это письмо.\n\n© 2026 HotelSync`
await transporter.sendMail({
from: `"HotelSync" <${fromAddr()}>`,
to,
subject: 'Сброс пароля — HotelSync',
html,
text,
headers: {
'List-Unsubscribe': unsubscribeHeader,
'Content-Language': 'ru',
},
})
}
export async function sendConfirmationEmail(to: string, name: string, token: string): Promise<void> {
const confirmUrl = `${apiUrl()}/api/auth/confirm-email?token=${token}`
const html = baseHtml(
'linear-gradient(135deg,#4f46e5,#6366f1)',
'🏨',
'Подтверждение email',
`<h2 style="color:#1e293b;font-size:20px;margin:0 0 12px;font-weight:600;">Добро пожаловать, ${name}!</h2>
<p style="color:#334155;line-height:1.7;margin:0 0 28px;font-size:15px;">
Спасибо за регистрацию в HotelSync. Для активации аккаунта нажмите на кнопку ниже.
</p>
<div style="text-align:center;margin:0 0 28px;">
<a href="${confirmUrl}" style="display:inline-block;background:#4f46e5;color:#ffffff;text-decoration:none;padding:14px 36px;border-radius:10px;font-weight:600;font-size:16px;letter-spacing:0.2px;">
Подтвердить email
</a>
</div>
<p style="color:#475569;font-size:13px;margin:0 0 16px;line-height:1.6;background:#f8fafc;padding:12px 16px;border-radius:8px;border-left:3px solid #e2e8f0;">
Ссылка действительна 24 часа.<br>
Если вы не регистрировались — просто проигнорируйте это письмо.
</p>
<p style="color:#475569;font-size:12px;margin:0;line-height:1.6;">
Кнопка не работает? Скопируйте ссылку в браузер:<br>
<a href="${confirmUrl}" style="color:#6366f1;word-break:break-all;font-size:11px;">${confirmUrl}</a>
</p>`,
)
const text = `Подтверждение email — HotelSync\n\обро пожаловать, ${name}!\n\nСпасибо за регистрацию в HotelSync.\n\nДля активации аккаунта перейдите по ссылке (действительна 24 часа):\n${confirmUrl}\n\nЕсли вы не регистрировались — проигнорируйте это письмо.\n\n© 2026 HotelSync`
await transporter.sendMail({
from: `"HotelSync" <${fromAddr()}>`,
to,
subject: 'Подтвердите email — HotelSync',
html,
text,
headers: {
'List-Unsubscribe': unsubscribeHeader,
'Content-Language': 'ru',
},
})
}