From 3a0566d001674a7c98f71c4fff255d2aaf31fbf9 Mon Sep 17 00:00:00 2001 From: Derek Malone Date: Wed, 21 Jan 2026 21:43:29 -0500 Subject: [PATCH 1/2] docs: add SendGrid to Resend migration guide --- .claude/EMAIL_MIGRATION.md | 187 +++++++++++++++++++++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 .claude/EMAIL_MIGRATION.md diff --git a/.claude/EMAIL_MIGRATION.md b/.claude/EMAIL_MIGRATION.md new file mode 100644 index 0000000..27517a0 --- /dev/null +++ b/.claude/EMAIL_MIGRATION.md @@ -0,0 +1,187 @@ +# SendGrid to Resend Migration Guide + +## Overview + +Migrating email service from SendGrid to Resend for verification and password reset emails. + +**Resend Docs:** https://resend.com/docs/send-with-python + +--- + +## Files to Modify + +| File | Changes | +|------|---------| +| `backend/requirements.txt` | Swap sendgrid → resend | +| `backend/bible_app/settings.py` | Rename 2 env var references | +| `backend/api/utils/email.py` | Change imports, update send logic in 2 functions | +| `.env` (all envs) | Swap env var names and values | + +--- + +## 1. Dependencies (`backend/requirements.txt`) + +**Remove:** +``` +sendgrid==6.11.0 +``` + +**Add:** +``` +resend==2.0.0 +``` + +After changing, rebuild: `docker compose build backend` + +--- + +## 2. Settings (`backend/bible_app/settings.py`, lines 187-190) + +**Current:** +```python +SENDGRID_API_KEY = os.environ.get('SENDGRID_API_KEY') +SENDGRID_FROM_EMAIL = os.environ.get('SENDGRID_FROM_EMAIL', 'noreply@biblememorization.com') +``` + +**Change to:** +```python +RESEND_API_KEY = os.environ.get('RESEND_API_KEY') +RESEND_FROM_EMAIL = os.environ.get('RESEND_FROM_EMAIL', 'onboarding@resend.dev') +``` + +--- + +## 3. Email Utility (`backend/api/utils/email.py`) + +### Imports (lines 8-9) + +**Current:** +```python +from sendgrid import SendGridAPIClient +from sendgrid.helpers.mail import Mail +``` + +**Change to:** +```python +import resend +``` + +### `send_verification_email()` - sending logic (lines 125-146) + +**Current:** +```python +if not settings.SENDGRID_API_KEY or settings.SENDGRID_API_KEY == 'your_sendgrid_api_key_here': + logger.warning(f"SendGrid API key not configured. Verification email for {user.email} not sent.") + logger.info(f"Verification URL (for testing): {verification_url}") + return (False, "SendGrid API key not configured. Check server logs for verification URL.") + +message = Mail( + from_email=settings.SENDGRID_FROM_EMAIL, + to_emails=user.email, + subject=subject, + plain_text_content=plain_content, + html_content=html_content +) + +sg = SendGridAPIClient(settings.SENDGRID_API_KEY) +response = sg.send(message) + +if response.status_code in [200, 201, 202]: + logger.info(f"Verification email sent successfully to {user.email}") + return (True, None) +else: + logger.error(f"SendGrid returned status {response.status_code} for {user.email}") + return (False, f"Email service returned status {response.status_code}") +``` + +**Change to:** +```python +if not settings.RESEND_API_KEY: + logger.warning(f"Resend API key not configured. Verification email for {user.email} not sent.") + logger.info(f"Verification URL (for testing): {verification_url}") + return (False, "Resend API key not configured. Check server logs for verification URL.") + +resend.api_key = settings.RESEND_API_KEY + +params = { + "from": settings.RESEND_FROM_EMAIL, + "to": [user.email], + "subject": subject, + "html": html_content, + "text": plain_content, +} + +response = resend.Emails.send(params) + +if response.get("id"): + logger.info(f"Verification email sent successfully to {user.email}, resend_id={response.get('id')}") + return (True, None) +else: + logger.error(f"Resend failed for {user.email}: {response}") + return (False, "Email service failed to send") +``` + +### `send_password_reset_email()` - sending logic (lines 252-273) + +Apply the same pattern as above. + +--- + +## 4. Environment Variables + +### Add to all environments: +``` +RESEND_API_KEY=re_xxxxxxxxx +RESEND_FROM_EMAIL=onboarding@resend.dev +``` + +### Remove from all environments: +``` +SENDGRID_API_KEY +SENDGRID_FROM_EMAIL +``` + +--- + +## 5. Error Handling Differences + +| SendGrid | Resend | +|----------|--------| +| Returns HTTP status codes (200, 201, 202) | Returns `{"id": "..."}` on success | +| Raises `SendGridException` on errors | Raises `resend.exceptions.ResendError` on errors | + +--- + +## 6. Sandbox Limitations + +When using `onboarding@resend.dev`: +- Can only send to verified email addresses +- All 5 dev team members need to add their email to Resend verified list +- Rate limit: 3k emails/month (~100/day) + +--- + +## 7. Testing Checklist + +- [ ] Verification email sends in dev +- [ ] Verification email sends in staging +- [ ] Verification email sends in production +- [ ] Password reset email sends (requires endpoint - separate ticket) + +--- + +## Related Tickets + +### Password Reset Endpoint (Not Yet Implemented) + +The `send_password_reset_email()` function exists in `backend/api/utils/email.py:153-277` but has NO endpoint in views.py. + +**Needs:** +- `POST /api/password-reset/request/` - takes email, sends reset link +- `POST /api/password-reset/confirm/` - takes token + new password + +--- + +## Follow-up Ticket + +Domain + DNS configuration when domain acquired (separate ticket for production `from` email). From e4776d25780299780cbb7c0173fb73981d11d444 Mon Sep 17 00:00:00 2001 From: Derek Malone Date: Tue, 27 Jan 2026 21:16:29 -0500 Subject: [PATCH 2/2] feat: migrate email service from SendGrid to Resend - Replace sendgrid package with resend in requirements.txt - Update settings.py with RESEND_* env var references - Refactor send functions in email.py to use Resend API - Update documentation with migration status and domain TODO --- .claude/CLAUDE.md | 1 + .claude/EMAIL_MIGRATION.md | 171 +++++++--------------------------- backend/api/utils/email.py | 77 +++++++-------- backend/bible_app/settings.py | 6 +- backend/requirements.txt | 2 +- 5 files changed, 78 insertions(+), 179 deletions(-) diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 1b23cef..d629e5d 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -121,6 +121,7 @@ docker compose exec backend python manage.py test api.tests.test_fetch_bible_dat - **All backend code changes auto-sync to Docker** via volume mount - **Dependencies:** After adding to `requirements.txt`, rebuild with `docker compose build backend` - **Frontend dependencies:** After adding to `package.json`, run `npm install` in frontend directory or rebuild with `docker compose build frontend` +- **Email Service:** Resend (https://resend.com). Currently using sandbox `onboarding@resend.dev`. **TODO:** Update `RESEND_FROM_EMAIL` once a custom domain is acquired. - **Run tests before creating PRs** --- diff --git a/.claude/EMAIL_MIGRATION.md b/.claude/EMAIL_MIGRATION.md index 27517a0..eb0d77d 100644 --- a/.claude/EMAIL_MIGRATION.md +++ b/.claude/EMAIL_MIGRATION.md @@ -1,187 +1,84 @@ # SendGrid to Resend Migration Guide -## Overview - -Migrating email service from SendGrid to Resend for verification and password reset emails. +## Status: Code Complete **Resend Docs:** https://resend.com/docs/send-with-python --- -## Files to Modify +## Completed Code Changes -| File | Changes | -|------|---------| -| `backend/requirements.txt` | Swap sendgrid → resend | -| `backend/bible_app/settings.py` | Rename 2 env var references | -| `backend/api/utils/email.py` | Change imports, update send logic in 2 functions | -| `.env` (all envs) | Swap env var names and values | +| File | Status | +|------|--------| +| `backend/requirements.txt` | ✅ `sendgrid==6.11.0` → `resend==2.0.0` | +| `backend/bible_app/settings.py` | ✅ Env vars renamed to `RESEND_*` | +| `backend/api/utils/email.py` | ✅ Both send functions updated | --- -## 1. Dependencies (`backend/requirements.txt`) +## Next Steps (Your Action Required) -**Remove:** -``` -sendgrid==6.11.0 -``` +### 1. Update Environment Variables -**Add:** +**Add to your `.env`:** ``` -resend==2.0.0 +RESEND_API_KEY=re_xxxxxxxxx +RESEND_FROM_EMAIL=onboarding@resend.dev ``` -After changing, rebuild: `docker compose build backend` - ---- - -## 2. Settings (`backend/bible_app/settings.py`, lines 187-190) - -**Current:** -```python -SENDGRID_API_KEY = os.environ.get('SENDGRID_API_KEY') -SENDGRID_FROM_EMAIL = os.environ.get('SENDGRID_FROM_EMAIL', 'noreply@biblememorization.com') +**Remove from your `.env`:** ``` - -**Change to:** -```python -RESEND_API_KEY = os.environ.get('RESEND_API_KEY') -RESEND_FROM_EMAIL = os.environ.get('RESEND_FROM_EMAIL', 'onboarding@resend.dev') +SENDGRID_API_KEY +SENDGRID_FROM_EMAIL ``` ---- - -## 3. Email Utility (`backend/api/utils/email.py`) +### 2. Resend Dashboard Setup -### Imports (lines 8-9) +1. Go to https://resend.com/api-keys and create an API key +2. Go to https://resend.com/audiences and add your email as a verified recipient (required for sandbox) +3. Have all dev team members add their emails to the verified list -**Current:** -```python -from sendgrid import SendGridAPIClient -from sendgrid.helpers.mail import Mail -``` - -**Change to:** -```python -import resend -``` +### 3. Restart Backend -### `send_verification_email()` - sending logic (lines 125-146) - -**Current:** -```python -if not settings.SENDGRID_API_KEY or settings.SENDGRID_API_KEY == 'your_sendgrid_api_key_here': - logger.warning(f"SendGrid API key not configured. Verification email for {user.email} not sent.") - logger.info(f"Verification URL (for testing): {verification_url}") - return (False, "SendGrid API key not configured. Check server logs for verification URL.") - -message = Mail( - from_email=settings.SENDGRID_FROM_EMAIL, - to_emails=user.email, - subject=subject, - plain_text_content=plain_content, - html_content=html_content -) - -sg = SendGridAPIClient(settings.SENDGRID_API_KEY) -response = sg.send(message) - -if response.status_code in [200, 201, 202]: - logger.info(f"Verification email sent successfully to {user.email}") - return (True, None) -else: - logger.error(f"SendGrid returned status {response.status_code} for {user.email}") - return (False, f"Email service returned status {response.status_code}") +After updating `.env`: +```bash +docker compose down && docker compose up -d ``` -**Change to:** -```python -if not settings.RESEND_API_KEY: - logger.warning(f"Resend API key not configured. Verification email for {user.email} not sent.") - logger.info(f"Verification URL (for testing): {verification_url}") - return (False, "Resend API key not configured. Check server logs for verification URL.") - -resend.api_key = settings.RESEND_API_KEY - -params = { - "from": settings.RESEND_FROM_EMAIL, - "to": [user.email], - "subject": subject, - "html": html_content, - "text": plain_content, -} - -response = resend.Emails.send(params) - -if response.get("id"): - logger.info(f"Verification email sent successfully to {user.email}, resend_id={response.get('id')}") - return (True, None) -else: - logger.error(f"Resend failed for {user.email}: {response}") - return (False, "Email service failed to send") -``` - -### `send_password_reset_email()` - sending logic (lines 252-273) - -Apply the same pattern as above. - ---- - -## 4. Environment Variables +### 4. Test -### Add to all environments: -``` -RESEND_API_KEY=re_xxxxxxxxx -RESEND_FROM_EMAIL=onboarding@resend.dev -``` - -### Remove from all environments: -``` -SENDGRID_API_KEY -SENDGRID_FROM_EMAIL -``` +- Register a new account to trigger verification email +- Check Resend dashboard for send logs: https://resend.com/emails --- -## 5. Error Handling Differences +## Testing Checklist -| SendGrid | Resend | -|----------|--------| -| Returns HTTP status codes (200, 201, 202) | Returns `{"id": "..."}` on success | -| Raises `SendGridException` on errors | Raises `resend.exceptions.ResendError` on errors | +- [x] Verification email sends in dev +- [ ] Verification email sends in staging +- [ ] Verification email sends in production +- [ ] Password reset email sends (requires endpoint - separate ticket) --- -## 6. Sandbox Limitations +## Sandbox Limitations When using `onboarding@resend.dev`: - Can only send to verified email addresses -- All 5 dev team members need to add their email to Resend verified list - Rate limit: 3k emails/month (~100/day) --- -## 7. Testing Checklist - -- [ ] Verification email sends in dev -- [ ] Verification email sends in staging -- [ ] Verification email sends in production -- [ ] Password reset email sends (requires endpoint - separate ticket) - ---- - ## Related Tickets ### Password Reset Endpoint (Not Yet Implemented) -The `send_password_reset_email()` function exists in `backend/api/utils/email.py:153-277` but has NO endpoint in views.py. +The `send_password_reset_email()` function exists in `backend/api/utils/email.py` but has NO endpoint in views.py. **Needs:** - `POST /api/password-reset/request/` - takes email, sends reset link - `POST /api/password-reset/confirm/` - takes token + new password ---- - -## Follow-up Ticket +### Domain Configuration Domain + DNS configuration when domain acquired (separate ticket for production `from` email). diff --git a/backend/api/utils/email.py b/backend/api/utils/email.py index 5ad6ae3..40415a1 100644 --- a/backend/api/utils/email.py +++ b/backend/api/utils/email.py @@ -5,8 +5,7 @@ from django.contrib.auth.tokens import default_token_generator from django.utils.http import urlsafe_base64_encode from django.utils.encoding import force_bytes -from sendgrid import SendGridAPIClient -from sendgrid.helpers.mail import Mail +import resend logger = logging.getLogger(__name__) @@ -121,29 +120,30 @@ def send_verification_email(user): © 2025 Bible Memorization App. All rights reserved. """ - # Send email via SendGrid - if not settings.SENDGRID_API_KEY or settings.SENDGRID_API_KEY == 'your_sendgrid_api_key_here': - logger.warning(f"SendGrid API key not configured. Verification email for {user.email} not sent.") + # Send email via Resend + if not settings.RESEND_API_KEY: + logger.warning(f"Resend API key not configured. Verification email for {user.email} not sent.") logger.info(f"Verification URL (for testing): {verification_url}") - return (False, "SendGrid API key not configured. Check server logs for verification URL.") - - message = Mail( - from_email=settings.SENDGRID_FROM_EMAIL, - to_emails=user.email, - subject=subject, - plain_text_content=plain_content, - html_content=html_content - ) + return (False, "Resend API key not configured. Check server logs for verification URL.") + + resend.api_key = settings.RESEND_API_KEY + + params = { + "from": settings.RESEND_FROM_EMAIL, + "to": [user.email], + "subject": subject, + "html": html_content, + "text": plain_content, + } - sg = SendGridAPIClient(settings.SENDGRID_API_KEY) - response = sg.send(message) + response = resend.Emails.send(params) - if response.status_code in [200, 201, 202]: - logger.info(f"Verification email sent successfully to {user.email}") + if response.get("id"): + logger.info(f"Verification email sent successfully to {user.email}, resend_id={response.get('id')}") return (True, None) else: - logger.error(f"SendGrid returned status {response.status_code} for {user.email}") - return (False, f"Email service returned status {response.status_code}") + logger.error(f"Resend failed for {user.email}: {response}") + return (False, "Email service failed to send") except Exception as e: logger.error(f"Error sending verification email to {user.email}: {str(e)}") @@ -248,29 +248,30 @@ def send_password_reset_email(user, reset_url): © 2025 Bible Memorization App. All rights reserved. """ - # Send email via SendGrid - if not settings.SENDGRID_API_KEY or settings.SENDGRID_API_KEY == 'your_sendgrid_api_key_here': - logger.warning(f"SendGrid API key not configured. Password reset email for {user.email} not sent.") + # Send email via Resend + if not settings.RESEND_API_KEY: + logger.warning(f"Resend API key not configured. Password reset email for {user.email} not sent.") logger.info(f"Password reset URL (for testing): {reset_url}") - return (False, "SendGrid API key not configured. Check server logs for reset URL.") - - message = Mail( - from_email=settings.SENDGRID_FROM_EMAIL, - to_emails=user.email, - subject=subject, - plain_text_content=plain_content, - html_content=html_content - ) + return (False, "Resend API key not configured. Check server logs for reset URL.") + + resend.api_key = settings.RESEND_API_KEY + + params = { + "from": settings.RESEND_FROM_EMAIL, + "to": [user.email], + "subject": subject, + "html": html_content, + "text": plain_content, + } - sg = SendGridAPIClient(settings.SENDGRID_API_KEY) - response = sg.send(message) + response = resend.Emails.send(params) - if response.status_code in [200, 201, 202]: - logger.info(f"Password reset email sent successfully to {user.email}") + if response.get("id"): + logger.info(f"Password reset email sent successfully to {user.email}, resend_id={response.get('id')}") return (True, None) else: - logger.error(f"SendGrid returned status {response.status_code} for {user.email}") - return (False, f"Email service returned status {response.status_code}") + logger.error(f"Resend failed for {user.email}: {response}") + return (False, "Email service failed to send") except Exception as e: logger.error(f"Error sending password reset email to {user.email}: {str(e)}") diff --git a/backend/bible_app/settings.py b/backend/bible_app/settings.py index 84ef44b..713c37a 100644 --- a/backend/bible_app/settings.py +++ b/backend/bible_app/settings.py @@ -184,7 +184,7 @@ # Email verification settings PASSWORD_RESET_TIMEOUT = 60 * 60 * 24 # 24 hours in seconds -# SendGrid configuration -SENDGRID_API_KEY = os.environ.get('SENDGRID_API_KEY') -SENDGRID_FROM_EMAIL = os.environ.get('SENDGRID_FROM_EMAIL', 'noreply@biblememorization.com') +# Resend configuration +RESEND_API_KEY = os.environ.get('RESEND_API_KEY') +RESEND_FROM_EMAIL = os.environ.get('RESEND_FROM_EMAIL', 'onboarding@resend.dev') FRONTEND_URL = os.environ.get('FRONTEND_URL', 'http://localhost:3000') diff --git a/backend/requirements.txt b/backend/requirements.txt index afab61c..b23ebd1 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -11,5 +11,5 @@ google-auth google-auth-oauthlib google-auth-httplib2 django-ratelimit==4.1.0 -sendgrid==6.11.0 +resend==2.0.0 Pillow>=10.0.0