Skip to content

Add Resend List Emails API for recipient-based status lookup#187

Merged
gregv merged 3 commits intodevelopfrom
feature/resend-list-emails-api
Mar 6, 2026
Merged

Add Resend List Emails API for recipient-based status lookup#187
gregv merged 3 commits intodevelopfrom
feature/resend-list-emails-api

Conversation

@gregv
Copy link
Contributor

@gregv gregv commented Feb 22, 2026

Summary

  • Adds 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)
  • Adds GET /api/admin/emails/resend-list?emails=a@b.com,c@d.com endpoint (auth-protected) for the frontend to query email delivery status by recipient address
  • Invalidates the Resend email list cache after sending emails in send_volunteer_message() and send_email_to_address()

This removes the dependency on stored resend_id values for showing email delivery status — older emails or emails sent before tracking was added will now show status.

Test plan

  • Call GET /api/admin/emails/resend-list with auth headers — verify it returns emails indexed by recipient
  • Call with ?emails=test@example.com filter — verify only matching recipients returned
  • Check Redis for resend:all_emails_index key — should appear after first call, expire after 5 min
  • Send a new email via the admin panel, verify cache invalidation (key deleted)
  • Verify the existing POST /api/admin/emails/resend-status endpoint still works unchanged

🤖 Generated with Claude Code

gregv and others added 3 commits February 22, 2026 10:46
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>
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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() and send_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.

Comment on lines +2034 to +2038
# Paginate through resend.Emails.list() (requires resend >= 2.5)
all_emails = []
max_pages = 10
params = {"limit": 100}

Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +2058 to +2061
except Exception as page_error:
warning(logger, "Error fetching Resend emails page",
page=page, exc_info=page_error)
break
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +2097 to +2103
# 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)
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
# 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 uses AI. Check for mistakes.
@gregv
Copy link
Contributor Author

gregv commented Mar 6, 2026

@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
${process.env.NEXT_PUBLIC_API_SERVER_URL}/api/admin/emails/resend-list?emails=${encodeURIComponent(uniqueEmails.join(','))},
{
headers: {
Authorization: Bearer ${accessToken},
'X-Org-Id': orgId,
},
${process.env.NEXT_PUBLIC_API_SERVER_URL}/api/admin/emails/resend-list,
{
method: 'POST',
headers: {
Authorization: Bearer ${accessToken},
'Content-Type': 'application/json',
'X-Org-Id': orgId,
},
body: JSON.stringify({ emails: uniqueEmails }),

Copy link
Contributor

Copilot AI commented Mar 6, 2026

@gregv I've opened a new pull request, #188, to work on those changes. Once the pull request is ready, I'll request review from you.

@gregv
Copy link
Contributor Author

gregv commented Mar 6, 2026

@copilot open a new pull request to apply changes based on the comments in this thread

Copy link
Contributor

Copilot AI commented Mar 6, 2026

@gregv I've opened a new pull request, #191, to work on those changes. Once the pull request is ready, I'll request review from you.

@gregv gregv merged commit 1321abd into develop Mar 6, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants