ваше сообщение коммита
This commit is contained in:
@@ -64,10 +64,13 @@ class EmailBotService {
|
||||
user: settings.smtp_user,
|
||||
pass: settings.smtp_password,
|
||||
},
|
||||
pool: true,
|
||||
maxConnections: 3,
|
||||
maxMessages: 5,
|
||||
pool: false, // Отключаем пул соединений
|
||||
maxConnections: 1, // Ограничиваем до 1 соединения
|
||||
maxMessages: 1, // Ограничиваем до 1 сообщения на соединение
|
||||
tls: { rejectUnauthorized: false },
|
||||
connectionTimeout: 30000, // 30 секунд на подключение
|
||||
greetingTimeout: 30000, // 30 секунд на приветствие
|
||||
socketTimeout: 60000, // 60 секунд на операции сокета
|
||||
});
|
||||
}
|
||||
|
||||
@@ -142,8 +145,8 @@ class EmailBotService {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ищем непрочитанные письма
|
||||
this.imap.search(['UNSEEN'], (err, results) => {
|
||||
// Ищем все письма и проверяем их флаги вручную
|
||||
this.imap.search(['ALL'], (err, results) => {
|
||||
if (err) {
|
||||
logger.error(`Error searching messages: ${err}`);
|
||||
this.imap.end();
|
||||
@@ -151,173 +154,106 @@ class EmailBotService {
|
||||
}
|
||||
|
||||
if (!results || results.length === 0) {
|
||||
logger.info('No new messages found');
|
||||
logger.info('No messages found');
|
||||
this.imap.end();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const f = this.imap.fetch(results, { bodies: '' });
|
||||
// Фильтруем только непрочитанные сообщения
|
||||
const f = this.imap.fetch(results, {
|
||||
bodies: '',
|
||||
markSeen: true, // Помечаем как прочитанные
|
||||
struct: true // Получаем структуру для Message-ID
|
||||
});
|
||||
|
||||
f.on('message', (msg, seqno) => {
|
||||
msg.on('body', (stream, info) => {
|
||||
simpleParser(stream, async (err, parsed) => {
|
||||
if (err) {
|
||||
logger.error(`Error parsing message: ${err}`);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const fromEmail = parsed.from?.value?.[0]?.address;
|
||||
const subject = parsed.subject || '';
|
||||
const text = parsed.text || '';
|
||||
const html = parsed.html || '';
|
||||
// 1. Найти или создать пользователя
|
||||
const { userId, role } = await identityService.findOrCreateUserWithRole('email', fromEmail);
|
||||
if (await isUserBlocked(userId)) {
|
||||
logger.info(`Email от заблокированного пользователя ${userId} проигнорирован.`);
|
||||
return;
|
||||
}
|
||||
// 1.1 Найти или создать беседу
|
||||
let conversationResult = await encryptedDb.getData(
|
||||
'conversations',
|
||||
{ user_id: userId },
|
||||
1,
|
||||
'updated_at DESC, created_at DESC'
|
||||
);
|
||||
let conversation;
|
||||
if (conversationResult.length === 0) {
|
||||
const title = `Чат с пользователем ${userId}`;
|
||||
const newConv = await encryptedDb.saveData(
|
||||
'conversations',
|
||||
{ user_id: userId, title: title, created_at: new Date(), updated_at: new Date() }
|
||||
);
|
||||
conversation = newConv;
|
||||
} else {
|
||||
conversation = conversationResult[0];
|
||||
}
|
||||
|
||||
// Проверяем, что conversation создан успешно
|
||||
if (!conversation || !conversation.id) {
|
||||
logger.error(`[EmailBot] Conversation is undefined or has no id for user ${userId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Сохранять все сообщения с conversation_id
|
||||
let hasAttachments = parsed.attachments && parsed.attachments.length > 0;
|
||||
if (hasAttachments) {
|
||||
for (const att of parsed.attachments) {
|
||||
await encryptedDb.saveData(
|
||||
'messages',
|
||||
{
|
||||
user_id: userId,
|
||||
conversation_id: conversation.id,
|
||||
sender_type: 'user',
|
||||
content: text,
|
||||
channel: 'email',
|
||||
role: role,
|
||||
direction: 'in',
|
||||
created_at: new Date(),
|
||||
attachment_filename: att.filename,
|
||||
attachment_mimetype: att.contentType,
|
||||
attachment_size: att.size,
|
||||
attachment_data: att.content,
|
||||
metadata: JSON.stringify({ subject, html })
|
||||
}
|
||||
);
|
||||
}
|
||||
} else {
|
||||
await encryptedDb.saveData(
|
||||
'messages',
|
||||
{
|
||||
user_id: userId,
|
||||
conversation_id: conversation.id,
|
||||
sender_type: 'user',
|
||||
content: text,
|
||||
channel: 'email',
|
||||
role: role,
|
||||
direction: 'in',
|
||||
created_at: new Date(),
|
||||
metadata: JSON.stringify({ subject, html })
|
||||
}
|
||||
);
|
||||
}
|
||||
// 3. Получить ответ от ИИ (RAG + LLM)
|
||||
const aiSettings = await aiAssistantSettingsService.getSettings();
|
||||
let ragTableId = null;
|
||||
if (aiSettings && aiSettings.selected_rag_tables) {
|
||||
ragTableId = Array.isArray(aiSettings.selected_rag_tables)
|
||||
? aiSettings.selected_rag_tables[0]
|
||||
: aiSettings.selected_rag_tables;
|
||||
}
|
||||
let aiResponse;
|
||||
if (ragTableId) {
|
||||
// Сначала ищем ответ через RAG
|
||||
const ragResult = await ragAnswer({ tableId: ragTableId, userQuestion: text });
|
||||
if (ragResult && ragResult.answer && typeof ragResult.score === 'number' && Math.abs(ragResult.score) <= 0.3) {
|
||||
aiResponse = ragResult.answer;
|
||||
} else {
|
||||
aiResponse = await generateLLMResponse({
|
||||
userQuestion: text,
|
||||
context: ragResult && ragResult.context ? ragResult.context : '',
|
||||
answer: ragResult && ragResult.answer ? ragResult.answer : '',
|
||||
systemPrompt: aiSettings ? aiSettings.system_prompt : '',
|
||||
history: null,
|
||||
model: aiSettings ? aiSettings.model : undefined,
|
||||
language: aiSettings && aiSettings.languages && aiSettings.languages.length > 0 ? aiSettings.languages[0] : 'ru'
|
||||
});
|
||||
}
|
||||
} else {
|
||||
aiResponse = await aiAssistant.getResponse(text, 'auto');
|
||||
}
|
||||
if (await isUserBlocked(userId)) {
|
||||
logger.info(`[EmailBot] Пользователь ${userId} заблокирован — ответ ИИ не отправляется.`);
|
||||
return;
|
||||
}
|
||||
// 4. Сохранить ответ в БД с conversation_id
|
||||
await encryptedDb.saveData(
|
||||
'messages',
|
||||
{
|
||||
user_id: userId,
|
||||
conversation_id: conversation.id,
|
||||
sender_type: 'assistant',
|
||||
content: aiResponse,
|
||||
channel: 'email',
|
||||
role: role,
|
||||
direction: 'out',
|
||||
created_at: new Date(),
|
||||
metadata: JSON.stringify({ subject, html })
|
||||
}
|
||||
);
|
||||
// 5. Отправить ответ на email
|
||||
await this.sendEmail(fromEmail, 'Re: ' + subject, aiResponse);
|
||||
// После каждого успешного создания пользователя:
|
||||
broadcastContactsUpdate();
|
||||
} catch (processErr) {
|
||||
logger.error('Error processing incoming email:', processErr);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
let unreadMessages = [];
|
||||
let processedCount = 0;
|
||||
let totalMessages = results.length;
|
||||
|
||||
f.once('error', (err) => {
|
||||
logger.error(`Fetch error: ${err}`);
|
||||
});
|
||||
|
||||
f.once('end', () => {
|
||||
try {
|
||||
this.imap.end();
|
||||
} catch (e) {
|
||||
logger.error(`Error ending IMAP connection: ${e.message}`);
|
||||
f.on('message', (msg, seqno) => {
|
||||
let messageId = null;
|
||||
let uid = null;
|
||||
let flags = [];
|
||||
|
||||
// Получаем UID, Message-ID и флаги
|
||||
msg.once('attributes', (attrs) => {
|
||||
uid = attrs.uid;
|
||||
flags = attrs.flags || [];
|
||||
if (attrs['x-gm-msgid']) {
|
||||
messageId = attrs['x-gm-msgid'];
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error(`Error fetching messages: ${e.message}`);
|
||||
try {
|
||||
this.imap.end();
|
||||
} catch (e) {
|
||||
// Игнорируем ошибки при закрытии
|
||||
}
|
||||
}
|
||||
|
||||
msg.on('body', (stream, info) => {
|
||||
simpleParser(stream, async (err, parsed) => {
|
||||
if (err) {
|
||||
logger.error(`Error parsing message: ${err}`);
|
||||
processedCount++;
|
||||
if (processedCount >= totalMessages) {
|
||||
if (unreadMessages.length === 0) {
|
||||
logger.info('No unread messages found');
|
||||
}
|
||||
this.imap.end();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Получаем Message-ID из заголовков
|
||||
if (!messageId && parsed.messageId) {
|
||||
messageId = parsed.messageId;
|
||||
}
|
||||
|
||||
const fromEmail = parsed.from?.value?.[0]?.address;
|
||||
const subject = parsed.subject || '';
|
||||
const text = parsed.text || '';
|
||||
const html = parsed.html || '';
|
||||
|
||||
// Проверяем, что сообщение непрочитанное (нет флага \Seen)
|
||||
const isUnread = !flags.includes('\\Seen');
|
||||
|
||||
logger.info(`[EmailBot] Проверяем письмо: UID=${uid}, Message-ID=${messageId}, From=${fromEmail}, Unread=${isUnread}`);
|
||||
|
||||
if (isUnread) {
|
||||
unreadMessages.push({
|
||||
uid,
|
||||
messageId,
|
||||
fromEmail,
|
||||
subject,
|
||||
text,
|
||||
html,
|
||||
parsed
|
||||
});
|
||||
}
|
||||
|
||||
processedCount++;
|
||||
if (processedCount >= totalMessages) {
|
||||
if (unreadMessages.length === 0) {
|
||||
logger.info('No unread messages found');
|
||||
this.imap.end();
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(`[EmailBot] Найдено ${unreadMessages.length} непрочитанных сообщений`);
|
||||
|
||||
// Обрабатываем каждое непрочитанное сообщение
|
||||
for (const messageData of unreadMessages) {
|
||||
try {
|
||||
await this.processIncomingEmail(messageData);
|
||||
} catch (processErr) {
|
||||
logger.error('Error processing incoming email:', processErr);
|
||||
}
|
||||
}
|
||||
|
||||
this.imap.end();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
f.once('error', (err) => {
|
||||
logger.error(`Error fetching messages: ${err}`);
|
||||
this.imap.end();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -334,22 +270,261 @@ class EmailBotService {
|
||||
}
|
||||
|
||||
// Метод для отправки email
|
||||
async sendEmail(to, subject, text) {
|
||||
async processIncomingEmail(messageData) {
|
||||
const { uid, messageId, fromEmail, subject, text, html, parsed } = messageData;
|
||||
|
||||
try {
|
||||
const settings = await this.getSettingsFromDb();
|
||||
const transporter = await this.getTransporter();
|
||||
const mailOptions = {
|
||||
from: settings.from_email,
|
||||
to,
|
||||
subject,
|
||||
text,
|
||||
};
|
||||
await transporter.sendMail(mailOptions);
|
||||
logger.info(`Email sent to ${to}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error('Error sending email:', error);
|
||||
throw error;
|
||||
logger.info(`[EmailBot] Обрабатываем письмо: UID=${uid}, Message-ID=${messageId}, From=${fromEmail}`);
|
||||
|
||||
// Фильтруем системные 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) {
|
||||
logger.info(`[EmailBot] Игнорируем системный email от ${fromEmail}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверяем, что email адрес валидный
|
||||
if (!fromEmail || !fromEmail.includes('@')) {
|
||||
logger.info(`[EmailBot] Игнорируем email с невалидным адресом: ${fromEmail}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверяем время письма - не обрабатываем письма старше 1 часа
|
||||
const emailDate = parsed.date || new Date();
|
||||
const now = new Date();
|
||||
const timeDiff = now.getTime() - emailDate.getTime();
|
||||
const hoursDiff = timeDiff / (1000 * 60 * 60);
|
||||
|
||||
if (hoursDiff > 1) {
|
||||
logger.info(`[EmailBot] Игнорируем старое письмо от ${fromEmail} (${hoursDiff.toFixed(1)} часов назад)`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверяем, не обрабатывали ли мы уже это письмо
|
||||
if (messageId) {
|
||||
const existingMessage = await encryptedDb.getData('messages', {
|
||||
metadata: { $like: `%"messageId":"${messageId}"%` }
|
||||
}, 1);
|
||||
|
||||
if (existingMessage.length > 0) {
|
||||
logger.info(`[EmailBot] Письмо с Message-ID ${messageId} уже обработано, пропускаем`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Найти или создать пользователя
|
||||
const { userId, role } = await identityService.findOrCreateUserWithRole('email', fromEmail);
|
||||
if (await isUserBlocked(userId)) {
|
||||
logger.info(`Email от заблокированного пользователя ${userId} проигнорирован.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 1.1 Найти или создать беседу
|
||||
let conversationResult = await encryptedDb.getData(
|
||||
'conversations',
|
||||
{ user_id: userId },
|
||||
1,
|
||||
'updated_at DESC, created_at DESC'
|
||||
);
|
||||
let conversation;
|
||||
if (conversationResult.length === 0) {
|
||||
const title = `Чат с пользователем ${userId}`;
|
||||
const newConv = await encryptedDb.saveData(
|
||||
'conversations',
|
||||
{ user_id: userId, title: title, created_at: new Date(), updated_at: new Date() }
|
||||
);
|
||||
conversation = newConv;
|
||||
} else {
|
||||
conversation = conversationResult[0];
|
||||
}
|
||||
|
||||
// Проверяем, что conversation создан успешно
|
||||
if (!conversation || !conversation.id) {
|
||||
logger.error(`[EmailBot] Conversation is undefined or has no id for user ${userId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Сохранять все сообщения с conversation_id
|
||||
let hasAttachments = parsed.attachments && parsed.attachments.length > 0;
|
||||
if (hasAttachments) {
|
||||
for (const att of parsed.attachments) {
|
||||
await encryptedDb.saveData(
|
||||
'messages',
|
||||
{
|
||||
user_id: userId,
|
||||
conversation_id: conversation.id,
|
||||
sender_type: 'user',
|
||||
content: text,
|
||||
channel: 'email',
|
||||
role: role,
|
||||
direction: 'in',
|
||||
created_at: new Date(),
|
||||
attachment_filename: att.filename,
|
||||
attachment_mimetype: att.contentType,
|
||||
attachment_size: att.size,
|
||||
attachment_data: att.content,
|
||||
metadata: JSON.stringify({
|
||||
subject,
|
||||
html,
|
||||
messageId: messageId,
|
||||
uid: uid,
|
||||
fromEmail: fromEmail
|
||||
})
|
||||
}
|
||||
);
|
||||
}
|
||||
} else {
|
||||
await encryptedDb.saveData(
|
||||
'messages',
|
||||
{
|
||||
user_id: userId,
|
||||
conversation_id: conversation.id,
|
||||
sender_type: 'user',
|
||||
content: text,
|
||||
channel: 'email',
|
||||
role: role,
|
||||
direction: 'in',
|
||||
created_at: new Date(),
|
||||
metadata: JSON.stringify({
|
||||
subject,
|
||||
html,
|
||||
messageId: messageId,
|
||||
uid: uid,
|
||||
fromEmail: fromEmail
|
||||
})
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// 3. Получить ответ от ИИ (RAG + LLM)
|
||||
const aiSettings = await aiAssistantSettingsService.getSettings();
|
||||
let ragTableId = null;
|
||||
if (aiSettings && aiSettings.selected_rag_tables) {
|
||||
ragTableId = Array.isArray(aiSettings.selected_rag_tables)
|
||||
? aiSettings.selected_rag_tables[0]
|
||||
: aiSettings.selected_rag_tables;
|
||||
}
|
||||
let aiResponse;
|
||||
if (ragTableId) {
|
||||
// Сначала ищем ответ через RAG
|
||||
const ragResult = await ragAnswer({ tableId: ragTableId, userQuestion: text });
|
||||
if (ragResult && ragResult.answer && typeof ragResult.score === 'number' && Math.abs(ragResult.score) <= 0.3) {
|
||||
aiResponse = ragResult.answer;
|
||||
} else {
|
||||
aiResponse = await generateLLMResponse({
|
||||
userQuestion: text,
|
||||
context: ragResult && ragResult.context ? ragResult.context : '',
|
||||
answer: ragResult && ragResult.answer ? ragResult.answer : '',
|
||||
systemPrompt: aiSettings ? aiSettings.system_prompt : '',
|
||||
history: null,
|
||||
model: aiSettings ? aiSettings.model : undefined,
|
||||
language: aiSettings && aiSettings.languages && aiSettings.languages.length > 0 ? aiSettings.languages[0] : 'ru'
|
||||
});
|
||||
}
|
||||
} else {
|
||||
aiResponse = await aiAssistant.getResponse(text, 'auto');
|
||||
}
|
||||
|
||||
if (await isUserBlocked(userId)) {
|
||||
logger.info(`[EmailBot] Пользователь ${userId} заблокирован — ответ ИИ не отправляется.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. Сохранить ответ в БД с conversation_id
|
||||
await encryptedDb.saveData(
|
||||
'messages',
|
||||
{
|
||||
user_id: userId,
|
||||
conversation_id: conversation.id,
|
||||
sender_type: 'assistant',
|
||||
content: aiResponse,
|
||||
channel: 'email',
|
||||
role: role,
|
||||
direction: 'out',
|
||||
created_at: new Date(),
|
||||
metadata: JSON.stringify({
|
||||
subject,
|
||||
html,
|
||||
originalMessageId: messageId,
|
||||
originalUid: uid,
|
||||
originalFromEmail: fromEmail,
|
||||
isResponse: true
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
// 5. Отправить ответ на email
|
||||
try {
|
||||
await this.sendEmail(fromEmail, 'Re: ' + subject, aiResponse);
|
||||
logger.info(`[EmailBot] Email response sent successfully to ${fromEmail}`);
|
||||
} catch (emailError) {
|
||||
logger.error(`[EmailBot] Failed to send email response to ${fromEmail}:`, emailError);
|
||||
// Продолжаем выполнение, даже если email не отправлен
|
||||
}
|
||||
|
||||
// После каждого успешного создания пользователя:
|
||||
broadcastContactsUpdate();
|
||||
|
||||
} catch (processErr) {
|
||||
logger.error('Error processing incoming email:', processErr);
|
||||
}
|
||||
}
|
||||
|
||||
async sendEmail(to, subject, text) {
|
||||
const maxRetries = 3;
|
||||
const retryDelay = 5000; // 5 секунд между попытками
|
||||
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
const settings = await this.getSettingsFromDb();
|
||||
const transporter = await this.getTransporter();
|
||||
|
||||
const mailOptions = {
|
||||
from: settings.from_email,
|
||||
to,
|
||||
subject,
|
||||
text,
|
||||
};
|
||||
|
||||
await transporter.sendMail(mailOptions);
|
||||
logger.info(`Email sent to ${to} (attempt ${attempt})`);
|
||||
|
||||
// Закрываем соединение после успешной отправки
|
||||
transporter.close();
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
logger.error(`Error sending email (attempt ${attempt}/${maxRetries}):`, error);
|
||||
|
||||
// Если это последняя попытка, выбрасываем ошибку
|
||||
if (attempt === maxRetries) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Если ошибка связана с превышением лимита соединений, ждем дольше
|
||||
const isConnectionLimitError = error.message && (
|
||||
error.message.includes('too many connections') ||
|
||||
error.message.includes('421 4.7.0') ||
|
||||
error.message.includes('EPROTOCOL')
|
||||
);
|
||||
|
||||
const waitTime = isConnectionLimitError ? retryDelay * 2 : retryDelay;
|
||||
logger.info(`Waiting ${waitTime}ms before retry...`);
|
||||
await new Promise(resolve => setTimeout(resolve, waitTime));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user