smtp.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. import asyncio
  2. import logging
  3. import os
  4. import smtplib
  5. import ssl
  6. from email.mime.multipart import MIMEMultipart
  7. from email.mime.text import MIMEText
  8. from typing import Optional
  9. from core.base import EmailConfig, EmailProvider
  10. logger = logging.getLogger(__name__)
  11. class AsyncSMTPEmailProvider(EmailProvider):
  12. """Email provider implementation using Brevo SMTP relay"""
  13. def __init__(self, config: EmailConfig):
  14. super().__init__(config)
  15. self.smtp_server = config.smtp_server or os.getenv("R2R_SMTP_SERVER")
  16. if not self.smtp_server:
  17. raise ValueError("SMTP server is required")
  18. self.smtp_port = config.smtp_port or os.getenv("R2R_SMTP_PORT")
  19. if not self.smtp_port:
  20. raise ValueError("SMTP port is required")
  21. self.smtp_username = config.smtp_username or os.getenv(
  22. "R2R_SMTP_USERNAME"
  23. )
  24. if not self.smtp_username:
  25. raise ValueError("SMTP username is required")
  26. self.smtp_password = config.smtp_password or os.getenv(
  27. "R2R_SMTP_PASSWORD"
  28. )
  29. if not self.smtp_password:
  30. raise ValueError("SMTP password is required")
  31. self.from_email: Optional[str] = (
  32. config.from_email
  33. or os.getenv("R2R_FROM_EMAIL")
  34. or self.smtp_username
  35. )
  36. self.ssl_context = ssl.create_default_context()
  37. async def _send_email_sync(self, msg: MIMEMultipart) -> None:
  38. """Synchronous email sending wrapped in asyncio executor"""
  39. loop = asyncio.get_running_loop()
  40. def _send():
  41. with smtplib.SMTP_SSL(
  42. self.smtp_server,
  43. self.smtp_port,
  44. context=self.ssl_context,
  45. timeout=30,
  46. ) as server:
  47. logger.info("Connected to SMTP server")
  48. server.login(self.smtp_username, self.smtp_password)
  49. logger.info("Login successful")
  50. server.send_message(msg)
  51. logger.info("Message sent successfully!")
  52. try:
  53. await loop.run_in_executor(None, _send)
  54. except Exception as e:
  55. error_msg = f"Failed to send email: {str(e)}"
  56. logger.error(error_msg)
  57. raise RuntimeError(error_msg) from e
  58. async def send_email(
  59. self,
  60. to_email: str,
  61. subject: str,
  62. body: str,
  63. html_body: Optional[str] = None,
  64. *args,
  65. **kwargs,
  66. ) -> None:
  67. msg = MIMEMultipart("alternative")
  68. msg["Subject"] = subject
  69. msg["From"] = self.from_email # type: ignore
  70. msg["To"] = to_email
  71. msg.attach(MIMEText(body, "plain"))
  72. if html_body:
  73. msg.attach(MIMEText(html_body, "html"))
  74. try:
  75. logger.info("Initializing SMTP connection...")
  76. async with asyncio.timeout(30): # Overall timeout
  77. await self._send_email_sync(msg)
  78. except asyncio.TimeoutError:
  79. error_msg = "Operation timed out while trying to send email"
  80. logger.error(error_msg)
  81. raise RuntimeError(error_msg)
  82. except Exception as e:
  83. error_msg = f"Failed to send email: {str(e)}"
  84. logger.error(error_msg)
  85. raise RuntimeError(error_msg) from e
  86. async def send_verification_email(
  87. self, to_email: str, verification_code: str, *args, **kwargs
  88. ) -> None:
  89. body = f"""
  90. Please verify your email address by entering the following code:
  91. Verification code: {verification_code}
  92. If you did not request this verification, please ignore this email.
  93. """
  94. html_body = f"""
  95. <p>Please verify your email address by entering the following code:</p>
  96. <p style="font-size: 24px; font-weight: bold; margin: 20px 0;">
  97. Verification code: {verification_code}
  98. </p>
  99. <p>If you did not request this verification, please ignore this email.</p>
  100. """
  101. await self.send_email(
  102. to_email=to_email,
  103. subject="Please verify your email address",
  104. body=body,
  105. html_body=html_body,
  106. )
  107. async def send_password_reset_email(
  108. self, to_email: str, reset_token: str, *args, **kwargs
  109. ) -> None:
  110. body = f"""
  111. You have requested to reset your password.
  112. Reset token: {reset_token}
  113. If you did not request a password reset, please ignore this email.
  114. """
  115. html_body = f"""
  116. <p>You have requested to reset your password.</p>
  117. <p style="font-size: 24px; font-weight: bold; margin: 20px 0;">
  118. Reset token: {reset_token}
  119. </p>
  120. <p>If you did not request a password reset, please ignore this email.</p>
  121. """
  122. await self.send_email(
  123. to_email=to_email,
  124. subject="Password Reset Request",
  125. body=body,
  126. html_body=html_body,
  127. )