feat: add HTML email body for --send (#48)
Add email.html config flag (default false). When enabled, --send includes an HTML version of the report body generated via HTMLFormatter alongside the plain-text part in a multipart/alternative message. - EmailConfig gains html: bool field - mailer.build_message/send_report accept rows for HTML generation - CLI passes rows to send_report - --init-config generates email.html: false - README.md and docs/CONFIG.md updated Bump version to 1.10.0. Closes #48
This commit is contained in:
@@ -1 +1 @@
|
||||
__version__ = "1.9.0"
|
||||
__version__ = "1.10.0"
|
||||
|
||||
@@ -76,6 +76,7 @@ def _run_init_config(config_path: str, force: bool) -> int:
|
||||
"no_time": False,
|
||||
},
|
||||
"email": {
|
||||
"html": False,
|
||||
"smtp": {
|
||||
"host": "",
|
||||
"port": 587,
|
||||
@@ -209,7 +210,13 @@ def _save_and_maybe_send(
|
||||
return 1
|
||||
|
||||
try:
|
||||
send_report(email_config, output_arg, author, f"{from_date}--{to_date}")
|
||||
send_report(
|
||||
email_config,
|
||||
output_arg,
|
||||
author,
|
||||
f"{from_date}--{to_date}",
|
||||
rows=rows,
|
||||
)
|
||||
print(f"📧 Report sent to {', '.join(email_config.to)}")
|
||||
except RedmineAPIError as e:
|
||||
print(f"❌ {e.message}", file=sys.stderr)
|
||||
|
||||
@@ -38,6 +38,7 @@ class EmailConfig:
|
||||
subject: str = "Отчёт {author} за {period}"
|
||||
body_text: str = "Во вложении отчёт."
|
||||
attach: bool = True
|
||||
html: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -193,6 +194,7 @@ class AppConfig:
|
||||
cls._safe_str(email_raw.get("body_text")) or "Во вложении отчёт."
|
||||
),
|
||||
attach=cls._safe_bool(email_raw.get("attach"), True),
|
||||
html=cls._safe_bool(email_raw.get("html"), False),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -6,10 +6,11 @@ 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 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.
|
||||
@@ -33,13 +34,22 @@ def _resolve_mime_type(file_path: str) -> str:
|
||||
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-письмо с подстановками и вложением."""
|
||||
"""Формирует MIME-письмо с подстановками, телами и вложением."""
|
||||
subject = email_config.subject.replace("{author}", author).replace(
|
||||
"{period}", period
|
||||
)
|
||||
@@ -54,7 +64,15 @@ def build_message(
|
||||
if email_config.cc:
|
||||
msg["Cc"] = ", ".join(email_config.cc)
|
||||
|
||||
msg.attach(MIMEText(body, "plain", "utf-8"))
|
||||
# Тела письма: 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:
|
||||
@@ -81,6 +99,7 @@ def send_report(
|
||||
file_path: str,
|
||||
author: str,
|
||||
period: str,
|
||||
rows: List[ReportRow],
|
||||
) -> None:
|
||||
"""Отправляет сгенерированный отчёт по email через SMTP.
|
||||
|
||||
@@ -89,12 +108,13 @@ def send_report(
|
||||
file_path: Путь к файлу отчёта для вложения.
|
||||
author: Имя автора (для подстановки в тему/тело).
|
||||
period: Строка периода (для подстановки в тему/тело).
|
||||
rows: Строки отчёта (для HTML-версии тела письма).
|
||||
|
||||
Raises:
|
||||
RedmineAPIError: При любой ошибке соединения или отправки.
|
||||
"""
|
||||
smtp_cfg = email_config.smtp
|
||||
msg = build_message(email_config, file_path, author, period)
|
||||
msg = build_message(email_config, file_path, author, period, rows)
|
||||
all_recipients = (
|
||||
list(email_config.to) + list(email_config.cc) + list(email_config.bcc)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user