Source code for regstack.email.base
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass
[docs]
@dataclass(frozen=True, slots=True)
class EmailMessage:
"""A rendered, multipart email ready to hand to an :class:`EmailService`.
Always carries both an ``html`` and a ``text`` body — the SMTP and
SES backends emit a ``multipart/alternative`` message. The
``from_*`` fields are pre-resolved at composition time (from
``EmailConfig``), so a backend doesn't have to know the host's
branding settings.
"""
to: str
"""Recipient email address."""
subject: str
"""The ``Subject:`` header."""
html: str
"""The HTML body."""
text: str
"""The plaintext body for clients that don't render HTML."""
from_address: str
"""The bare ``user@host`` address."""
from_name: str
"""The display name shown to the recipient."""
@property
def from_header(self) -> str:
"""The composed ``"Name <user@host>"`` ``From:`` header."""
return f"{self.from_name} <{self.from_address}>"
[docs]
class EmailService(ABC):
"""Pluggable transport for sending an :class:`EmailMessage`.
Bundled implementations:
- :class:`~regstack.email.console.ConsoleEmailService` — prints to
stdout, used in dev and tests.
- SMTP (``aiosmtplib``-backed) — for any SMTP relay.
- Amazon SES (``aioboto3``) — needs the ``ses`` extra.
To plug in a different provider (Postmark, SendGrid, MessageBird,
…) implement :meth:`send` and pass the instance to
:meth:`RegStack.set_email_backend
<regstack.app.RegStack.set_email_backend>`.
"""
[docs]
@abstractmethod
async def send(self, message: EmailMessage) -> None:
"""Deliver one email. Implementations should not retry — the
caller decides whether a transient failure is fatal.
Args:
message: The pre-rendered message to deliver.
Raises:
Exception: Implementations may raise transport-specific
errors. The caller (typically a router endpoint) is
responsible for translating them into HTTP responses.
"""
...