nakama/backend/utils/email.js

330 lines
13 KiB
JavaScript
Raw Normal View History

2025-12-08 23:42:32 +00:00
const AWS = require('aws-sdk');
const nodemailer = require('nodemailer');
2025-12-14 15:00:08 +00:00
const axios = require('axios');
const crypto = require('crypto');
2025-12-08 23:42:32 +00:00
const config = require('../config');
2025-12-14 15:00:08 +00:00
// Инициализация AWS SES
2025-12-08 23:42:32 +00:00
let sesClient = null;
let transporter = null;
const initializeEmailService = () => {
const emailProvider = process.env.EMAIL_PROVIDER || 'aws'; // aws, yandex, smtp
if (emailProvider === 'aws' && config.email?.aws) {
2025-12-14 14:41:29 +00:00
const awsRegion = config.email.aws.region || 'us-east-1';
const validAWSRegions = [
'us-east-1', 'us-east-2', 'us-west-1', 'us-west-2',
'eu-west-1', 'eu-west-2', 'eu-west-3', 'eu-central-1',
'ap-southeast-1', 'ap-southeast-2', 'ap-northeast-1',
'sa-east-1', 'ca-central-1'
];
// Проверка на Yandex Cloud Postbox (использует ru-central1)
const isYandexCloud = awsRegion === 'ru-central1';
const endpointUrl = config.email.aws.endpoint || process.env.AWS_SES_ENDPOINT_URL ||
(isYandexCloud ? 'https://postbox.cloud.yandex.net' : null);
const sesConfig = {
2025-12-08 23:42:32 +00:00
accessKeyId: config.email.aws.accessKeyId,
secretAccessKey: config.email.aws.secretAccessKey,
2025-12-14 14:41:29 +00:00
region: awsRegion
};
2025-12-14 15:00:08 +00:00
// Для Yandex Cloud Postbox нужен кастомный endpoint
2025-12-14 14:41:29 +00:00
if (endpointUrl) {
2025-12-14 15:00:08 +00:00
// Yandex Cloud Postbox использует SESv2 API, сохраняем конфигурацию для прямых запросов
2025-12-14 14:41:29 +00:00
sesConfig.endpoint = endpointUrl;
2025-12-14 15:00:08 +00:00
sesConfig.isYandexCloud = true;
2025-12-14 14:41:29 +00:00
console.log(`[Email] Используется Yandex Cloud Postbox с endpoint: ${endpointUrl}`);
2025-12-14 15:00:08 +00:00
// Не создаем SES клиент для Yandex Cloud, будем использовать прямые HTTP запросы
2025-12-14 14:41:29 +00:00
} else if (!validAWSRegions.includes(awsRegion)) {
console.warn(`[Email] Невалидный регион AWS SES: ${awsRegion}. Используется us-east-1`);
sesConfig.region = 'us-east-1';
2025-12-14 14:52:58 +00:00
sesClient = new AWS.SES(sesConfig);
} else {
sesClient = new AWS.SES(sesConfig);
2025-12-14 14:41:29 +00:00
}
2025-12-14 15:00:08 +00:00
// Сохраняем конфигурацию для Yandex Cloud
if (endpointUrl) {
sesClient = { config: sesConfig, isYandexCloud: true };
}
2025-12-08 23:42:32 +00:00
} else if (emailProvider === 'yandex' || emailProvider === 'smtp') {
const emailConfig = config.email?.[emailProvider] || config.email?.smtp || {};
2025-12-14 23:45:41 +00:00
const smtpHost = emailConfig.host || process.env.SMTP_HOST || process.env.YANDEX_SMTP_HOST;
const smtpPort = emailConfig.port || parseInt(process.env.SMTP_PORT || process.env.YANDEX_SMTP_PORT || '587', 10);
const smtpSecure = emailConfig.secure !== undefined ? emailConfig.secure :
(process.env.SMTP_SECURE === 'true' || process.env.YANDEX_SMTP_SECURE === 'true' || smtpPort === 465);
const smtpUser = emailConfig.user || process.env.SMTP_USER || process.env.YANDEX_SMTP_USER;
const smtpPassword = emailConfig.password || process.env.SMTP_PASSWORD || process.env.YANDEX_SMTP_PASSWORD;
console.log('[Email] Настройка SMTP:', {
provider: emailProvider,
host: smtpHost,
port: smtpPort,
secure: smtpSecure,
user: smtpUser ? `${smtpUser.substring(0, 3)}***` : 'не указан',
hasPassword: !!smtpPassword,
envVars: {
YANDEX_SMTP_HOST: !!process.env.YANDEX_SMTP_HOST,
YANDEX_SMTP_USER: !!process.env.YANDEX_SMTP_USER,
YANDEX_SMTP_PASSWORD: !!process.env.YANDEX_SMTP_PASSWORD,
SMTP_HOST: !!process.env.SMTP_HOST,
SMTP_USER: !!process.env.SMTP_USER,
SMTP_PASSWORD: !!process.env.SMTP_PASSWORD
}
});
if (!smtpHost || !smtpUser || !smtpPassword) {
console.error('[Email] Неполная конфигурация SMTP:', {
hasHost: !!smtpHost,
hasUser: !!smtpUser,
hasPassword: !!smtpPassword,
emailConfig: emailConfig,
configEmail: config.email
});
throw new Error('SMTP конфигурация неполная. Проверьте настройки в .env. Для Yandex используйте YANDEX_SMTP_* переменные.');
}
2025-12-08 23:42:32 +00:00
transporter = nodemailer.createTransport({
2025-12-14 23:45:41 +00:00
host: smtpHost,
port: smtpPort,
secure: smtpSecure,
2025-12-08 23:42:32 +00:00
auth: {
2025-12-14 23:45:41 +00:00
user: smtpUser,
pass: smtpPassword
},
tls: {
rejectUnauthorized: false // Для отладки, в production лучше true
2025-12-08 23:42:32 +00:00
}
});
2025-12-14 23:45:41 +00:00
console.log('[Email] SMTP transporter создан успешно');
2025-12-08 23:42:32 +00:00
}
};
// Генерация HTML письма с кодом
const generateVerificationEmail = (code) => {
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.code { font-size: 32px; font-weight: bold; color: #007bff;
text-align: center; padding: 20px; background: #f8f9fa;
border-radius: 8px; margin: 20px 0; letter-spacing: 8px; }
.footer { margin-top: 30px; font-size: 12px; color: #666; }
</style>
</head>
<body>
<div class="container">
<h1>Код подтверждения</h1>
<p>Ваш код для регистрации в Nakama:</p>
<div class="code">${code}</div>
<p>Код действителен в течение 15 минут.</p>
<div class="footer">
<p>Если вы не запрашивали этот код, просто проигнорируйте это письмо.</p>
</div>
</div>
</body>
</html>
`;
};
const sendEmail = async (to, subject, html, text) => {
try {
const emailProvider = process.env.EMAIL_PROVIDER || 'aws';
const fromEmail = process.env.EMAIL_FROM || config.email?.from || 'noreply@nakama.guru';
2025-12-14 15:00:08 +00:00
if (emailProvider === 'aws' && sesClient) {
// Проверка на Yandex Cloud Postbox
if (sesClient.isYandexCloud) {
// Yandex Cloud Postbox использует SESv2 API - используем прямой HTTP запрос
const endpoint = sesClient.config.endpoint;
const payload = {
2025-12-14 14:52:58 +00:00
FromEmailAddress: fromEmail,
Destination: {
ToAddresses: [to]
},
Content: {
Simple: {
Subject: {
Data: subject,
Charset: 'UTF-8'
},
Body: {
Html: {
Data: html,
Charset: 'UTF-8'
},
Text: {
Data: text || html.replace(/<[^>]*>/g, ''),
Charset: 'UTF-8'
}
}
}
}
};
2025-12-14 15:00:08 +00:00
// Используем AWS SDK для создания подписи, но отправляем через axios
// Создаем временный SES клиент для подписи запроса
const tempSES = new AWS.SES({
accessKeyId: sesClient.config.accessKeyId,
secretAccessKey: sesClient.config.secretAccessKey,
region: sesClient.config.region,
endpoint: endpoint
});
// Пробуем использовать обычный SES API (может не работать)
try {
const params = {
Source: fromEmail,
Destination: { ToAddresses: [to] },
Message: {
Subject: { Data: subject, Charset: 'UTF-8' },
Body: {
Html: { Data: html, Charset: 'UTF-8' },
Text: { Data: text || html.replace(/<[^>]*>/g, ''), Charset: 'UTF-8' }
}
}
};
const result = await tempSES.sendEmail(params).promise();
return { success: true, messageId: result.MessageId };
} catch (sesError) {
// Если SES API не работает, пробуем через SMTP
console.warn('[Email] SES API не работает с Yandex Cloud, используйте EMAIL_PROVIDER=smtp или yandex');
throw new Error('Yandex Cloud Postbox требует SESv2 API. Используйте EMAIL_PROVIDER=yandex или smtp');
}
} else {
// Обычный AWS SES
2025-12-14 14:52:58 +00:00
const params = {
Source: fromEmail,
Destination: {
ToAddresses: [to]
2025-12-08 23:42:32 +00:00
},
2025-12-14 14:52:58 +00:00
Message: {
Subject: {
Data: subject,
2025-12-08 23:42:32 +00:00
Charset: 'UTF-8'
},
2025-12-14 14:52:58 +00:00
Body: {
Html: {
Data: html,
Charset: 'UTF-8'
},
Text: {
Data: text || html.replace(/<[^>]*>/g, ''),
Charset: 'UTF-8'
}
2025-12-08 23:42:32 +00:00
}
}
2025-12-14 14:52:58 +00:00
};
2025-12-08 23:42:32 +00:00
2025-12-14 14:52:58 +00:00
const result = await sesClient.sendEmail(params).promise();
return { success: true, messageId: result.MessageId };
}
2025-12-08 23:42:32 +00:00
} else if (transporter) {
// Отправка через SMTP (Yandex, Gmail и т.д.)
const info = await transporter.sendMail({
from: fromEmail,
to,
subject,
html,
text: text || html.replace(/<[^>]*>/g, '')
});
return { success: true, messageId: info.messageId };
} else {
throw new Error('Email service not configured');
}
} catch (error) {
console.error('Ошибка отправки email:', error);
2025-12-14 23:45:41 +00:00
// Более информативные сообщения об ошибках
if (error.code === 'EAUTH') {
throw new Error('Неверные учетные данные SMTP. Проверьте YANDEX_SMTP_USER и YANDEX_SMTP_PASSWORD в .env файле. Для Yandex используйте пароль приложения, а не основной пароль.');
} else if (error.code === 'ECONNECTION') {
throw new Error('Не удалось подключиться к SMTP серверу. Проверьте YANDEX_SMTP_HOST и YANDEX_SMTP_PORT.');
} else if (error.message && error.message.includes('Authentication credentials invalid')) {
throw new Error('Неверные учетные данные SMTP. Убедитесь, что используете пароль приложения для Yandex, а не основной пароль аккаунта.');
}
2025-12-08 23:42:32 +00:00
throw error;
}
};
const sendVerificationCode = async (email, code) => {
const subject = 'Код подтверждения регистрации - Nakama';
const html = generateVerificationEmail(code);
const text = `Ваш код подтверждения: ${code}. Код действителен 15 минут.`;
return await sendEmail(email, subject, html, text);
};
// Генерация HTML письма с кодом для админа
const generateAdminConfirmationEmail = (code, action, userInfo) => {
const actionText = action === 'add' ? 'добавления админа' : 'удаления админа';
const userDetails = userInfo ? `
<p><strong>Пользователь:</strong> @${userInfo.username} (${userInfo.firstName})</p>
${userInfo.adminNumber ? `<p><strong>Номер админа:</strong> ${userInfo.adminNumber}</p>` : ''}
` : '';
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.code { font-size: 32px; font-weight: bold; color: #007bff;
text-align: center; padding: 20px; background: #f8f9fa;
border-radius: 8px; margin: 20px 0; letter-spacing: 8px; }
.footer { margin-top: 30px; font-size: 12px; color: #666; }
.info { background: #e7f3ff; padding: 15px; border-radius: 8px; margin: 20px 0; }
</style>
</head>
<body>
<div class="container">
<h1>Подтверждение ${actionText}</h1>
${userDetails}
<div class="info">
<p><strong>Код подтверждения:</strong></p>
<div class="code">${code}</div>
<p>Код действителен в течение 5 минут.</p>
</div>
<div class="footer">
<p>Если вы не запрашивали это подтверждение, проигнорируйте это письмо.</p>
</div>
</div>
</body>
</html>
`;
};
const sendAdminConfirmationCode = async (code, action, userInfo) => {
const ownerEmail = config.ownerEmail || process.env.OWNER_EMAIL || 'aaem9848@gmail.com';
const actionText = action === 'add' ? 'добавления админа' : 'удаления админа';
const subject = `Код подтверждения ${actionText} - Nakama Moderation`;
const html = generateAdminConfirmationEmail(code, action, userInfo);
const text = `Код подтверждения ${actionText}: ${code}\n\nПользователь: @${userInfo?.username || 'не указан'}\nКод действителен 5 минут.`;
return await sendEmail(ownerEmail, subject, html, text);
};
// Инициализация при загрузке модуля
initializeEmailService();
module.exports = {
sendEmail,
sendVerificationCode,
sendAdminConfirmationCode,
initializeEmailService
};