- 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>
128 lines
6.8 KiB
TypeScript
128 lines
6.8 KiB
TypeScript
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\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\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',
|
||
},
|
||
})
|
||
}
|