aboutsummaryrefslogtreecommitdiff
path: root/.venv/lib/python3.12/site-packages/core/providers/email
diff options
context:
space:
mode:
Diffstat (limited to '.venv/lib/python3.12/site-packages/core/providers/email')
-rw-r--r--.venv/lib/python3.12/site-packages/core/providers/email/__init__.py11
-rw-r--r--.venv/lib/python3.12/site-packages/core/providers/email/console_mock.py67
-rw-r--r--.venv/lib/python3.12/site-packages/core/providers/email/mailersend.py281
-rw-r--r--.venv/lib/python3.12/site-packages/core/providers/email/sendgrid.py257
-rw-r--r--.venv/lib/python3.12/site-packages/core/providers/email/smtp.py176
5 files changed, 792 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/core/providers/email/__init__.py b/.venv/lib/python3.12/site-packages/core/providers/email/__init__.py
new file mode 100644
index 00000000..38753695
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/core/providers/email/__init__.py
@@ -0,0 +1,11 @@
+from .console_mock import ConsoleMockEmailProvider
+from .mailersend import MailerSendEmailProvider
+from .sendgrid import SendGridEmailProvider
+from .smtp import AsyncSMTPEmailProvider
+
+__all__ = [
+ "ConsoleMockEmailProvider",
+ "AsyncSMTPEmailProvider",
+ "SendGridEmailProvider",
+ "MailerSendEmailProvider",
+]
diff --git a/.venv/lib/python3.12/site-packages/core/providers/email/console_mock.py b/.venv/lib/python3.12/site-packages/core/providers/email/console_mock.py
new file mode 100644
index 00000000..459a978d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/core/providers/email/console_mock.py
@@ -0,0 +1,67 @@
+import logging
+from typing import Optional
+
+from core.base import EmailProvider
+
+logger = logging.getLogger()
+
+
+class ConsoleMockEmailProvider(EmailProvider):
+ """A simple email provider that logs emails to console, useful for
+ testing."""
+
+ async def send_email(
+ self,
+ to_email: str,
+ subject: str,
+ body: str,
+ html_body: Optional[str] = None,
+ *args,
+ **kwargs,
+ ) -> None:
+ logger.info(f"""
+ -------- Email Message --------
+ To: {to_email}
+ Subject: {subject}
+ Body:
+ {body}
+ -----------------------------
+ """)
+
+ async def send_verification_email(
+ self, to_email: str, verification_code: str, *args, **kwargs
+ ) -> None:
+ logger.info(f"""
+ -------- Email Message --------
+ To: {to_email}
+ Subject: Please verify your email address
+ Body:
+ Verification code: {verification_code}
+ -----------------------------
+ """)
+
+ async def send_password_reset_email(
+ self, to_email: str, reset_token: str, *args, **kwargs
+ ) -> None:
+ logger.info(f"""
+ -------- Email Message --------
+ To: {to_email}
+ Subject: Password Reset Request
+ Body:
+ Reset token: {reset_token}
+ -----------------------------
+ """)
+
+ async def send_password_changed_email(
+ self, to_email: str, *args, **kwargs
+ ) -> None:
+ logger.info(f"""
+ -------- Email Message --------
+ To: {to_email}
+ Subject: Your Password Has Been Changed
+ Body:
+ Your password has been successfully changed.
+
+ For security reasons, you will need to log in again on all your devices.
+ -----------------------------
+ """)
diff --git a/.venv/lib/python3.12/site-packages/core/providers/email/mailersend.py b/.venv/lib/python3.12/site-packages/core/providers/email/mailersend.py
new file mode 100644
index 00000000..10fccd56
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/core/providers/email/mailersend.py
@@ -0,0 +1,281 @@
+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
diff --git a/.venv/lib/python3.12/site-packages/core/providers/email/sendgrid.py b/.venv/lib/python3.12/site-packages/core/providers/email/sendgrid.py
new file mode 100644
index 00000000..8b2553f1
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/core/providers/email/sendgrid.py
@@ -0,0 +1,257 @@
+import logging
+import os
+from typing import Optional
+
+from sendgrid import SendGridAPIClient
+from sendgrid.helpers.mail import Content, From, Mail
+
+from core.base import EmailConfig, EmailProvider
+
+logger = logging.getLogger(__name__)
+
+
+class SendGridEmailProvider(EmailProvider):
+ """Email provider implementation using SendGrid API."""
+
+ def __init__(self, config: EmailConfig):
+ super().__init__(config)
+ self.api_key = config.sendgrid_api_key or os.getenv("SENDGRID_API_KEY")
+ if not self.api_key or not isinstance(self.api_key, str):
+ raise ValueError("A valid SendGrid 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("SENDGRID_EMAIL_TEMPLATE_ID")
+ )
+ self.reset_password_template_id = (
+ config.reset_password_template_id
+ or os.getenv("SENDGRID_RESET_TEMPLATE_ID")
+ )
+ self.password_changed_template_id = (
+ config.password_changed_template_id
+ or os.getenv("SENDGRID_PASSWORD_CHANGED_TEMPLATE_ID")
+ )
+ self.client = SendGridAPIClient(api_key=self.api_key)
+ self.sender_name = config.sender_name
+
+ # 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 SendGrid message...")
+ message = Mail(
+ from_email=From(self.from_email, self.sender_name),
+ to_emails=to_email,
+ )
+
+ if template_id:
+ logger.info(f"Using dynamic template with ID: {template_id}")
+ message.template_id = template_id
+ base_data = self._get_base_template_data(to_email)
+ message.dynamic_template_data = {
+ **base_data,
+ **(dynamic_template_data or {}),
+ }
+ else:
+ if not subject:
+ raise ValueError(
+ "Subject is required when not using a template"
+ )
+ message.subject = subject
+ message.add_content(Content("text/plain", body or ""))
+ if html_body:
+ message.add_content(Content("text/html", html_body))
+
+ import asyncio
+
+ response = await asyncio.to_thread(self.client.send, message)
+
+ if response.status_code >= 400:
+ raise RuntimeError(
+ f"Failed to send email: {response.status_code}"
+ )
+ elif response.status_code == 202:
+ logger.info("Message sent successfully!")
+ else:
+ error_msg = f"Failed to send email. Status code: {response.status_code}, Body: {response.body}"
+ logger.error(error_msg)
+ raise RuntimeError(error_msg)
+
+ except Exception as e:
+ error_msg = f"Failed to send email to {to_email}: {str(e)}"
+ logger.error(error_msg)
+ raise RuntimeError(error_msg) from e
+
+ 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?token={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)
+ raise RuntimeError(error_msg) from e
+
+ 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)
+ raise RuntimeError(error_msg) from e
+
+ 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>
+ """
+ # Move send_email inside the else block
+ 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
diff --git a/.venv/lib/python3.12/site-packages/core/providers/email/smtp.py b/.venv/lib/python3.12/site-packages/core/providers/email/smtp.py
new file mode 100644
index 00000000..bd68ff36
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/core/providers/email/smtp.py
@@ -0,0 +1,176 @@
+import asyncio
+import logging
+import os
+import smtplib
+import ssl
+from email.mime.multipart import MIMEMultipart
+from email.mime.text import MIMEText
+from typing import Optional
+
+from core.base import EmailConfig, EmailProvider
+
+logger = logging.getLogger(__name__)
+
+
+class AsyncSMTPEmailProvider(EmailProvider):
+ """Email provider implementation using Brevo SMTP relay."""
+
+ def __init__(self, config: EmailConfig):
+ super().__init__(config)
+ self.smtp_server = config.smtp_server or os.getenv("R2R_SMTP_SERVER")
+ if not self.smtp_server:
+ raise ValueError("SMTP server is required")
+
+ self.smtp_port = config.smtp_port or os.getenv("R2R_SMTP_PORT")
+ if not self.smtp_port:
+ raise ValueError("SMTP port is required")
+
+ self.smtp_username = config.smtp_username or os.getenv(
+ "R2R_SMTP_USERNAME"
+ )
+ if not self.smtp_username:
+ raise ValueError("SMTP username is required")
+
+ self.smtp_password = config.smtp_password or os.getenv(
+ "R2R_SMTP_PASSWORD"
+ )
+ if not self.smtp_password:
+ raise ValueError("SMTP password is required")
+
+ self.from_email: Optional[str] = (
+ config.from_email
+ or os.getenv("R2R_FROM_EMAIL")
+ or self.smtp_username
+ )
+ self.ssl_context = ssl.create_default_context()
+
+ async def _send_email_sync(self, msg: MIMEMultipart) -> None:
+ """Synchronous email sending wrapped in asyncio executor."""
+ loop = asyncio.get_running_loop()
+
+ def _send():
+ with smtplib.SMTP_SSL(
+ self.smtp_server,
+ self.smtp_port,
+ context=self.ssl_context,
+ timeout=30,
+ ) as server:
+ logger.info("Connected to SMTP server")
+ server.login(self.smtp_username, self.smtp_password)
+ logger.info("Login successful")
+ server.send_message(msg)
+ logger.info("Message sent successfully!")
+
+ try:
+ await loop.run_in_executor(None, _send)
+ except Exception as e:
+ error_msg = f"Failed to send email: {str(e)}"
+ logger.error(error_msg)
+ raise RuntimeError(error_msg) from e
+
+ async def send_email(
+ self,
+ to_email: str,
+ subject: str,
+ body: str,
+ html_body: Optional[str] = None,
+ *args,
+ **kwargs,
+ ) -> None:
+ msg = MIMEMultipart("alternative")
+ msg["Subject"] = subject
+ msg["From"] = self.from_email # type: ignore
+ msg["To"] = to_email
+
+ msg.attach(MIMEText(body, "plain"))
+ if html_body:
+ msg.attach(MIMEText(html_body, "html"))
+
+ try:
+ logger.info("Initializing SMTP connection...")
+ async with asyncio.timeout(30): # Overall timeout
+ await self._send_email_sync(msg)
+ except asyncio.TimeoutError as e:
+ error_msg = "Operation timed out while trying to send email"
+ logger.error(error_msg)
+ raise RuntimeError(error_msg) from e
+ except Exception as e:
+ error_msg = f"Failed to send email: {str(e)}"
+ logger.error(error_msg)
+ raise RuntimeError(error_msg) from e
+
+ async def send_verification_email(
+ self, to_email: str, verification_code: str, *args, **kwargs
+ ) -> None:
+ body = f"""
+ Please verify your email address by entering the following code:
+
+ Verification code: {verification_code}
+
+ If you did not request this verification, please ignore this email.
+ """
+
+ html_body = f"""
+ <p>Please verify your email address by entering the following code:</p>
+ <p style="font-size: 24px; font-weight: bold; margin: 20px 0;">
+ Verification code: {verification_code}
+ </p>
+ <p>If you did not request this verification, please ignore this email.</p>
+ """
+
+ await self.send_email(
+ to_email=to_email,
+ subject="Please verify your email address",
+ body=body,
+ html_body=html_body,
+ )
+
+ async def send_password_reset_email(
+ self, to_email: str, reset_token: str, *args, **kwargs
+ ) -> None:
+ body = f"""
+ You have requested to reset your password.
+
+ Reset token: {reset_token}
+
+ If you did not request a password reset, please ignore this email.
+ """
+
+ html_body = f"""
+ <p>You have requested to reset your password.</p>
+ <p style="font-size: 24px; font-weight: bold; margin: 20px 0;">
+ Reset token: {reset_token}
+ </p>
+ <p>If you did not request a password reset, please ignore this email.</p>
+ """
+
+ await self.send_email(
+ to_email=to_email,
+ subject="Password Reset Request",
+ body=body,
+ html_body=html_body,
+ )
+
+ async def send_password_changed_email(
+ self, to_email: str, *args, **kwargs
+ ) -> None:
+ 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="Your Password Has Been Changed",
+ body=body,
+ html_body=html_body,
+ )