Files
anti-plagiarism/services/worker-notifier/app/email_sender.py
jze9 c1cf1ddd2f feat(auth): подтверждение email + актуализация SMTP/Ollama конфигов
- Резенд письма верификации (/auth/resend-verification), модалка на
  фронте с поллингом статуса, страница /verify-email/:token
- SMTP переведён на собственный Postfix (mail.jze9mail.ru, STARTTLS,
  SMTP_TLS_VERIFY) вместо Yandex-заглушки в дефолтах и .env.example
- OLLAMA_URL и модель в worker-gpu синхронизированы с новым GPU-хостом
  (llama3:8b -> qwen2.5:7b, которой раньше не было на сервере)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 18:37:22 +05:00

180 lines
7.0 KiB
Python
Raw 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.
"""Отправка email уведомлений через SMTP."""
import logging
import smtplib
import ssl
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from app.config import settings
logger = logging.getLogger(__name__)
TASK_TYPE_SUBJECTS = {
"search": "Источники найдены — Академический помощник",
"plagiarism": "Проверка плагиата завершена — Академический помощник",
"summarize": "Краткое изложение готово — Академический помощник",
"gost": "Библиография отформатирована — Академический помощник",
}
class EmailSender:
"""Отправщик email уведомлений."""
def _send(self, to_email: str, subject: str, html_body: str) -> None:
"""
Отправить email через SMTP SSL.
Args:
to_email: Email получателя
subject: Тема письма
html_body: HTML тело письма
"""
msg = MIMEMultipart("alternative")
msg["Subject"] = subject
msg["From"] = settings.SMTP_FROM
msg["To"] = to_email
msg.attach(MIMEText(html_body, "html", "utf-8"))
try:
tls_ctx = ssl.create_default_context()
if not settings.SMTP_TLS_VERIFY:
tls_ctx.check_hostname = False
tls_ctx.verify_mode = ssl.CERT_NONE
if settings.SMTP_PORT == 465:
with smtplib.SMTP_SSL(settings.SMTP_HOST, settings.SMTP_PORT, context=tls_ctx) as smtp:
smtp.login(settings.SMTP_USER, settings.SMTP_PASSWORD)
smtp.send_message(msg)
else:
with smtplib.SMTP(settings.SMTP_HOST, settings.SMTP_PORT) as smtp:
smtp.ehlo()
smtp.starttls(context=tls_ctx)
smtp.ehlo()
smtp.login(settings.SMTP_USER, settings.SMTP_PASSWORD)
smtp.send_message(msg)
logger.info(f"Email отправлен: {to_email!r}, тема: {subject!r}")
except smtplib.SMTPException as e:
logger.error(f"SMTP ошибка при отправке письма на {to_email!r}: {e}")
raise
def send_task_done(
self,
user_email: str,
user_name: str,
task_id: str,
task_type: str,
summary: str,
app_url: str,
) -> None:
"""
Отправить уведомление о завершении задачи.
Args:
user_email: Email пользователя
user_name: Имя пользователя
task_id: ID завершённой задачи
task_type: Тип задачи (search, plagiarism, и т.д.)
summary: Краткое описание результата
app_url: Базовый URL приложения
"""
subject = TASK_TYPE_SUBJECTS.get(task_type, "Задача выполнена — Академический помощник")
task_url = f"{app_url}/tasks/{task_id}"
html_body = f"""<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body style="margin:0;padding:0;background-color:#f8fafc;font-family:system-ui,-apple-system,sans-serif;">
<div style="max-width:600px;margin:40px auto;background:#fff;border-radius:12px;overflow:hidden;box-shadow:0 4px 6px rgba(0,0,0,0.07);">
<!-- Header -->
<div style="background:#2563eb;padding:32px 40px;">
<h1 style="margin:0;color:#fff;font-size:22px;font-weight:600;">
Академический помощник
</h1>
<p style="margin:4px 0 0;color:#bfdbfe;font-size:14px;">academic.jze9.ru</p>
</div>
<!-- Body -->
<div style="padding:40px;">
<p style="margin:0 0 16px;color:#1e293b;font-size:16px;">
Здравствуйте, <strong>{user_name}</strong>!
</p>
<p style="margin:0 0 24px;color:#475569;font-size:15px;line-height:1.6;">
{summary}
</p>
<a href="{task_url}"
style="display:inline-block;background:#2563eb;color:#fff;padding:14px 28px;
border-radius:8px;text-decoration:none;font-size:15px;font-weight:500;">
Открыть результат →
</a>
</div>
<!-- Footer -->
<div style="padding:24px 40px;border-top:1px solid #e2e8f0;background:#f8fafc;">
<p style="margin:0;color:#94a3b8;font-size:12px;">
Академический помощник · <a href="{app_url}" style="color:#94a3b8;">academic.jze9.ru</a><br>
Это автоматическое письмо, не нужно отвечать на него.
</p>
</div>
</div>
</body>
</html>"""
self._send(user_email, subject, html_body)
def send_verification(self, user_email: str, user_name: str, token: str, app_url: str) -> None:
"""
Отправить письмо для верификации email.
Args:
user_email: Email для верификации
user_name: Имя пользователя
token: Токен верификации
app_url: Базовый URL приложения
"""
verify_url = f"{app_url}/verify-email/{token}"
subject = "Подтвердите email — Академический помощник"
html_body = f"""<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
</head>
<body style="margin:0;padding:0;background:#f8fafc;font-family:system-ui,-apple-system,sans-serif;">
<div style="max-width:600px;margin:40px auto;background:#fff;border-radius:12px;overflow:hidden;box-shadow:0 4px 6px rgba(0,0,0,0.07);">
<div style="background:#2563eb;padding:32px 40px;">
<h1 style="margin:0;color:#fff;font-size:22px;font-weight:600;">Подтвердите ваш email</h1>
</div>
<div style="padding:40px;">
<p style="margin:0 0 16px;color:#1e293b;font-size:16px;">
Здравствуйте, <strong>{user_name}</strong>!
</p>
<p style="margin:0 0 24px;color:#475569;font-size:15px;line-height:1.6;">
Нажмите кнопку ниже, чтобы подтвердить ваш email и активировать аккаунт.
</p>
<a href="{verify_url}"
style="display:inline-block;background:#16a34a;color:#fff;padding:14px 28px;
border-radius:8px;text-decoration:none;font-size:15px;font-weight:500;">
Подтвердить email →
</a>
<p style="margin:24px 0 0;color:#94a3b8;font-size:12px;">
Ссылка действительна 48 часов. Если вы не регистрировались, проигнорируйте это письмо.
</p>
</div>
</div>
</body>
</html>"""
self._send(user_email, subject, html_body)