1+ from attrs import define , field
12import smtplib
23from email .mime .multipart import MIMEMultipart
34from email .mime .text import MIMEText
1516logger = AppLogger ().get_logger ()
1617
1718
19+ @define
1820class 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.
2623
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 (
2864 self ,
2965 sender : EmailStr ,
3066 recipients : list [EmailStr ],
3167 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+ """
3584 msg = MIMEMultipart ()
3685 msg ["From" ] = sender
3786 msg ["To" ] = "," .join (recipients )
3887 msg ["Subject" ] = subject
88+ # Add plain text and HTML content (if provided)
3989 msg .attach (MIMEText (body_text , "plain" ))
4090 if body_html :
4191 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
43126
44127 def send_template_email (
45128 self ,
46129 recipients : list [EmailStr ],
47130 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 ,
51134 ):
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