18
18
19
19
@define
20
20
class 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
+
21
39
# SMTP configuration
22
40
server_host : str = field (default = global_settings .smtp .server )
23
41
server_port : int = field (default = global_settings .smtp .port )
@@ -26,15 +44,21 @@ class SMTPEmailService(metaclass=SingletonMetaNoArgs):
26
44
27
45
# Dependencies
28
46
templates : Jinja2Templates = field (
29
- factory = lambda : Jinja2Templates (global_settings .templates_dir )
47
+ factory = lambda : Jinja2Templates (global_settings .smtp . template_path )
30
48
)
31
49
server : smtplib .SMTP = field (init = False ) # Deferred initialization in post-init
32
50
33
51
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
+ """
35
58
self .server = smtplib .SMTP (self .server_host , self .server_port )
36
- self .server .starttls ()
59
+ self .server .starttls () # Upgrade the connection to secure TLS
37
60
self .server .login (self .username , self .password )
61
+ logger .info ("SMTPEmailService initialized successfully and connected to SMTP server." )
38
62
39
63
def _prepare_email (
40
64
self ,
@@ -44,14 +68,28 @@ def _prepare_email(
44
68
body_text : str ,
45
69
body_html : str ,
46
70
) -> 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
+ """
48
84
msg = MIMEMultipart ()
49
85
msg ["From" ] = sender
50
86
msg ["To" ] = "," .join (recipients )
51
87
msg ["Subject" ] = subject
88
+ # Add plain text and HTML content (if provided)
52
89
msg .attach (MIMEText (body_text , "plain" ))
53
90
if body_html :
54
91
msg .attach (MIMEText (body_html , "html" ))
92
+ logger .debug (f"Prepared email from { sender } to { recipients } ." )
55
93
return msg
56
94
57
95
def send_email (
@@ -62,9 +100,29 @@ def send_email(
62
100
body_text : str = "" ,
63
101
body_html : str = None ,
64
102
):
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
68
126
69
127
def send_template_email (
70
128
self ,
@@ -74,7 +132,28 @@ def send_template_email(
74
132
context : dict ,
75
133
sender : EmailStr ,
76
134
):
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