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 : 600 px ; margin : 0 auto ; padding : 20 px ; }
. code { font - size : 32 px ; font - weight : bold ; color : # 007 bff ;
text - align : center ; padding : 20 px ; background : # f8f9fa ;
border - radius : 8 px ; margin : 20 px 0 ; letter - spacing : 8 px ; }
. footer { margin - top : 30 px ; font - size : 12 px ; color : # 666 ; }
< / s t y l e >
< / h e a d >
< body >
< div class = "container" >
< h1 > Код подтверждения < / h 1 >
< p > Ваш код для регистрации в Nakama : < / p >
< div class = "code" > $ { code } < / d i v >
< p > Код действителен в течение 15 минут . < / p >
< div class = "footer" >
< p > Если вы не запрашивали этот код , просто проигнорируйте это письмо . < / p >
< / d i v >
< / d i v >
< / b o d y >
< / h t m l >
` ;
} ;
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-15 00:37:34 +00:00
const params = {
Source : fromEmail ,
Destination : {
ToAddresses : [ to ]
} ,
Message : {
Subject : {
Data : subject ,
Charset : 'UTF-8'
2025-12-08 23:42:32 +00:00
} ,
2025-12-15 00:37:34 +00:00
Body : {
Html : {
Data : html ,
2025-12-08 23:42:32 +00:00
Charset : 'UTF-8'
} ,
2025-12-15 00:37:34 +00:00
Text : {
Data : text || html . replace ( /<[^>]*>/g , '' ) ,
Charset : 'UTF-8'
2025-12-08 23:42:32 +00:00
}
}
2025-12-15 00:37:34 +00:00
}
} ;
2025-12-08 23:42:32 +00:00
2025-12-15 00:37:34 +00:00
const result = await sesClient . sendEmail ( params ) . promise ( ) ;
return { success : true , messageId : result . MessageId } ;
2025-12-14 14:52:58 +00:00
}
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 > Пользователь : < / s t r o n g > @ $ { u s e r I n f o . u s e r n a m e } ( $ { u s e r I n f o . f i r s t N a m e } ) < / 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 : 600 px ; margin : 0 auto ; padding : 20 px ; }
. code { font - size : 32 px ; font - weight : bold ; color : # 007 bff ;
text - align : center ; padding : 20 px ; background : # f8f9fa ;
border - radius : 8 px ; margin : 20 px 0 ; letter - spacing : 8 px ; }
. footer { margin - top : 30 px ; font - size : 12 px ; color : # 666 ; }
. info { background : # e7f3ff ; padding : 15 px ; border - radius : 8 px ; margin : 20 px 0 ; }
< / s t y l e >
< / h e a d >
< body >
< div class = "container" >
< h1 > Подтверждение $ { actionText } < / h 1 >
$ { userDetails }
< div class = "info" >
< p > < strong > Код подтверждения : < / s t r o n g > < / p >
< div class = "code" > $ { code } < / d i v >
< p > Код действителен в течение 5 минут . < / p >
< / d i v >
< div class = "footer" >
< p > Если вы не запрашивали это подтверждение , проигнорируйте это письмо . < / p >
< / d i v >
< / d i v >
< / b o d y >
< / h t m l >
` ;
} ;
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
} ;