email_service.py 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. import email
  2. from config.settings import MAIL, MAIL_PASSWORD
  3. import smtplib
  4. from email.message import EmailMessage
  5. from logging import getLogger
  6. from typing import Optional
  7. from services.logging_service import structured_logger, LogLevel
  8. logger = getLogger(__name__)
  9. class EmailSender:
  10. def __init__(self):
  11. self.email = MAIL
  12. self.password = MAIL_PASSWORD
  13. self._smtp: Optional[smtplib.SMTP_SSL] = None
  14. self.connect()
  15. def connect(self):
  16. if self._smtp is None:
  17. logger.info("Establishing new SMTP connection...")
  18. structured_logger.log_email_event(
  19. "Establishing SMTP connection",
  20. LogLevel.INFO,
  21. {"email_server": "smtp.gmail.com", "port": 465, "sender_email": self.email}
  22. )
  23. try:
  24. self._smtp = smtplib.SMTP_SSL('smtp.gmail.com', 465)
  25. self._smtp.login(self.email, self.password)
  26. self._smtp.ehlo()
  27. logger.info("SMTP connection established and authenticated.")
  28. structured_logger.log_email_event(
  29. "SMTP authentication successful",
  30. LogLevel.INFO,
  31. {"sender_email": self.email}
  32. )
  33. except smtplib.SMTPAuthenticationError as e:
  34. error_msg = f"SMTP authentication failed: {e}"
  35. logger.error(error_msg)
  36. structured_logger.log_email_event(
  37. "SMTP authentication failed",
  38. LogLevel.ERROR,
  39. {
  40. "sender_email": self.email,
  41. "error": str(e),
  42. "error_type": "SMTPAuthenticationError"
  43. }
  44. )
  45. raise
  46. except Exception as e:
  47. error_msg = f"Failed to establish SMTP connection: {e}"
  48. logger.error(error_msg)
  49. structured_logger.log_email_event(
  50. "SMTP connection failed",
  51. LogLevel.ERROR,
  52. {
  53. "sender_email": self.email,
  54. "error": str(e),
  55. "error_type": type(e).__name__
  56. }
  57. )
  58. raise
  59. def close(self):
  60. if self._smtp:
  61. logger.info("Closing SMTP connection.")
  62. structured_logger.log_email_event(
  63. "Closing SMTP connection",
  64. LogLevel.INFO,
  65. {"sender_email": self.email}
  66. )
  67. try:
  68. self._smtp.quit()
  69. self._smtp = None
  70. except Exception as e:
  71. logger.warning(f"Error closing SMTP connection: {e}")
  72. self._smtp = None # Force reset even if quit fails
  73. def send_email(self, subject: str, body: str, to: list[str], **kwargs):
  74. if self._smtp is None:
  75. self.connect()
  76. logger.debug(f"Preparing to send email to: {to} with subject: '{subject}'")
  77. structured_logger.log_email_event(
  78. f"Preparing to send email with subject: '{subject}'",
  79. LogLevel.INFO,
  80. {
  81. "subject": subject,
  82. "recipients": to,
  83. "sender_email": self.email,
  84. "body_length": len(body),
  85. "kwargs_count": len(kwargs)
  86. }
  87. )
  88. try:
  89. msg = EmailMessage()
  90. msg['Subject'] = subject
  91. msg['From'] = self.email
  92. msg['To'] = ", ".join(to)
  93. msg.set_content('Este correo tiene contenido HTML.')
  94. msg.add_alternative(body.format(**kwargs), subtype='html')
  95. if not self._smtp:
  96. error_msg = "Cannot send email because SMTP connection is not established"
  97. logger.error(error_msg)
  98. structured_logger.log_email_event(
  99. "Email send failed: SMTP connection not established",
  100. LogLevel.ERROR,
  101. {
  102. "subject": subject,
  103. "recipients": to,
  104. "sender_email": self.email
  105. }
  106. )
  107. raise ConnectionError(error_msg)
  108. self._smtp.send_message(msg)
  109. logger.info(f"Email sent to {to} with subject '{subject}'.")
  110. structured_logger.log_email_event(
  111. f"Email sent successfully",
  112. LogLevel.INFO,
  113. {
  114. "subject": subject,
  115. "recipients": to,
  116. "sender_email": self.email,
  117. "recipients_count": len(to)
  118. }
  119. )
  120. except smtplib.SMTPServerDisconnected as e:
  121. logger.warning("SMTP connection disconnected, retrying...")
  122. structured_logger.log_email_event(
  123. "SMTP disconnected during send, attempting retry",
  124. LogLevel.WARNING,
  125. {
  126. "subject": subject,
  127. "recipients": to,
  128. "error": str(e)
  129. }
  130. )
  131. try:
  132. self.close()
  133. self.connect()
  134. self._smtp.send_message(msg)
  135. logger.info(f"Email resent successfully to {to}.")
  136. structured_logger.log_email_event(
  137. "Email sent successfully after retry",
  138. LogLevel.INFO,
  139. {
  140. "subject": subject,
  141. "recipients": to,
  142. "sender_email": self.email,
  143. "retry_attempt": True
  144. }
  145. )
  146. except Exception as retry_error:
  147. error_msg = f"Failed to resend email after retry: {retry_error}"
  148. logger.error(error_msg)
  149. structured_logger.log_email_event(
  150. "Email retry failed",
  151. LogLevel.ERROR,
  152. {
  153. "subject": subject,
  154. "recipients": to,
  155. "original_error": str(e),
  156. "retry_error": str(retry_error),
  157. "error_type": type(retry_error).__name__
  158. }
  159. )
  160. self.close()
  161. raise
  162. except Exception as e:
  163. error_msg = f"Failed to send email to {to}: {e}"
  164. logger.error(error_msg)
  165. structured_logger.log_email_event(
  166. "Email send failed",
  167. LogLevel.ERROR,
  168. {
  169. "subject": subject,
  170. "recipients": to,
  171. "sender_email": self.email,
  172. "error": str(e),
  173. "error_type": type(e).__name__
  174. }
  175. )
  176. self.close()
  177. raise
  178. email_sender: Optional[EmailSender] = None
  179. def initialize_email_sender():
  180. global email_sender
  181. email_sender = EmailSender()
  182. def get_email_sender() -> EmailSender:
  183. if email_sender is None:
  184. initialize_email_sender()
  185. if not email_sender:
  186. raise ValueError("Email sender is not initialized and failed to initialize.")
  187. return email_sender