// email-notifications.js
const nodemailer = require('nodemailer');
const { getDb } = require('./database');
class EmailNotifications {
constructor() {
this.transporter = null;
this.initialized = false;
this.init();
}
async init() {
try {
console.log('π§ ΠΠ½ΠΈΡΠΈΠ°Π»ΠΈΠ·Π°ΡΠΈΡ Email ΡΠ²Π΅Π΄ΠΎΠΌΠ»Π΅Π½ΠΈΠΉ...');
if (!process.env.YANDEX_EMAIL || !process.env.YANDEX_PASSWORD) {
console.warn('β οΈ ΠΠ°ΡΡΡΠΎΠΉΠΊΠΈ Π―Π½Π΄Π΅ΠΊΡ ΠΏΠΎΡΡΡ Π½Π΅ ΡΠΊΠ°Π·Π°Π½Ρ Π² .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
}
});
// Π’Π΅ΡΡΠΈΡΡΠ΅ΠΌ ΠΏΠΎΠ΄ΠΊΠ»ΡΡΠ΅Π½ΠΈΠ΅
await this.transporter.verify();
this.initialized = true;
console.log('β
Email ΡΠ²Π΅Π΄ΠΎΠΌΠ»Π΅Π½ΠΈΡ ΠΈΠ½ΠΈΡΠΈΠ°Π»ΠΈΠ·ΠΈΡΠΎΠ²Π°Π½Ρ');
console.log(`π§ ΠΡΠΏΡΠ°Π²ΠΈΡΠ΅Π»Ρ: ${process.env.YANDEX_EMAIL}`);
} catch (error) {
console.error('β ΠΡΠΈΠ±ΠΊΠ° ΠΈΠ½ΠΈΡΠΈΠ°Π»ΠΈΠ·Π°ΡΠΈΠΈ Email ΡΠ²Π΅Π΄ΠΎΠΌΠ»Π΅Π½ΠΈΠΉ:', error.message);
this.initialized = false;
}
}
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) {
if (!this.initialized || !this.transporter) {
console.warn('β οΈ Email ΡΠ²Π΅Π΄ΠΎΠΌΠ»Π΅Π½ΠΈΡ ΠΎΡΠΊΠ»ΡΡΠ΅Π½Ρ');
return false;
}
try {
const info = await this.transporter.sendMail({
from: `"School CRM" <${process.env.YANDEX_EMAIL}>`,
to: to,
subject: subject,
html: htmlContent,
text: htmlContent.replace(/<[^>]*>/g, '') // ΠΠΎΠ½Π²Π΅ΡΡΠΈΡΡΠ΅ΠΌ HTML Π² ΡΠ΅ΠΊΡΡ
});
console.log(`π§ Email ΠΎΡΠΏΡΠ°Π²Π»Π΅Π½: ${to}, Message ID: ${info.messageId}`);
return true;
} catch (error) {
console.error('β ΠΡΠΈΠ±ΠΊΠ° ΠΎΡΠΏΡΠ°Π²ΠΊΠΈ email:', error.message);
return false;
}
}
async sendTaskNotification(userId, taskData, notificationType) {
try {
const settings = await this.getUserNotificationSettings(userId);
if (!settings || !settings.email_notifications) {
return false;
}
// ΠΡΠΏΠΎΠ»ΡΠ·ΡΠ΅ΠΌ ΡΠΊΠ°Π·Π°Π½Π½ΡΡ email ΠΈΠ»ΠΈ email ΠΈΠ· ΠΏΡΠΎΡΠΈΠ»Ρ ΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΠ΅Π»Ρ
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;
default:
subject = `Π£Π²Π΅Π΄ΠΎΠΌΠ»Π΅Π½ΠΈΠ΅ ΠΏΠΎ Π·Π°Π΄Π°ΡΠ΅: ${taskData.title}`;
htmlContent = this.getDefaultHtml(taskData);
}
return await this.sendEmailNotification(emailTo, subject, htmlContent);
} catch (error) {
console.error('β ΠΡΠΈΠ±ΠΊΠ° ΠΎΡΠΏΡΠ°Π²ΠΊΠΈ ΡΠ²Π΅Π΄ΠΎΠΌΠ»Π΅Π½ΠΈΡ ΠΎ Π·Π°Π΄Π°ΡΠ΅:', error);
return false;
}
}
// HTML ΡΠ°Π±Π»ΠΎΠ½Ρ Π΄Π»Ρ ΡΠ°Π·Π½ΡΡ
ΡΠΈΠΏΠΎΠ² ΡΠ²Π΅Π΄ΠΎΠΌΠ»Π΅Π½ΠΈΠΉ
getTaskCreatedHtml(taskData) {
return `
${taskData.title}
ΠΠΏΠΈΡΠ°Π½ΠΈΠ΅: ${taskData.description || 'ΠΠ΅Π· ΠΎΠΏΠΈΡΠ°Π½ΠΈΡ'}
Π‘ΡΠΎΠΊ Π²ΡΠΏΠΎΠ»Π½Π΅Π½ΠΈΡ: ${new Date(taskData.due_date).toLocaleString('ru-RU')}
Π‘ΠΎΠ·Π΄Π°Π»: ${taskData.author_name || 'ΠΠ΅ΠΈΠ·Π²Π΅ΡΡΠ½ΠΎ'}
ΠΠ»Ρ ΠΏΡΠΎΡΠΌΠΎΡΡΠ° ΠΏΠΎΠ΄ΡΠΎΠ±Π½ΠΎΠΉ ΠΈΠ½ΡΠΎΡΠΌΠ°ΡΠΈΠΈ ΠΏΠ΅ΡΠ΅ΠΉΠ΄ΠΈΡΠ΅ Π² ΡΠΈΡΡΠ΅ΠΌΡ ΡΠΏΡΠ°Π²Π»Π΅Π½ΠΈΡ Π·Π°Π΄Π°ΡΠ°ΠΌΠΈ.
ΠΠ΅ΡΠ΅ΠΉΡΠΈ Π² CRM
`;
}
getTaskUpdatedHtml(taskData) {
return `
${taskData.title}
ΠΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΡ Π²Π½Π΅Ρ: ${taskData.author_name || 'ΠΠ΅ΠΈΠ·Π²Π΅ΡΡΠ½ΠΎ'}
ΠΡΠ΅ΠΌΡ: ${new Date().toLocaleString('ru-RU')}
ΠΠ»Ρ ΠΏΡΠΎΡΠΌΠΎΡΡΠ° ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΠΉ ΠΏΠ΅ΡΠ΅ΠΉΠ΄ΠΈΡΠ΅ Π² ΡΠΈΡΡΠ΅ΠΌΡ ΡΠΏΡΠ°Π²Π»Π΅Π½ΠΈΡ Π·Π°Π΄Π°ΡΠ°ΠΌΠΈ.
ΠΠ΅ΡΠ΅ΠΉΡΠΈ Π² CRM
`;
}
getTaskReworkHtml(taskData) {
return `
${taskData.title}
ΠΠ²ΡΠΎΡ Π·Π°ΠΌΠ΅ΡΠ°Π½ΠΈΡ: ${taskData.author_name || 'ΠΠ΅ΠΈΠ·Π²Π΅ΡΡΠ½ΠΎ'}
ΠΠΎΠΆΠ°Π»ΡΠΉΡΡΠ°, ΠΈΡΠΏΡΠ°Π²ΡΡΠ΅ Π·Π°ΠΌΠ΅ΡΠ°Π½ΠΈΡ ΠΈ ΠΎΠ±Π½ΠΎΠ²ΠΈΡΠ΅ ΡΡΠ°ΡΡΡ Π·Π°Π΄Π°ΡΠΈ.
ΠΠ΅ΡΠ΅ΠΉΡΠΈ ΠΊ Π·Π°Π΄Π°ΡΠ΅
`;
}
getTaskClosedHtml(taskData) {
return `
${taskData.title}
ΠΠ°ΠΊΡΡΡΠ°: ${taskData.author_name || 'ΠΠ΅ΠΈΠ·Π²Π΅ΡΡΠ½ΠΎ'}
ΠΡΠ΅ΠΌΡ Π·Π°ΠΊΡΡΡΠΈΡ: ${new Date().toLocaleString('ru-RU')}
ΠΠ°Π΄Π°ΡΠ° Π·Π°Π²Π΅ΡΡΠ΅Π½Π° ΠΈ ΠΏΠ΅ΡΠ΅ΠΌΠ΅ΡΠ΅Π½Π° Π² Π°ΡΡ
ΠΈΠ².
ΠΠ΅ΡΠ΅ΠΉΡΠΈ Π² CRM
`;
}
getStatusChangedHtml(taskData) {
return `
${taskData.title}
ΠΠΎΠ²ΡΠΉ ΡΡΠ°ΡΡΡ: ${this.getStatusText(taskData.status)}
ΠΠ·ΠΌΠ΅Π½ΠΈΠ»: ${taskData.user_name || taskData.author_name || 'ΠΠ΅ΠΈΠ·Π²Π΅ΡΡΠ½ΠΎ'}
ΠΡΠ΅ΠΌΡ: ${new Date().toLocaleString('ru-RU')}
ΠΠ»Ρ ΠΏΡΠΎΡΠΌΠΎΡΡΠ° Π΄Π΅ΡΠ°Π»Π΅ΠΉ ΠΏΠ΅ΡΠ΅ΠΉΠ΄ΠΈΡΠ΅ Π² ΡΠΈΡΡΠ΅ΠΌΡ ΡΠΏΡΠ°Π²Π»Π΅Π½ΠΈΡ Π·Π°Π΄Π°ΡΠ°ΠΌΠΈ.
ΠΠ΅ΡΠ΅ΠΉΡΠΈ Π² CRM
`;
}
getDeadlineHtml(taskData) {
return `
${taskData.title}
ΠΠΠΠΠΠΠΠ! ΠΠΎ ΠΎΠΊΠΎΠ½ΡΠ°Π½ΠΈΡ ΡΡΠΎΠΊΠ° Π·Π°Π΄Π°ΡΠΈ ΠΎΡΡΠ°Π»ΠΎΡΡ ΠΌΠ΅Π½Π΅Π΅ ${taskData.hours_left} ΡΠ°ΡΠΎΠ²!
Π‘ΡΠΎΠΊ Π²ΡΠΏΠΎΠ»Π½Π΅Π½ΠΈΡ: ${new Date(taskData.due_date).toLocaleString('ru-RU')}
ΠΠΎΠΆΠ°Π»ΡΠΉΡΡΠ°, Π·Π°Π²Π΅ΡΡΠΈΡΠ΅ Π·Π°Π΄Π°ΡΡ Π² ΡΠΊΠ°Π·Π°Π½Π½ΡΠΉ ΡΡΠΎΠΊ.
ΠΠ΅ΡΠ΅ΠΉΡΠΈ ΠΊ Π·Π°Π΄Π°ΡΠ΅
`;
}
getStatusText(status) {
const statusMap = {
'assigned': 'ΠΠ°Π·Π½Π°ΡΠ΅Π½Π°',
'in_progress': 'Π ΡΠ°Π±ΠΎΡΠ΅',
'completed': 'ΠΠ°Π²Π΅ΡΡΠ΅Π½Π°',
'overdue': 'ΠΡΠΎΡΡΠΎΡΠ΅Π½Π°',
'rework': 'ΠΠ° Π΄ΠΎΡΠ°Π±ΠΎΡΠΊΠ΅'
};
return statusMap[status] || status;
}
getDefaultHtml(taskData) {
return `
${taskData.title}
${taskData.message || 'ΠΠΎΠ²ΠΎΠ΅ ΡΠ²Π΅Π΄ΠΎΠΌΠ»Π΅Π½ΠΈΠ΅ ΠΏΠΎ Π·Π°Π΄Π°ΡΠ΅'}
ΠΠ΅ΡΠ΅ΠΉΡΠΈ Π² CRM
`;
}
isReady() {
return this.initialized;
}
}
// Singleton
const emailNotifications = new EmailNotifications();
module.exports = emailNotifications;
ΠΠΎΠΌΠΌΠ΅Π½ΡΠ°ΡΠΈΠΉ:
${taskData.comment || 'Π’ΡΠ΅Π±ΡΠ΅ΡΡΡ Π΄ΠΎΡΠ°Π±ΠΎΡΠΊΠ°'}