Coverage for src/lexigram/admin/services/notifications/sender.py: 100%

16 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-21 14:56 +0800

1"""Email sending utilities for admin notifications.""" 

2 

3from __future__ import annotations 

4 

5from typing import TYPE_CHECKING 

6 

7from lexigram.contracts.mailer import EmailMessage 

8 

9if TYPE_CHECKING: 

10 from lexigram.admin.services.notifications.models import NotificationRecipient 

11 from lexigram.contracts.mailer.protocols import MailerProtocol 

12 

13 

14class EmailSender: 

15 """Handles sending of notification emails via a MailerProtocol backend.""" 

16 

17 def __init__( 

18 self, 

19 mailer: MailerProtocol | None = None, 

20 from_email: str = "admin@example.com", 

21 from_name: str = "Admin System", 

22 ): 

23 """Initialize email sender. 

24 

25 Args: 

26 mailer: MailerProtocol backend for email delivery; raises 

27 RuntimeError in send_email() when None. 

28 from_email: Default from email address. 

29 from_name: Default from name. 

30 """ 

31 self.mailer = mailer 

32 self.from_email = from_email 

33 self.from_name = from_name 

34 

35 async def send_email( 

36 self, 

37 recipient: NotificationRecipient, 

38 subject: str, 

39 body: str, 

40 html_body: str | None = None, 

41 ) -> None: 

42 """Send email to recipient. 

43 

44 Args: 

45 recipient: Email recipient. 

46 subject: Email subject. 

47 body: Plain text body. 

48 html_body: Optional HTML body. 

49 

50 Raises: 

51 RuntimeError: If email sending fails. 

52 """ 

53 if self.mailer: 

54 message = EmailMessage( 

55 to=[recipient.email], 

56 subject=subject, 

57 body=body, 

58 html_body=html_body, 

59 from_email=self.from_email, 

60 from_name=self.from_name, 

61 ) 

62 result = await self.mailer.send(message) 

63 if result.is_err(): 

64 raise RuntimeError(str(result.unwrap_err())) 

65 else: 

66 raise RuntimeError( 

67 "No mailer backend is configured. Register a MailerProtocol " 

68 "(e.g. lexigram-notification MailerModule with driver " 

69 "'smtp'/'sendgrid', or 'console' for development logging) so " 

70 f"emails like '{subject}' can be delivered to {recipient.email}." 

71 ) 

72 

73 

74__all__ = ["EmailSender"]