diff --git a/email-notifications.js b/email-notifications.js index 2c5e069..82e4052 100644 --- a/email-notifications.js +++ b/email-notifications.js @@ -3,52 +3,105 @@ const { getDb } = require('./database'); class EmailNotifications { constructor() { - this.transporter = null; + this.transporters = []; + this.activeTransporterIndex = 0; this.initialized = false; - this.notificationCooldown = 12 * 60 * 60 * 1000; // 12 часов в миллисекундах - this.spamBlockCooldown = 15 * 60 * 1000; // 60 минут при блокировке спама + this.notificationCooldown = 12 * 60 * 60 * 1000; // 12 часов + this.spamBlockCooldown = 15 * 60 * 1000; // 15 минут при блокировке спама this.spamBlockedUntil = null; this.isSpamBlocked = false; - this.maxRetries = 3; - this.init(); + this.transporterNames = ['Yandex', 'SMTP1', 'SMTP2']; - // Запускаем обработку очереди каждые 5 минут - setInterval(() => this.processRetryQueue(), 5 * 60 * 1000); + // Новая переменная для игнорирования спам-блокировки + this.ignoreSpamBlock = process.env.IGNORE_SPAM_BLOCK === 'true'; + + if (this.ignoreSpamBlock) { + console.log('🔓 Режим игнорирования спам-блокировки включен'); + console.log(' При детекции спама будет только ротация SMTP без глобальной блокировки'); + } + + this.init(); } async init() { try { - console.log('🔧 Инициализация Email уведомлений...'); - - if (!process.env.YANDEX_EMAIL || !process.env.YANDEX_PASSWORD) { - console.warn('⚠️ Настройки Яндекс почты не указаны в .env'); + console.log('🔧 Инициализация Email уведомлений с несколькими SMTP...'); + + // Проверяем и инициализируем все SMTP аккаунты + const smtpConfigs = this.getSmtpConfigs(); + + if (smtpConfigs.length === 0) { + console.warn('⚠️ Настройки SMTP не указаны в .env'); console.warn(' Email уведомления будут отключены'); this.initialized = false; return; } - this.transporter = nodemailer.createTransport({ - host: process.env.YANDEX_SMTP_HOST || 'smtp.yandex.ru', - port: parseInt(process.env.YANDEX_SMTP_PORT) || 587, - secure: process.env.YANDEX_SMTP_SECURE === 'true', - auth: { - user: process.env.YANDEX_EMAIL, - pass: process.env.YANDEX_PASSWORD - }, - tls: { - rejectUnauthorized: false - } - }); + console.log(`📧 Найдено ${smtpConfigs.length} SMTP конфигураций`); + + // Создаем транспортеры для каждого SMTP + this.transporters = []; + let successfulTransports = 0; + + for (const config of smtpConfigs) { + try { + const transporter = nodemailer.createTransport({ + host: config.host, + port: config.port, + secure: config.secure, + auth: { + user: config.user, + pass: config.password + }, + tls: { + rejectUnauthorized: false + } + }); + + // Тестируем подключение + await transporter.verify(); + + this.transporters.push({ + name: config.name, + transporter: transporter, + email: config.user, + host: config.host, + status: 'active', + port: config.port, + secure: config.secure, + configSource: config.source + }); + + successfulTransports++; + console.log(`✅ ${config.name} инициализирован: ${config.user}@${config.host}:${config.port}`); + + } catch (error) { + console.warn(`⚠️ ${config.name} не доступен: ${error.message}`); + console.log(` Конфигурация: ${config.user}@${config.host}:${config.port}`); + } + } + + if (this.transporters.length === 0) { + console.error('❌ Все SMTP серверы недоступны'); + console.log(' Проверьте настройки в .env файле:'); + console.log(' - YANDEX_EMAIL, YANDEX_PASSWORD'); + console.log(' - EMAIL_LOGIN, EMAIL_PASSWORD, EMAIL_HOST, EMAIL_PORT'); + console.log(' - EMAIL2_LOGIN, EMAIL2_PASSWORD, EMAIL2_HOST, EMAIL2_PORT'); + this.initialized = false; + return; + } - // Тестируем подключение - await this.transporter.verify(); this.initialized = true; // Восстанавливаем состояние блокировки из БД await this.restoreSpamBlockState(); - console.log('✅ Email уведомления инициализированы'); - console.log(`📧 Отправитель: ${process.env.YANDEX_EMAIL}`); + console.log(`✅ Email уведомления инициализированы с ${this.transporters.length} SMTP серверами`); + console.log(`📧 Активный SMTP: ${this.transporters[this.activeTransporterIndex].name} (${this.transporters[this.activeTransporterIndex].email})`); + console.log(`🔓 Режим игнорирования спам-блокировки: ${this.ignoreSpamBlock ? 'ВКЛЮЧЕН' : 'ВЫКЛЮЧЕН'}`); + + // Выводим статус всех SMTP + this.printSmtpStatus(); if (this.isSpamBlocked && this.spamBlockedUntil) { const now = new Date(); @@ -56,16 +109,162 @@ class EmailNotifications { const minutesLeft = Math.ceil((this.spamBlockedUntil - now) / (60 * 1000)); console.log(`⏸️ Email отправка заблокирована из-за спама. До разблокировки: ${minutesLeft} минут`); } else { - this.clearSpamBlock(); + await this.clearSpamBlock(); } } } catch (error) { console.error('❌ Ошибка инициализации Email уведомлений:', error.message); + console.error(error.stack); this.initialized = false; } } + // Получаем все SMTP конфигурации из env + getSmtpConfigs() { + const configs = []; + + // Яндекс SMTP + if (process.env.YANDEX_EMAIL && process.env.YANDEX_PASSWORD) { + configs.push({ + name: 'Yandex', + user: process.env.YANDEX_EMAIL, + password: process.env.YANDEX_PASSWORD, + host: process.env.YANDEX_SMTP_HOST || 'smtp.yandex.ru', + port: parseInt(process.env.YANDEX_SMTP_PORT) || 587, + secure: process.env.YANDEX_SMTP_SECURE === 'true', + source: 'YANDEX' + }); + } + + // SMTP 1 + if (process.env.EMAIL_LOGIN && process.env.EMAIL_PASSWORD) { + configs.push({ + name: 'SMTP1', + user: process.env.EMAIL_LOGIN, + password: process.env.EMAIL_PASSWORD, + host: process.env.EMAIL_HOST || 'smtp.gmail.com', + port: parseInt(process.env.EMAIL_PORT) || 587, + secure: process.env.EMAIL_SECURE === 'true', + source: 'EMAIL' + }); + } + + // SMTP 2 + if (process.env.EMAIL2_LOGIN && process.env.EMAIL2_PASSWORD) { + configs.push({ + name: 'SMTP2', + user: process.env.EMAIL2_LOGIN, + password: process.env.EMAIL2_PASSWORD, + host: process.env.EMAIL2_HOST || 'smtp.gmail.com', + port: parseInt(process.env.EMAIL2_PORT) || 587, + secure: process.env.EMAIL2_SECURE === 'true', + source: 'EMAIL2' + }); + } + + return configs; + } + + // Выводим статус всех SMTP + printSmtpStatus() { + console.log('\n📊 Статус SMTP серверов:'); + console.log('─'.repeat(80)); + console.log('│ № │ Название │ Email │ Хост │ Порт │ Статус │'); + console.log('├────┼──────────┼───────┼──────┼──────┼────────┤'); + + this.transporters.forEach((smtp, index) => { + const isActive = index === this.activeTransporterIndex ? ' ✅' : ''; + console.log(`│ ${(index + 1).toString().padEnd(2)} │ ${smtp.name.padEnd(8)} │ ${smtp.email.substring(0, 15).padEnd(15)} │ ${smtp.host.substring(0, 15).padEnd(15)} │ ${smtp.port.toString().padEnd(4)} │ ${smtp.status.padEnd(6)}${isActive} │`); + }); + + console.log('─'.repeat(80)); + + // Показываем конфигурации, которые не были добавлены + const allConfigs = this.getSmtpConfigs(); + const missingConfigs = allConfigs.filter(config => + !this.transporters.some(t => t.email === config.user) + ); + + if (missingConfigs.length > 0) { + console.log('\n⚠️ Следующие конфигурации не были добавлены (ошибка инициализации):'); + missingConfigs.forEach(config => { + console.log(` • ${config.name}: ${config.user}@${config.host}:${config.port}`); + }); + } + } + + getActiveTransporter() { + if (this.transporters.length === 0) { + return null; + } + + if (this.activeTransporterIndex >= this.transporters.length) { + this.activeTransporterIndex = 0; + } + + return this.transporters[this.activeTransporterIndex]; + } + + rotateTransporter() { + if (this.transporters.length <= 1) { + return false; + } + + const oldIndex = this.activeTransporterIndex; + const originalIndex = oldIndex; + + // Ищем следующий активный SMTP + let attempts = 0; + while (attempts < this.transporters.length) { + this.activeTransporterIndex = (this.activeTransporterIndex + 1) % this.transporters.length; + const nextTransporter = this.transporters[this.activeTransporterIndex]; + + // Пропускаем failed SMTP + if (nextTransporter.status === 'failed') { + attempts++; + continue; + } + + // Если следующий SMTP активен, используем его + if (nextTransporter.status === 'active') { + console.log(`🔄 Смена SMTP сервера: ${this.transporters[oldIndex].name} → ${nextTransporter.name}`); + return true; + } + + attempts++; + } + + // Если все SMTP не активны или failed, ищем любой не-failed + for (let i = 0; i < this.transporters.length; i++) { + if (this.transporters[i].status !== 'failed') { + this.activeTransporterIndex = i; + console.log(`⚠️ Все активные SMTP не работают, переключение на ${this.transporters[i].name}`); + return true; + } + } + + // Если все SMTP failed + this.activeTransporterIndex = 0; + console.log(`❌ Все SMTP помечены как failed`); + return false; + } + + markTransporterAsFailed(reason = 'unknown') { + if (this.transporters.length === 0) { + return false; + } + + const current = this.transporters[this.activeTransporterIndex]; + current.status = 'failed'; + current.failureReason = reason; + current.failedAt = new Date(); + + console.log(`❌ SMTP ${current.name} помечен как нерабочий: ${reason}`); + + return true; + } + async restoreSpamBlockState() { if (!getDb) return; @@ -90,6 +289,7 @@ class EmailNotifications { this.spamBlockedUntil = blockedUntil; console.log(`🔄 Восстановлена блокировка из-за спама до: ${blockedUntil.toLocaleString('ru-RU')}`); } else { + // Автоматически очищаем истекшую блокировку this.clearSpamBlockFromDB(); } } @@ -103,6 +303,12 @@ class EmailNotifications { } async setSpamBlock() { + // Если включен режим игнорирования, не устанавливаем блокировку + if (this.ignoreSpamBlock) { + console.log('🔓 Режим игнорирования спам-блокировки активен, глобальная блокировка не устанавливается'); + return; + } + this.isSpamBlocked = true; this.spamBlockedUntil = new Date(Date.now() + this.spamBlockCooldown); @@ -113,7 +319,7 @@ class EmailNotifications { } async saveSpamBlockToDB() { - if (!getDb) return; + if (!getDb || this.ignoreSpamBlock) return; try { const db = getDb(); @@ -146,9 +352,6 @@ class EmailNotifications { // Очищаем из БД await this.clearSpamBlockFromDB(); - - // Запускаем обработку очереди - this.processRetryQueue(); } async clearSpamBlockFromDB() { @@ -176,6 +379,11 @@ class EmailNotifications { } isSpamBlockActive() { + // Если режим игнорирования включен, блокировка никогда не активна + if (this.ignoreSpamBlock) { + return false; + } + if (!this.isSpamBlocked || !this.spamBlockedUntil) { return false; } @@ -189,237 +397,45 @@ class EmailNotifications { return true; } - async saveToQueue(to, subject, htmlContent, userId, taskId, notificationType, retryCount = 0) { - if (!getDb) { - console.warn('⚠️ БД не доступна для сохранения в очередь'); - return false; - } - - try { - const db = getDb(); - return new Promise((resolve, reject) => { - db.run( - `INSERT INTO email_queue - (to_email, subject, html_content, user_id, task_id, notification_type, retry_count, status, created_at) - VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', CURRENT_TIMESTAMP)`, - [to, subject, htmlContent, userId, taskId, notificationType, retryCount], - function(err) { - if (err) { - console.error('❌ Ошибка сохранения в очередь:', err); - reject(err); - } else { - console.log(`📝 Email сохранен в очередь (ID: ${this.lastID}): ${to}, ${subject}`); - resolve(this.lastID); - } - } - ); - }); - } catch (error) { - console.error('❌ Ошибка сохранения в очередь:', error); - return false; - } - } - - async updateQueueStatus(queueId, status, errorMessage = null, retryCount = null) { - if (!getDb) return false; - - try { - const db = getDb(); - return new Promise((resolve, reject) => { - let query = `UPDATE email_queue SET status = ?, updated_at = CURRENT_TIMESTAMP`; - const params = [status]; - - if (errorMessage) { - query += `, error_message = ?`; - params.push(errorMessage); - } - - if (retryCount !== null) { - query += `, retry_count = ?`; - params.push(retryCount); - } - - query += ` WHERE id = ?`; - params.push(queueId); - - db.run(query, params, function(err) { - if (err) { - console.error('❌ Ошибка обновления статуса в очереди:', err); - reject(err); - } else { - console.log(`📝 Статус email в очереди обновлен (ID: ${queueId}): ${status}`); - resolve(this.changes > 0); - } - }); - }); - } catch (error) { - console.error('❌ Ошибка обновления статуса очереди:', error); - return false; - } - } - - async removeFromQueue(queueId) { - if (!getDb) return false; - - try { - const db = getDb(); - return new Promise((resolve, reject) => { - db.run( - `DELETE FROM email_queue WHERE id = ?`, - [queueId], - function(err) { - if (err) { - console.error('❌ Ошибка удаления из очереди:', err); - reject(err); - } else { - console.log(`🗑️ Email удален из очереди (ID: ${queueId})`); - resolve(this.changes > 0); - } - } - ); - }); - } catch (error) { - console.error('❌ Ошибка удаления из очереди:', error); - return false; - } - } - - async getPendingEmails(limit = 10) { - if (!getDb) return []; - - try { - const db = getDb(); - return new Promise((resolve, reject) => { - db.all( - `SELECT * FROM email_queue - WHERE status = 'pending' - ORDER BY created_at ASC - LIMIT ?`, - [limit], - (err, rows) => { - if (err) { - console.error('❌ Ошибка получения очереди:', err); - reject(err); - } else { - resolve(rows || []); - } - } - ); - }); - } catch (error) { - console.error('❌ Ошибка получения очереди:', error); - return []; - } - } - - async processRetryQueue() { - if (this.isSpamBlockActive()) { - const minutesLeft = Math.ceil((this.spamBlockedUntil - new Date()) / (60 * 1000)); - console.log(`⏸️ Пропуск обработки очереди: блокировка из-за спама (осталось ${minutesLeft} минут)`); - return; - } - - if (!this.initialized || !this.transporter) { - console.warn('⚠️ Пропуск обработки очереди: Email не инициализирован'); - return; - } - - console.log('🔍 Проверка очереди email для повторной отправки...'); - - try { - const pendingEmails = await this.getPendingEmails(20); - - if (pendingEmails.length === 0) { - console.log('📭 Очередь email пуста'); - return; + // Сброс всех failed статусов + resetAllFailedTransporters() { + let resetCount = 0; + this.transporters.forEach(transporter => { + if (transporter.status === 'failed') { + transporter.status = 'active'; + delete transporter.failureReason; + delete transporter.failedAt; + resetCount++; } - - console.log(`📧 Найдено ${pendingEmails.length} email в очереди`); - - for (const email of pendingEmails) { - try { - // Проверяем, не превышено ли максимальное количество попыток - if (email.retry_count >= this.maxRetries) { - console.log(`⏭️ Пропуск email ${email.id}: превышено максимальное количество попыток (${email.retry_count})`); - await this.updateQueueStatus(email.id, 'failed', 'Превышено максимальное количество попыток отправки'); - continue; - } - - // Обновляем статус на "в процессе отправки" - await this.updateQueueStatus(email.id, 'sending'); - - // Отправляем email - const info = await this.transporter.sendMail({ - from: `"School CRM" <${process.env.YANDEX_EMAIL}>`, - to: email.to_email, - subject: email.subject, - html: email.html_content, - text: email.html_content.replace(/<[^>]*>/g, '') - }); - - console.log(`✅ Email отправлен из очереди (ID: ${email.id}): ${email.to_email}, Message ID: ${info.messageId}`); - - // Удаляем из очереди при успешной отправке - await this.removeFromQueue(email.id); - - // Если есть связанная задача, обновляем историю уведомлений - if (email.user_id && email.task_id && email.notification_type) { - try { - await this.recordNotificationSent(email.user_id, email.task_id, email.notification_type); - console.log(`📝 История уведомлений обновлена для email из очереди (ID: ${email.id})`); - } catch (historyError) { - console.error('❌ Ошибка обновления истории для email из очереди:', historyError); - } - } - - // Делаем небольшую паузу между отправками - await new Promise(resolve => setTimeout(resolve, 1000)); - - } catch (emailError) { - console.error(`❌ Ошибка отправки email из очереди (ID: ${email.id}):`, emailError.message); - - // Проверяем, является ли ошибка блокировкой спама - if (emailError.message.includes('554 5.7.1 Message rejected under suspicion of SPAM') || - emailError.message.includes('suspicion of SPAM') || - emailError.message.includes('SPAM')) { - - console.log('🚫 Обнаружена блокировка из-за спама при обработке очереди'); - await this.setSpamBlock(); - - // Увеличиваем счетчик попыток и возвращаем в очередь - const newRetryCount = (email.retry_count || 0) + 1; - await this.updateQueueStatus( - email.id, - 'pending', - `SPAM блокировка: ${emailError.message}`, - newRetryCount - ); - break; // Прерываем цикл при блокировке спама - } - - // Для других ошибок просто увеличиваем счетчик попыток - const newRetryCount = (email.retry_count || 0) + 1; - await this.updateQueueStatus( - email.id, - 'pending', - `Ошибка отправки: ${emailError.message}`, - newRetryCount - ); - - // Делаем паузу перед следующей попыткой - await new Promise(resolve => setTimeout(resolve, 2000)); - } - } - - console.log(`✅ Обработка очереди email завершена`); - - } catch (error) { - console.error('❌ Ошибка обработки очереди email:', error); + }); + + if (resetCount > 0) { + console.log(`🔄 Сброшены статусы ${resetCount} SMTP серверов (помечены как активные)`); + // Сбрасываем на первый активный + this.activeTransporterIndex = 0; } + + return resetCount; + } + + // Принудительная разблокировка + async forceUnblockSpam() { + console.log('🔓 Принудительная разблокировка спам-блокировки'); + this.isSpamBlocked = false; + this.spamBlockedUntil = null; + + // Сбрасываем все failed SMTP + this.resetAllFailedTransporters(); + + // Очищаем из БД + await this.clearSpamBlockFromDB(); + + console.log('✅ Спам-блокировка снята, все SMTP сброшены'); + return true; } async canSendNotification(userId, taskId, notificationType) { - if (!getDb) return true; // Если БД не готова, разрешаем отправку + if (!getDb) return true; return new Promise((resolve, reject) => { const db = getDb(); @@ -430,17 +446,15 @@ class EmailNotifications { (err, record) => { if (err) { console.error('❌ Ошибка проверки истории уведомлений:', err); - resolve(true); // В случае ошибки разрешаем отправку - return; - } - - if (!record) { - // Записи нет, можно отправлять resolve(true); return; } - // Проверяем прошло ли 12 часов + if (!record) { + resolve(true); + return; + } + const lastSent = new Date(record.last_sent_at); const now = new Date(); const timeDiff = now.getTime() - lastSent.getTime(); @@ -484,7 +498,6 @@ class EmailNotifications { } async forceSendNotification(userId, taskId, notificationType) { - // Принудительно удаляем запись из истории, чтобы можно было отправить уведомление сразу if (!getDb) return; return new Promise((resolve, reject) => { @@ -539,7 +552,6 @@ class EmailNotifications { vk_user_id = '' } = settings; - // Проверяем существование записи db.get("SELECT id FROM user_settings WHERE user_id = ?", [userId], (err, existing) => { if (err) { reject(err); @@ -547,7 +559,6 @@ class EmailNotifications { } if (existing) { - // Обновляем существующую запись db.run(`UPDATE user_settings SET email_notifications = ?, notification_email = ?, @@ -570,7 +581,6 @@ class EmailNotifications { else resolve(true); }); } else { - // Создаем новую запись db.run(`INSERT INTO user_settings ( user_id, email_notifications, notification_email, telegram_notifications, telegram_chat_id, @@ -594,66 +604,148 @@ class EmailNotifications { } async sendEmailNotification(to, subject, htmlContent, userId = null, taskId = null, notificationType = null) { - // Проверяем блокировку из-за спама - if (this.isSpamBlockActive()) { + // Проверяем блокировку из-за спама, если не игнорируем + if (!this.ignoreSpamBlock && this.isSpamBlockActive()) { const minutesLeft = Math.ceil((this.spamBlockedUntil - new Date()) / (60 * 1000)); - console.log(`⏸️ Email отправка заблокирована из-за спама. Сохраняем в очередь. Осталось: ${minutesLeft} минут`); - - // Сохраняем в очередь для последующей отправки - const queueId = await this.saveToQueue(to, subject, htmlContent, userId, taskId, notificationType); - return queueId ? { queued: true, queueId } : false; + console.log(`⏸️ Email отправка заблокирована из-за спама. Осталось: ${minutesLeft} минут`); + return { sent: false, error: 'spam_blocked', minutesLeft }; } - if (!this.initialized || !this.transporter) { - console.warn('⚠️ Email уведомления отключены'); - - // Также сохраняем в очередь на случай, если сервис восстановится - const queueId = await this.saveToQueue(to, subject, htmlContent, userId, taskId, notificationType); - return queueId ? { queued: true, queueId } : false; + // Если игнорируем блокировку, но она активна - просто сообщаем + if (this.ignoreSpamBlock && this.isSpamBlockActive()) { + console.log(`🔓 Спам-блокировка активна, но игнорируется (режим IGNORE_SPAM_BLOCK=true)`); + } + + if (!this.initialized || this.transporters.length === 0) { + console.warn('⚠️ Email уведомления отключены - нет доступных SMTP серверов'); + return { sent: false, error: 'not_initialized' }; } - try { - const info = await this.transporter.sendMail({ - from: `"School CRM" <${process.env.YANDEX_EMAIL}>`, - to: to, - subject: subject, - html: htmlContent, - text: htmlContent.replace(/<[^>]*>/g, '') - }); + const originalTransporterIndex = this.activeTransporterIndex; + let attempts = 0; + const maxAttempts = this.transporters.length; + let spamDetectedCount = 0; // Счетчик SMTP с детекцией спама + let maxTotalAttempts = this.ignoreSpamBlock ? maxAttempts * 3 : maxAttempts; // Больше попыток при ignore режиме - console.log(`📧 Email отправлен: ${to}, Message ID: ${info.messageId}`); - return { sent: true, messageId: info.messageId }; - - } catch (error) { - console.error('❌ Ошибка отправки email:', error.message); + while (attempts < maxTotalAttempts) { + const activeTransporter = this.getActiveTransporter(); - // Проверяем, является ли ошибка блокировкой спама - if (error.message.includes('554 5.7.1 Message rejected under suspicion of SPAM') || - error.message.includes('suspicion of SPAM') || - error.message.includes('SPAM')) { + try { + console.log(`📤 Попытка ${attempts + 1}/${maxTotalAttempts} через ${activeTransporter.name} (${activeTransporter.email})`); - console.log('🚫 Обнаружена блокировка из-за спама. Активируем блокировку на 60 минут.'); - await this.setSpamBlock(); + const info = await activeTransporter.transporter.sendMail({ + from: `"School CRM" <${activeTransporter.email}>`, + to: to, + subject: subject, + html: htmlContent, + text: htmlContent.replace(/<[^>]*>/g, '') + }); + + console.log(`✅ Email отправлен через ${activeTransporter.name}: ${to}, Message ID: ${info.messageId}`); - // Сохраняем email в очередь для повторной отправки после блокировки - const queueId = await this.saveToQueue(to, subject, htmlContent, userId, taskId, notificationType, 1); - return queueId ? { queued: true, queueId, spamBlocked: true } : false; + // Если это была не первая попытка, записываем в историю смены SMTP + if (attempts > 0) { + console.log(`🔄 Успешная отправка после ${attempts} неудачных попыток`); + } + + return { + sent: true, + messageId: info.messageId, + smtpUsed: activeTransporter.name, + smtpEmail: activeTransporter.email, + attempts: attempts + 1 + }; + + } catch (error) { + attempts++; + console.error(`❌ Ошибка отправки через ${activeTransporter.name}:`, error.message); + + // Проверяем, является ли ошибка блокировкой спама + const isSpamError = error.message.includes('554 5.7.1 Message rejected under suspicion of SPAM') || + error.message.includes('suspicion of SPAM') || + error.message.includes('SPAM') || + error.message.includes('spam'); + + if (isSpamError) { + spamDetectedCount++; + console.log(`⚠️ Детекция спама на ${activeTransporter.name}. Пробуем следующий SMTP...`); + + // Помечаем текущий SMTP как проблемный + this.markTransporterAsFailed('spam_detected'); + + // НЕМЕДЛЕННО ПЕРЕКЛЮЧАЕМСЯ на следующий SMTP + this.rotateTransporter(); + + // Проверяем, нужно ли активировать глобальную блокировку + if (spamDetectedCount >= maxAttempts) { + if (!this.ignoreSpamBlock) { + console.log(`🚫 Спам детектирован на всех SMTP серверах. Активируем глобальную блокировку.`); + await this.setSpamBlock(); + return { + sent: false, + error: 'spam_blocked_on_all_smtp', + attempts: attempts + }; + } else { + console.log(`🔓 Спам детектирован на всех SMTP, но блокировка отключена. Цикл ротации.`); + + // Если все SMTP помечены как failed, сбрасываем их статус + const allFailed = this.transporters.every(t => t.status === 'failed'); + if (allFailed) { + console.log(`🔄 Все SMTP помечены как failed, сбрасываем статусы для повторного использования`); + this.resetAllFailedTransporters(); + } + + // Продолжаем попытки + if (attempts < maxTotalAttempts) { + await new Promise(resolve => setTimeout(resolve, 2000)); // Увеличиваем задержку + continue; + } else { + return { + sent: false, + error: 'all_smtp_failed_after_retries', + attempts: attempts + }; + } + } + } + } else { + // Для не-спам ошибок просто переключаем SMTP + console.log(`🔄 Проблема с ${activeTransporter.name}, пробуем следующий SMTP...`); + this.rotateTransporter(); + } + + // Если это не последняя попытка, продолжаем + if (attempts < maxTotalAttempts) { + // Делаем паузу перед следующей попыткой + await new Promise(resolve => setTimeout(resolve, 1000)); + continue; + } else { + // Все SMTP не сработали + console.log(`❌ Все ${attempts} попыток отправки не сработали.`); + + // Возвращаем активный транспортер к оригинальному + this.activeTransporterIndex = originalTransporterIndex; + + return { + sent: false, + error: 'all_smtp_failed', + attempts: attempts + }; + } } - - // Для других ошибок также сохраняем в очередь - const queueId = await this.saveToQueue(to, subject, htmlContent, userId, taskId, notificationType, 1); - return queueId ? { queued: true, queueId } : false; } + + // Если мы здесь, значит что-то пошло не так + return { sent: false, error: 'unknown_error' }; } - getTaskIdFromData(taskData) { - // Ищем ID задачи в различных возможных полях объекта + async getTaskIdFromData(taskData) { if (!taskData) { console.error('❌ taskData is null or undefined'); return null; } - // Проверяем различные возможные поля if (taskData.id) { return taskData.id; } @@ -666,13 +758,11 @@ class EmailNotifications { return taskData.taskId; } - // Если есть assignment_id, пытаемся найти задачу через БД if (taskData.assignment_id && getDb) { console.log(`🔍 Ищу ID задачи через assignment_id: ${taskData.assignment_id}`); try { const db = getDb(); - // Используем синхронный запрос через промис - return new Promise((resolve) => { + return await new Promise((resolve) => { db.get("SELECT task_id FROM task_assignments WHERE id = ?", [taskData.assignment_id], (err, row) => { @@ -701,14 +791,11 @@ class EmailNotifications { async sendTaskNotification(userId, taskData, notificationType) { try { - // Получаем ID задачи (обрабатываем и синхронные и асинхронные случаи) let taskId; if (taskData.assignment_id && getDb) { - // Асинхронный поиск через assignment_id taskId = await this.getTaskIdFromData(taskData); } else { - // Синхронный поиск в полях объекта taskId = this.getTaskIdFromData(taskData); } @@ -730,7 +817,6 @@ class EmailNotifications { console.log(`🔍 Отправка уведомления: пользователь ${userId}, задача ${taskId}, тип ${notificationType}`); - // Проверяем, можно ли отправлять уведомление const canSend = await this.canSendNotification(userId, taskId, notificationType); if (!canSend) { console.log(`⏰ Пропущено уведомление для пользователя ${userId}, задача ${taskId}, тип ${notificationType} (12-часовой кд)`); @@ -743,7 +829,6 @@ class EmailNotifications { return false; } - // Используем указанную email или email из профиля пользователя const emailTo = settings.notification_email || settings.user_email; if (!emailTo) { console.log(`⚠️ У пользователя ${userId} не указан email для уведомлений`); @@ -789,21 +874,20 @@ class EmailNotifications { const result = await this.sendEmailNotification(emailTo, subject, htmlContent, userId, taskId, notificationType); - // Если уведомление успешно отправлено (не в очередь), записываем в историю - if (result && result.sent) { + if (result.sent) { await this.recordNotificationSent(userId, taskId, notificationType); - console.log(`✅ Уведомление отправлено: пользователь ${userId}, задача ${taskId}, тип ${notificationType}`); - } else if (result && result.queued) { - console.log(`📝 Уведомление сохранено в очередь (ID: ${result.queueId}): пользователь ${userId}, задача ${taskId}, тип ${notificationType}`); - - if (result.spamBlocked) { - console.log(`🚫 Отправка заблокирована из-за спама. Email будет отправлен после разблокировки.`); - } + console.log(`✅ Уведомление отправлено через ${result.smtpUsed} (попыток: ${result.attempts}): пользователь ${userId}, задача ${taskId}, тип ${notificationType}`); + return true; } else { - console.log(`❌ Не удалось отправить/сохранить уведомление: пользователь ${userId}, задача ${taskId}, тип ${notificationType}`); + if (result.error === 'spam_blocked') { + console.log(`🚫 Уведомление не отправлено: заблокировано из-за спама`); + } else if (result.error === 'all_smtp_failed') { + console.log(`❌ Уведомление не отправлено: все SMTP серверы не сработали (попыток: ${result.attempts})`); + } else { + console.log(`❌ Уведомление не отправлено: ${result.error}`); + } + return false; } - - return result; } catch (error) { console.error('❌ Ошибка отправки уведомления о задаче:', error); @@ -812,7 +896,7 @@ class EmailNotifications { } } - // HTML шаблоны для разных типов уведомлений + // HTML шаблоны (остаются без изменений) getTaskCreatedHtml(taskData) { return ` @@ -1252,167 +1336,69 @@ class EmailNotifications { }); } - async getEmailQueueStats() { - if (!getDb) return { pending: 0, failed: 0, total: 0 }; - - return new Promise((resolve, reject) => { - const db = getDb(); - const query = ` - SELECT - SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending, - SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) as failed, - COUNT(*) as total - FROM email_queue - `; - - db.get(query, [], (err, stats) => { - if (err) { - console.error('❌ Ошибка получения статистики очереди:', err); - resolve({ pending: 0, failed: 0, total: 0 }); - } else { - resolve(stats || { pending: 0, failed: 0, total: 0 }); - } - }); - }); - } - - async getEmailQueueItems(status = null, limit = 50) { - if (!getDb) return []; - - return new Promise((resolve, reject) => { - const db = getDb(); - let query = `SELECT * FROM email_queue`; - const params = []; - - if (status) { - query += ` WHERE status = ?`; - params.push(status); - } - - query += ` ORDER BY created_at DESC LIMIT ?`; - params.push(limit); - - db.all(query, params, (err, items) => { - if (err) { - console.error('❌ Ошибка получения элементов очереди:', err); - resolve([]); - } else { - resolve(items || []); - } - }); - }); - } - - async retryFailedEmails() { - if (!getDb) return { retried: 0, total: 0 }; - - try { - const db = getDb(); - - // Получаем все проваленные email - const failedEmails = await new Promise((resolve, reject) => { - db.all( - `SELECT * FROM email_queue WHERE status = 'failed' AND retry_count < ?`, - [this.maxRetries], - (err, emails) => { - if (err) reject(err); - else resolve(emails || []); - } - ); - }); - - if (failedEmails.length === 0) { - console.log('📭 Нет проваленных email для повторной отправки'); - return { retried: 0, total: 0 }; - } - - console.log(`🔍 Найдено ${failedEmails.length} проваленных email для повторной отправки`); - - let retriedCount = 0; - - for (const email of failedEmails) { - try { - // Сбрасываем статус на pending для повторной попытки - await new Promise((resolve, reject) => { - db.run( - `UPDATE email_queue SET status = 'pending', retry_count = retry_count + 1 WHERE id = ?`, - [email.id], - function(err) { - if (err) reject(err); - else { - console.log(`🔄 Email ${email.id} подготовлен для повторной отправки`); - retriedCount++; - resolve(); - } - } - ); - }); - - } catch (error) { - console.error(`❌ Ошибка подготовки email ${email.id} для повторной отправки:`, error); - } - } - - console.log(`✅ Подготовлено ${retriedCount} email для повторной отправки`); - - // Запускаем обработку очереди - this.processRetryQueue(); - - return { retried: retriedCount, total: failedEmails.length }; - - } catch (error) { - console.error('❌ Ошибка при попытке повторной отправки email:', error); - return { retried: 0, total: 0 }; - } - } - - async clearOldQueueItems(days = 30) { - if (!getDb) return { deleted: 0 }; - - try { - const db = getDb(); - - const result = await new Promise((resolve, reject) => { - db.run( - `DELETE FROM email_queue - WHERE created_at < datetime('now', '-${days} days') - AND (status = 'sent' OR status = 'failed')`, - function(err) { - if (err) reject(err); - else resolve(this.changes || 0); - } - ); - }); - - console.log(`🗑️ Удалено ${result} старых записей из очереди email (старше ${days} дней)`); - return { deleted: result }; - - } catch (error) { - console.error('❌ Ошибка очистки старых записей очереди:', error); - return { deleted: 0 }; - } + getSmtpStatus() { + return this.transporters.map((smtp, index) => ({ + name: smtp.name, + email: smtp.email, + host: smtp.host, + port: smtp.port, + status: smtp.status, + isActive: index === this.activeTransporterIndex, + failureReason: smtp.failureReason || null, + failedAt: smtp.failedAt || null, + configSource: smtp.configSource || 'unknown' + })); } isReady() { - return this.initialized; + return this.initialized && this.transporters.length > 0; } getSpamBlockStatus() { - if (!this.isSpamBlocked || !this.spamBlockedUntil) { - return { blocked: false, blockedUntil: null, minutesLeft: 0 }; + if (!this.isSpamBlocked || !this.spamBlockedUntil || this.ignoreSpamBlock) { + return { + blocked: false, + blockedUntil: null, + minutesLeft: 0, + ignoreMode: this.ignoreSpamBlock + }; } const now = new Date(); if (now >= this.spamBlockedUntil) { this.clearSpamBlock(); - return { blocked: false, blockedUntil: null, minutesLeft: 0 }; + return { + blocked: false, + blockedUntil: null, + minutesLeft: 0, + ignoreMode: this.ignoreSpamBlock + }; } const minutesLeft = Math.ceil((this.spamBlockedUntil - now) / (60 * 1000)); return { blocked: true, blockedUntil: this.spamBlockedUntil, - minutesLeft: minutesLeft + minutesLeft: minutesLeft, + ignoreMode: this.ignoreSpamBlock + }; + } + + // Получить статистику работы SMTP + getSmtpStats() { + const total = this.transporters.length; + const active = this.transporters.filter(t => t.status === 'active').length; + const failed = this.transporters.filter(t => t.status === 'failed').length; + + return { + total: total, + active: active, + failed: failed, + activePercentage: total > 0 ? Math.round((active / total) * 100) : 0, + currentActive: this.activeTransporterIndex, + ignoreSpamBlock: this.ignoreSpamBlock, + spamBlocked: this.isSpamBlocked, + initialized: this.initialized }; } } @@ -1420,7 +1406,4 @@ class EmailNotifications { // Singleton const emailNotifications = new EmailNotifications(); -// Экспортируем функцию для очистки старых записей (можно запускать по cron) -emailNotifications.clearOldQueueItems = emailNotifications.clearOldQueueItems.bind(emailNotifications); - module.exports = emailNotifications; \ No newline at end of file