Files
redmine-reporter/redmine_reporter/mailer.py
Кокос Артем Николаевич 0968560090 style: apply ruff format to all source files
2026-07-10 12:39:24 +07:00

124 lines
4.2 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 email.charset as _charset
import os
import smtplib
from email.mime.application import MIMEApplication
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from typing import Dict
from .client import RedmineAPIError
from .config import EmailConfig
# Use 8bit transfer encoding for UTF-8 so non-ASCII text (e.g. Russian)
# appears literally in MIME output instead of base64.
_charset.add_charset("utf-8", _charset.SHORTEST, None, "utf-8")
SMTP_TIMEOUT = 30
MIME_TYPES: Dict[str, str] = {
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
".odt": "application/vnd.oasis.opendocument.text",
".csv": "text/csv",
".html": "text/html",
".json": "application/json",
".md": "text/markdown",
}
def _resolve_mime_type(file_path: str) -> str:
"""Определяет MIME-тип по расширению файла."""
ext = os.path.splitext(file_path)[1].lower()
return MIME_TYPES.get(ext, "application/octet-stream")
def build_message(
email_config: EmailConfig,
file_path: str,
author: str,
period: str,
) -> MIMEMultipart:
"""Формирует MIME-письмо с подстановками и вложением."""
subject = email_config.subject.replace("{author}", author).replace(
"{period}", period
)
body = email_config.body_text.replace("{author}", author).replace(
"{period}", period
)
msg = MIMEMultipart()
msg["Subject"] = subject
msg["From"] = email_config.from_
msg["To"] = ", ".join(email_config.to)
if email_config.cc:
msg["Cc"] = ", ".join(email_config.cc)
msg.attach(MIMEText(body, "plain", "utf-8"))
if email_config.attach:
try:
with open(file_path, "rb") as fh:
attachment = MIMEApplication(fh.read())
except OSError:
raise RedmineAPIError(
f"Не удалось прочитать файл отчёта: {file_path}"
) from None
attachment.add_header(
"Content-Disposition",
"attachment",
filename=os.path.basename(file_path),
)
mime_type = _resolve_mime_type(file_path)
attachment.set_type(mime_type)
msg.attach(attachment)
return msg
def send_report(
email_config: EmailConfig,
file_path: str,
author: str,
period: str,
) -> None:
"""Отправляет сгенерированный отчёт по email через SMTP.
Args:
email_config: Настройки SMTP и письма.
file_path: Путь к файлу отчёта для вложения.
author: Имя автора (для подстановки в тему/тело).
period: Строка периода (для подстановки в тему/тело).
Raises:
RedmineAPIError: При любой ошибке соединения или отправки.
"""
smtp_cfg = email_config.smtp
msg = build_message(email_config, file_path, author, period)
all_recipients = (
list(email_config.to) + list(email_config.cc) + list(email_config.bcc)
)
try:
with smtplib.SMTP(smtp_cfg.host, smtp_cfg.port, timeout=SMTP_TIMEOUT) as server:
if smtp_cfg.tls:
server.starttls()
if smtp_cfg.user:
server.login(smtp_cfg.user, smtp_cfg.password)
server.send_message(
msg, from_addr=email_config.from_, to_addrs=all_recipients
)
except smtplib.SMTPAuthenticationError:
raise RedmineAPIError(
"Ошибка аутентификации SMTP. Проверьте логин и пароль."
) from None
except TimeoutError:
raise RedmineAPIError("Таймаут соединения с SMTP-сервером.") from None
except smtplib.SMTPException as exc:
raise RedmineAPIError(f"Ошибка отправки письма: {exc}") from exc
except OSError as exc:
raise RedmineAPIError(
f"Не удалось подключиться к SMTP-серверу {smtp_cfg.host}:{smtp_cfg.port}"
) from exc