436 lines
16 KiB
Python
436 lines
16 KiB
Python
import httpx
|
|
from typing import List, Optional, Dict, Any
|
|
from datetime import datetime
|
|
from ..core.database import get_db
|
|
from ..models.models import EmailTemplate
|
|
from sqlalchemy.orm import Session
|
|
from ..core.config import settings
|
|
from .bounce_service import bounce_service
|
|
|
|
|
|
class EmailService:
|
|
"""Email service using SMTP2GO API with database-stored templates"""
|
|
|
|
def __init__(self):
|
|
self.api_key = settings.SMTP2GO_API_KEY
|
|
self.api_url = settings.SMTP2GO_API_URL
|
|
self.from_email = settings.EMAIL_FROM
|
|
self.from_name = settings.EMAIL_FROM_NAME
|
|
|
|
async def send_email(
|
|
self,
|
|
to_email: str,
|
|
subject: str,
|
|
html_body: str,
|
|
text_body: Optional[str] = None,
|
|
db: Optional[Session] = None
|
|
) -> dict:
|
|
"""
|
|
Send an email using SMTP2GO API
|
|
|
|
Args:
|
|
to_email: Recipient email address
|
|
subject: Email subject
|
|
html_body: HTML content of the email
|
|
text_body: Plain text content (optional)
|
|
db: Database session for bounce checking
|
|
|
|
Returns:
|
|
dict: API response
|
|
|
|
Raises:
|
|
ValueError: If email is bounced or invalid
|
|
"""
|
|
# Check if email is bounced before sending
|
|
if db and bounce_service.is_email_bounced(to_email, db):
|
|
raise ValueError(f"Email {to_email} is in bounce list and cannot receive emails")
|
|
|
|
payload = {
|
|
"to": [to_email],
|
|
"sender": f"{self.from_name} <{self.from_email}>",
|
|
"subject": subject,
|
|
"html_body": html_body,
|
|
}
|
|
|
|
if text_body:
|
|
payload["text_body"] = text_body
|
|
|
|
headers = {
|
|
"Content-Type": "application/json",
|
|
"X-Smtp2go-Api-Key": self.api_key
|
|
}
|
|
|
|
async with httpx.AsyncClient() as client:
|
|
response = await client.post(self.api_url, json=payload, headers=headers)
|
|
return response.json()
|
|
|
|
async def get_template(self, template_key: str, db: Session) -> Optional[EmailTemplate]:
|
|
"""Get email template from database"""
|
|
return db.query(EmailTemplate).filter(
|
|
EmailTemplate.template_key == template_key,
|
|
EmailTemplate.is_active == True
|
|
).first()
|
|
|
|
def render_template(self, template_body: str, variables: Dict[str, Any]) -> str:
|
|
"""Render template with variables"""
|
|
result = template_body
|
|
for key, value in variables.items():
|
|
result = result.replace(f"{{{key}}}", str(value))
|
|
return result
|
|
|
|
async def send_templated_email(
|
|
self,
|
|
template_key: str,
|
|
to_email: str,
|
|
variables: Dict[str, Any],
|
|
db: Session
|
|
) -> dict:
|
|
"""Send email using database template"""
|
|
template = await self.get_template(template_key, db)
|
|
|
|
if not template:
|
|
raise ValueError(f"Email template '{template_key}' not found or inactive")
|
|
|
|
# Render subject and bodies with variables
|
|
subject = self.render_template(template.subject, variables)
|
|
html_body = self.render_template(template.html_body, variables)
|
|
text_body = None
|
|
|
|
if template.text_body:
|
|
text_body = self.render_template(template.text_body, variables)
|
|
|
|
return await self.send_email(to_email, subject, html_body, text_body, db)
|
|
|
|
async def send_welcome_email(self, to_email: str, first_name: str, db: Session) -> dict:
|
|
"""Send welcome email to new user"""
|
|
variables = {
|
|
"first_name": first_name,
|
|
"app_name": settings.APP_NAME
|
|
}
|
|
return await self.send_templated_email("welcome", to_email, variables, db)
|
|
|
|
async def send_payment_confirmation(
|
|
self,
|
|
to_email: str,
|
|
first_name: str,
|
|
amount: float,
|
|
payment_method: str,
|
|
membership_tier: str,
|
|
db: Session
|
|
) -> dict:
|
|
"""Send payment confirmation email"""
|
|
variables = {
|
|
"first_name": first_name,
|
|
"amount": f"£{amount:.2f}",
|
|
"payment_method": payment_method,
|
|
"membership_tier": membership_tier,
|
|
"app_name": settings.APP_NAME
|
|
}
|
|
return await self.send_templated_email("payment_confirmation", to_email, variables, db)
|
|
|
|
async def send_membership_activation_email(
|
|
self,
|
|
to_email: str,
|
|
first_name: str,
|
|
membership_tier: str,
|
|
annual_fee: float,
|
|
payment_amount: float,
|
|
payment_method: str,
|
|
renewal_date: str,
|
|
db: Session
|
|
) -> dict:
|
|
"""Send membership activation email with payment details and renewal date"""
|
|
variables = {
|
|
"first_name": first_name,
|
|
"membership_tier": membership_tier,
|
|
"annual_fee": f"£{annual_fee:.2f}",
|
|
"payment_amount": f"£{payment_amount:.2f}",
|
|
"payment_method": payment_method,
|
|
"renewal_date": renewal_date,
|
|
"payment_date": datetime.now().strftime("%d %B %Y"),
|
|
"app_name": settings.APP_NAME
|
|
}
|
|
return await self.send_templated_email("membership_activation", to_email, variables, db)
|
|
|
|
async def send_membership_renewal_reminder(
|
|
self,
|
|
to_email: str,
|
|
first_name: str,
|
|
expiry_date: str,
|
|
membership_tier: str,
|
|
annual_fee: float,
|
|
db: Session
|
|
) -> dict:
|
|
"""Send membership renewal reminder"""
|
|
variables = {
|
|
"first_name": first_name,
|
|
"expiry_date": expiry_date,
|
|
"membership_tier": membership_tier,
|
|
"annual_fee": f"£{annual_fee:.2f}",
|
|
"app_name": settings.APP_NAME
|
|
}
|
|
return await self.send_templated_email("renewal_reminder", to_email, variables, db)
|
|
|
|
async def send_password_reset_email(
|
|
self,
|
|
to_email: str,
|
|
first_name: str,
|
|
reset_token: str,
|
|
db: Session
|
|
) -> dict:
|
|
"""Send password reset email with reset link"""
|
|
reset_url = f"{settings.FRONTEND_URL}/reset-password?token={reset_token}"
|
|
variables = {
|
|
"first_name": first_name,
|
|
"reset_url": reset_url,
|
|
"app_name": settings.APP_NAME
|
|
}
|
|
return await self.send_templated_email("password_reset", to_email, variables, db)
|
|
|
|
|
|
def get_default_templates() -> List[Dict[str, Any]]:
|
|
"""Get default email templates for seeding the database"""
|
|
return [
|
|
{
|
|
"template_key": "welcome",
|
|
"name": "Welcome Email",
|
|
"subject": "Welcome to {app_name}!",
|
|
"html_body": """
|
|
<html>
|
|
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
|
<h2 style="color: #0066cc;">Welcome to {app_name}!</h2>
|
|
<p>Hello {first_name},</p>
|
|
<p>Thank you for registering with us. Your account has been successfully created.</p>
|
|
<p>You can now:</p>
|
|
<ul>
|
|
<li>Browse membership tiers and select one that suits you</li>
|
|
<li>View upcoming events and meetings</li>
|
|
<li>Access your membership portal</li>
|
|
</ul>
|
|
<p>If you have any questions, please don't hesitate to contact us.</p>
|
|
<p>Best regards,<br>
|
|
<strong>{app_name}</strong></p>
|
|
</body>
|
|
</html>
|
|
""".strip(),
|
|
"text_body": """
|
|
Welcome to {app_name}!
|
|
|
|
Hello {first_name},
|
|
|
|
Thank you for registering with us. Your account has been successfully created.
|
|
|
|
You can now:
|
|
- Browse membership tiers and select one that suits you
|
|
- View upcoming events and meetings
|
|
- Access your membership portal
|
|
|
|
If you have any questions, please don't hesitate to contact us.
|
|
|
|
Best regards,
|
|
{app_name}
|
|
""".strip(),
|
|
"variables": '["first_name", "app_name"]',
|
|
"is_active": True
|
|
},
|
|
{
|
|
"template_key": "payment_confirmation",
|
|
"name": "Payment Confirmation",
|
|
"subject": "Payment Confirmation",
|
|
"html_body": """
|
|
<html>
|
|
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
|
<h2 style="color: #0066cc;">Payment Confirmed!</h2>
|
|
<p>Hello {first_name},</p>
|
|
<p>We have received your payment. Thank you!</p>
|
|
<div style="background-color: #f5f5f5; padding: 15px; border-radius: 5px; margin: 20px 0;">
|
|
<p style="margin: 5px 0;"><strong>Amount:</strong> {amount}</p>
|
|
<p style="margin: 5px 0;"><strong>Payment Method:</strong> {payment_method}</p>
|
|
<p style="margin: 5px 0;"><strong>Membership Tier:</strong> {membership_tier}</p>
|
|
</div>
|
|
<p>Your membership is now active. You can access all the benefits associated with your tier.</p>
|
|
<p>Best regards,<br>
|
|
<strong>{app_name}</strong></p>
|
|
</body>
|
|
</html>
|
|
""".strip(),
|
|
"text_body": """
|
|
Payment Confirmed!
|
|
|
|
Hello {first_name},
|
|
|
|
We have received your payment. Thank you!
|
|
|
|
Amount: {amount}
|
|
Payment Method: {payment_method}
|
|
Membership Tier: {membership_tier}
|
|
|
|
Your membership is now active. You can access all the benefits associated with your tier.
|
|
|
|
Best regards,
|
|
{app_name}
|
|
""".strip(),
|
|
"variables": '["first_name", "amount", "payment_method", "membership_tier", "app_name"]',
|
|
"is_active": True
|
|
},
|
|
{
|
|
"template_key": "membership_activation",
|
|
"name": "Membership Activation",
|
|
"subject": "Your {app_name} Membership is Now Active!",
|
|
"html_body": """
|
|
<html>
|
|
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
|
<h2 style="color: #28a745;">Welcome to {app_name}!</h2>
|
|
<p>Hello {first_name},</p>
|
|
<p>Great news! Your membership has been successfully activated. You now have full access to all the benefits of your membership tier.</p>
|
|
|
|
<div style="background-color: #f8f9fa; padding: 20px; border-radius: 8px; margin: 20px 0; border-left: 4px solid #28a745;">
|
|
<h3 style="margin-top: 0; color: #28a745;">Membership Details</h3>
|
|
<p style="margin: 8px 0;"><strong>Membership Tier:</strong> {membership_tier}</p>
|
|
<p style="margin: 8px 0;"><strong>Annual Fee:</strong> {annual_fee}</p>
|
|
<p style="margin: 8px 0;"><strong>Next Renewal Date:</strong> {renewal_date}</p>
|
|
</div>
|
|
|
|
<div style="background-color: #e9ecef; padding: 20px; border-radius: 8px; margin: 20px 0;">
|
|
<h3 style="margin-top: 0; color: #495057;">Payment Information</h3>
|
|
<p style="margin: 8px 0;"><strong>Amount Paid:</strong> {payment_amount}</p>
|
|
<p style="margin: 8px 0;"><strong>Payment Method:</strong> {payment_method}</p>
|
|
<p style="margin: 8px 0;"><strong>Payment Date:</strong> {payment_date}</p>
|
|
</div>
|
|
|
|
<p>Your membership will automatically renew on <strong>{renewal_date}</strong> unless you choose to cancel it. You can manage your membership settings in your account dashboard.</p>
|
|
|
|
<p>If you have any questions about your membership or need assistance, please don't hesitate to contact us.</p>
|
|
|
|
<p>Welcome to the {app_name} community!</p>
|
|
|
|
<p>Best regards,<br>
|
|
<strong>{app_name} Team</strong></p>
|
|
</body>
|
|
</html>
|
|
""".strip(),
|
|
"text_body": """
|
|
Welcome to {app_name}!
|
|
|
|
Hello {first_name},
|
|
|
|
Great news! Your membership has been successfully activated. You now have full access to all the benefits of your membership tier.
|
|
|
|
MEMBERSHIP DETAILS
|
|
------------------
|
|
Membership Tier: {membership_tier}
|
|
Annual Fee: {annual_fee}
|
|
Next Renewal Date: {renewal_date}
|
|
|
|
PAYMENT INFORMATION
|
|
-------------------
|
|
Amount Paid: {payment_amount}
|
|
Payment Method: {payment_method}
|
|
Payment Date: {payment_date}
|
|
|
|
Your membership will automatically renew on {renewal_date} unless you choose to cancel it. You can manage your membership settings in your account dashboard.
|
|
|
|
If you have any questions about your membership or need assistance, please don't hesitate to contact us.
|
|
|
|
Welcome to the {app_name} community!
|
|
|
|
Best regards,
|
|
{app_name} Team
|
|
""".strip(),
|
|
"variables": '["first_name", "membership_tier", "annual_fee", "payment_amount", "payment_method", "renewal_date", "payment_date", "app_name"]',
|
|
"is_active": True
|
|
},
|
|
{
|
|
"template_key": "renewal_reminder",
|
|
"name": "Membership Renewal Reminder",
|
|
"subject": "Membership Renewal Reminder",
|
|
"html_body": """
|
|
<html>
|
|
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
|
<h2 style="color: #0066cc;">Membership Renewal Reminder</h2>
|
|
<p>Hello {first_name},</p>
|
|
<p>This is a friendly reminder that your <strong>{membership_tier}</strong> membership will expire on <strong>{expiry_date}</strong>.</p>
|
|
<p>To continue enjoying your membership benefits, please renew your membership.</p>
|
|
<div style="background-color: #f5f5f5; padding: 15px; border-radius: 5px; margin: 20px 0;">
|
|
<p style="margin: 5px 0;"><strong>Membership Tier:</strong> {membership_tier}</p>
|
|
<p style="margin: 5px 0;"><strong>Annual Fee:</strong> {annual_fee}</p>
|
|
<p style="margin: 5px 0;"><strong>Expires:</strong> {expiry_date}</p>
|
|
</div>
|
|
<p>Please log in to your account to renew your membership.</p>
|
|
<p>Best regards,<br>
|
|
<strong>{app_name}</strong></p>
|
|
</body>
|
|
</html>
|
|
""".strip(),
|
|
"text_body": """
|
|
Membership Renewal Reminder
|
|
|
|
Hello {first_name},
|
|
|
|
This is a friendly reminder that your {membership_tier} membership will expire on {expiry_date}.
|
|
|
|
To continue enjoying your membership benefits, please renew your membership.
|
|
|
|
Membership Tier: {membership_tier}
|
|
Annual Fee: {annual_fee}
|
|
Expires: {expiry_date}
|
|
|
|
Please log in to your account to renew your membership.
|
|
|
|
Best regards,
|
|
{app_name}
|
|
""".strip(),
|
|
"variables": '["first_name", "expiry_date", "membership_tier", "annual_fee", "app_name"]',
|
|
"is_active": True
|
|
},
|
|
{
|
|
"template_key": "password_reset",
|
|
"name": "Password Reset",
|
|
"subject": "Password Reset - {app_name}",
|
|
"html_body": """
|
|
<html>
|
|
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
|
<h2 style="color: #0066cc;">Password Reset Request</h2>
|
|
<p>Hello {first_name},</p>
|
|
<p>You have requested to reset your password for your {app_name} account.</p>
|
|
<p>Please click the button below to reset your password:</p>
|
|
<div style="text-align: center; margin: 30px 0;">
|
|
<a href="{reset_url}" style="background-color: #0066cc; color: white; padding: 12px 24px; text-decoration: none; border-radius: 5px; display: inline-block;">Reset Password</a>
|
|
</div>
|
|
<p>If the button doesn't work, you can copy and paste this link into your browser:</p>
|
|
<p style="word-break: break-all; background-color: #f5f5f5; padding: 10px; border-radius: 3px;">{reset_url}</p>
|
|
<p><strong>This link will expire in 1 hour.</strong></p>
|
|
<p>If you didn't request this password reset, please ignore this email. Your password will remain unchanged.</p>
|
|
<p>For security reasons, please don't share this email with anyone.</p>
|
|
<p>Best regards,<br>
|
|
<strong>{app_name}</strong></p>
|
|
</body>
|
|
</html>
|
|
""".strip(),
|
|
"text_body": """
|
|
Password Reset Request
|
|
|
|
Hello {first_name},
|
|
|
|
You have requested to reset your password for your {app_name} account.
|
|
|
|
Please use this link to reset your password: {reset_url}
|
|
|
|
This link will expire in 1 hour.
|
|
|
|
If you didn't request this password reset, please ignore this email. Your password will remain unchanged.
|
|
|
|
For security reasons, please don't share this email with anyone.
|
|
|
|
Best regards,
|
|
{app_name}
|
|
""".strip(),
|
|
"variables": '["first_name", "reset_url", "app_name"]',
|
|
"is_active": True
|
|
}
|
|
]
|
|
|
|
|
|
# Create a singleton instance
|
|
email_service = EmailService()
|