- ruff: fix all 328 errors (autofix + manual); move config to [tool.ruff.lint] - mypy: fix all errors (annotations, nullable-column guards, stale kwargs in weekly report caller) - black: reformat src/tests so black --check passes - CI: remove `|| true` from ruff/black/mypy/pytest steps in both .gitea and .github workflows; install project deps and add mypy step in gitea python-ci so gates actually run - docs: move 14 root status/guide markdown files into docs/, update links
114 lines
3.4 KiB
Python
114 lines
3.4 KiB
Python
"""
|
|
Email Reporter for POTE
|
|
|
|
Sends formatted reports via SMTP email.
|
|
"""
|
|
|
|
import logging
|
|
import smtplib
|
|
from email.mime.multipart import MIMEMultipart
|
|
from email.mime.text import MIMEText
|
|
|
|
from pote.config import settings
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class EmailReporter:
|
|
"""Sends email reports via SMTP."""
|
|
|
|
def __init__(
|
|
self,
|
|
smtp_host: str | None = None,
|
|
smtp_port: int | None = None,
|
|
smtp_user: str | None = None,
|
|
smtp_password: str | None = None,
|
|
from_email: str | None = None,
|
|
):
|
|
"""
|
|
Initialize email reporter.
|
|
|
|
If parameters are not provided, will attempt to use settings from config.
|
|
"""
|
|
# Settings always defines these fields (with defaults), so direct access is safe.
|
|
self.smtp_host: str = smtp_host or settings.smtp_host
|
|
self.smtp_port: int = smtp_port or settings.smtp_port
|
|
self.smtp_user: str = smtp_user or settings.smtp_user
|
|
self.smtp_password: str = smtp_password or settings.smtp_password
|
|
self.from_email: str = from_email or settings.from_email or "pote@localhost"
|
|
|
|
def send_report(
|
|
self,
|
|
to_emails: list[str],
|
|
subject: str,
|
|
body_text: str,
|
|
body_html: str | None = None,
|
|
) -> bool:
|
|
"""
|
|
Send an email report.
|
|
|
|
Args:
|
|
to_emails: List of recipient email addresses
|
|
subject: Email subject line
|
|
body_text: Plain text email body
|
|
body_html: Optional HTML email body
|
|
|
|
Returns:
|
|
True if email sent successfully, False otherwise
|
|
"""
|
|
try:
|
|
msg = MIMEMultipart("alternative")
|
|
msg["Subject"] = subject
|
|
msg["From"] = self.from_email
|
|
msg["To"] = ", ".join(to_emails)
|
|
|
|
# Attach plain text part
|
|
msg.attach(MIMEText(body_text, "plain"))
|
|
|
|
# Attach HTML part if provided
|
|
if body_html:
|
|
msg.attach(MIMEText(body_html, "html"))
|
|
|
|
# Connect to SMTP server and send
|
|
with smtplib.SMTP(self.smtp_host, self.smtp_port) as server:
|
|
server.ehlo()
|
|
if self.smtp_port == 587: # TLS
|
|
server.starttls()
|
|
server.ehlo()
|
|
|
|
if self.smtp_user and self.smtp_password:
|
|
server.login(self.smtp_user, self.smtp_password)
|
|
|
|
server.send_message(msg)
|
|
|
|
logger.info(f"Email sent successfully to {', '.join(to_emails)}")
|
|
return True
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to send email: {e}")
|
|
return False
|
|
|
|
def test_connection(self) -> bool:
|
|
"""
|
|
Test SMTP connection.
|
|
|
|
Returns:
|
|
True if connection successful, False otherwise
|
|
"""
|
|
try:
|
|
with smtplib.SMTP(self.smtp_host, self.smtp_port, timeout=10) as server:
|
|
server.ehlo()
|
|
if self.smtp_port == 587:
|
|
server.starttls()
|
|
server.ehlo()
|
|
|
|
if self.smtp_user and self.smtp_password:
|
|
server.login(self.smtp_user, self.smtp_password)
|
|
|
|
logger.info("SMTP connection test successful")
|
|
return True
|
|
|
|
except Exception as e:
|
|
logger.error(f"SMTP connection test failed: {e}")
|
|
return False
|