const nodemailer = require('nodemailer'); const { getDb } = require('./database'); class EmailNotifications { constructor() { this.transporters = []; this.activeTransporterIndex = 0; this.initialized = false; this.notificationCooldown = 12 * 60 * 60 * 1000; // 12 часов this.spamBlockCooldown = 15 * 60 * 1000; // 15 минут при блокировке спама this.spamBlockedUntil = null; this.isSpamBlocked = false; this.transporterNames = ['Yandex', 'SMTP1', 'SMTP2']; // Новая переменная для игнорирования спам-блокировки this.ignoreSpamBlock = process.env.IGNORE_SPAM_BLOCK === 'true'; if (this.ignoreSpamBlock) { console.log('🔓 Режим игнорирования спам-блокировки включен'); console.log(' При детекции спама будет только ротация SMTP без глобальной блокировки'); } this.init(); } async init() { try { console.log('🔧 Инициализация Email уведомлений с несколькими SMTP...'); // Проверяем и инициализируем все SMTP аккаунты const smtpConfigs = this.getSmtpConfigs(); if (smtpConfigs.length === 0) { console.warn('⚠️ Настройки SMTP не указаны в .env'); console.warn(' Email уведомления будут отключены'); this.initialized = false; return; } 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; } this.initialized = true; // Восстанавливаем состояние блокировки из БД await this.restoreSpamBlockState(); 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(); if (now < this.spamBlockedUntil) { const minutesLeft = Math.ceil((this.spamBlockedUntil - now) / (60 * 1000)); console.log(`⏸️ Email отправка заблокирована из-за спама. До разблокировки: ${minutesLeft} минут`); } else { 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; try { const db = getDb(); return new Promise((resolve, reject) => { db.get( `SELECT spam_blocked_until FROM email_settings WHERE setting_key = 'spam_block'`, (err, row) => { if (err) { console.error('❌ Ошибка получения состояния блокировки:', err); resolve(); return; } if (row && row.spam_blocked_until) { const blockedUntil = new Date(row.spam_blocked_until); const now = new Date(); if (now < blockedUntil) { this.isSpamBlocked = true; this.spamBlockedUntil = blockedUntil; console.log(`🔄 Восстановлена блокировка из-за спама до: ${blockedUntil.toLocaleString('ru-RU')}`); } else { // Автоматически очищаем истекшую блокировку this.clearSpamBlockFromDB(); } } resolve(); } ); }); } catch (error) { console.error('❌ Ошибка восстановления состояния блокировки:', error); } } async setSpamBlock() { // Если включен режим игнорирования, не устанавливаем блокировку if (this.ignoreSpamBlock) { console.log('🔓 Режим игнорирования спам-блокировки активен, глобальная блокировка не устанавливается'); return; } this.isSpamBlocked = true; this.spamBlockedUntil = new Date(Date.now() + this.spamBlockCooldown); console.log(`🚫 Email отправка заблокирована из-за спама до: ${this.spamBlockedUntil.toLocaleString('ru-RU')}`); // Сохраняем в БД await this.saveSpamBlockToDB(); } async saveSpamBlockToDB() { if (!getDb || this.ignoreSpamBlock) return; try { const db = getDb(); return new Promise((resolve, reject) => { db.run( `INSERT OR REPLACE INTO email_settings (setting_key, setting_value, spam_blocked_until, updated_at) VALUES ('spam_block', 'blocked', ?, CURRENT_TIMESTAMP)`, [this.spamBlockedUntil.toISOString()], (err) => { if (err) { console.error('❌ Ошибка сохранения блокировки в БД:', err); reject(err); } else { console.log('✅ Состояние блокировки сохранено в БД'); resolve(); } } ); }); } catch (error) { console.error('❌ Ошибка сохранения блокировки:', error); } } async clearSpamBlock() { this.isSpamBlocked = false; this.spamBlockedUntil = null; console.log('✅ Блокировка из-за спама снята'); // Очищаем из БД await this.clearSpamBlockFromDB(); } async clearSpamBlockFromDB() { if (!getDb) return; try { const db = getDb(); return new Promise((resolve, reject) => { db.run( `DELETE FROM email_settings WHERE setting_key = 'spam_block'`, (err) => { if (err) { console.error('❌ Ошибка очистки блокировки из БД:', err); reject(err); } else { console.log('✅ Состояние блокировки очищено из БД'); resolve(); } } ); }); } catch (error) { console.error('❌ Ошибка очистки блокировки:', error); } } isSpamBlockActive() { // Если режим игнорирования включен, блокировка никогда не активна if (this.ignoreSpamBlock) { return false; } if (!this.isSpamBlocked || !this.spamBlockedUntil) { return false; } const now = new Date(); if (now >= this.spamBlockedUntil) { this.clearSpamBlock(); return false; } return true; } // Сброс всех failed статусов resetAllFailedTransporters() { let resetCount = 0; this.transporters.forEach(transporter => { if (transporter.status === 'failed') { transporter.status = 'active'; delete transporter.failureReason; delete transporter.failedAt; resetCount++; } }); 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; return new Promise((resolve, reject) => { const db = getDb(); db.get( `SELECT last_sent_at FROM notification_history WHERE user_id = ? AND task_id = ? AND notification_type = ?`, [userId, taskId, notificationType], (err, record) => { if (err) { console.error('❌ Ошибка проверки истории уведомлений:', err); resolve(true); return; } if (!record) { resolve(true); return; } const lastSent = new Date(record.last_sent_at); const now = new Date(); const timeDiff = now.getTime() - lastSent.getTime(); if (timeDiff >= this.notificationCooldown) { resolve(true); } else { const hoursLeft = Math.ceil((this.notificationCooldown - timeDiff) / (60 * 60 * 1000)); console.log(`⏰ Уведомление пропущено: пользователь ${userId}, задача ${taskId}, тип ${notificationType}`); console.log(` Последнее уведомление было отправлено: ${lastSent.toLocaleString('ru-RU')}`); console.log(` Следующее уведомление можно отправить через: ${hoursLeft} часов`); console.log(` Время следующей отправки: ${new Date(lastSent.getTime() + this.notificationCooldown).toLocaleString('ru-RU')}`); resolve(false); } } ); }); } async recordNotificationSent(userId, taskId, notificationType) { if (!getDb) return; return new Promise((resolve, reject) => { const db = getDb(); db.run( `INSERT OR REPLACE INTO notification_history (user_id, task_id, notification_type, last_sent_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP)`, [userId, taskId, notificationType], (err) => { if (err) { console.error('❌ Ошибка записи истории уведомлений:', err); reject(err); } else { console.log(`📝 Записана история уведомления: пользователь ${userId}, задача ${taskId}, тип ${notificationType}`); resolve(); } } ); }); } async forceSendNotification(userId, taskId, notificationType) { if (!getDb) return; return new Promise((resolve, reject) => { const db = getDb(); db.run( `DELETE FROM notification_history WHERE user_id = ? AND task_id = ? AND notification_type = ?`, [userId, taskId, notificationType], (err) => { if (err) { console.error('❌ Ошибка принудительного удаления истории:', err); reject(err); } else { console.log(`♻️ История уведомления принудительно очищена: пользователь ${userId}, задача ${taskId}, тип ${notificationType}`); resolve(); } } ); }); } async getUserNotificationSettings(userId) { if (!getDb) return null; return new Promise((resolve, reject) => { const db = getDb(); db.get(`SELECT us.*, u.email as user_email, u.name as user_name FROM user_settings us LEFT JOIN users u ON us.user_id = u.id WHERE us.user_id = ?`, [userId], (err, settings) => { if (err) { reject(err); } else { resolve(settings); } }); }); } async saveUserNotificationSettings(userId, settings) { if (!getDb) return false; return new Promise((resolve, reject) => { const db = getDb(); const { email_notifications = true, notification_email = '', telegram_notifications = false, telegram_chat_id = '', vk_notifications = false, vk_user_id = '' } = settings; db.get("SELECT id FROM user_settings WHERE user_id = ?", [userId], (err, existing) => { if (err) { reject(err); return; } if (existing) { db.run(`UPDATE user_settings SET email_notifications = ?, notification_email = ?, telegram_notifications = ?, telegram_chat_id = ?, vk_notifications = ?, vk_user_id = ?, updated_at = CURRENT_TIMESTAMP WHERE user_id = ?`, [ email_notifications ? 1 : 0, notification_email, telegram_notifications ? 1 : 0, telegram_chat_id, vk_notifications ? 1 : 0, vk_user_id, userId ], function(err) { if (err) reject(err); else resolve(true); }); } else { db.run(`INSERT INTO user_settings ( user_id, email_notifications, notification_email, telegram_notifications, telegram_chat_id, vk_notifications, vk_user_id ) VALUES (?, ?, ?, ?, ?, ?, ?)`, [ userId, email_notifications ? 1 : 0, notification_email, telegram_notifications ? 1 : 0, telegram_chat_id, vk_notifications ? 1 : 0, vk_user_id ], function(err) { if (err) reject(err); else resolve(true); }); } }); }); } async sendEmailNotification(to, subject, htmlContent, userId = null, taskId = null, notificationType = null) { // Проверяем блокировку из-за спама, если не игнорируем if (!this.ignoreSpamBlock && this.isSpamBlockActive()) { const minutesLeft = Math.ceil((this.spamBlockedUntil - new Date()) / (60 * 1000)); console.log(`⏸️ Email отправка заблокирована из-за спама. Осталось: ${minutesLeft} минут`); return { sent: false, error: 'spam_blocked', minutesLeft }; } // Если игнорируем блокировку, но она активна - просто сообщаем 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' }; } const originalTransporterIndex = this.activeTransporterIndex; let attempts = 0; const maxAttempts = this.transporters.length; let spamDetectedCount = 0; // Счетчик SMTP с детекцией спама let maxTotalAttempts = this.ignoreSpamBlock ? maxAttempts * 3 : maxAttempts; // Больше попыток при ignore режиме while (attempts < maxTotalAttempts) { const activeTransporter = this.getActiveTransporter(); try { console.log(`📤 Попытка ${attempts + 1}/${maxTotalAttempts} через ${activeTransporter.name} (${activeTransporter.email})`); 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}`); // Если это была не первая попытка, записываем в историю смены 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 }; } } } // Если мы здесь, значит что-то пошло не так return { sent: false, error: 'unknown_error' }; } async getTaskIdFromData(taskData) { if (!taskData) { console.error('❌ taskData is null or undefined'); return null; } if (taskData.id) { return taskData.id; } if (taskData.task_id) { return taskData.task_id; } if (taskData.taskId) { return taskData.taskId; } if (taskData.assignment_id && getDb) { console.log(`🔍 Ищу ID задачи через assignment_id: ${taskData.assignment_id}`); try { const db = getDb(); return await new Promise((resolve) => { db.get("SELECT task_id FROM task_assignments WHERE id = ?", [taskData.assignment_id], (err, row) => { if (err || !row) { console.error(`❌ Не удалось найти задачу по assignment_id ${taskData.assignment_id}:`, err?.message); resolve(null); } else { console.log(`✅ Найдена задача: ${row.task_id} для assignment_id ${taskData.assignment_id}`); resolve(row.task_id); } } ); }); } catch (error) { console.error('❌ Ошибка поиска задачи по assignment_id:', error); return null; } } console.error('❌ Не удалось определить ID задачи из данных:', Object.keys(taskData).length > 0 ? JSON.stringify(taskData, null, 2).substring(0, 500) : 'empty object'); return null; } async sendTaskNotification(userId, taskData, notificationType) { try { let taskId; if (taskData.assignment_id && getDb) { taskId = await this.getTaskIdFromData(taskData); } else { taskId = this.getTaskIdFromData(taskData); } if (!taskId) { console.error('❌ Не удалось определить ID задачи для уведомления. Данные:', { userId, notificationType, taskDataKeys: Object.keys(taskData || {}), taskDataSample: taskData ? { id: taskData.id, task_id: taskData.task_id, taskId: taskData.taskId, assignment_id: taskData.assignment_id, title: taskData.title } : 'null' }); return false; } console.log(`🔍 Отправка уведомления: пользователь ${userId}, задача ${taskId}, тип ${notificationType}`); const canSend = await this.canSendNotification(userId, taskId, notificationType); if (!canSend) { console.log(`⏰ Пропущено уведомление для пользователя ${userId}, задача ${taskId}, тип ${notificationType} (12-часовой кд)`); return false; } const settings = await this.getUserNotificationSettings(userId); if (!settings || !settings.email_notifications) { console.log(`⚠️ Пользователь ${userId} отключил email уведомления`); return false; } const emailTo = settings.notification_email || settings.user_email; if (!emailTo) { console.log(`⚠️ У пользователя ${userId} не указан email для уведомлений`); return false; } let subject = ''; let htmlContent = ''; switch (notificationType) { case 'created': subject = `Новая задача: ${taskData.title}`; htmlContent = this.getTaskCreatedHtml(taskData); break; case 'updated': subject = `Обновлена задача: ${taskData.title}`; htmlContent = this.getTaskUpdatedHtml(taskData); break; case 'rework': subject = `Задача возвращена на доработку: ${taskData.title}`; htmlContent = this.getTaskReworkHtml(taskData); break; case 'closed': subject = `Задача закрыта: ${taskData.title}`; htmlContent = this.getTaskClosedHtml(taskData); break; case 'status_changed': subject = `Изменен статус задачи: ${taskData.title}`; htmlContent = this.getStatusChangedHtml(taskData); break; case 'deadline': subject = `Скоро срок выполнения: ${taskData.title}`; htmlContent = this.getDeadlineHtml(taskData); break; case 'overdue': subject = `Задача просрочена: ${taskData.title}`; htmlContent = this.getOverdueHtml(taskData); break; default: subject = `Уведомление по задаче: ${taskData.title}`; htmlContent = this.getDefaultHtml(taskData); } const result = await this.sendEmailNotification(emailTo, subject, htmlContent, userId, taskId, notificationType); if (result.sent) { await this.recordNotificationSent(userId, taskId, notificationType); console.log(`✅ Уведомление отправлено через ${result.smtpUsed} (попыток: ${result.attempts}): пользователь ${userId}, задача ${taskId}, тип ${notificationType}`); return true; } else { 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; } } catch (error) { console.error('❌ Ошибка отправки уведомления о задаче:', error); console.error('Stack trace:', error.stack); return false; } } // HTML шаблоны (остаются без изменений) getTaskCreatedHtml(taskData) { return `

📋 Новая задача

${taskData.title || 'Без названия'}

Описание: ${taskData.description || 'Без описания'}

${taskData.due_date ? `

Срок выполнения: ${new Date(taskData.due_date).toLocaleString('ru-RU')}

` : ''} ${taskData.author_name ? `

Создал: ${taskData.author_name}

` : ''}

Для просмотра подробной информации перейдите в систему управления задачами.

Перейти в CRM

⚠️ Следующее уведомление по этой задаче будет отправлено не ранее чем через 12 часов.

Вы можете изменить настройки уведомлений в личном кабинете.

`; } getTaskUpdatedHtml(taskData) { return `

🔄 Обновлена задача

${taskData.title || 'Без названия'}
${taskData.author_name ? `

Изменения внес: ${taskData.author_name}

` : ''}

Время: ${new Date().toLocaleString('ru-RU')}

Для просмотра изменений перейдите в систему управления задачами.

Перейти в CRM

⚠️ Следующее уведомление по этой задаче будет отправлено не ранее чем через 12 часов.

`; } getTaskReworkHtml(taskData) { return `

🔄 Задача возвращена на доработку

${taskData.title || 'Без названия'}
${taskData.author_name ? `

Автор замечания: ${taskData.author_name}

` : ''}

Комментарий:

${taskData.comment || 'Требуется доработка'}

Пожалуйста, исправьте замечания и обновите статус задачи.

Перейти к задаче

⚠️ Следующее уведомление по этой задаче будет отправлено не ранее чем через 12 часов.

`; } getTaskClosedHtml(taskData) { return `

✅ Задача закрыта

${taskData.title || 'Без названия'}
${taskData.author_name ? `

Закрыта: ${taskData.author_name}

` : ''}

Время закрытия: ${new Date().toLocaleString('ru-RU')}

Задача завершена и перемещена в архив.

Перейти в CRM
`; } getStatusChangedHtml(taskData) { const statusText = this.getStatusText(taskData.status); return `

🔄 Изменен статус задачи

${taskData.title || 'Без названия'}

Новый статус: ${statusText}

Изменил: ${taskData.user_name || taskData.author_name || 'Неизвестно'}

Время: ${new Date().toLocaleString('ru-RU')}

Для просмотра деталей перейдите в систему управления задачами.

Перейти в CRM

⚠️ Следующее уведомление по этой задаче будет отправлено не ранее чем через 12 часов.

`; } getDeadlineHtml(taskData) { const hoursLeft = taskData.hours_left || 24; return `

⚠️ Скоро срок выполнения

${taskData.title || 'Без названия'}

ВНИМАНИЕ! До окончания срока задачи осталось менее ${hoursLeft} часов!

${taskData.due_date ? `

Срок выполнения: ${new Date(taskData.due_date).toLocaleString('ru-RU')}

` : ''}

Пожалуйста, завершите задачу в указанный срок.

Перейти к задаче

⚠️ Следующее уведомление о дедлайне будет отправлено не ранее чем через 12 часов.

Если дедлайн изменится, вы получите новое уведомление.

`; } getOverdueHtml(taskData) { return `

🚨 Задача просрочена

${taskData.title || 'Без названия'}

ВНИМАНИЕ! Срок выполнения задачи истек!

${taskData.due_date ? `

Срок выполнения был: ${new Date(taskData.due_date).toLocaleString('ru-RU')}

` : ''}

Текущее время: ${new Date().toLocaleString('ru-RU')}

Пожалуйста, завершите задачу как можно скорее и обновите ее статус.

Перейти к задаче
`; } getStatusText(status) { const statusMap = { 'assigned': 'Назначена', 'in_progress': 'В работе', 'completed': 'Завершена', 'overdue': 'Просрочена', 'rework': 'На доработке', 'pending': 'В ожидании', 'in_review': 'На проверке', 'approved': 'Согласовано', 'rejected': 'Отклонено' }; return statusMap[status] || status; } getDefaultHtml(taskData) { return `

📢 Уведомление от School CRM

${taskData.title || 'Без названия'}

${taskData.message || 'Новое уведомление по задаче'}

Перейти в CRM
`; } async getNotificationHistory(userId, taskId = null) { if (!getDb) return []; return new Promise((resolve, reject) => { const db = getDb(); let query = ` SELECT nh.*, t.title as task_title FROM notification_history nh LEFT JOIN tasks t ON nh.task_id = t.id WHERE nh.user_id = ? `; const params = [userId]; if (taskId) { query += " AND nh.task_id = ?"; params.push(taskId); } query += " ORDER BY nh.last_sent_at DESC LIMIT 100"; db.all(query, params, (err, history) => { if (err) { reject(err); } else { resolve(history || []); } }); }); } async getNotificationCooldownInfo(userId, taskId, notificationType) { if (!getDb) return { canSend: true, timeUntilNext: 0 }; return new Promise((resolve, reject) => { const db = getDb(); db.get( `SELECT last_sent_at FROM notification_history WHERE user_id = ? AND task_id = ? AND notification_type = ?`, [userId, taskId, notificationType], (err, record) => { if (err) { reject(err); return; } if (!record) { resolve({ canSend: true, timeUntilNext: 0, lastSent: null }); return; } const lastSent = new Date(record.last_sent_at); const now = new Date(); const timeDiff = now.getTime() - lastSent.getTime(); const timeUntilNext = Math.max(0, this.notificationCooldown - timeDiff); resolve({ canSend: timeDiff >= this.notificationCooldown, timeUntilNext: timeUntilNext, lastSent: lastSent, nextAvailable: new Date(lastSent.getTime() + this.notificationCooldown) }); } ); }); } 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 && this.transporters.length > 0; } getSpamBlockStatus() { 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, ignoreMode: this.ignoreSpamBlock }; } const minutesLeft = Math.ceil((this.spamBlockedUntil - now) / (60 * 1000)); return { blocked: true, blockedUntil: this.spamBlockedUntil, 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 }; } } // Singleton const emailNotifications = new EmailNotifications(); module.exports = emailNotifications;