1818
1919@define
2020class SMTPEmailService (metaclass = SingletonMetaNoArgs ):
21+ """
22+ SMTPEmailService provides a reusable interface to send emails via an SMTP server.
23+
24+ This service supports plaintext and HTML emails, and also allows
25+ sending template-based emails using the Jinja2 template engine.
26+
27+ It is implemented as a singleton to ensure that only one SMTP connection is maintained
28+ throughout the application lifecycle, optimizing resource usage.
29+
30+ Attributes:
31+ server_host (str): SMTP server hostname or IP address.
32+ server_port (int): Port number for the SMTP connection.
33+ username (str): SMTP username for authentication.
34+ password (str): SMTP password for authentication.
35+ templates (Jinja2Templates): Jinja2Templates instance for loading and rendering email templates.
36+ server (smtplib.SMTP): An SMTP object for sending emails, initialized after object creation.
37+ """
38+
2139 # SMTP configuration
2240 server_host : str = field (default = global_settings .smtp .server )
2341 server_port : int = field (default = global_settings .smtp .port )
@@ -26,15 +44,21 @@ class SMTPEmailService(metaclass=SingletonMetaNoArgs):
2644
2745 # Dependencies
2846 templates : Jinja2Templates = field (
29- factory = lambda : Jinja2Templates (global_settings .templates_dir )
47+ factory = lambda : Jinja2Templates (global_settings .smtp . template_path )
3048 )
3149 server : smtplib .SMTP = field (init = False ) # Deferred initialization in post-init
3250
3351 def __attrs_post_init__ (self ):
34- """Initialize the SMTP server connection after object creation."""
52+ """
53+ Initializes the SMTP server connection after the object is created.
54+
55+ This method sets up a secure connection to the SMTP server, including STARTTLS encryption
56+ and logs in using the provided credentials.
57+ """
3558 self .server = smtplib .SMTP (self .server_host , self .server_port )
36- self .server .starttls ()
59+ self .server .starttls () # Upgrade the connection to secure TLS
3760 self .server .login (self .username , self .password )
61+ logger .info ("SMTPEmailService initialized successfully and connected to SMTP server." )
3862
3963 def _prepare_email (
4064 self ,
@@ -44,14 +68,28 @@ def _prepare_email(
4468 body_text : str ,
4569 body_html : str ,
4670 ) -> MIMEMultipart :
47- """Prepare the email message."""
71+ """
72+ Prepares a MIME email message with the given plaintext and HTML content.
73+
74+ Args:
75+ sender (EmailStr): The email address of the sender.
76+ recipients (list[EmailStr]): A list of recipient email addresses.
77+ subject (str): The subject line of the email.
78+ body_text (str): The plaintext content of the email.
79+ body_html (str): The HTML content of the email (optional).
80+
81+ Returns:
82+ MIMEMultipart: A MIME email object ready to be sent.
83+ """
4884 msg = MIMEMultipart ()
4985 msg ["From" ] = sender
5086 msg ["To" ] = "," .join (recipients )
5187 msg ["Subject" ] = subject
88+ # Add plain text and HTML content (if provided)
5289 msg .attach (MIMEText (body_text , "plain" ))
5390 if body_html :
5491 msg .attach (MIMEText (body_html , "html" ))
92+ logger .debug (f"Prepared email from { sender } to { recipients } ." )
5593 return msg
5694
5795 def send_email (
@@ -62,9 +100,29 @@ def send_email(
62100 body_text : str = "" ,
63101 body_html : str = None ,
64102 ):
65- """Send a regular email (plain text or HTML)."""
66- msg = self ._prepare_email (sender , recipients , subject , body_text , body_html )
67- self .server .sendmail (sender , recipients , msg .as_string ())
103+ """
104+ Sends an email to the specified recipients.
105+
106+ Supports plaintext and HTML email content. This method constructs
107+ the email message using `_prepare_email` and sends it using the SMTP server.
108+
109+ Args:
110+ sender (EmailStr): The email address of the sender.
111+ recipients (list[EmailStr]): A list of recipient email addresses.
112+ subject (str): The subject line of the email.
113+ body_text (str): The plaintext content of the email.
114+ body_html (str): The HTML content of the email (optional).
115+
116+ Raises:
117+ smtplib.SMTPException: If the email cannot be sent.
118+ """
119+ try :
120+ msg = self ._prepare_email (sender , recipients , subject , body_text , body_html )
121+ self .server .sendmail (sender , recipients , msg .as_string ())
122+ logger .info (f"Email sent successfully to { recipients } from { sender } ." )
123+ except smtplib .SMTPException as e :
124+ logger .error ("Failed to send email" , exc_info = e )
125+ raise
68126
69127 def send_template_email (
70128 self ,
@@ -74,7 +132,28 @@ def send_template_email(
74132 context : dict ,
75133 sender : EmailStr ,
76134 ):
77- """Send an email using a template with the provided context."""
78- template_str = self .templates .get_template (template )
79- body_html = template_str .render (context )
80- self .send_email (sender , recipients , subject , body_html = body_html )
135+ """
136+ Sends an email using a Jinja2 template.
137+
138+ This method renders the template with the provided context and sends it
139+ to the specified recipients.
140+
141+ Args:
142+ recipients (list[EmailStr]): A list of recipient email addresses.
143+ subject (str): The subject line of the email.
144+ template (str): The name of the template file in the templates directory.
145+ context (dict): A dictionary of values to render the template with.
146+ sender (EmailStr): The email address of the sender.
147+
148+ Raises:
149+ jinja2.TemplateNotFound: If the specified template is not found.
150+ smtplib.SMTPException: If the email cannot be sent.
151+ """
152+ try :
153+ template_str = self .templates .get_template (template )
154+ body_html = template_str .render (context ) # Render the HTML using context variables
155+ self .send_email (sender , recipients , subject , body_html = body_html )
156+ logger .info (f"Template email sent successfully to { recipients } using template { template } ." )
157+ except Exception as e :
158+ logger .error ("Failed to send template email" , exc_info = e )
159+ raise
0 commit comments