"""Отправка сгенерированного отчёта по 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