import logging from django.conf import settings from django.core.mail import EmailMessage, get_connection from infrasynth.shared.enums import ChannelType from infrasynth.shared.results import Result from infrasynth.shared.settings_utils import get_setting from .base import BaseChannel logger = logging.getLogger(__name__) class SMTPChannel(BaseChannel): """Email channel that sends via a configurable SMTP backend. Configuration keys (read from ``ChannelConfig.config``): - ``host``, ``port``, ``username``, ``password`` - ``use_tls``, ``use_ssl``, ``timeout`` - ``backend`` (dotted path, defaults to Django's SMTP backend) - ``from_email`` (falls back to DEFAULT_FROM_EMAIL / INFRASYNTH_NOTIFICATIONS) """ channel_type = ChannelType.EMAIL def __init__( self, host=None, port=None, username=None, password=None, use_tls=True, use_ssl=False, timeout=30, backend=None, from_email=None, ): self.host = host self.port = port or 587 self.username = username self.password = password self.use_tls = use_tls self.use_ssl = use_ssl self.timeout = timeout self.backend = backend or "django.core.mail.backends.smtp.EmailBackend" self.from_email = ( from_email or getattr(settings, "DEFAULT_FROM_EMAIL", None) or get_setting("INFRASYNTH_NOTIFICATIONS", "DEFAULT_FROM_EMAIL", "noreply@example.com") ) @classmethod def from_config(cls, config: dict) -> "SMTPChannel": config = {str(key).lower(): value for key, value in (config or {}).items()} return cls(**config) def send( self, recipient: str, subject: str, body: str, is_html: bool = True, attachments=None, ) -> Result[bool, str]: if not self.host: return Result.err("SMTP host not configured") try: message = EmailMessage( subject=subject, body=body, from_email=self.from_email, to=[recipient], connection=self._get_connection(), ) if is_html: message.content_subtype = "html" for attachment in attachments or []: message.attach(attachment.filename, attachment.content, attachment.mime_type) message.send() except Exception as exc: # noqa: BLE001 logger.exception("SMTP send failed for %s", recipient) return Result.err(str(exc)) return Result.ok(True) def health_check(self) -> bool: return bool(self.host) def _get_connection(self): return get_connection( backend=self.backend, host=self.host or None, port=self.port, username=self.username or "", password=self.password or "", use_tls=self.use_tls, use_ssl=self.use_ssl, timeout=self.timeout, )