from __future__ import annotations from dataclasses import dataclass from email.message import EmailMessage import smtplib from app.config import Settings class EmailConfigurationError(ValueError): """Raised when SMTP settings are incomplete or disabled.""" class EmailDeliveryError(RuntimeError): """Raised when sending email fails.""" @dataclass(frozen=True, slots=True) class SMTPConfig: host: str port: int username: str password: str from_address: str to_address: str use_starttls: bool def get_smtp_config(settings: Settings, *, require_enabled: bool = True) -> SMTPConfig: if require_enabled and not settings.smtp_enabled: raise EmailConfigurationError("SMTP is disabled") if not settings.smtp_host: raise EmailConfigurationError("SMTP host is required") if settings.smtp_port <= 0: raise EmailConfigurationError("SMTP port must be greater than zero") if not settings.smtp_from_address: raise EmailConfigurationError("SMTP from address is required") if not settings.smtp_to_address: raise EmailConfigurationError("SMTP to address is required") return SMTPConfig( host=settings.smtp_host, port=settings.smtp_port, username=settings.smtp_username, password=settings.smtp_password, from_address=settings.smtp_from_address, to_address=settings.smtp_to_address, use_starttls=settings.smtp_use_starttls, ) def is_smtp_ready(settings: Settings) -> bool: try: get_smtp_config(settings, require_enabled=False) except EmailConfigurationError: return False return True def send_plaintext_email( settings: Settings, *, subject: str, body: str, recipient: str | None = None, require_enabled: bool = True, ) -> None: smtp_config = get_smtp_config(settings, require_enabled=require_enabled) message = EmailMessage() message["Subject"] = subject message["From"] = smtp_config.from_address message["To"] = recipient or smtp_config.to_address message.set_content(body) try: with smtplib.SMTP(smtp_config.host, smtp_config.port, timeout=10) as smtp: smtp.ehlo() if smtp_config.use_starttls: smtp.starttls() smtp.ehlo() if smtp_config.username: smtp.login(smtp_config.username, smtp_config.password) smtp.send_message(message) except (OSError, smtplib.SMTPException) as exc: error_message = _sanitize_error_message(str(exc), smtp_config.password) raise EmailDeliveryError(error_message or "SMTP delivery failed") from exc def send_smtp_test_email(settings: Settings) -> None: send_plaintext_email( settings, subject="Home Automation SMTP Test", body="This is a test email from Home Automation SMTP settings.", require_enabled=False, ) def _sanitize_error_message(message: str, password: str) -> str: sanitized = message if password: sanitized = sanitized.replace(password, "[redacted]") return sanitized