Skip to content
Open
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
25 changes: 25 additions & 0 deletions api/volunteers/volunteers_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
send_volunteer_message,
send_email_to_address,
get_resend_email_statuses,
list_all_resend_emails,
)
from common.auth import auth, auth_user

Expand Down Expand Up @@ -656,3 +657,27 @@ def admin_get_resend_email_statuses():
logger.error("Error in admin_get_resend_email_statuses: %s", str(e))
logger.exception(e)
return _error_response(f"Failed to fetch email statuses: {str(e)}")


@bp.route('/admin/emails/resend-list', methods=['GET'])
@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

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

result = list_all_resend_emails(filter_emails=filter_emails)

if result['success']:
return _success_response(result, "Resend email list fetched successfully")

logger.error("Failed to list Resend emails. Error: %s", result.get('error', 'Unknown error'))
return _error_response(result.get('error', 'Unknown error'), 500)

except Exception as e:
logger.error("Error in admin_list_resend_emails: %s", str(e))
logger.exception(e)
return _error_response(f"Failed to list Resend emails: {str(e)}")
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ et-xmlfile==1.1.0
openpyxl==3.1.2
pylint==3.2.5
pytest==8.2.2
resend==2.3.0
resend==2.22.0
readme-metrics[Flask]==3.1.0
redis>=6.1.0
tiktoken==0.9.0
Expand Down
133 changes: 132 additions & 1 deletion services/volunteers_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from google.cloud import firestore
from common.utils.slack import get_slack_user_by_email, send_slack
from common.log import get_logger, info, debug, warning, error, exception
from common.utils.redis_cache import redis_cached, delete_cached, clear_pattern
from common.utils.redis_cache import redis_cached, delete_cached, clear_pattern, get_cached, set_cached
from common.utils.oauth_providers import SLACK_PREFIX, normalize_slack_user_id, is_oauth_user_id, is_slack_user_id, extract_slack_user_id
import os
import requests
Expand Down Expand Up @@ -1791,6 +1791,10 @@ def send_volunteer_message(
# Determine success based on whether at least one message was sent
success = delivery_status['slack_sent'] or delivery_status['email_sent']

# Invalidate Resend email list cache so next fetch picks up the new email
if delivery_status['email_sent']:
delete_cached("resend:all_emails_index")

result = {
'success': success,
'volunteer_id': volunteer_id,
Expand Down Expand Up @@ -1925,6 +1929,10 @@ def send_email_to_address(
}
)

# Invalidate Resend email list cache so next fetch picks up the new email
if email_success:
delete_cached("resend:all_emails_index")

result = {
'success': email_success,
'recipient_email': email,
Expand Down Expand Up @@ -1989,3 +1997,126 @@ def get_resend_email_statuses(email_ids: list) -> Dict[str, Any]:
except Exception as e:
error(logger, "Error fetching Resend email statuses", exc_info=e)
return {'success': False, 'error': str(e)}


def list_all_resend_emails(filter_emails=None):
"""
Fetch all sent emails from Resend's List Emails API, cached in Redis.
Returns an index of {recipient_email: [{id, subject, created_at, last_event}, ...]}.

Args:
filter_emails: Optional list of email addresses to filter results for.

Returns:
Dict with 'success', 'emails_by_recipient', and 'total_fetched'.
"""
try:
resend.api_key = os.environ.get('RESEND_EMAIL_STATUS_KEY')
if not resend.api_key:
return {'success': False, 'error': 'Resend API key not configured'}

cache_key = "resend:all_emails_index"
cached = get_cached(cache_key)
if cached is not None:
info(logger, "Using cached Resend email index",
total_emails=cached.get('total_fetched', 0))
index = cached['emails_by_recipient']
if filter_emails:
filter_set = {e.lower() for e in filter_emails}
index = {k: v for k, v in index.items() if k in filter_set}
return {
'success': True,
'emails_by_recipient': index,
'total_fetched': cached['total_fetched'],
'from_cache': True
}

# Paginate through resend.Emails.list() (requires resend >= 2.5)
all_emails = []
max_pages = 10
params = {"limit": 100}

for page in range(max_pages):
try:
response = resend.Emails.list(params)
email_list = response.get('data', []) if isinstance(response, dict) else getattr(response, 'data', [])
all_emails.extend(email_list)
info(logger, "Fetched Resend emails page",
page=page, count=len(email_list), total_so_far=len(all_emails))

has_more = response.get('has_more', False) if isinstance(response, dict) else getattr(response, 'has_more', False)
if not has_more or len(email_list) == 0:
break

# Use last email ID as cursor for next page
last_item = email_list[-1]
last_id = last_item.get('id', '') if isinstance(last_item, dict) else getattr(last_item, 'id', '')
if not last_id:
break
params = {"limit": 100, "after": last_id}

except Exception as page_error:
warning(logger, "Error fetching Resend emails page",
page=page, exc_info=page_error)
break

# Build index by recipient email
index = {}
for email_data in all_emails:
if isinstance(email_data, dict):
recipients = email_data.get('to', [])
email_id = email_data.get('id', '')
subject = email_data.get('subject', '')
created_at = email_data.get('created_at', '')
last_event = email_data.get('last_event', '')
else:
recipients = getattr(email_data, 'to', [])
email_id = getattr(email_data, 'id', '')
subject = getattr(email_data, 'subject', '')
created_at = getattr(email_data, 'created_at', '')
last_event = getattr(email_data, 'last_event', '')

if isinstance(recipients, str):
recipients = [recipients]

entry = {
'id': email_id,
'subject': subject,
'created_at': created_at,
'last_event': last_event,
}

for recipient in recipients:
key = recipient.lower().strip()
if key not in index:
index[key] = []
index[key].append(entry)

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

# Filter if requested
if filter_emails:
filter_set = {e.lower() for e in filter_emails}
index = {k: v for k, v in index.items() if k in filter_set}

return {
'success': True,
'emails_by_recipient': index,
'total_fetched': total_fetched,
'from_cache': False
}

except Exception as e:
error(logger, "Error listing Resend emails", exc_info=e)
return {'success': False, 'error': str(e)}