From b73f9934759bd2b85e051665b28499d8042088ac Mon Sep 17 00:00:00 2001 From: fullsizemalt <106900403+fullsizemalt@users.noreply.github.com> Date: Sun, 21 Dec 2025 19:28:29 -0800 Subject: [PATCH] feat: Email Service Integration with AWS SES - Implemented backend/services/email_service.py with boto3 SES client - Added EmailService class with verification and password reset methods - Updated auth router to use the new email service - Configured docker-compose.yml to pass AWS SES environment variables --- backend/routers/auth.py | 2 +- backend/services/email_service.py | 151 ++++++++++++++++++++++++++++++ docker-compose.yml | 5 + 3 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 backend/services/email_service.py diff --git a/backend/routers/auth.py b/backend/routers/auth.py index 8101782..495e09a 100644 --- a/backend/routers/auth.py +++ b/backend/routers/auth.py @@ -8,7 +8,7 @@ from database import get_session from models import User, Profile from schemas import UserCreate, Token, UserRead from auth import verify_password, get_password_hash, create_access_token, ACCESS_TOKEN_EXPIRE_MINUTES, get_current_user -from email_service import ( +from services.email_service import ( send_verification_email, send_password_reset_email, generate_token, get_verification_expiry, get_reset_expiry ) diff --git a/backend/services/email_service.py b/backend/services/email_service.py new file mode 100644 index 0000000..738fb30 --- /dev/null +++ b/backend/services/email_service.py @@ -0,0 +1,151 @@ +import os +import boto3 +import secrets +from datetime import datetime, timedelta +from botocore.exceptions import ClientError +from typing import Optional + +class EmailService: + def __init__(self): + self.region_name = os.getenv("AWS_SES_REGION", "us-east-1") + self.aws_access_key_id = os.getenv("AWS_ACCESS_KEY_ID") + self.aws_secret_access_key = os.getenv("AWS_SECRET_ACCESS_KEY") + self.email_from = os.getenv("EMAIL_FROM", "noreply@elmeg.xyz") + self.frontend_url = os.getenv("FRONTEND_URL", "https://elmeg.xyz") + + # Initialize SES client if credentials exist + if self.aws_access_key_id and self.aws_secret_access_key: + self.client = boto3.client( + "ses", + region_name=self.region_name, + aws_access_key_id=self.aws_access_key_id, + aws_secret_access_key=self.aws_secret_access_key, + ) + else: + self.client = None + print("WARNING: AWS credentials not found. Email service running in dummy mode.") + + def send_email(self, to_email: str, subject: str, html_content: str, text_content: str): + """Send an email using AWS SES""" + if not self.client: + print(f"DUMMY EMAIL to {to_email}: {subject}") + print(text_content) + return True + + try: + response = self.client.send_email( + Source=self.email_from, + Destination={ + "ToAddresses": [to_email], + }, + Message={ + "Subject": { + "Data": subject, + "Charset": "UTF-8", + }, + "Body": { + "Html": { + "Data": html_content, + "Charset": "UTF-8", + }, + "Text": { + "Data": text_content, + "Charset": "UTF-8", + }, + }, + }, + ) + return response + except ClientError as e: + print(f"Error sending email: {e.response['Error']['Message']}") + return False + +# Global instance +email_service = EmailService() + +# --- Helper Functions (used by auth router) --- + +def generate_token() -> str: + """Generate a secure random token""" + return secrets.token_urlsafe(32) + +def get_verification_expiry() -> datetime: + """Get expiration time for verification token (48 hours)""" + return datetime.utcnow() + timedelta(hours=48) + +def get_reset_expiry() -> datetime: + """Get expiration time for reset token (1 hour)""" + return datetime.utcnow() + timedelta(hours=1) + +def send_verification_email(to_email: str, token: str): + """Send account verification email""" + verify_url = f"{email_service.frontend_url}/verify-email?token={token}" + + subject = "Verify your Elmeg account" + + html_content = f""" + +
+Thanks for signing up. Please verify your email address to get started.
+ +Or copy this link to your browser:
+{verify_url}
+If you didn't create an account, you can safely ignore this email.
+We received a request to reset your password. Click the button below to choose a new one.
+Or copy this link to your browser:
+{reset_url}
+This link expires in 1 hour.
+If you didn't request a password reset, you can safely ignore this email.
+