Email template management

This commit is contained in:
James Pattinson
2025-11-10 16:07:22 +00:00
parent 43b13ef52d
commit 7fd237c28b
17 changed files with 1421 additions and 259 deletions

View File

@@ -1,11 +1,14 @@
import httpx
from typing import List, Optional
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
class EmailService:
"""Email service using SMTP2GO API"""
"""Email service using SMTP2GO API with database-stored templates"""
def __init__(self):
self.api_key = settings.SMTP2GO_API_KEY
@@ -51,96 +54,69 @@ class EmailService:
response = await client.post(self.api_url, json=payload, headers=headers)
return response.json()
async def send_welcome_email(self, to_email: str, first_name: str) -> dict:
"""Send welcome email to new user"""
subject = f"Welcome to {settings.APP_NAME}!"
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)
html_body = f"""
<html>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
<h2 style="color: #0066cc;">Welcome to {settings.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>{settings.APP_NAME}</strong></p>
</body>
</html>
"""
if not template:
raise ValueError(f"Email template '{template_key}' not found or inactive")
text_body = f"""
Welcome to {settings.APP_NAME}!
# 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
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,
{settings.APP_NAME}
"""
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)
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
membership_tier: str,
db: Session
) -> dict:
"""Send payment confirmation email"""
subject = "Payment Confirmation"
html_body = f"""
<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:.2f}</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>{settings.APP_NAME}</strong></p>
</body>
</html>
"""
text_body = f"""
Payment Confirmed!
Hello {first_name},
We have received your payment. Thank you!
Amount: £{amount:.2f}
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,
{settings.APP_NAME}
"""
return await self.send_email(to_email, subject, html_body, text_body)
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,
@@ -150,74 +126,21 @@ class EmailService:
annual_fee: float,
payment_amount: float,
payment_method: str,
renewal_date: str
renewal_date: str,
db: Session
) -> dict:
"""Send membership activation email with payment details and renewal date"""
subject = f"Your {settings.APP_NAME} Membership is Now Active!"
html_body = f"""
<html>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
<h2 style="color: #28a745;">Welcome to {settings.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:.2f}</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:.2f}</p>
<p style="margin: 8px 0;"><strong>Payment Method:</strong> {payment_method}</p>
<p style="margin: 8px 0;"><strong>Payment Date:</strong> {datetime.now().strftime('%d %B %Y')}</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 {settings.APP_NAME} community!</p>
<p>Best regards,<br>
<strong>{settings.APP_NAME} Team</strong></p>
</body>
</html>
"""
text_body = f"""
Welcome to {settings.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:.2f}
Next Renewal Date: {renewal_date}
PAYMENT INFORMATION
-------------------
Amount Paid: £{payment_amount:.2f}
Payment Method: {payment_method}
Payment Date: {datetime.now().strftime('%d %B %Y')}
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 {settings.APP_NAME} community!
Best regards,
{settings.APP_NAME} Team
"""
return await self.send_email(to_email, subject, html_body, text_body)
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,
@@ -225,103 +148,277 @@ class EmailService:
first_name: str,
expiry_date: str,
membership_tier: str,
annual_fee: float
annual_fee: float,
db: Session
) -> dict:
"""Send membership renewal reminder"""
subject = "Membership Renewal Reminder"
html_body = f"""
<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:.2f}</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>{settings.APP_NAME}</strong></p>
</body>
</html>
"""
text_body = f"""
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:.2f}
Expires: {expiry_date}
Please log in to your account to renew your membership.
Best regards,
{settings.APP_NAME}
"""
return await self.send_email(to_email, subject, html_body, text_body)
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
reset_token: str,
db: Session
) -> dict:
"""Send password reset email with reset link"""
subject = f"Password Reset - {settings.APP_NAME}"
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>
html_body = f"""
<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 {settings.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>{settings.APP_NAME}</strong></p>
</body>
</html>
"""
<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>
text_body = f"""
Password Reset Request
<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>
Hello {first_name},
<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>
You have requested to reset your password for your {settings.APP_NAME} account.
<p>If you have any questions about your membership or need assistance, please don't hesitate to contact us.</p>
Please use this link to reset your password: {reset_url}
<p>Welcome to the {app_name} community!</p>
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,
{settings.APP_NAME}
"""
return await self.send_email(to_email, subject, html_body, text_body)
<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