1
+ from attrs import define , field
1
2
import smtplib
2
3
from email .mime .multipart import MIMEMultipart
3
4
from email .mime .text import MIMEText
15
16
logger = AppLogger ().get_logger ()
16
17
17
18
19
+ @define
18
20
class SMTPEmailService (metaclass = SingletonMetaNoArgs ):
19
- def __init__ (self ):
20
- self .server = smtplib .SMTP (
21
- global_settings .smtp .server , global_settings .smtp .port
22
- )
23
- self .server .starttls ()
24
- self .server .login (global_settings .smtp .username , global_settings .smtp .password )
25
- self .templates = Jinja2Templates ("templates" )
21
+ """
22
+ SMTPEmailService provides a reusable interface to send emails via an SMTP server.
26
23
27
- def send_email (
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
+
39
+ # SMTP configuration
40
+ server_host : str = field (default = global_settings .smtp .server )
41
+ server_port : int = field (default = global_settings .smtp .port )
42
+ username : str = field (default = global_settings .smtp .username )
43
+ password : str = field (default = global_settings .smtp .password )
44
+
45
+ # Dependencies
46
+ templates : Jinja2Templates = field (
47
+ factory = lambda : Jinja2Templates (global_settings .smtp .template_path )
48
+ )
49
+ server : smtplib .SMTP = field (init = False ) # Deferred initialization in post-init
50
+
51
+ def __attrs_post_init__ (self ):
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
+ """
58
+ self .server = smtplib .SMTP (self .server_host , self .server_port )
59
+ self .server .starttls () # Upgrade the connection to secure TLS
60
+ self .server .login (self .username , self .password )
61
+ logger .info ("SMTPEmailService initialized successfully and connected to SMTP server." )
62
+
63
+ def _prepare_email (
28
64
self ,
29
65
sender : EmailStr ,
30
66
recipients : list [EmailStr ],
31
67
subject : str ,
32
- body_text : str = "" ,
33
- body_html = None ,
34
- ):
68
+ body_text : str ,
69
+ body_html : str ,
70
+ ) -> MIMEMultipart :
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
+ """
35
84
msg = MIMEMultipart ()
36
85
msg ["From" ] = sender
37
86
msg ["To" ] = "," .join (recipients )
38
87
msg ["Subject" ] = subject
88
+ # Add plain text and HTML content (if provided)
39
89
msg .attach (MIMEText (body_text , "plain" ))
40
90
if body_html :
41
91
msg .attach (MIMEText (body_html , "html" ))
42
- self .server .sendmail (sender , recipients , msg .as_string ())
92
+ logger .debug (f"Prepared email from { sender } to { recipients } ." )
93
+ return msg
94
+
95
+ def send_email (
96
+ self ,
97
+ sender : EmailStr ,
98
+ recipients : list [EmailStr ],
99
+ subject : str ,
100
+ body_text : str = "" ,
101
+ body_html : str = None ,
102
+ ):
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
43
126
44
127
def send_template_email (
45
128
self ,
46
129
recipients : list [EmailStr ],
47
130
subject : str ,
48
- template : str = None ,
49
- context : dict = None ,
50
- sender : EmailStr = global_settings . smtp . from_email ,
131
+ template : str ,
132
+ context : dict ,
133
+ sender : EmailStr ,
51
134
):
52
- template_str = self .templates .get_template (template )
53
- body_html = template_str .render (context )
54
- 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