astro_avtourist/frontend/src/lib/email.ts

223 lines
10 KiB
TypeScript
Raw Normal View History

import nodemailer from 'nodemailer';
import type { EmailOptions } from '../globalInterfaces';
const isDev = import.meta.env.DEV;
2026-05-04 03:51:17 +05:00
const hasProductionFlag = import.meta.env.PROD === 'true';
const forceProduction = import.meta.env.PB_POCKETBASE_URL?.includes('avt-back') || false;
const isProduction = hasProductionFlag || forceProduction || !isDev;
2026-05-04 02:45:06 +05:00
const SMTP_HOST = isProduction
2026-05-04 04:55:29 +05:00
? (import.meta.env.SMTP_HOST || 'smtp.gmail.com')
2026-05-04 02:45:06 +05:00
: 'localhost';
const SMTP_PORT = isProduction
2026-05-04 03:39:36 +05:00
? (import.meta.env.SMTP_PORT || '587')
2026-05-04 02:45:06 +05:00
: '1025';
const SMTP_AUTH_USER = isProduction ? (import.meta.env.SMTP_AUTH_USER || '') : '';
const SMTP_AUTH_PASS = isProduction ? (import.meta.env.SMTP_AUTH_PASS || '') : '';
const FROM_EMAIL = isProduction
2026-05-04 04:05:35 +05:00
? (import.meta.env.FROM_EMAIL || 'noreply@localhost')
2026-05-04 02:45:06 +05:00
: 'noreply@localhost';
const FROM_NAME = isProduction ? (import.meta.env.FROM_NAME || 'Автоюрист Сургут') : 'Dev';
const SITE_URL = isProduction
? (import.meta.env.SITE_URL || 'https://avtourist-surgut.ru')
: 'http://localhost:4321';
let transporter: nodemailer.Transporter | null = null;
2026-05-04 03:51:17 +05:00
console.log('[EMAIL] isDev:', isDev);
console.log('[EMAIL] isProduction:', isProduction);
2026-05-04 04:05:35 +05:00
console.log('[EMAIL] SMTP:', SMTP_HOST, ':', SMTP_PORT);
function getTransporter() {
if (!transporter) {
2026-05-04 03:39:36 +05:00
const useTLS = SMTP_PORT === '465';
transporter = nodemailer.createTransport({
host: SMTP_HOST,
port: parseInt(SMTP_PORT),
2026-05-04 03:39:36 +05:00
secure: useTLS,
requireTLS: !useTLS,
connectionTimeout: 10000,
greetingTimeout: 10000,
2026-05-04 02:45:06 +05:00
auth: isProduction && SMTP_AUTH_USER ? {
user: SMTP_AUTH_USER,
pass: SMTP_AUTH_PASS,
2026-05-04 02:45:06 +05:00
} : undefined,
});
}
return transporter;
}
2026-05-04 03:51:17 +05:00
async function sendViaResendApi(options: EmailOptions): Promise<boolean> {
try {
console.log('[RESEND_API] Sending to:', options.to);
const response = await fetch('https://api.resend.com/emails', {
method: 'POST',
headers: {
'Authorization': `Bearer ${RESEND_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
from: `${FROM_NAME} <${FROM_EMAIL}>`,
to: options.to,
subject: options.subject,
html: options.html,
}),
});
const data = await response.json();
if (response.ok) {
console.log('[RESEND_API] Email sent:', data.id);
return true;
} else {
console.error('[RESEND_API] Error:', data);
return false;
}
} catch (error) {
console.error('[RESEND_API] Error:', error);
return false;
}
}
2026-05-04 03:39:36 +05:00
function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
return Promise.race([
promise,
new Promise<T>((_, reject) =>
setTimeout(() => reject(new Error('Email send timeout')), ms)
)
]);
}
export async function sendEmail(options: EmailOptions): Promise<boolean> {
2026-05-04 03:51:17 +05:00
console.log('[NODEMAILER] Sending to:', options.to);
2026-05-04 04:05:35 +05:00
console.log('[NODEMAILER] SMTP config:', { host: SMTP_HOST, port: SMTP_PORT, user: SMTP_AUTH_USER });
2026-05-04 03:51:17 +05:00
try {
2026-05-04 03:39:36 +05:00
const info = await withTimeout(
getTransporter().sendMail({
from: `${FROM_NAME} <${FROM_EMAIL}>`,
to: options.to,
subject: options.subject,
html: options.html,
}),
15000
);
2026-05-04 03:51:17 +05:00
console.log('[NODEMAILER] Email sent:', info.messageId);
return true;
} catch (error) {
2026-05-04 03:51:17 +05:00
console.error('[NODEMAILER] Email send error:', error);
return false;
}
}
export function getSiteUrl(): string {
return SITE_URL;
}
export function generateVerifyEmailHtml(firstName: string, verifyLink: string): string {
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Подтверждение регистрации</title>
</head>
<body style="margin: 0; padding: 0; background-color: #f5f7fa; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;">
<table width="100%" cellpadding="0" cellspacing="0" style="background-color: #f5f7fa; padding: 40px 20px;">
<tr>
<td align="center">
<table width="100%" cellpadding="0" cellspacing="0" style="max-width: 600px; background-color: #ffffff; border-radius: 16px; overflow: hidden; box-shadow: 0 4px 20px rgba(0,0,0,0.08);">
<!-- Header -->
<tr>
<td style="background: linear-gradient(135deg, #1e3050 0%, #2d4a6f 100%); padding: 30px 40px; text-align: center;">
<h1 style="color: #ffffff; margin: 0; font-size: 28px; font-weight: 700;">Автоюрист Сургут</h1>
<p style="color: rgba(255,255,255,0.8); margin: 10px 0 0 0; font-size: 16px;">Юридические услуги для автовладельцев</p>
</td>
</tr>
<!-- Content -->
<tr>
<td style="padding: 40px;">
<h2 style="color: #1e3050; margin: 0 0 20px 0; font-size: 24px; font-weight: 700;">Добро пожаловать, ${firstName}!</h2>
<p style="color: #64748b; font-size: 16px; line-height: 1.6; margin: 0 0 20px 0;">
Благодарим за регистрацию на сайте <strong>avtourist-surgut.ru</strong>. Мы рады, что вы выбрали нас для решения юридических вопросов, связанных с автомобилем.
</p>
<p style="color: #64748b; font-size: 16px; line-height: 1.6; margin: 0 0 30px 0;">
Для завершения регистрации и активации вашего аккаунта, пожалуйста, подтвердите ваш email, нажав на кнопку ниже:
</p>
<!-- Button -->
<table width="100%" cellpadding="0" cellspacing="0">
<tr>
<td align="center">
<a href="${verifyLink}" style="display: inline-block; background: linear-gradient(135deg, #eac26e 0%, #ce9f40 100%); color: #ffffff; text-decoration: none; padding: 16px 32px; border-radius: 8px; font-size: 16px; font-weight: 700;">
Подтвердить регистрацию
</a>
</td>
</tr>
</table>
</td>
</tr>
<!-- Features -->
<tr>
<td style="background-color: #f8fafc; padding: 30px 40px;">
<table width="100%" cellpadding="0" cellspacing="0">
<tr>
<td align="center" style="padding: 0 10px;">
<div style="width: 48px; height: 48px; background: linear-gradient(135deg, #eac26e 0%, #ce9f40 100%); border-radius: 12px; display: inline-flex; align-items: center; justify-content: center; margin-bottom: 12px;">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"></path>
</svg>
</div>
<p style="color: #1e3050; font-size: 13px; font-weight: 600; margin: 0;">Бесплатная консультация</p>
</td>
<td align="center" style="padding: 0 10px;">
<div style="width: 48px; height: 48px; background: linear-gradient(135deg, #eac26e 0%, #ce9f40 100%); border-radius: 12px; display: inline-flex; align-items: center; justify-content: center; margin-bottom: 12px;">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
<polyline points="14 2 14 8 20 8"></polyline>
<line x1="16" y1="13" x2="8" y2="13"></line>
<line x1="16" y1="17" x2="8" y2="17"></line>
</svg>
</div>
<p style="color: #1e3050; font-size: 13px; font-weight: 600; margin: 0;">Оплата за результат</p>
</td>
<td align="center" style="padding: 0 10px;">
<div style="width: 48px; height: 48px; background: linear-gradient(135deg, #eac26e 0%, #ce9f40 100%); border-radius: 12px; display: inline-flex; align-items: center; justify-content: center; margin-bottom: 12px;">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<polyline points="12 6 12 12 16 14"></polyline>
</svg>
</div>
<p style="color: #1e3050; font-size: 13px; font-weight: 600; margin: 0;">Работаем 24/7</p>
</td>
</tr>
</table>
</td>
</tr>
<!-- Footer -->
<tr>
<td style="background-color: #1e3050; padding: 24px 40px; text-align: center;">
<p style="color: rgba(255,255,255,0.6); font-size: 14px; margin: 0;">
© 2026 Автоюрист Сургут. Все права защищены.
</p>
<p style="color: rgba(255,255,255,0.4); font-size: 12px; margin: 10px 0 0 0;">
Это письмо отправлено автоматически, пожалуйста, не отвечайте на него.
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
`;
}