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"""
Please verify your email address to get started with R2R - the most advanced AI retrieval system.
Click the link below to verify your email:
Or enter this verification code: {verification_code}
If you didn't create an account with R2R, please ignore this email.
You've requested to reset your R2R password.
Click the link below to reset your password:
Or use this reset token: {reset_token}
If you didn't request a password reset, please ignore this email.
Your password has been successfully changed.