Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**

---
Expand Down
84 changes: 84 additions & 0 deletions .claude/EMAIL_MIGRATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# SendGrid to Resend Migration Guide

## Status: Code Complete

**Resend Docs:** https://resend.com/docs/send-with-python

---

## Completed Code Changes

| 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 |

---

## Next Steps (Your Action Required)

### 1. Update Environment Variables

**Add to your `.env`:**
```
RESEND_API_KEY=re_xxxxxxxxx
RESEND_FROM_EMAIL=onboarding@resend.dev
```

**Remove from your `.env`:**
```
SENDGRID_API_KEY
SENDGRID_FROM_EMAIL
```

### 2. Resend Dashboard Setup

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

### 3. Restart Backend

After updating `.env`:
```bash
docker compose down && docker compose up -d
```

### 4. Test

- Register a new account to trigger verification email
- Check Resend dashboard for send logs: https://resend.com/emails

---

## Testing Checklist

- [x] Verification email sends in dev
- [ ] Verification email sends in staging
- [ ] Verification email sends in production
- [ ] Password reset email sends (requires endpoint - separate ticket)

---

## Sandbox Limitations

When using `onboarding@resend.dev`:
- Can only send to verified email addresses
- Rate limit: 3k emails/month (~100/day)

---

## Related Tickets

### Password Reset Endpoint (Not Yet Implemented)

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

### Domain Configuration

Domain + DNS configuration when domain acquired (separate ticket for production `from` email).
77 changes: 39 additions & 38 deletions backend/api/utils/email.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand Down Expand Up @@ -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)}")
Expand Down Expand Up @@ -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)}")
Expand Down
6 changes: 3 additions & 3 deletions backend/bible_app/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
2 changes: 1 addition & 1 deletion backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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