Coverage for src/lexigram/notification/mailer/mailable.py: 100%
10 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-26 02:32 +0800
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-26 02:32 +0800
1"""Mailable — base class for structured email templates."""
3from __future__ import annotations
5import abc
6import html
8from lexigram.contracts.mailer import EmailMessage
11class Mailable(abc.ABC):
12 """Base class for structured email templates.
14 Subclass ``Mailable`` to encapsulate the data and rendering logic for a
15 specific email type. Call :meth:`to_message` to build the
16 :class:`~lexigram.contracts.mailer.types.EmailMessage` that is
17 passed to :meth:`~lexigram.contracts.mailer.protocols.MailerProtocol.send`.
19 Example::
21 class WelcomeMail(Mailable):
22 def __init__(self, user_name: str, user_email: str) -> None:
23 self.user_name = user_name
24 self.user_email = user_email
26 def to_message(self) -> EmailMessage:
27 return EmailMessage(
28 to=[self.user_email],
29 subject=f"Welcome, {self.user_name}!",
30 body=f"Hi {self.user_name}, welcome aboard.",
31 html_body=f"<p>Hi <b>{escape_html(self.user_name)}</b>, welcome aboard.</p>",
32 )
33 """
35 @abc.abstractmethod
36 def to_message(self) -> EmailMessage:
37 """Build the EmailMessage for this mailable.
39 Returns:
40 A fully populated :class:`~lexigram.contracts.mailer.types.EmailMessage`.
41 """
44def escape_html(text: str) -> str:
45 """Escape text for inclusion in an HTML email body.
47 HTML email bodies are rendered by mail clients; user-controlled text
48 interpolated unescaped (as in the pre-fix docstring example) becomes
49 markup. Escape before building ``html_body``.
51 Args:
52 text: Untrusted text, e.g. a user-entered name.
54 Returns:
55 Text with ``&``, ``<``, ``>``, and ``"`` escaped so it renders as
56 literal text in an HTML email.
57 """
58 return html.escape(text, quote=True)
61__all__ = ["Mailable", "escape_html"]