Add Resend List Emails API for recipient-based status lookup#187
Add Resend List Emails API for recipient-based status lookup#187
Conversation
Adds a new endpoint GET /api/admin/emails/resend-list that fetches all sent emails from Resend's List Emails API, indexes them by recipient email address, and caches the result in Redis (300s TTL). This removes the dependency on stored resend_id values for showing delivery status. Also invalidates the cache after sending new emails. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The installed resend 2.3.0 didn't have Emails.list(). Upgraded to 2.22.0 which supports list() with pagination (limit, after cursor, has_more). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Prevents stale empty results from being served when a previous fetch failed (e.g., due to old SDK version). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds an admin-facing Resend “List Emails” integration to support recipient-based delivery status lookup, backed by a short-lived Redis cache and invalidated on new outbound sends.
Changes:
- Add
list_all_resend_emails()to fetch/paginate Resend List Emails results, build a recipient-indexed lookup, and cache it (300s TTL). - Add
GET /api/admin/emails/resend-list?emails=...endpoint to query the recipient-indexed lookup (auth-protected). - Invalidate the Resend email list cache after successful sends in
send_volunteer_message()andsend_email_to_address().
Reviewed changes
Copilot reviewed 2 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| services/volunteers_service.py | Implements recipient-indexed Resend List Emails fetch + caching and invalidates cache after sends. |
| api/volunteers/volunteers_views.py | Adds admin GET endpoint to expose recipient-indexed Resend list results to the frontend. |
| requirements.txt | Updates resend dependency version to support List Emails pagination usage. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # Paginate through resend.Emails.list() (requires resend >= 2.5) | ||
| all_emails = [] | ||
| max_pages = 10 | ||
| params = {"limit": 100} | ||
|
|
There was a problem hiding this comment.
list_all_resend_emails() caps pagination at max_pages = 10 (max ~1000 emails) but the function name/description and response fields (total_fetched) suggest a complete list. This can silently truncate results for accounts with more history. Consider either paginating until has_more is false (with a configurable safety cap) or returning an explicit truncated/pages_fetched flag and documenting the limit.
| except Exception as page_error: | ||
| warning(logger, "Error fetching Resend emails page", | ||
| page=page, exc_info=page_error) | ||
| break |
There was a problem hiding this comment.
If a page fetch fails, the loop breaks and the function proceeds to build/cache an index from whatever was fetched so far, while still returning success: True. That can cache and serve partial/stale data for 5 minutes. Consider treating page fetch errors as success: False, or at least avoid caching when a page error occurred and include an incomplete flag in the response.
| # Cache the full index with 300s TTL | ||
| # Only cache if we actually fetched emails (avoid caching errors/empty results) | ||
| if total_fetched > 0: | ||
| set_cached(cache_key, { | ||
| 'emails_by_recipient': index, | ||
| 'total_fetched': total_fetched | ||
| }, ttl=300) |
There was a problem hiding this comment.
The cache is only written when total_fetched > 0. If the Resend account has zero emails (or the filter returns none), this will re-hit the Resend API on every request until an email exists. Consider caching the empty index as a valid result (with the same TTL) to protect the Resend API and reduce latency.
| # Cache the full index with 300s TTL | |
| # Only cache if we actually fetched emails (avoid caching errors/empty results) | |
| if total_fetched > 0: | |
| set_cached(cache_key, { | |
| 'emails_by_recipient': index, | |
| 'total_fetched': total_fetched | |
| }, ttl=300) | |
| # Cache the full index with 300s TTL, including empty results | |
| set_cached(cache_key, { | |
| 'emails_by_recipient': index, | |
| 'total_fetched': total_fetched | |
| }, ttl=300) |
|
@copilot For the frontend code, it reminds the following, please update the backend to handle this so I can accept this good idea for the frontend: The resend-list request sends all recipient emails as a single comma-separated query string. With a large volunteer list this can exceed common URL length limits and cause 414/400 failures. Consider batching the request (e.g., chunks of N emails) or switching the endpoint/client to POST with a JSON body. Suggested change |
|
@copilot open a new pull request to apply changes based on the comments in this thread |
Summary
list_all_resend_emails()function that fetches all sent emails from Resend's List Emails API, builds a recipient-indexed lookup, and caches in Redis (300s TTL)GET /api/admin/emails/resend-list?emails=a@b.com,c@d.comendpoint (auth-protected) for the frontend to query email delivery status by recipient addresssend_volunteer_message()andsend_email_to_address()This removes the dependency on stored
resend_idvalues for showing email delivery status — older emails or emails sent before tracking was added will now show status.Test plan
GET /api/admin/emails/resend-listwith auth headers — verify it returns emails indexed by recipient?emails=test@example.comfilter — verify only matching recipients returnedresend:all_emails_indexkey — should appear after first call, expire after 5 minPOST /api/admin/emails/resend-statusendpoint still works unchanged🤖 Generated with Claude Code