diff --git a/backend/email_service.py b/backend/email_service.py index e32a2dc..8cdeacc 100644 --- a/backend/email_service.py +++ b/backend/email_service.py @@ -1,43 +1,56 @@ """ -Email Service - SendGrid integration for verification and password reset emails. +Email Service - AWS SES integration for verification and password reset emails. """ import os import secrets from datetime import datetime, timedelta -from typing import Optional -import httpx +import boto3 +from botocore.exceptions import ClientError -SENDGRID_API_KEY = os.getenv("SENDGRID_API_KEY", "") +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.runfoo.run") -async def send_email(to: str, subject: str, html_content: str) -> bool: - """Send email via SendGrid API""" - if not SENDGRID_API_KEY: +# 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 - async with httpx.AsyncClient() as client: - response = await client.post( - "https://api.sendgrid.com/v3/mail/send", - headers={ - "Authorization": f"Bearer {SENDGRID_API_KEY}", - "Content-Type": "application/json" - }, - json={ - "personalizations": [{"to": [{"email": to}]}], - "from": {"email": EMAIL_FROM, "name": "Elmeg"}, - "subject": subject, - "content": [{"type": "text/html", "value": html_content}] + 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'} + } } ) - return response.status_code in (200, 202) + 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}" @@ -61,7 +74,8 @@ async def send_verification_email(email: str, token: str) -> bool: """ - return await send_email(email, "Verify your Elmeg account", html) + return send_email(email, "Verify your Elmeg account", html) + async def send_password_reset_email(email: str, token: str) -> bool: """Send password reset link""" @@ -86,13 +100,16 @@ async def send_password_reset_email(email: str, token: str) -> bool: """ - return await send_email(email, "Reset your Elmeg password", html) + 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) + diff --git a/backend/requirements.txt b/backend/requirements.txt index 9ad696e..1b70326 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -11,3 +11,4 @@ argon2-cffi psycopg2-binary requests beautifulsoup4 +boto3 diff --git a/docs/AWS_SES_SETUP.md b/docs/AWS_SES_SETUP.md new file mode 100644 index 0000000..352b335 --- /dev/null +++ b/docs/AWS_SES_SETUP.md @@ -0,0 +1,143 @@ +# AWS SES Email Setup + +## Overview + +The Elmeg platform uses AWS SES (Simple Email Service) for sending transactional emails (verification, password reset). Cost: ~$0.10 per 1,000 emails. + +--- + +## Prerequisites + +- AWS Account +- Domain: `elmeg.xyz` (for verified sender) +- Production server: `tangible-aacorn` / `nexus-vector` + +--- + +## Setup Steps + +### 1. Verify Email Domain in SES + +1. Go to AWS Console → SES → Verified Identities +2. Click "Create identity" → Choose "Domain" +3. Enter: `elmeg.xyz` +4. Enable "Use a custom MAIL FROM domain" (optional) +5. Add the provided DNS records to your domain + +**Required DNS Records** (example): + +``` +Type: TXT +Name: _amazonses.elmeg.xyz +Value: [provided by AWS] + +Type: CNAME +Name: [dkim1]._domainkey.elmeg.xyz +Value: [provided by AWS] +``` + +### 2. Move Out of SES Sandbox + +By default, SES is in sandbox mode (can only send to verified emails). + +1. Go to SES → Account dashboard +2. Click "Request production access" +3. Fill out: + - Mail type: Transactional + - Website URL: + - Use case: User registration verification, password reset +4. Wait for approval (usually 24hrs) + +### 3. Create IAM User for SES + +1. Go to IAM → Users → Create user +2. Name: `elmeg-ses-sender` +3. Attach policy: `AmazonSESFullAccess` (or create custom with `ses:SendEmail` only) +4. Create access key for "Application running outside AWS" +5. Save: + - Access Key ID + - Secret Access Key + +### 4. Configure Production Server + +SSH into server and update environment: + +```bash +ssh root@tangible-aacorn +cd /srv/containers/elmeg-demo +``` + +Edit `docker-compose.yml`, add to `backend` service: + +```yaml +backend: + environment: + - DATABASE_URL=postgresql://elmeg:elmeg@db:5432/elmeg + - AWS_ACCESS_KEY_ID=AKIA... + - AWS_SECRET_ACCESS_KEY=... + - AWS_SES_REGION=us-east-1 + - EMAIL_FROM=noreply@elmeg.xyz + - FRONTEND_URL=https://elmeg.runfoo.run +``` + +Or create `.env` file: + +```env +AWS_ACCESS_KEY_ID=AKIA... +AWS_SECRET_ACCESS_KEY=your-secret-key +AWS_SES_REGION=us-east-1 +EMAIL_FROM=noreply@elmeg.xyz +FRONTEND_URL=https://elmeg.runfoo.run +``` + +### 5. Add boto3 to Requirements + +```bash +# Add to backend/requirements.txt +boto3>=1.28.0 +``` + +### 6. Run Migration & Restart + +```bash +docker compose build backend +docker compose exec backend python migrations/add_email_verification.py +docker compose restart backend +``` + +### 7. Test + +1. Register at +2. Check for verification email +3. Test forgot password at + +--- + +## Environment Variables + +| Variable | Description | Example | +|----------|-------------|---------| +| `AWS_ACCESS_KEY_ID` | IAM user access key | `AKIA...` | +| `AWS_SECRET_ACCESS_KEY` | IAM user secret | `wJalr...` | +| `AWS_SES_REGION` | SES region | `us-east-1` | +| `EMAIL_FROM` | Verified sender address | `noreply@elmeg.xyz` | +| `FRONTEND_URL` | Base URL for links | `https://elmeg.runfoo.run` | + +--- + +## Cost + +- $0.10 per 1,000 emails +- No monthly minimum +- First 62,000 emails/month free if sending from EC2 + +--- + +## Troubleshooting + +| Issue | Solution | +|-------|----------| +| "Email address is not verified" | Domain not verified in SES, or still in sandbox | +| "Access Denied" | Check IAM permissions for ses:SendEmail | +| Emails not sending | Check `docker compose logs backend` | +| Emails in spam | Complete DKIM setup, verify domain |