123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281 |
- import logging
- import os
- from typing import Optional
- from mailersend import emails
- from core.base import EmailConfig, EmailProvider
- logger = logging.getLogger(__name__)
- class MailerSendEmailProvider(EmailProvider):
- """Email provider implementation using MailerSend API."""
- def __init__(self, config: EmailConfig):
- super().__init__(config)
- self.api_key = config.mailersend_api_key or os.getenv(
- "MAILERSEND_API_KEY"
- )
- if not self.api_key or not isinstance(self.api_key, str):
- raise ValueError("A valid MailerSend API key is required.")
- self.from_email = config.from_email or os.getenv("R2R_FROM_EMAIL")
- if not self.from_email or not isinstance(self.from_email, str):
- raise ValueError("A valid from email is required.")
- self.frontend_url = config.frontend_url or os.getenv(
- "R2R_FRONTEND_URL"
- )
- if not self.frontend_url or not isinstance(self.frontend_url, str):
- raise ValueError("A valid frontend URL is required.")
- self.verify_email_template_id = (
- config.verify_email_template_id
- or os.getenv("MAILERSEND_VERIFY_EMAIL_TEMPLATE_ID")
- )
- self.reset_password_template_id = (
- config.reset_password_template_id
- or os.getenv("MAILERSEND_RESET_PASSWORD_TEMPLATE_ID")
- )
- self.password_changed_template_id = (
- config.password_changed_template_id
- or os.getenv("MAILERSEND_PASSWORD_CHANGED_TEMPLATE_ID")
- )
- self.client = emails.NewEmail(self.api_key)
- self.sender_name = config.sender_name or "R2R"
- # Logo and documentation URLs
- self.docs_base_url = f"{self.frontend_url}/documentation"
- def _get_base_template_data(self, to_email: str) -> dict:
- """Get base template data used across all email templates."""
- return {
- "user_email": to_email,
- "docs_url": self.docs_base_url,
- "quickstart_url": f"{self.docs_base_url}/quickstart",
- "frontend_url": self.frontend_url,
- }
- async def send_email(
- self,
- to_email: str,
- subject: Optional[str] = None,
- body: Optional[str] = None,
- html_body: Optional[str] = None,
- template_id: Optional[str] = None,
- dynamic_template_data: Optional[dict] = None,
- ) -> None:
- try:
- logger.info("Preparing MailerSend message...")
- mail_body = {
- "from": {
- "email": self.from_email,
- "name": self.sender_name,
- },
- "to": [{"email": to_email}],
- }
- if template_id:
- # Transform the template data to MailerSend's expected format
- if dynamic_template_data:
- formatted_substitutions = {}
- for key, value in dynamic_template_data.items():
- formatted_substitutions[key] = {
- "var": key,
- "value": value,
- }
- mail_body["variables"] = [
- {
- "email": to_email,
- "substitutions": formatted_substitutions,
- }
- ]
- mail_body["template_id"] = template_id
- else:
- mail_body.update(
- {
- "subject": subject or "",
- "text": body or "",
- "html": html_body or "",
- }
- )
- import asyncio
- response = await asyncio.to_thread(self.client.send, mail_body)
- # Handle different response formats
- if isinstance(response, str):
- # Clean the string response by stripping whitespace
- response_clean = response.strip()
- if response_clean in ["202", "200"]:
- logger.info(
- f"Email accepted for delivery with status code {response_clean}"
- )
- return
- elif isinstance(response, int) and response in [200, 202]:
- logger.info(
- f"Email accepted for delivery with status code {response}"
- )
- return
- elif isinstance(response, dict) and response.get(
- "status_code"
- ) in [200, 202]:
- logger.info(
- f"Email accepted for delivery with status code {response.get('status_code')}"
- )
- return
- # If we get here, it's an error
- error_msg = f"MailerSend error: {response}"
- logger.error(error_msg)
- except Exception as e:
- error_msg = f"Failed to send email to {to_email}: {str(e)}"
- logger.error(error_msg)
- async def send_verification_email(
- self,
- to_email: str,
- verification_code: str,
- dynamic_template_data: Optional[dict] = None,
- ) -> None:
- try:
- if self.verify_email_template_id:
- verification_data = {
- "verification_link": f"{self.frontend_url}/verify-email?verification_code={verification_code}&email={to_email}",
- "verification_code": verification_code, # Include code separately for flexible template usage
- }
- # Merge with any additional template data
- template_data = {
- **(dynamic_template_data or {}),
- **verification_data,
- }
- await self.send_email(
- to_email=to_email,
- template_id=self.verify_email_template_id,
- dynamic_template_data=template_data,
- )
- else:
- # Fallback to basic email if no template ID is configured
- subject = "Verify Your R2R Account"
- html_body = f"""
- <div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
- <h1>Welcome to R2R!</h1>
- <p>Please verify your email address to get started with R2R - the most advanced AI retrieval system.</p>
- <p>Click the link below to verify your email:</p>
- <p><a href="{self.frontend_url}/verify-email?verification_code={verification_code}&email={to_email}"
- style="background-color: #007bff; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px;">
- Verify Email
- </a></p>
- <p>Or enter this verification code: <strong>{verification_code}</strong></p>
- <p>If you didn't create an account with R2R, please ignore this email.</p>
- </div>
- """
- await self.send_email(
- to_email=to_email,
- subject=subject,
- html_body=html_body,
- body=f"Welcome to R2R! Please verify your email using this code: {verification_code}",
- )
- except Exception as e:
- error_msg = (
- f"Failed to send verification email to {to_email}: {str(e)}"
- )
- logger.error(error_msg)
- async def send_password_reset_email(
- self,
- to_email: str,
- reset_token: str,
- dynamic_template_data: Optional[dict] = None,
- ) -> None:
- try:
- if self.reset_password_template_id:
- reset_data = {
- "reset_link": f"{self.frontend_url}/reset-password?token={reset_token}",
- "reset_token": reset_token,
- }
- template_data = {**(dynamic_template_data or {}), **reset_data}
- await self.send_email(
- to_email=to_email,
- template_id=self.reset_password_template_id,
- dynamic_template_data=template_data,
- )
- else:
- subject = "Reset Your R2R Password"
- html_body = f"""
- <div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
- <h1>Password Reset Request</h1>
- <p>You've requested to reset your R2R password.</p>
- <p>Click the link below to reset your password:</p>
- <p><a href="{self.frontend_url}/reset-password?token={reset_token}"
- style="background-color: #007bff; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px;">
- Reset Password
- </a></p>
- <p>Or use this reset token: <strong>{reset_token}</strong></p>
- <p>If you didn't request a password reset, please ignore this email.</p>
- </div>
- """
- await self.send_email(
- to_email=to_email,
- subject=subject,
- html_body=html_body,
- body=f"Reset your R2R password using this token: {reset_token}",
- )
- except Exception as e:
- error_msg = (
- f"Failed to send password reset email to {to_email}: {str(e)}"
- )
- logger.error(error_msg)
- async def send_password_changed_email(
- self,
- to_email: str,
- dynamic_template_data: Optional[dict] = None,
- *args,
- **kwargs,
- ) -> None:
- try:
- if (
- hasattr(self, "password_changed_template_id")
- and self.password_changed_template_id
- ):
- await self.send_email(
- to_email=to_email,
- template_id=self.password_changed_template_id,
- dynamic_template_data=dynamic_template_data,
- )
- else:
- subject = "Your Password Has Been Changed"
- body = """
- Your password has been successfully changed.
- If you did not make this change, please contact support immediately and secure your account.
- """
- html_body = """
- <div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
- <h1>Password Changed Successfully</h1>
- <p>Your password has been successfully changed.</p>
- </div>
- """
- await self.send_email(
- to_email=to_email,
- subject=subject,
- html_body=html_body,
- body=body,
- )
- except Exception as e:
- error_msg = f"Failed to send password change notification to {to_email}: {str(e)}"
- logger.error(error_msg)
- raise RuntimeError(error_msg) from e
|