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

1"""Console mailer backend for development. 

2 

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"``. 

6 

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""" 

11 

12from __future__ import annotations 

13 

14from typing import TYPE_CHECKING 

15import uuid 

16 

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 

21 

22if TYPE_CHECKING: 

23 from lexigram.contracts.mailer.errors import MailerError 

24 

25logger = get_logger(__name__) 

26 

27 

28class ConsoleMailer: 

29 """MailerProtocol backend that prints emails to the application log. 

30 

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. 

35 

36 Args: 

37 log_level: Structlog level name to emit at (``"info"``). Useful to 

38 raise to ``"warning"`` in noisy logs. 

39 """ 

40 

41 def __init__(self, log_level: str = "info") -> None: 

42 """Initialise the console mailer. 

43 

44 Args: 

45 log_level: Log level to emit messages at (default ``"info"``). 

46 """ 

47 self.log_level = log_level 

48 

49 async def send( 

50 self, message: EmailMessage 

51 ) -> Result[MessageDeliveryReceipt, MailerError]: 

52 """Log the email to the console and return a fake receipt. 

53 

54 Args: 

55 message: The email to log. 

56 

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 ) 

77 

78 async def health_check(self, timeout: float = 5.0) -> HealthCheckResult: 

79 """Report the console backend as always healthy. 

80 

81 Args: 

82 timeout: Ignored — no network I/O is performed. 

83 

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 ) 

92 

93 

94__all__ = ["ConsoleMailer"]