Coverage for src / lexigram / admin / services / notifications / sender.py: 50%
18 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-13 22:14 +0800
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-13 22:14 +0800
1"""Email sending utilities for admin notifications."""
3from __future__ import annotations
5from typing import TYPE_CHECKING
7from lexigram.contracts.mailer import EmailMessage
8from lexigram.logging import get_logger
10if TYPE_CHECKING:
11 from lexigram.admin.services.notifications.models import NotificationRecipient
12 from lexigram.contracts.mailer.protocols import MailerProtocol
14logger = get_logger(__name__)
17class EmailSender:
18 """Handles sending of notification emails via a MailerProtocol backend."""
20 def __init__(
21 self,
22 mailer: MailerProtocol | None = None,
23 from_email: str = "admin@example.com",
24 from_name: str = "Admin System",
25 ):
26 """Initialize email sender.
28 Args:
29 mailer: MailerProtocol backend for email delivery; no-ops when None.
30 from_email: Default from email address.
31 from_name: Default from name.
32 """
33 self.mailer = mailer
34 self.from_email = from_email
35 self.from_name = from_name
37 async def send_email(
38 self,
39 recipient: NotificationRecipient,
40 subject: str,
41 body: str,
42 html_body: str | None = None,
43 ) -> None:
44 """Send email to recipient.
46 Args:
47 recipient: Email recipient.
48 subject: Email subject.
49 body: Plain text body.
50 html_body: Optional HTML body.
52 Raises:
53 RuntimeError: If email sending fails.
54 """
55 if self.mailer:
56 message = EmailMessage(
57 to=[recipient.email],
58 subject=subject,
59 body=body,
60 html_body=html_body,
61 from_email=self.from_email,
62 from_name=self.from_name,
63 )
64 result = await self.mailer.send(message)
65 if result.is_err():
66 raise RuntimeError(str(result.unwrap_err()))
67 else:
68 logger.info(
69 "notification_email_skipped",
70 to=recipient.email,
71 subject=subject,
72 )
75__all__ = ["EmailSender"]