282 lines
11 KiB
Python
282 lines
11 KiB
Python
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
|