-
Notifications
You must be signed in to change notification settings - Fork 26
Add Resend List Emails API for recipient-based status lookup #187
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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 | ||||||||||||||||||||||||||
|
|
@@ -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, | ||||||||||||||||||||||||||
|
|
@@ -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, | ||||||||||||||||||||||||||
|
|
@@ -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 | ||||||||||||||||||||||||||
|
Comment on lines
+2058
to
+2061
|
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| # 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) | ||||||||||||||||||||||||||
|
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) | |
| # Cache the full index with 300s TTL, including empty results | |
| set_cached(cache_key, { | |
| 'emails_by_recipient': index, | |
| 'total_fetched': total_fetched | |
| }, ttl=300) |
There was a problem hiding this comment.
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 atmax_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 untilhas_moreis false (with a configurable safety cap) or returning an explicittruncated/pages_fetchedflag and documenting the limit.