Files
POTE/src/pote/reporting/email_reporter.py
T
ilia 5df21d82f4
CI / skip-ci-check (pull_request) Successful in 19s
CI / secret-scan (pull_request) Successful in 19s
CI / python-ci (pull_request) Successful in 2m38s
Fix lint debt and make CI gates honest
- 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
2026-07-26 15:41:07 -04:00

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