/** * Copyright (c) 2024-2025 Тарабанов Александр Викторович * All rights reserved. * * This software is proprietary and confidential. * Unauthorized copying, modification, or distribution is prohibited. * * For licensing inquiries: info@hb3-accelerator.com * Website: https://hb3-accelerator.com * GitHub: https://github.com/VC-HB3-Accelerator */ const nodemailer = require('nodemailer'); const Imap = require('imap'); const simpleParser = require('mailparser').simpleParser; const logger = require('../utils/logger'); const encryptedDb = require('./encryptedDatabaseService'); const db = require('../db'); const universalMediaProcessor = require('./UniversalMediaProcessor'); /** * EmailBot - обработчик Email сообщений * Унифицированный интерфейс для работы с Email (IMAP + SMTP) */ class EmailBot { constructor() { this.name = 'EmailBot'; this.channel = 'email'; this.imap = null; this.transporter = null; this.settings = null; this.isInitialized = false; this.status = 'inactive'; this.reconnectAttempts = 0; this.maxReconnectAttempts = 3; this.periodicCheckInterval = null; } /** * Инициализация Email Bot */ async initialize() { try { logger.info('[EmailBot] 🚀 Инициализация Email Bot...'); // Загружаем настройки из БД this.settings = await this.loadSettings(); if (!this.settings) { logger.warn('[EmailBot] ⚠️ Настройки Email не найдены'); this.status = 'not_configured'; return { success: false, reason: 'not_configured' }; } // Создаем SMTP транспортер this.transporter = await this.createTransporter(); // Создаем IMAP соединение await this.initializeImap(); this.isInitialized = true; this.status = 'active'; logger.info('[EmailBot] ✅ Email Bot успешно инициализирован'); return { success: true }; } catch (error) { logger.error('[EmailBot] ❌ Ошибка инициализации:', error); this.status = 'error'; return { success: false, error: error.message }; } } /** * Загрузка настроек из БД */ async loadSettings() { try { const encryptionUtils = require('../utils/encryptionUtils'); const encryptionKey = encryptionUtils.getEncryptionKey(); const { rows } = await db.getQuery()( 'SELECT id, smtp_port, imap_port, created_at, updated_at, ' + 'decrypt_text(smtp_host_encrypted, $1) as smtp_host, ' + 'decrypt_text(smtp_user_encrypted, $1) as smtp_user, ' + 'decrypt_text(smtp_password_encrypted, $1) as smtp_password, ' + 'decrypt_text(imap_host_encrypted, $1) as imap_host, ' + 'decrypt_text(imap_user_encrypted, $1) as imap_user, ' + 'decrypt_text(imap_password_encrypted, $1) as imap_password, ' + 'decrypt_text(from_email_encrypted, $1) as from_email ' + 'FROM email_settings ORDER BY id LIMIT 1', [encryptionKey] ); if (!rows.length) { return null; } return rows[0]; } catch (error) { logger.error('[EmailBot] Ошибка загрузки настроек:', error); throw error; } } /** * Создание SMTP транспортера */ async createTransporter() { return nodemailer.createTransport({ host: this.settings.smtp_host, port: 465, secure: true, auth: { user: this.settings.smtp_user, pass: this.settings.smtp_password, }, pool: false, maxConnections: 1, maxMessages: 1, tls: { rejectUnauthorized: false }, connectionTimeout: 30000, greetingTimeout: 30000, socketTimeout: 60000, }); } /** * Инициализация IMAP соединения */ async initializeImap() { try { // Очищаем предыдущее соединение this.cleanupImap(); this.imap = new Imap({ user: this.settings.imap_user, password: this.settings.imap_password, host: this.settings.imap_host, port: 993, tls: true, tlsOptions: { rejectUnauthorized: false, servername: this.settings.imap_host, ciphers: 'HIGH:!aNULL:!MD5:!RC4' }, keepalive: { interval: 10000, idleInterval: 300000, forceNoop: true, }, connTimeout: 60000, authTimeout: 60000, greetingTimeout: 30000, socketTimeout: 60000, debug: (info) => { logger.debug(`[EmailBot IMAP] ${info}`); } }); // Настраиваем обработчики событий this.setupImapHandlers(); // Подключаемся this.imap.connect(); } catch (error) { logger.error('[EmailBot] Ошибка инициализации IMAP:', error); throw error; } } /** * Настройка обработчиков IMAP событий */ setupImapHandlers() { this.imap.once('ready', () => { logger.info('[EmailBot] IMAP соединение установлено'); this.reconnectAttempts = 0; this.checkEmails(); // Запускаем периодическую проверку новых писем каждые 5 минут this.startPeriodicCheck(); }); this.imap.once('end', () => { logger.info('[EmailBot] IMAP соединение завершено'); this.cleanupImap(); }); this.imap.once('close', () => { logger.info('[EmailBot] IMAP соединение закрыто'); this.cleanupImap(); }); this.imap.once('error', (err) => { logger.error('[EmailBot] IMAP ошибка:', err.message); this.cleanupImap(); this.handleReconnection(err); }); } /** * Очистка IMAP соединения */ cleanupImap() { // Останавливаем периодическую проверку this.stopPeriodicCheck(); if (this.imap) { try { this.imap.removeAllListeners('error'); this.imap.removeAllListeners('ready'); this.imap.removeAllListeners('end'); this.imap.removeAllListeners('close'); if (this.imap.state !== 'disconnected') { this.imap.end(); } } catch (error) { logger.error('[EmailBot] Ошибка очистки IMAP:', error); } finally { this.imap = null; } } } /** * Обработка переподключения IMAP */ handleReconnection(err) { if (this.reconnectAttempts >= this.maxReconnectAttempts) { logger.error('[EmailBot] Достигнут максимум попыток переподключения'); this.status = 'connection_failed'; return; } let reconnectDelay = 10000; if (err.message && err.message.toLowerCase().includes('timed out')) { reconnectDelay = 15000; } else if (err.code === 'ECONNREFUSED') { reconnectDelay = 30000; } else if (err.code === 'ENOTFOUND') { reconnectDelay = 60000; } this.reconnectAttempts++; logger.warn(`[EmailBot] Переподключение через ${reconnectDelay/1000}с (попытка ${this.reconnectAttempts}/${this.maxReconnectAttempts})`); setTimeout(() => this.initializeImap(), reconnectDelay); } /** * Запуск периодической проверки новых писем */ startPeriodicCheck() { // Останавливаем предыдущий интервал, если он есть this.stopPeriodicCheck(); // Проверяем новые письма каждые 5 минут this.periodicCheckInterval = setInterval(() => { if (this.imap && this.imap.state === 'authenticated') { logger.info('[EmailBot] Периодическая проверка новых писем...'); this.checkEmails(); } else { logger.warn('[EmailBot] IMAP соединение не активно, пропускаем периодическую проверку'); } }, 5 * 60 * 1000); // 5 минут logger.info('[EmailBot] Периодическая проверка новых писем запущена (каждые 5 минут)'); } /** * Остановка периодической проверки */ stopPeriodicCheck() { if (this.periodicCheckInterval) { clearInterval(this.periodicCheckInterval); this.periodicCheckInterval = null; logger.info('[EmailBot] Периодическая проверка остановлена'); } } /** * Проверка входящих писем */ checkEmails() { try { logger.info('[EmailBot] Проверка входящих писем...'); this.imap.openBox('INBOX', false, (err, box) => { if (err) { logger.error('[EmailBot] Ошибка открытия INBOX:', err); return; } logger.info(`[EmailBot] INBOX открыт. Всего сообщений: ${box.messages.total}`); // Ищем только непрочитанные сообщения (UNSEEN) this.imap.search(['UNSEEN'], (err, results) => { if (err) { logger.error('[EmailBot] Ошибка поиска писем:', err); // Не закрываем соединение при ошибке поиска, оставляем его открытым для keepalive return; } if (!results || results.length === 0) { logger.info('[EmailBot] Новых непрочитанных писем нет'); // Не закрываем соединение, оставляем его открытым для keepalive return; } logger.info(`[EmailBot] Найдено ${results.length} непрочитанных писем`); const f = this.imap.fetch(results, { bodies: '', markSeen: true, struct: true }); let processedCount = 0; const totalMessages = results.length; f.on('message', (msg, seqno) => { let messageId = null; let uid = null; msg.once('attributes', (attrs) => { uid = attrs.uid; if (attrs['x-gm-msgid']) { messageId = attrs['x-gm-msgid']; } }); msg.on('body', (stream, info) => { simpleParser(stream, async (err, parsed) => { if (err) { logger.error(`[EmailBot] Ошибка парсинга письма ${seqno}:`, err); processedCount++; if (processedCount >= totalMessages) { logger.info('[EmailBot] Обработка всех писем завершена'); // Не закрываем соединение, оставляем его открытым для keepalive } return; } if (!messageId && parsed.messageId) { messageId = parsed.messageId; } const fromEmail = parsed.from?.value?.[0]?.address; logger.info(`[EmailBot] Обработка письма ${seqno} от ${fromEmail || 'неизвестного отправителя'}`); const messageData = await this.extractMessageData(parsed, messageId, uid); if (messageData && this.messageProcessor) { try { // Обрабатываем сообщение через унифицированный процессор // Системное сообщение о согласиях будет добавлено к ответу ИИ внутри процессора const result = await this.messageProcessor(messageData); logger.info(`[EmailBot] Письмо ${seqno} обработано успешно`); // Если есть ответ ИИ с информацией о согласиях, отправляем email if (result && result.success && result.aiResponse) { if (fromEmail) { logger.info(`[EmailBot] Отправка ответа ИИ на ${fromEmail}`); // Ответ ИИ уже содержит системное сообщение о согласиях (если нужно) await this.sendEmail( fromEmail, 'Ответ на ваше сообщение', result.aiResponse.response ); } } } catch (processError) { logger.error(`[EmailBot] Ошибка обработки письма ${seqno}:`, processError); } } else { if (!messageData) { logger.warn(`[EmailBot] Письмо ${seqno} отфильтровано (системное или некорректное)`); } else if (!this.messageProcessor) { logger.warn('[EmailBot] messageProcessor не установлен, письмо не обработано'); } } processedCount++; if (processedCount >= totalMessages) { logger.info('[EmailBot] Обработка всех писем завершена'); // Не закрываем соединение, оставляем его открытым для keepalive } }); }); }); f.once('error', (err) => { logger.error('[EmailBot] Ошибка получения писем:', err); // Не закрываем соединение при ошибке fetch, оставляем его открытым для keepalive }); }); }); } catch (error) { logger.error('[EmailBot] Ошибка проверки писем:', error); try { this.imap.end(); } catch (e) { // Игнорируем ошибки при закрытии } } } /** * Извлечение данных из Email сообщения с поддержкой медиа * @param {Object} parsed - Распарсенное письмо * @param {string} messageId - ID сообщения * @param {number} uid - UID сообщения * @returns {Object|null} - Стандартизированные данные сообщения */ async extractMessageData(parsed, messageId, uid) { try { const fromEmail = parsed.from?.value?.[0]?.address; const subject = parsed.subject || ''; const text = parsed.text || ''; // Фильтруем системные email адреса const systemEmails = [ 'mailer-daemon@smtp.hostland.ru', 'mailer-daemon@', 'noreply@', 'no-reply@', 'postmaster@', 'bounce@', 'daemon@' ]; const isSystemEmail = systemEmails.some(systemEmail => fromEmail && fromEmail.toLowerCase().includes(systemEmail.toLowerCase()) ); if (isSystemEmail || !fromEmail || !fromEmail.includes('@')) { return null; } let contentData = null; const mediaFiles = []; if (parsed.attachments && parsed.attachments.length > 0) { for (const att of parsed.attachments) { try { // Обрабатываем вложение через медиа-процессор const processedFile = await universalMediaProcessor.processFile( att.content, att.filename, { emailAttachment: true, originalSize: att.size, mimeType: att.contentType } ); mediaFiles.push(processedFile); } catch (fileError) { logger.error('[EmailBot] Ошибка обработки вложения:', fileError); // Fallback: сохраняем как есть mediaFiles.push({ type: 'document', content: `[Вложение: ${att.filename}]`, processed: false, error: fileError.message, file: { filename: att.filename, mimetype: att.contentType, size: att.size, data: att.content } }); } } } // Создаем структурированные данные контента если есть медиа if (mediaFiles.length > 0) { contentData = { text: text, files: mediaFiles.map(file => ({ data: file.file?.data || file.file?.content, filename: file.file?.originalName || file.file?.filename, metadata: { type: file.type, processed: file.processed, emailAttachment: true, mimeType: file.file?.contentType || file.file?.mimetype, originalSize: file.file?.size } })) }; } return { channel: 'email', identifier: `email:${fromEmail}`, // Формируем identifier с префиксом provider content: text, contentData: contentData, attachments: mediaFiles, // Обратная совместимость metadata: { subject: subject, messageId: messageId, uid: uid, fromEmail: fromEmail, html: parsed.html || '', hasMedia: mediaFiles.length > 0, mediaTypes: mediaFiles.map(f => f.type) } }; } catch (error) { logger.error('[EmailBot] Ошибка извлечения данных из письма:', error); return null; } } /** * Отправка email сообщения * @param {string} to - Адрес получателя * @param {string} subject - Тема письма * @param {string} text - Текст письма * @returns {Promise} - Успешность отправки */ async sendEmail(to, subject, text) { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(to)) { throw new Error(`Неверный формат email адреса: ${to}`); } try { const mailOptions = { from: this.settings.from_email, to, subject, text, }; await this.transporter.sendMail(mailOptions); this.transporter.close(); logger.info(`[EmailBot] Email отправлен успешно: ${to}`); return true; } catch (error) { logger.error('[EmailBot] Ошибка отправки email:', error); throw error; } } /** * Отправка email с HTML содержимым * @param {string} to - Email получателя * @param {string} subject - Тема письма * @param {string} text - Текстовая версия * @param {string} html - HTML версия */ async sendEmailWithHtml(to, subject, text, html) { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(to)) { throw new Error(`Неверный формат email адреса: ${to}`); } try { const mailOptions = { from: this.settings.from_email, to, subject, text, html }; await this.transporter.sendMail(mailOptions); this.transporter.close(); logger.info(`[EmailBot] Email с HTML отправлен успешно: ${to}`); return true; } catch (error) { logger.error('[EmailBot] Ошибка отправки email с HTML:', error); throw error; } } /** * Отправка кода верификации * @param {string} email - Email получателя * @param {string} code - Код верификации */ async sendVerificationCode(email, code) { try { const mailOptions = { from: this.settings.from_email, to: email, subject: 'Код подтверждения', text: `Ваш код подтверждения: ${code}\n\nКод действителен в течение 15 минут.`, html: `

Код подтверждения

Ваш код подтверждения:

${code}

Код действителен в течение 15 минут.

`, }; await this.transporter.sendMail(mailOptions); logger.info('[EmailBot] Код верификации отправлен'); } catch (error) { logger.error('[EmailBot] Ошибка отправки кода верификации:', error); throw error; } } /** * Отправка приветственного письма с ссылкой для подключения кошелька * @param {string} email - Email получателя * @param {string} linkUrl - Ссылка для подключения кошелька */ async sendWelcomeWithLink(email, linkUrl) { try { const mailOptions = { from: this.settings.from_email, to: email, subject: 'Подключите Web3 кошелек', text: `Добро пожаловать!\n\nДля полного доступа к системе подключите Web3 кошелек:\n${linkUrl}\n\nСсылка действительна 1 час.`, html: `

🔗 Подключите Web3 кошелек

Добро пожаловать! Для сохранения истории сообщений и полного доступа к системе подключите ваш кошелек:

Подключить кошелек

⏱ Ссылка действительна 1 час

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

`, }; await this.transporter.sendMail(mailOptions); logger.info('[EmailBot] Приветственное письмо с ссылкой отправлено'); } catch (error) { logger.error('[EmailBot] Ошибка отправки приветственного письма:', error); throw error; } } /** * Установка процессора сообщений * @param {Function} processor - Функция обработки сообщений */ setMessageProcessor(processor) { this.messageProcessor = processor; } /** * Проверка статуса бота * @returns {Object} - Статус бота */ getStatus() { return { name: this.name, channel: this.channel, isInitialized: this.isInitialized, status: this.status, hasSettings: !!this.settings, reconnectAttempts: this.reconnectAttempts }; } /** * Остановка бота */ async stop() { try { logger.info('[EmailBot] 🛑 Остановка Email Bot...'); this.cleanupImap(); if (this.transporter) { this.transporter.close(); this.transporter = null; } this.isInitialized = false; this.status = 'inactive'; logger.info('[EmailBot] ✅ Email Bot остановлен'); } catch (error) { logger.error('[EmailBot] ❌ Ошибка остановки:', error); throw error; } } } module.exports = EmailBot;