"""Отправка сгенерированного отчёта по 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, List from .client import RedmineAPIError from .config import EmailConfig from .types import ReportRow # 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_html_body(rows: List[ReportRow]) -> str: """Генерирует HTML-версию тела письма через HTMLFormatter.""" from .formatters.html import HTMLFormatter formatter = HTMLFormatter() return formatter.format(rows) def build_message( email_config: EmailConfig, file_path: str, author: str, period: str, rows: List[ReportRow], ) -> 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) # Тела письма: plain-text всегда, HTML по флагу body_container = MIMEMultipart("alternative") body_container.attach(MIMEText(body, "plain", "utf-8")) if email_config.html: html_body = _build_html_body(rows) body_container.attach(MIMEText(html_body, "html", "utf-8")) msg.attach(body_container) 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, rows: List[ReportRow], ) -> None: """Отправляет сгенерированный отчёт по email через SMTP. Args: email_config: Настройки SMTP и письма. file_path: Путь к файлу отчёта для вложения. author: Имя автора (для подстановки в тему/тело). period: Строка периода (для подстановки в тему/тело). rows: Строки отчёта (для HTML-версии тела письма). Raises: RedmineAPIError: При любой ошибке соединения или отправки. """ smtp_cfg = email_config.smtp msg = build_message(email_config, file_path, author, period, rows) 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