diff --git a/api/volunteers/volunteers_views.py b/api/volunteers/volunteers_views.py index a2a8cfe..8c8fc8d 100644 --- a/api/volunteers/volunteers_views.py +++ b/api/volunteers/volunteers_views.py @@ -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) diff --git a/services/volunteers_service.py b/services/volunteers_service.py index ed5e3d0..cd1a923 100644 --- a/services/volunteers_service.py +++ b/services/volunteers_service.py @@ -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: @@ -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 = {} @@ -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: @@ -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 }