add smtp module and testing
This commit is contained in:
@@ -27,6 +27,14 @@ CONFIG_FIELDS: tuple[ConfigField, ...] = (
|
||||
ConfigField("System", "APP_ENV", "app_env", "App Env"),
|
||||
ConfigField("System", "APP_DEBUG", "app_debug", "App Debug"),
|
||||
ConfigField("System", "APP_HOSTNAME", "app_hostname", "App Hostname"),
|
||||
ConfigField("SMTP", "SMTP_ENABLED", "smtp_enabled", "SMTP Enabled"),
|
||||
ConfigField("SMTP", "SMTP_HOST", "smtp_host", "SMTP Host"),
|
||||
ConfigField("SMTP", "SMTP_PORT", "smtp_port", "SMTP Port"),
|
||||
ConfigField("SMTP", "SMTP_USERNAME", "smtp_username", "SMTP Username"),
|
||||
ConfigField("SMTP", "SMTP_PASSWORD", "smtp_password", "SMTP Password", secret=True),
|
||||
ConfigField("SMTP", "SMTP_FROM_ADDRESS", "smtp_from_address", "SMTP From Address"),
|
||||
ConfigField("SMTP", "SMTP_TO_ADDRESS", "smtp_to_address", "SMTP To Address"),
|
||||
ConfigField("SMTP", "SMTP_USE_STARTTLS", "smtp_use_starttls", "SMTP Use STARTTLS"),
|
||||
ConfigField(
|
||||
"Authentication",
|
||||
"AUTH_SESSION_COOKIE_NAME",
|
||||
@@ -260,6 +268,14 @@ def _settings_payload(settings: Settings) -> dict[str, Any]:
|
||||
"home_assistant_auth_token": settings.home_assistant_auth_token,
|
||||
"home_assistant_timeout_seconds": settings.home_assistant_timeout_seconds,
|
||||
"home_assistant_action_task_project_id": settings.home_assistant_action_task_project_id,
|
||||
"smtp_enabled": settings.smtp_enabled,
|
||||
"smtp_host": settings.smtp_host,
|
||||
"smtp_port": settings.smtp_port,
|
||||
"smtp_username": settings.smtp_username,
|
||||
"smtp_password": settings.smtp_password,
|
||||
"smtp_from_address": settings.smtp_from_address,
|
||||
"smtp_to_address": settings.smtp_to_address,
|
||||
"smtp_use_starttls": settings.smtp_use_starttls,
|
||||
"poo_webhook_id": settings.poo_webhook_id,
|
||||
"poo_sensor_entity_name": settings.poo_sensor_entity_name,
|
||||
"poo_sensor_friendly_name": settings.poo_sensor_friendly_name,
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
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
|
||||
Reference in New Issue
Block a user