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
14 changes: 11 additions & 3 deletions api/volunteers/volunteers_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -659,13 +659,21 @@ def admin_get_resend_email_statuses():
return _error_response(f"Failed to fetch email statuses: {str(e)}")


@bp.route('/admin/emails/resend-list', methods=['GET'])
@bp.route('/admin/emails/resend-list', methods=['POST'])
@auth.require_org_member_with_permission("volunteer.admin", req_to_org_id=getOrgId)
def admin_list_resend_emails():
"""Admin endpoint to list all sent emails from Resend, indexed by recipient."""
try:
emails_param = request.args.get('emails', '')
filter_emails = [e.strip() for e in emails_param.split(',') if e.strip()] if emails_param else None
body = request.get_json(silent=True) or {}
emails_param = body.get('emails', [])
if isinstance(emails_param, str):
parsed = [e.strip() for e in emails_param.split(',') if e.strip()]
filter_emails = parsed if parsed else None
elif isinstance(emails_param, list):
parsed = [e.strip() for e in emails_param if isinstance(e, str) and e.strip()]
filter_emails = parsed if parsed else None
else:
filter_emails = None

logger.info("Listing Resend emails, filter_count=%d", len(filter_emails) if filter_emails else 0)

Expand Down
41 changes: 31 additions & 10 deletions services/volunteers_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -2028,13 +2028,17 @@ def list_all_resend_emails(filter_emails=None):
'success': True,
'emails_by_recipient': index,
'total_fetched': cached['total_fetched'],
'truncated': cached.get('truncated', False),
'from_cache': True
}

# Paginate through resend.Emails.list() (requires resend >= 2.5)
# Safety cap of 100 pages (~10,000 emails); for-else detects if we hit it.
all_emails = []
max_pages = 10
max_pages = 100
params = {"limit": 100}
page_error_occurred = False
truncated = False

for page in range(max_pages):
try:
Expand All @@ -2058,7 +2062,22 @@ def list_all_resend_emails(filter_emails=None):
except Exception as page_error:
warning(logger, "Error fetching Resend emails page",
page=page, exc_info=page_error)
page_error_occurred = True
break
else:
# Loop exhausted max_pages without a natural break — results are truncated.
truncated = True
warning(logger, "Resend email pagination hit safety cap",
max_pages=max_pages, total_so_far=len(all_emails))

# If a page fetch failed, return an error rather than caching partial data.
if page_error_occurred:
return {
'success': False,
'error': 'Failed to fetch all email pages from Resend',
'emails_by_recipient': {},
'total_fetched': len(all_emails)
}

# Build index by recipient email
index = {}
Expand Down Expand Up @@ -2094,16 +2113,17 @@ def list_all_resend_emails(filter_emails=None):

total_fetched = len(all_emails)

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

info(logger, "Built Resend email index",
total_fetched=total_fetched, unique_recipients=len(index))
total_fetched=total_fetched, unique_recipients=len(index),
truncated=truncated)

# Cache the full index with 300s TTL, including empty results so we
# don't hammer the Resend API on every request when there are no emails.
set_cached(cache_key, {
'emails_by_recipient': index,
'total_fetched': total_fetched,
'truncated': truncated
}, ttl=300)

# Filter if requested
if filter_emails:
Expand All @@ -2114,6 +2134,7 @@ def list_all_resend_emails(filter_emails=None):
'success': True,
'emails_by_recipient': index,
'total_fetched': total_fetched,
'truncated': truncated,
'from_cache': False
}

Expand Down