sendgrid.py 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. import logging
  2. import os
  3. from typing import Optional
  4. from sendgrid import SendGridAPIClient
  5. from sendgrid.helpers.mail import Content, From, Mail
  6. from core.base import EmailConfig, EmailProvider
  7. logger = logging.getLogger(__name__)
  8. class SendGridEmailProvider(EmailProvider):
  9. """Email provider implementation using SendGrid API"""
  10. def __init__(self, config: EmailConfig):
  11. super().__init__(config)
  12. self.api_key = config.sendgrid_api_key or os.getenv("SENDGRID_API_KEY")
  13. if not self.api_key or not isinstance(self.api_key, str):
  14. raise ValueError("A valid SendGrid API key is required.")
  15. self.from_email = config.from_email or os.getenv("R2R_FROM_EMAIL")
  16. if not self.from_email or not isinstance(self.from_email, str):
  17. raise ValueError("A valid from email is required.")
  18. self.frontend_url = config.frontend_url or os.getenv(
  19. "R2R_FRONTEND_URL"
  20. )
  21. if not self.frontend_url or not isinstance(self.frontend_url, str):
  22. raise ValueError("A valid frontend URL is required.")
  23. self.verify_email_template_id = config.verify_email_template_id
  24. self.reset_password_template_id = config.reset_password_template_id
  25. self.client = SendGridAPIClient(api_key=self.api_key)
  26. self.sender_name = config.sender_name
  27. async def send_email(
  28. self,
  29. to_email: str,
  30. subject: Optional[str] = None,
  31. body: Optional[str] = None,
  32. html_body: Optional[str] = None,
  33. template_id: Optional[str] = None,
  34. dynamic_template_data: Optional[dict] = None,
  35. ) -> None:
  36. try:
  37. logger.info("Preparing SendGrid message...")
  38. message = Mail(
  39. from_email=From(self.from_email, self.sender_name),
  40. to_emails=to_email,
  41. )
  42. if template_id:
  43. logger.info(f"Using dynamic template with ID: {template_id}")
  44. message.template_id = template_id
  45. message.dynamic_template_data = dynamic_template_data or {}
  46. else:
  47. if not subject:
  48. raise ValueError(
  49. "Subject is required when not using a template"
  50. )
  51. message.subject = subject
  52. # Add plain text content
  53. message.add_content(Content("text/plain", body or ""))
  54. # Add HTML content if provided
  55. if html_body:
  56. message.add_content(Content("text/html", html_body))
  57. # Send email
  58. import asyncio
  59. response = await asyncio.to_thread(self.client.send, message)
  60. if response.status_code >= 400:
  61. raise RuntimeError(
  62. f"Failed to send email: {response.status_code}"
  63. )
  64. if response.status_code == 202:
  65. logger.info("Message sent successfully!")
  66. else:
  67. error_msg = f"Failed to send email. Status code: {response.status_code}, Body: {response.body}"
  68. logger.error(error_msg)
  69. raise RuntimeError(error_msg)
  70. except Exception as e:
  71. error_msg = f"Failed to send email to {to_email}: {str(e)}"
  72. logger.error(error_msg)
  73. raise RuntimeError(error_msg) from e
  74. async def send_verification_email(
  75. self,
  76. to_email: str,
  77. verification_code: str,
  78. dynamic_template_data: Optional[dict] = None,
  79. ) -> None:
  80. try:
  81. if self.verify_email_template_id:
  82. # Use dynamic template
  83. dynamic_data = {
  84. "url": f"{self.frontend_url}/verify-email?token={verification_code}&email={to_email}",
  85. }
  86. if dynamic_template_data:
  87. dynamic_data |= dynamic_template_data
  88. await self.send_email(
  89. to_email=to_email,
  90. template_id=self.verify_email_template_id,
  91. dynamic_template_data=dynamic_data,
  92. )
  93. else:
  94. # Fallback to default content
  95. subject = "Please verify your email address"
  96. body = f"""
  97. Please verify your email address by entering the following code:
  98. Verification code: {verification_code}
  99. If you did not request this verification, please ignore this email.
  100. """
  101. html_body = f"""
  102. <p>Please verify your email address by entering the following code:</p>
  103. <p style="font-size: 24px; font-weight: bold; margin: 20px 0;">
  104. Verification code: {verification_code}
  105. </p>
  106. <p>If you did not request this verification, please ignore this email.</p>
  107. """
  108. await self.send_email(
  109. to_email=to_email,
  110. subject=subject,
  111. body=body,
  112. html_body=html_body,
  113. )
  114. except Exception as e:
  115. error_msg = f"Failed to send email to {to_email}: {str(e)}"
  116. logger.error(error_msg)
  117. raise RuntimeError(error_msg) from e
  118. async def send_password_reset_email(
  119. self,
  120. to_email: str,
  121. reset_token: str,
  122. dynamic_template_data: Optional[dict] = None,
  123. ) -> None:
  124. try:
  125. if self.reset_password_template_id:
  126. # Use dynamic template
  127. dynamic_data = {
  128. "url": f"{self.frontend_url}/reset-password?token={reset_token}",
  129. }
  130. if dynamic_template_data:
  131. dynamic_data |= dynamic_template_data
  132. await self.send_email(
  133. to_email=to_email,
  134. template_id=self.reset_password_template_id,
  135. dynamic_template_data=dynamic_data,
  136. )
  137. else:
  138. # Fallback to default content
  139. subject = "Password Reset Request"
  140. body = f"""
  141. You have requested to reset your password.
  142. Reset token: {reset_token}
  143. If you did not request a password reset, please ignore this email.
  144. """
  145. html_body = f"""
  146. <p>You have requested to reset your password.</p>
  147. <p style="font-size: 24px; font-weight: bold; margin: 20px 0;">
  148. Reset token: {reset_token}
  149. </p>
  150. <p>If you did not request a password reset, please ignore this email.</p>
  151. """
  152. await self.send_email(
  153. to_email=to_email,
  154. subject=subject,
  155. body=body,
  156. html_body=html_body,
  157. )
  158. except Exception as e:
  159. error_msg = f"Failed to send email to {to_email}: {str(e)}"
  160. logger.error(error_msg)
  161. raise RuntimeError(error_msg) from e