elmeg-demo/backend/email_service.py
2025-12-21 14:32:36 -08:00

115 lines
3.9 KiB
Python

"""
Email Service - AWS SES integration for verification and password reset emails.
"""
import os
import secrets
from datetime import datetime, timedelta
import boto3
from botocore.exceptions import ClientError
AWS_REGION = os.getenv("AWS_SES_REGION", "us-east-1")
EMAIL_FROM = os.getenv("EMAIL_FROM", "noreply@elmeg.xyz")
FRONTEND_URL = os.getenv("FRONTEND_URL", "https://elmeg.xyz")
# AWS credentials from environment (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY)
# or from IAM role if running on AWS infrastructure
def get_ses_client():
"""Get boto3 SES client"""
return boto3.client('ses', region_name=AWS_REGION)
def send_email(to: str, subject: str, html_content: str) -> bool:
"""Send email via AWS SES"""
# Dev mode - just log
if not os.getenv("AWS_ACCESS_KEY_ID"):
print(f"[EMAIL DEV MODE] To: {to}, Subject: {subject}")
print(f"[EMAIL DEV MODE] Content: {html_content[:200]}...")
return True
try:
client = get_ses_client()
response = client.send_email(
Source=EMAIL_FROM,
Destination={'ToAddresses': [to]},
Message={
'Subject': {'Data': subject, 'Charset': 'UTF-8'},
'Body': {
'Html': {'Data': html_content, 'Charset': 'UTF-8'}
}
}
)
print(f"Email sent to {to}, MessageId: {response['MessageId']}")
return True
except ClientError as e:
print(f"Failed to send email: {e.response['Error']['Message']}")
return False
def generate_token() -> str:
"""Generate a secure random token"""
return secrets.token_urlsafe(32)
async def send_verification_email(email: str, token: str) -> bool:
"""Send email verification link"""
verify_url = f"{FRONTEND_URL}/verify-email?token={token}"
html = f"""
<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto;">
<h1 style="color: #333;">Welcome to Elmeg!</h1>
<p>Please verify your email address by clicking the button below:</p>
<a href="{verify_url}"
style="display: inline-block; background: #4F46E5; color: white;
padding: 12px 24px; text-decoration: none; border-radius: 6px;
margin: 16px 0;">
Verify Email
</a>
<p style="color: #666; font-size: 14px;">
Or copy this link: {verify_url}
</p>
<p style="color: #999; font-size: 12px;">
This link expires in 48 hours.
</p>
</div>
"""
return send_email(email, "Verify your Elmeg account", html)
async def send_password_reset_email(email: str, token: str) -> bool:
"""Send password reset link"""
reset_url = f"{FRONTEND_URL}/reset-password?token={token}"
html = f"""
<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto;">
<h1 style="color: #333;">Password Reset</h1>
<p>You requested a password reset. Click below to set a new password:</p>
<a href="{reset_url}"
style="display: inline-block; background: #4F46E5; color: white;
padding: 12px 24px; text-decoration: none; border-radius: 6px;
margin: 16px 0;">
Reset Password
</a>
<p style="color: #666; font-size: 14px;">
Or copy this link: {reset_url}
</p>
<p style="color: #999; font-size: 12px;">
This link expires in 1 hour. If you didn't request this, ignore this email.
</p>
</div>
"""
return send_email(email, "Reset your Elmeg password", html)
# Token expiration helpers
def get_verification_expiry() -> datetime:
"""48 hour expiry for email verification"""
return datetime.utcnow() + timedelta(hours=48)
def get_reset_expiry() -> datetime:
"""1 hour expiry for password reset"""
return datetime.utcnow() + timedelta(hours=1)