nakama/backend/utils/email.js

349 lines
15 KiB
JavaScript
Raw Permalink 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.

const AWS = require('aws-sdk');
const nodemailer = require('nodemailer');
const axios = require('axios');
const crypto = require('crypto');
const config = require('../config');
// Инициализация AWS SES
let sesClient = null;
let transporter = null;
const initializeEmailService = () => {
const emailProvider = process.env.EMAIL_PROVIDER || 'aws'; // aws, yandex, smtp
if (emailProvider === 'aws' && config.email?.aws) {
const accessKeyId = config.email.aws.accessKeyId;
const secretAccessKey = config.email.aws.secretAccessKey;
// Проверка наличия credentials
if (!accessKeyId || !secretAccessKey) {
console.error('[Email] ❌ AWS SES credentials не установлены!');
console.error('[Email] Установите AWS_SES_ACCESS_KEY_ID и AWS_SES_SECRET_ACCESS_KEY в .env');
console.error('[Email] Или используйте EMAIL_PROVIDER=yandex или EMAIL_PROVIDER=smtp');
sesClient = null;
return;
}
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 = {
accessKeyId: accessKeyId,
secretAccessKey: secretAccessKey,
region: awsRegion
};
// Для Yandex Cloud Postbox нужен кастомный endpoint
if (endpointUrl) {
// Yandex Cloud Postbox использует SESv2 API, сохраняем конфигурацию для прямых запросов
sesConfig.endpoint = endpointUrl;
sesConfig.isYandexCloud = true;
console.log(`[Email] Используется Yandex Cloud Postbox с endpoint: ${endpointUrl}`);
// Не создаем SES клиент для Yandex Cloud, будем использовать прямые HTTP запросы
sesClient = { config: sesConfig, isYandexCloud: true };
} else if (!validAWSRegions.includes(awsRegion)) {
console.warn(`[Email] Невалидный регион AWS SES: ${awsRegion}. Используется us-east-1`);
sesConfig.region = 'us-east-1';
sesClient = new AWS.SES(sesConfig);
console.log('[Email] ✅ AWS SES клиент инициализирован');
} else {
sesClient = new AWS.SES(sesConfig);
console.log('[Email] ✅ AWS SES клиент инициализирован');
}
} else if (emailProvider === 'yandex' || emailProvider === 'smtp') {
const emailConfig = config.email?.[emailProvider] || config.email?.smtp || {};
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_* переменные.');
}
transporter = nodemailer.createTransport({
host: smtpHost,
port: smtpPort,
secure: smtpSecure,
auth: {
user: smtpUser,
pass: smtpPassword
},
tls: {
rejectUnauthorized: false // Для отладки, в production лучше true
}
});
console.log('[Email] SMTP transporter создан успешно');
}
};
// Генерация 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';
if (emailProvider === 'aws') {
if (!sesClient) {
const errorMsg = 'AWS SES не инициализирован. Проверьте AWS_SES_ACCESS_KEY_ID и AWS_SES_SECRET_ACCESS_KEY в .env файле. Или используйте EMAIL_PROVIDER=yandex или EMAIL_PROVIDER=smtp';
console.error('[Email] ❌', errorMsg);
throw new Error(errorMsg);
}
// Проверка на Yandex Cloud Postbox
if (sesClient.isYandexCloud) {
// Yandex Cloud Postbox использует SESv2 API - используем прямой HTTP запрос
const endpoint = sesClient.config.endpoint;
const payload = {
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'
}
}
}
}
};
// Используем 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
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 sesClient.sendEmail(params).promise();
return { success: true, messageId: result.MessageId };
}
} 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);
// Более информативные сообщения об ошибках
if (error.code === 'CredentialsError' || (error.message && error.message.includes('Missing credentials'))) {
const errorMsg = 'AWS SES credentials не настроены. Установите AWS_SES_ACCESS_KEY_ID и AWS_SES_SECRET_ACCESS_KEY в .env файле. Или используйте EMAIL_PROVIDER=yandex или EMAIL_PROVIDER=smtp';
console.error('[Email] ❌', errorMsg);
throw new Error(errorMsg);
} else 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, а не основной пароль аккаунта.');
}
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
};