diff --git a/frontend/public/index.html b/frontend/public/index.html
index 6a9f8c2..3b3cc32 100644
--- a/frontend/public/index.html
+++ b/frontend/public/index.html
@@ -2,39 +2,16 @@
-
+
-
-
+
-
React App
-
diff --git a/frontend/public/logo.ico b/frontend/public/logo.ico
new file mode 100644
index 0000000..c4ec188
Binary files /dev/null and b/frontend/public/logo.ico differ
diff --git a/frontend/public/logo.png b/frontend/public/logo.png
new file mode 100644
index 0000000..32c5f00
Binary files /dev/null and b/frontend/public/logo.png differ
diff --git a/mail-service/src/controllers/deliveryLogController.js b/mail-service/src/controllers/deliveryLogController.js
index a2414c2..004d422 100644
--- a/mail-service/src/controllers/deliveryLogController.js
+++ b/mail-service/src/controllers/deliveryLogController.js
@@ -216,5 +216,74 @@ export default {
} catch (err) {
res.status(500).json({ error: err.message });
}
+ },
+
+ // Получить статистику по статусам подписчиков после ошибок
+ async getSubscriberStatusStatistics(req, res) {
+ try {
+ const { campaignId } = req.query;
+
+ let whereClause = {};
+ if (campaignId) {
+ whereClause.campaign_id = campaignId;
+ }
+
+ // Получаем статистику по статусам подписчиков
+ const activeCount = await Subscriber.count({
+ where: { status: 'active' }
+ });
+ const unsubscribedCount = await Subscriber.count({
+ where: { status: 'unsubscribed' }
+ });
+ const bouncedCount = await Subscriber.count({
+ where: { status: 'bounced' }
+ });
+
+ // Получаем статистику по причинам отписки (из DeliveryLog)
+ const failedLogs = await DeliveryLog.findAll({
+ where: {
+ ...whereClause,
+ status: 'failed'
+ },
+ include: [Subscriber],
+ attributes: [
+ 'error_message',
+ [sequelize.fn('COUNT', sequelize.col('id')), 'count']
+ ],
+ group: ['error_message'],
+ order: [[sequelize.fn('COUNT', sequelize.col('id')), 'DESC']],
+ limit: 10,
+ raw: true
+ });
+
+ // Получаем последние отписки
+ const recentUnsubscribes = await Subscriber.findAll({
+ where: {
+ status: 'unsubscribed',
+ unsubscribed_at: {
+ [Op.not]: null
+ }
+ },
+ order: [['unsubscribed_at', 'DESC']],
+ limit: 20
+ });
+
+ res.json({
+ subscriberStatus: {
+ active: activeCount,
+ unsubscribed: unsubscribedCount,
+ bounced: bouncedCount,
+ total: activeCount + unsubscribedCount + bouncedCount
+ },
+ failedReasons: failedLogs,
+ recentUnsubscribes: recentUnsubscribes.map(sub => ({
+ id: sub.id,
+ email: sub.email,
+ unsubscribed_at: sub.unsubscribed_at
+ }))
+ });
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
}
};
\ No newline at end of file
diff --git a/mail-service/src/routes/deliveryLog.js b/mail-service/src/routes/deliveryLog.js
index 5f1653b..fd5c9d6 100644
--- a/mail-service/src/routes/deliveryLog.js
+++ b/mail-service/src/routes/deliveryLog.js
@@ -7,6 +7,7 @@ router.post('/', deliveryLogController.create);
router.get('/', deliveryLogController.getAll);
router.get('/pending-count', deliveryLogController.getPendingCount);
router.get('/statistics', deliveryLogController.getDeliveryStatistics);
+router.get('/subscriber-status', deliveryLogController.getSubscriberStatusStatistics);
router.get('/campaign/:campaignId', deliveryLogController.getLogsByCampaign);
router.get('/:id', deliveryLogController.getById);
router.put('/:id', deliveryLogController.update);
diff --git a/mail-service/src/service/dynamicConsumer.js b/mail-service/src/service/dynamicConsumer.js
index 3738d06..c6f99e6 100644
--- a/mail-service/src/service/dynamicConsumer.js
+++ b/mail-service/src/service/dynamicConsumer.js
@@ -1,6 +1,6 @@
import { Kafka } from 'kafkajs';
import nodemailer from 'nodemailer';
-import { SmtpServer, DeliveryLog } from '../models/index.js';
+import { SmtpServer, DeliveryLog, Subscriber } from '../models/index.js';
import { topicManager } from './topicManager.js';
const kafka = new Kafka({
@@ -201,6 +201,9 @@ async function processEmailTask(task, topic) {
status: 'failed',
error_message: errorMsg
});
+
+ // Устанавливаем статус подписчика как "unsubscribed"
+ await updateSubscriberStatus(task.subscriberId, 'unsubscribed', errorMsg);
return;
}
@@ -237,6 +240,9 @@ async function processEmailTask(task, topic) {
} catch (err) {
console.error('Error sending email:', err, 'task:', task);
+ // Определяем тип ошибки и соответствующий статус
+ const { status, reason } = analyzeSmtpError(err);
+
// Обновляем запись в DeliveryLog с ошибкой
if (deliveryLog) {
await deliveryLog.update({
@@ -244,6 +250,87 @@ async function processEmailTask(task, topic) {
error_message: err.message
});
}
+
+ // Обновляем статус подписчика в зависимости от типа ошибки
+ await updateSubscriberStatus(task.subscriberId, status, reason);
+ }
+}
+
+// Функция для анализа SMTP ошибок и определения статуса подписчика
+function analyzeSmtpError(error) {
+ const errorMessage = error.message.toLowerCase();
+ const errorCode = error.code || '';
+
+ // Ошибки, указывающие на недействительный email или отписку
+ const unsubscribeErrors = [
+ '550', '553', '554', // SMTP коды для недействительных адресов
+ 'user not found',
+ 'mailbox not found',
+ 'address not found',
+ 'recipient not found',
+ 'user unknown',
+ 'mailbox unavailable',
+ 'address rejected',
+ 'recipient rejected',
+ 'bounce',
+ 'hard bounce',
+ 'permanent failure'
+ ];
+
+ // Ошибки, указывающие на временные проблемы
+ const temporaryErrors = [
+ '421', '450', '451', '452', // SMTP коды для временных ошибок
+ 'temporary failure',
+ 'temporarily unavailable',
+ 'try again later',
+ 'quota exceeded',
+ 'rate limit',
+ 'throttled'
+ ];
+
+ // Проверяем на ошибки отписки
+ for (const unsubscribeError of unsubscribeErrors) {
+ if (errorMessage.includes(unsubscribeError) || errorCode.includes(unsubscribeError)) {
+ return {
+ status: 'unsubscribed',
+ reason: `SMTP error: ${error.message}`
+ };
+ }
+ }
+
+ // Проверяем на временные ошибки
+ for (const tempError of temporaryErrors) {
+ if (errorMessage.includes(tempError) || errorCode.includes(tempError)) {
+ return {
+ status: 'bounced',
+ reason: `Temporary SMTP error: ${error.message}`
+ };
+ }
+ }
+
+ // По умолчанию устанавливаем статус "unsubscribed" для любых других ошибок
+ return {
+ status: 'unsubscribed',
+ reason: `SMTP error: ${error.message}`
+ };
+}
+
+// Функция для обновления статуса подписчика
+async function updateSubscriberStatus(subscriberId, status, reason = null) {
+ try {
+ const subscriber = await Subscriber.findByPk(subscriberId);
+ if (subscriber) {
+ await subscriber.update({
+ status: status,
+ unsubscribed_at: status === 'unsubscribed' ? new Date() : null
+ });
+
+ console.log(`[DynamicConsumer] Updated subscriber ${subscriberId} status to "${status}"${reason ? ` (reason: ${reason})` : ''}`);
+ } else {
+ console.error(`[DynamicConsumer] Subscriber ${subscriberId} not found`);
+ }
+ } catch (error) {
+ console.error(`[DynamicConsumer] Error updating subscriber ${subscriberId} status:`, error);
}
}