Files
replicator/app/utils/email_sender.py
2026-03-13 17:11:39 +09:00

355 lines
13 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import asyncio
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.application import MIMEApplication
from typing import Dict, List, Optional
from datetime import datetime
from app.core.config import settings
from app.core.logging import migration_logger
class EmailSender:
"""Класс для отправки email уведомлений"""
def __init__(self):
self.smtp_server = settings.EMAIL_HOST
self.smtp_port = settings.EMAIL_PORT
self.username = settings.EMAIL_USER
self.password = settings.EMAIL_PASSWORD
self.from_addr = settings.EMAIL_FROM
self.to_addrs = settings.EMAIL_TO
def send_email(self, subject: str, body: str, attachments: Optional[List[str]] = None) -> bool:
"""Отправка email с вложениями"""
if not all([self.smtp_server, self.username, self.password, self.from_addr]):
migration_logger.warning("Настройки email не заполнены. Отправка email пропущена.")
return False
try:
# Создаем сообщение
msg = MIMEMultipart()
msg['From'] = self.from_addr
msg['To'] = ', '.join(self.to_addrs)
msg['Subject'] = subject
msg['Date'] = datetime.now().strftime('%a, %d %b %Y %H:%M:%S %z')
# Добавляем текст
msg.attach(MIMEText(body, 'plain', 'utf-8'))
# Добавляем вложения
if attachments:
for file_path in attachments:
try:
with open(file_path, 'rb') as f:
part = MIMEApplication(f.read(), Name=file_path.split('/')[-1])
part['Content-Disposition'] = f'attachment; filename="{file_path.split("/")[-1]}"'
msg.attach(part)
except Exception as e:
migration_logger.error(f"Ошибка при добавлении вложения {file_path}: {e}")
# Отправляем
with smtplib.SMTP_SSL(self.smtp_server, self.smtp_port) as server:
server.login(self.username, self.password)
server.send_message(msg)
migration_logger.info(f"Email успешно отправлен на {', '.join(self.to_addrs)}")
return True
except Exception as e:
migration_logger.error(f"Ошибка при отправке email: {e}")
return False
def send_error_notification(self, error_message: str, traceback_str: str = None, table_name: str = None):
"""Отправить уведомление об ошибке"""
subject = f"ОШИБКА МИГРАЦИИ - {datetime.now().strftime('%Y-%m-%d %H:%M')}"
body = f"""
🚨 ОШИБКА В ПРОЦЕССЕ МИГРАЦИИ ДАННЫХ
{'='*60}
Время: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
Сервис: Migration Service
"""
if table_name:
body += f"Таблица: {table_name}\n"
body += f"""
Ошибка:
{error_message}
"""
if traceback_str:
body += f"""
📎 Детали:
{traceback_str}
"""
body += """
🔧 Необходимо проверить логи и устранить проблему.
"""
return self.send_email(subject, body)
def send_success_notification(self, stats: dict, duration: float):
"""Отправить уведомление об успешной миграции"""
subject = f"УСПЕШНАЯ МИГРАЦИЯ - {datetime.now().strftime('%Y-%m-%d %H:%M')}"
body = f"""
МИГРАЦИЯ ДАННЫХ УСПЕШНО ЗАВЕРШЕНА
{'='*60}
Время: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
Длительность: {duration:.1f} сек
СТАТИСТИКА:
{'='*40}
Всего строк в БД: {stats.get('total_rows', 0)}
Таблиц обработано: {stats.get('total_tables', 0)}
Последняя репликация: {stats.get('last_replication', 'Нет данных')}
Миграция выполнена успешно!
"""
return self.send_email(subject, body)
def send_start_notification(self, tables: List[str], full_reload: bool):
"""Отправить уведомление о начале миграции"""
subject = f"НАЧАЛО МИГРАЦИИ - {datetime.now().strftime('%Y-%m-%d %H:%M')}"
body = f"""
НАЧАЛО МИГРАЦИИ ДАННЫХ
{'='*60}
Время старта: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
Режим: {'ПОЛНАЯ ПЕРЕЗАГРУЗКА' if full_reload else 'ИНКРЕМЕНТАЛЬНАЯ'}
Таблиц для обработки: {len(tables)}
СПИСОК ТАБЛИЦ:
{chr(10).join([f'{t}' for t in tables])}
Миграция запущена...
"""
return self.send_email(subject, body)
async def send_migration_summary_email(self, email_data: Dict) -> bool:
"""
Отправить сводный email о результатах миграции.
Args:
email_data: Словарь с данными:
- batch_id: ID батча
- total: Всего таблиц
- completed: Завершено
- successful: Успешно
- failed: Ошибок
- results: Список результатов по таблицам
- timestamp: Время отчёта
Returns:
bool: True если email отправлен успешно
"""
try:
# Формируем HTML-отчёт
html_content = self._create_email_html(email_data)
# Создаём письмо
msg = MIMEMultipart('alternative')
msg['Subject'] = f"Отчёт о миграции: {email_data.get('successful', 0)} успешно, {email_data.get('failed', 0)} ошибок"
msg['From'] = self.from_addr
msg['To'] = ', '.join(self.to_addrs)
# Текстовая версия
text_content = self._create_email_text(email_data)
msg.attach(MIMEText(text_content, 'plain', 'utf-8'))
# HTML версия
msg.attach(MIMEText(html_content, 'html', 'utf-8'))
# Отправляем
await asyncio.to_thread(
self._send_email_sync,
smtp_server=self.smtp_server,
smtp_port=self.smtp_port,
login=self.username,
password=self.password,
from_addr=self.from_addr,
to_addrs=self.to_addrs,
message=msg.as_string()
)
migration_logger.info(f"Email отправлен: {len(self.to_addrs)} получателей")
return True
except Exception as e:
migration_logger.error(f"Ошибка отправки email: {e}")
return False
def _create_email_html(self, data: Dict) -> str:
"""Создать HTML-версию письма"""
batch_id = data.get('batch_id', 'N/A')
total = data.get('total', 0)
successful = data.get('successful', 0)
failed = data.get('failed', 0)
timestamp = data.get('timestamp', 'N/A')
results = data.get('results', [])
# Цвет статуса
if failed == 0:
status_color = "#28a745"
status_text = "Все успешно"
elif failed < total:
status_color = "#ffc107"
status_text = "⚠️ Частичный успех"
else:
status_color = "#dc3545"
status_text = "Все плохо"
# Таблица результатов
rows = ""
for r in results:
table_name = r.get('table', 'Unknown')
success = r.get('success', False)
error = r.get('error', '')
if success:
row_color = "#d4edda"
status_icon = ""
else:
row_color = "#f8d7da"
status_icon = ""
rows += f"""
<tr style="background-color: {row_color};">
<td style="padding: 8px; border: 1px solid #ddd;">{table_name}</td>
<td style="padding: 8px; border: 1px solid #ddd;">{"Успешно" if success else "Ошибка"}</td>
<td style="padding: 8px; border: 1px solid #ddd;">{error[:50] if error else "-"}</td>
</tr>
"""
html = f"""
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body {{ font-family: Arial, sans-serif; line-height: 1.6; }}
.container {{ max-width: 100%; margin: 0 auto; padding: 16px; }}
.header {{ background-color: #007bff; color: white; padding: 16px; border-radius: 5px; }}
.summary {{ margin: 16px 0; padding: 16px; background-color: #f8f9fa; border-radius: 5px; }}
.status {{ font-size: 18px; font-weight: bold; color: {status_color}; }}
table {{ width: 100%; border-collapse: collapse; margin-top: 16px; }}
th {{ background-color: #007bff; color: white; padding: 10px; text-align: left; }}
.footer {{ margin-top: 30px; padding-top: 16px; border-top: 1px solid #ddd; color: #666; font-size: 12px; }}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Репликация данных</h1>
</div>
<div class="summary">
<p><strong>Batch ID:</strong> {batch_id}</p>
<p><strong>Время:</strong> {timestamp}</p>
<p class="status">{status_text}</p>
<ul>
<li>Всего таблиц: <strong>{total}</strong></li>
<li>Успешно: <strong style="color: #28a745;">{successful}</strong></li>
<li>Ошибок: <strong style="color: #dc3545;">{failed}</strong></li>
</ul>
</div>
<h2>Детали по таблицам</h2>
<table>
<thead>
<tr>
<th>Таблица</th>
<th>Результат</th>
<th>Ошибка</th>
</tr>
</thead>
<tbody>
{rows}
</tbody>
</table>
<div class="footer">
<p>Автоматическое сообщение от системы репликации</p>
<p>Не отвечайте на это письмо</p>
</div>
</div>
</body>
</html>
"""
return html
def _create_email_text(self, data: Dict) -> str:
"""Создать текстовую версию письма"""
batch_id = data.get('batch_id', 'N/A')
total = data.get('total', 0)
successful = data.get('successful', 0)
failed = data.get('failed', 0)
timestamp = data.get('timestamp', 'N/A')
results = data.get('results', [])
text = f"""
ОТЧЁТ О МИГРАЦИИ ДАННЫХ
=======================
Batch ID: {batch_id}
Время: {timestamp}
СТАТИСТИКА:
- Всего таблиц: {total}
- Успешно: {successful}
- Ошибок: {failed}
ДЕТАЛИ ПО ТАБЛИЦАМ:
"""
for r in results:
table_name = r.get('table', 'Unknown')
success = r.get('success', False)
error = r.get('error', '')
status = "✅ Успешно" if success else f"❌ Ошибка: {error}"
text += f"\n{table_name}: {status}"
text += """
=======================
Автоматическое сообщение от системы репликации
Не отвечайте на это письмо
"""
return text
def _send_email_sync(
self,
smtp_server: str,
smtp_port: int,
login: str,
password: str,
from_addr: str,
to_addrs: List[str],
message: str
):
"""Синхронная отправка email (запускается в потоке)"""
with smtplib.SMTP_SSL(smtp_server, smtp_port) as server:
server.login(login, password)
server.sendmail(from_addr, to_addrs, message)
# server = smtplib.SMTP_SSL(smtp_server, smtp_port)
# try:
# server.starttls()
# server.login(login, password)
# server.sendmail(from_addr, to_addrs, message)
# finally:
# server.quit()
email_sender = EmailSender()