Coverage for src/lexigram/notification/mailer/console_mailer.py: 0%
18 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-26 07:17 +0800
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-26 07:17 +0800
1"""Console mailer backend for development.
3Logs every outgoing email to the backend console instead of delivering it
4over the network. Used automatically when no real backend is configured
5(``MailerConfig.console_fallback``) or explicitly via ``driver: "console"``.
7The verification/password-reset flows embed clickable links in the email
8body, so in development the printed body is enough to complete the flow by
9copying the link from the backend log.
10"""
12from __future__ import annotations
14from typing import TYPE_CHECKING
15import uuid
17from lexigram.contracts.core import HealthCheckResult, HealthStatus
18from lexigram.contracts.mailer import EmailMessage, MessageDeliveryReceipt
19from lexigram.logging import get_logger
20from lexigram.result import Ok, Result
22if TYPE_CHECKING:
23 from lexigram.contracts.mailer.errors import MailerError
25logger = get_logger(__name__)
28class ConsoleMailer:
29 """MailerProtocol backend that prints emails to the application log.
31 Implements :class:`~lexigram.contracts.mailer.protocols.MailerProtocol`.
32 Never touches the network: the full message (subject, recipients, and
33 body including any verification links) is emitted as a single structured
34 log line so developers can see and use the content in local runs.
36 Args:
37 log_level: Structlog level name to emit at (``"info"``). Useful to
38 raise to ``"warning"`` in noisy logs.
39 """
41 def __init__(self, log_level: str = "info") -> None:
42 """Initialise the console mailer.
44 Args:
45 log_level: Log level to emit messages at (default ``"info"``).
46 """
47 self.log_level = log_level
49 async def send(
50 self, message: EmailMessage
51 ) -> Result[MessageDeliveryReceipt, MailerError]:
52 """Log the email to the console and return a fake receipt.
54 Args:
55 message: The email to log.
57 Returns:
58 ``Ok(MessageDeliveryReceipt)`` — the message is always accepted.
59 """
60 log = getattr(logger, self.log_level, logger.info)
61 log(
62 "mailer.console_email",
63 to=", ".join(message.to),
64 subject=message.subject,
65 from_email=message.from_email,
66 body=message.body or "",
67 html_body=message.html_body or "",
68 )
69 return Ok(
70 MessageDeliveryReceipt(
71 message_id=f"console-{uuid.uuid4().hex}",
72 backend="console",
73 channel="email",
74 provider_reference="console",
75 )
76 )
78 async def health_check(self, timeout: float = 5.0) -> HealthCheckResult:
79 """Report the console backend as always healthy.
81 Args:
82 timeout: Ignored — no network I/O is performed.
84 Returns:
85 Healthy :class:`~lexigram.contracts.core.HealthCheckResult`.
86 """
87 return HealthCheckResult(
88 component="mailer.console",
89 status=HealthStatus.HEALTHY,
90 message="Console mailer is available",
91 )
94__all__ = ["ConsoleMailer"]