106 lines
3.1 KiB
Python
106 lines
3.1 KiB
Python
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 |