"""EmailProvider interface: SMTP for production, console for dev/tests.""" from __future__ import annotations import logging from dataclasses import dataclass, field from email.message import EmailMessage as MimeMessage from typing import Protocol import aiosmtplib from gogo.config import get_settings log = logging.getLogger("gogo.email") @dataclass class EmailMessage: to: list[str] subject: str text: str html: str | None = None # attachments: (filename, mimetype, bytes) attachments: list[tuple[str, str, bytes]] = field(default_factory=list) class EmailProvider(Protocol): async def send(self, msg: EmailMessage) -> None: ... class ConsoleEmailProvider: """Logs emails instead of sending; keeps them in memory for tests.""" sent: list[EmailMessage] def __init__(self) -> None: self.sent = [] async def send(self, msg: EmailMessage) -> None: self.sent.append(msg) log.info( "EMAIL to=%s subject=%r attachments=%d\n%s", msg.to, msg.subject, len(msg.attachments), msg.text, ) class SmtpEmailProvider: async def send(self, msg: EmailMessage) -> None: s = get_settings() mime = MimeMessage() mime["From"] = s.email_from mime["To"] = ", ".join(msg.to) mime["Subject"] = msg.subject mime.set_content(msg.text) if msg.html: mime.add_alternative(msg.html, subtype="html") for filename, mimetype, data in msg.attachments: maintype, subtype = mimetype.split("/", 1) mime.add_attachment(data, maintype=maintype, subtype=subtype, filename=filename) await aiosmtplib.send( mime, hostname=s.smtp_host, port=s.smtp_port, username=s.smtp_user or None, password=s.smtp_password or None, start_tls=s.smtp_starttls, ) _provider: EmailProvider | None = None def get_email_provider() -> EmailProvider: global _provider if _provider is None: _provider = ( SmtpEmailProvider() if get_settings().email_provider == "smtp" else ConsoleEmailProvider() ) return _provider def set_email_provider(p: EmailProvider | None) -> None: """Test hook.""" global _provider _provider = p async def send_email(msg: EmailMessage) -> None: await get_email_provider().send(msg)