From 4e0584a03d053170eafbb5de9487a34ce70e6e14 Mon Sep 17 00:00:00 2001 From: Derich Pacheco Date: Sun, 19 Oct 2025 21:45:53 -0300 Subject: [PATCH 1/5] feat: inbound get --- examples/receiving_email.py | 58 +++++++++++++++ resend/__init__.py | 5 ++ resend/emails/_emails.py | 2 + resend/emails/_received_email.py | 118 +++++++++++++++++++++++++++++++ resend/emails/_receiving.py | 28 ++++++++ tests/emails_test.py | 83 ++++++++++++++++++++++ 6 files changed, 294 insertions(+) create mode 100644 examples/receiving_email.py create mode 100644 resend/emails/_received_email.py create mode 100644 resend/emails/_receiving.py diff --git a/examples/receiving_email.py b/examples/receiving_email.py new file mode 100644 index 0000000..bf1d7bf --- /dev/null +++ b/examples/receiving_email.py @@ -0,0 +1,58 @@ +import os + +import resend + +if not os.environ["RESEND_API_KEY"]: + raise EnvironmentError("RESEND_API_KEY is missing") + +# Retrieve a single received email by ID +email_id = "006e2796-ff6a-4436-91ad-0429e600bf8a" + +received_email: resend.ReceivedEmail = resend.Emails.Receiving.get( + email_id=email_id +) + +print(f"Retrieved received email: {received_email['id']}") +print("\n--- Email Details ---") +print(f"From: {received_email['from']}") +print(f"To: {received_email['to']}") +print(f"Subject: {received_email['subject']}") +print(f"Created at: {received_email['created_at']}") +print(f"Object type: {received_email['object']}") + +print("\n--- Email Content ---") +if received_email.get('html'): + print(f"HTML: {received_email['html'][:100]}...") # Show first 100 chars +else: + print("HTML: None") + +if received_email.get('text'): + print(f"Text: {received_email['text'][:100]}...") # Show first 100 chars +else: + print("Text: None") + +print("\n--- Recipients ---") +print(f"CC: {received_email.get('cc', [])}") +print(f"BCC: {received_email.get('bcc', [])}") +print(f"Reply-To: {received_email.get('reply_to', [])}") + +print("\n--- Headers ---") +if received_email.get('headers'): + for header_name, header_value in received_email['headers'].items(): + print(f"{header_name}: {header_value}") +else: + print("No custom headers") + +print("\n--- Attachments ---") +if received_email['attachments']: + print(f"Total attachments: {len(received_email['attachments'])}") + for idx, attachment in enumerate(received_email['attachments'], 1): + print(f"\nAttachment {idx}:") + print(f" ID: {attachment['id']}") + print(f" Filename: {attachment['filename']}") + print(f" Content Type: {attachment['content_type']}") + print(f" Content Disposition: {attachment['content_disposition']}") + if attachment.get('content_id'): + print(f" Content ID: {attachment['content_id']}") +else: + print("No attachments") diff --git a/resend/__init__.py b/resend/__init__.py index 0d0a1a9..c1f8182 100644 --- a/resend/__init__.py +++ b/resend/__init__.py @@ -14,6 +14,8 @@ from .emails._batch import Batch, BatchValidationError from .emails._email import Email from .emails._emails import Emails +from .emails._received_email import ReceivedEmail, ReceivedEmailAttachment +from .emails._receiving import Receiving from .emails._tag import Tag from .http_client import HTTPClient from .http_client_requests import RequestsClient @@ -52,6 +54,9 @@ "Tag", "Broadcast", "BatchValidationError", + "Receiving", + "ReceivedEmail", + "ReceivedEmailAttachment", # Default HTTP Client "RequestsClient", ] diff --git a/resend/emails/_emails.py b/resend/emails/_emails.py index e780d9a..377607a 100644 --- a/resend/emails/_emails.py +++ b/resend/emails/_emails.py @@ -5,6 +5,7 @@ from resend import request from resend.emails._attachment import Attachment, RemoteAttachment from resend.emails._email import Email +from resend.emails._receiving import Receiving from resend.emails._tag import Tag from resend.pagination_helper import PaginationHelper @@ -103,6 +104,7 @@ class _SendParamsDefault(_SendParamsFrom): class Emails: + Receiving = Receiving class CancelScheduledEmailResponse(_CancelScheduledEmailResponse): """ diff --git a/resend/emails/_received_email.py b/resend/emails/_received_email.py new file mode 100644 index 0000000..37380d4 --- /dev/null +++ b/resend/emails/_received_email.py @@ -0,0 +1,118 @@ +from typing import Dict, List, Optional + +from typing_extensions import NotRequired, TypedDict + + +class ReceivedEmailAttachment(TypedDict): + """ + ReceivedEmailAttachment type that wraps an attachment object from a received email. + + Attributes: + id (str): The attachment ID. + filename (str): The filename of the attachment. + content_type (str): The content type of the attachment. + content_disposition (str): The content disposition of the attachment. + content_id (NotRequired[str]): The content ID for inline attachments. + """ + + id: str + """ + The attachment ID. + """ + filename: str + """ + The filename of the attachment. + """ + content_type: str + """ + The content type of the attachment. + """ + content_disposition: str + """ + The content disposition of the attachment. + """ + content_id: NotRequired[str] + """ + The content ID for inline attachments. + """ + + +# Uses functional typed dict syntax here in order to support "from" reserved keyword +_ReceivedEmailFromParam = TypedDict( + "_ReceivedEmailFromParam", + { + "from": str, + }, +) + + +class _ReceivedEmailDefaultAttrs(_ReceivedEmailFromParam): + object: str + """ + The object type. + """ + id: str + """ + The received email ID. + """ + to: List[str] + """ + List of recipient email addresses. + """ + created_at: str + """ + When the email was received. + """ + subject: str + """ + The subject of the email. + """ + html: Optional[str] + """ + The HTML content of the email. + """ + text: Optional[str] + """ + The text content of the email. + """ + bcc: Optional[List[str]] + """ + Bcc recipients. + """ + cc: Optional[List[str]] + """ + Cc recipients. + """ + reply_to: Optional[List[str]] + """ + Reply-to addresses. + """ + headers: NotRequired[Dict[str, str]] + """ + Email headers. + """ + attachments: List[ReceivedEmailAttachment] + """ + List of attachments. + """ + + +class ReceivedEmail(_ReceivedEmailDefaultAttrs): + """ + ReceivedEmail type that wraps a received (inbound) email object. + + Attributes: + object (str): The object type. + id (str): The received email ID. + to (List[str]): List of recipient email addresses. + from (str): The sender email address. + created_at (str): When the email was received. + subject (str): The subject of the email. + html (Optional[str]): The HTML content of the email. + text (Optional[str]): The text content of the email. + bcc (Optional[List[str]]): Bcc recipients. + cc (Optional[List[str]]): Cc recipients. + reply_to (Optional[List[str]]): Reply-to addresses. + headers (NotRequired[Dict[str, str]]): Email headers. + attachments (List[ReceivedEmailAttachment]): List of attachments. + """ diff --git a/resend/emails/_receiving.py b/resend/emails/_receiving.py new file mode 100644 index 0000000..6b8aadc --- /dev/null +++ b/resend/emails/_receiving.py @@ -0,0 +1,28 @@ +from resend import request +from resend.emails._received_email import ReceivedEmail + + +class Receiving: + """ + Receiving class that provides methods for retrieving received (inbound) emails. + """ + + @classmethod + def get(cls, email_id: str) -> ReceivedEmail: + """ + Retrieve a single received email. + see more: https://resend.com/docs/api-reference/emails/retrieve-received-email + + Args: + email_id (str): The ID of the received email to retrieve + + Returns: + ReceivedEmail: The received email object + """ + path = f"/emails/receiving/{email_id}" + resp = request.Request[ReceivedEmail]( + path=path, + params={}, + verb="get", + ).perform_with_content() + return resp diff --git a/tests/emails_test.py b/tests/emails_test.py index d23ca8f..9b2e73e 100644 --- a/tests/emails_test.py +++ b/tests/emails_test.py @@ -254,3 +254,86 @@ def test_should_list_email_raise_exception_when_no_content(self) -> None: self.set_mock_json(None) with self.assertRaises(NoContentError): _ = resend.Emails.list() + + def test_receiving_get(self) -> None: + self.set_mock_json( + { + "object": "inbound", + "id": "67d9bcdb-5a02-42d7-8da9-0d6feea18cff", + "to": ["received@example.com"], + "from": "sender@example.com", + "created_at": "2023-04-07T23:13:52.669661+00:00", + "subject": "Test inbound email", + "html": "

hello world

", + "text": "hello world", + "bcc": None, + "cc": ["cc@example.com"], + "reply_to": ["reply@example.com"], + "headers": { + "example": "value", + }, + "attachments": [ + { + "id": "att_123", + "filename": "document.pdf", + "content_type": "application/pdf", + "content_id": "cid_123", + "content_disposition": "attachment", + } + ], + } + ) + + email: resend.ReceivedEmail = resend.Emails.Receiving.get( + email_id="67d9bcdb-5a02-42d7-8da9-0d6feea18cff", + ) + assert email["id"] == "67d9bcdb-5a02-42d7-8da9-0d6feea18cff" + assert email["object"] == "inbound" + assert email["to"] == ["received@example.com"] + assert email["from"] == "sender@example.com" + assert email["subject"] == "Test inbound email" + assert email["html"] == "

hello world

" + assert email["text"] == "hello world" + assert email["bcc"] is None + assert email["cc"] == ["cc@example.com"] + assert email["reply_to"] == ["reply@example.com"] + assert email["headers"]["example"] == "value" + assert len(email["attachments"]) == 1 + assert email["attachments"][0]["id"] == "att_123" + assert email["attachments"][0]["filename"] == "document.pdf" + + def test_receiving_get_with_no_attachments(self) -> None: + self.set_mock_json( + { + "object": "inbound", + "id": "67d9bcdb-5a02-42d7-8da9-0d6feea18cff", + "to": ["received@example.com"], + "from": "sender@example.com", + "created_at": "2023-04-07T23:13:52.669661+00:00", + "subject": "Test inbound email", + "html": None, + "text": "hello world", + "bcc": None, + "cc": None, + "reply_to": None, + "headers": {}, + "attachments": [], + } + ) + + email: resend.ReceivedEmail = resend.Emails.Receiving.get( + email_id="67d9bcdb-5a02-42d7-8da9-0d6feea18cff", + ) + assert email["id"] == "67d9bcdb-5a02-42d7-8da9-0d6feea18cff" + assert email["html"] is None + assert email["bcc"] is None + assert email["cc"] is None + assert email["reply_to"] is None + assert len(email["attachments"]) == 0 + + def test_should_receiving_get_raise_exception_when_no_content(self) -> None: + self.set_mock_json(None) + with self.assertRaises(NoContentError): + _ = resend.Emails.Receiving.get( + email_id="67d9bcdb-5a02-42d7-8da9-0d6feea18cff", + ) From d6721415c77c5f5a0187919e8d4711ef6337b0f9 Mon Sep 17 00:00:00 2001 From: Derich Pacheco Date: Mon, 20 Oct 2025 01:48:11 -0300 Subject: [PATCH 2/5] feat: finalize inbound --- examples/receiving_email.py | 114 ++++++++++-- resend/__init__.py | 9 +- resend/attachments/__init__.py | 0 resend/attachments/_attachments.py | 9 + .../_received_email_attachment_details.py | 50 +++++ resend/attachments/_receiving.py | 128 +++++++++++++ resend/emails/_received_email.py | 77 ++++++++ resend/emails/_receiving.py | 79 +++++++- tests/attachments_test.py | 176 ++++++++++++++++++ tests/emails_test.py | 105 +++++++++++ 10 files changed, 734 insertions(+), 13 deletions(-) create mode 100644 resend/attachments/__init__.py create mode 100644 resend/attachments/_attachments.py create mode 100644 resend/attachments/_received_email_attachment_details.py create mode 100644 resend/attachments/_receiving.py create mode 100644 tests/attachments_test.py diff --git a/examples/receiving_email.py b/examples/receiving_email.py index bf1d7bf..1c948c0 100644 --- a/examples/receiving_email.py +++ b/examples/receiving_email.py @@ -8,11 +8,9 @@ # Retrieve a single received email by ID email_id = "006e2796-ff6a-4436-91ad-0429e600bf8a" -received_email: resend.ReceivedEmail = resend.Emails.Receiving.get( - email_id=email_id -) +received_email: resend.ReceivedEmail = resend.Emails.Receiving.get(email_id=email_id) -print(f"Retrieved received email: {received_email['id']}") +print(f"\nRetrieved received email: {received_email['id']}") print("\n--- Email Details ---") print(f"From: {received_email['from']}") print(f"To: {received_email['to']}") @@ -21,12 +19,12 @@ print(f"Object type: {received_email['object']}") print("\n--- Email Content ---") -if received_email.get('html'): +if received_email.get("html"): print(f"HTML: {received_email['html'][:100]}...") # Show first 100 chars else: print("HTML: None") -if received_email.get('text'): +if received_email.get("text"): print(f"Text: {received_email['text'][:100]}...") # Show first 100 chars else: print("Text: None") @@ -37,22 +35,116 @@ print(f"Reply-To: {received_email.get('reply_to', [])}") print("\n--- Headers ---") -if received_email.get('headers'): - for header_name, header_value in received_email['headers'].items(): +if received_email.get("headers"): + for header_name, header_value in received_email["headers"].items(): print(f"{header_name}: {header_value}") else: print("No custom headers") print("\n--- Attachments ---") -if received_email['attachments']: +if received_email["attachments"]: print(f"Total attachments: {len(received_email['attachments'])}") - for idx, attachment in enumerate(received_email['attachments'], 1): + for idx, attachment in enumerate(received_email["attachments"], 1): print(f"\nAttachment {idx}:") print(f" ID: {attachment['id']}") print(f" Filename: {attachment['filename']}") print(f" Content Type: {attachment['content_type']}") print(f" Content Disposition: {attachment['content_disposition']}") - if attachment.get('content_id'): + if attachment.get("content_id"): print(f" Content ID: {attachment['content_id']}") + if attachment.get("size"): + print(f" Size: {attachment['size']} bytes") else: print("No attachments") + +# List all received emails +print("\n--- Listing All Received Emails ---") +all_emails: resend.Emails.Receiving.ListResponse = resend.Emails.Receiving.list() + +print(f"Total emails in this batch: {len(all_emails['data'])}") +print(f"Has more emails: {all_emails['has_more']}") + +if all_emails["data"]: + for idx, email in enumerate(all_emails["data"], 1): + print(f"\nEmail {idx}:") + print(f" ID: {email['id']}") + print(f" From: {email['from']}") + print(f" To: {email['to']}") + print(f" Subject: {email['subject']}") + print(f" Created: {email['created_at']}") + print(f" Attachments: {len(email['attachments'])}") + +# List with pagination +print("\n--- Listing Received Emails with Pagination ---") +list_params: resend.Emails.Receiving.ListParams = { + "limit": 5, +} +paginated_emails: resend.Emails.Receiving.ListResponse = resend.Emails.Receiving.list( + params=list_params +) + +print(f"Retrieved {len(paginated_emails['data'])} emails (limited to 5)") +print(f"Has more: {paginated_emails['has_more']}") + +# Example with cursor-based pagination +if paginated_emails["data"] and paginated_emails["has_more"]: + last_email_id = paginated_emails["data"][-1]["id"] + print(f"\n--- Getting Next Page (after {last_email_id}) ---") + next_page_params: resend.Emails.Receiving.ListParams = { + "limit": 5, + "after": last_email_id, + } + next_page: resend.Emails.Receiving.ListResponse = resend.Emails.Receiving.list( + params=next_page_params + ) + print(f"Next page has {len(next_page['data'])} emails") + print(f"Next page has more: {next_page['has_more']}") + +print("\n" + "=" * 60) +print("ATTACHMENTS EXAMPLES") +print("=" * 60) + +# List all attachments for a received email +print("\n--- Listing All Attachments ---") +list_attachments_params: resend.Attachments.Receiving.ListParams = { + "email_id": email_id, +} +all_attachments: resend.Attachments.Receiving.ListResponse = ( + resend.Attachments.Receiving.list(params=list_attachments_params) +) + +print(f"Total attachments: {len(all_attachments['data'])}") +print(f"Has more: {all_attachments['has_more']}") + +if all_attachments["data"]: + for idx, att in enumerate(all_attachments["data"], 1): + print(f"\nAttachment {idx}:") + print(f" ID: {att['id']}") + print(f" Filename: {att['filename']}") + print(f" Content Type: {att['content_type']}") + print(f" Size: {att.get('size', 'N/A')} bytes") + +# Retrieve a specific attachment from a received email +if received_email["attachments"] and len(received_email["attachments"]) > 0: + first_attachment = received_email["attachments"][0] + attachment_id = first_attachment["id"] + + print(f"\n--- Retrieving Attachment Details: {first_attachment['filename']} ---") + + attachment_details: resend.ReceivedEmailAttachmentDetails = ( + resend.Attachments.Receiving.get( + email_id=email_id, + attachment_id=attachment_id, + ) + ) + + print(f"Attachment ID: {attachment_details['id']}") + print(f"Filename: {attachment_details['filename']}") + print(f"Content Type: {attachment_details['content_type']}") + print(f"Content Disposition: {attachment_details['content_disposition']}") + if attachment_details.get("content_id"): + print(f" Content ID: {attachment_details['content_id']}") + print(f"Download URL: {attachment_details['download_url']}") + print(f"Expires At: {attachment_details['expires_at']}") +else: + print("\nNo attachments available to retrieve in this example.") diff --git a/resend/__init__.py b/resend/__init__.py index c1f8182..e7a85e6 100644 --- a/resend/__init__.py +++ b/resend/__init__.py @@ -2,6 +2,9 @@ from .api_keys._api_key import ApiKey from .api_keys._api_keys import ApiKeys +from .attachments._attachments import Attachments +from .attachments._received_email_attachment_details import \ + ReceivedEmailAttachmentDetails from .audiences._audience import Audience from .audiences._audiences import Audiences from .broadcasts._broadcast import Broadcast @@ -14,7 +17,8 @@ from .emails._batch import Batch, BatchValidationError from .emails._email import Email from .emails._emails import Emails -from .emails._received_email import ReceivedEmail, ReceivedEmailAttachment +from .emails._received_email import (ListReceivedEmail, ReceivedEmail, + ReceivedEmailAttachment) from .emails._receiving import Receiving from .emails._tag import Tag from .http_client import HTTPClient @@ -43,6 +47,7 @@ "Audiences", "Contacts", "Broadcasts", + "Attachments", # Types "Audience", "Contact", @@ -57,6 +62,8 @@ "Receiving", "ReceivedEmail", "ReceivedEmailAttachment", + "ReceivedEmailAttachmentDetails", + "ListReceivedEmail", # Default HTTP Client "RequestsClient", ] diff --git a/resend/attachments/__init__.py b/resend/attachments/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/resend/attachments/_attachments.py b/resend/attachments/_attachments.py new file mode 100644 index 0000000..0f17356 --- /dev/null +++ b/resend/attachments/_attachments.py @@ -0,0 +1,9 @@ +from resend.attachments._receiving import Receiving + + +class Attachments: + """ + Attachments class that provides methods for managing email attachments. + """ + + Receiving = Receiving diff --git a/resend/attachments/_received_email_attachment_details.py b/resend/attachments/_received_email_attachment_details.py new file mode 100644 index 0000000..485a6ba --- /dev/null +++ b/resend/attachments/_received_email_attachment_details.py @@ -0,0 +1,50 @@ +from typing_extensions import NotRequired, TypedDict + + +class ReceivedEmailAttachmentDetails(TypedDict): + """ + ReceivedEmailAttachmentDetails type that wraps a received email attachment with download details. + + Attributes: + object (str): The object type. + id (str): The attachment ID. + filename (str): The filename of the attachment. + content_type (str): The content type of the attachment. + content_disposition (str): The content disposition of the attachment. + content_id (NotRequired[str]): The content ID for inline attachments. + download_url (str): The URL to download the attachment. + expires_at (str): When the download URL expires. + """ + + object: str + """ + The object type. + """ + id: str + """ + The attachment ID. + """ + filename: str + """ + The filename of the attachment. + """ + content_type: str + """ + The content type of the attachment. + """ + content_disposition: str + """ + The content disposition of the attachment. + """ + content_id: NotRequired[str] + """ + The content ID for inline attachments. + """ + download_url: str + """ + The URL to download the attachment. + """ + expires_at: str + """ + When the download URL expires. + """ diff --git a/resend/attachments/_receiving.py b/resend/attachments/_receiving.py new file mode 100644 index 0000000..7b96aae --- /dev/null +++ b/resend/attachments/_receiving.py @@ -0,0 +1,128 @@ +from typing import Any, Dict, List, Optional, cast + +from typing_extensions import NotRequired, TypedDict + +from resend import request +from resend.attachments._received_email_attachment_details import ( + ReceivedEmailAttachmentDetails, +) +from resend.emails._received_email import ReceivedEmailAttachment +from resend.pagination_helper import PaginationHelper + + +# Internal wrapper type for API response +class _GetAttachmentResponse(TypedDict): + object: str + data: ReceivedEmailAttachmentDetails + + +class _ListParams(TypedDict): + email_id: str + """ + The ID of the received email. + """ + limit: NotRequired[int] + """ + The maximum number of attachments to return. Maximum 100, minimum 1. + """ + after: NotRequired[str] + """ + Return attachments after this cursor for pagination. + """ + before: NotRequired[str] + """ + Return attachments before this cursor for pagination. + """ + + +class _ListResponse(TypedDict): + object: str + """ + The object type: "list" + """ + data: List[ReceivedEmailAttachment] + """ + The list of attachment objects. + """ + has_more: bool + """ + Whether there are more attachments available for pagination. + """ + + +class Receiving: + """ + Receiving class that provides methods for retrieving attachments from received emails. + """ + + class ListParams(_ListParams): + """ + ListParams is the class that wraps the parameters for the list method. + + Attributes: + email_id (str): The ID of the received email. + limit (NotRequired[int]): The maximum number of attachments to return. Maximum 100, minimum 1. + after (NotRequired[str]): Return attachments after this cursor for pagination. + before (NotRequired[str]): Return attachments before this cursor for pagination. + """ + + class ListResponse(_ListResponse): + """ + ListResponse is the type that wraps the response for listing attachments. + + Attributes: + object (str): The object type: "list" + data (List[ReceivedEmailAttachment]): The list of attachment objects. + has_more (bool): Whether there are more attachments available for pagination. + """ + + @classmethod + def get(cls, email_id: str, attachment_id: str) -> ReceivedEmailAttachmentDetails: + """ + Retrieve a single attachment from a received email. + see more: https://resend.com/docs/api-reference/attachments/retrieve-attachment + + Args: + email_id (str): The ID of the received email + attachment_id (str): The ID of the attachment to retrieve + + Returns: + ReceivedEmailAttachmentDetails: The attachment details including download URL + """ + path = f"/emails/receiving/{email_id}/attachments/{attachment_id}" + resp = request.Request[_GetAttachmentResponse]( + path=path, + params={}, + verb="get", + ).perform_with_content() + # Extract the data field from the wrapped response + return resp["data"] + + @classmethod + def list(cls, params: ListParams) -> ListResponse: + """ + Retrieve a list of attachments from a received email. + see more: https://resend.com/docs/api-reference/attachments/list-attachments + + Args: + params (ListParams): The list parameters including email_id and optional pagination + + Returns: + ListResponse: A paginated list of attachment objects + """ + email_id = params["email_id"] + base_path = f"/emails/receiving/{email_id}/attachments" + + # Extract pagination params only (exclude email_id) + pagination_params = { + k: v for k, v in params.items() if k in ["limit", "after", "before"] + } + query_params = cast(Dict[Any, Any], pagination_params) if pagination_params else None + path = PaginationHelper.build_paginated_path(base_path, query_params) + + resp = request.Request[Receiving.ListResponse]( + path=path, + params={}, + verb="get", + ).perform_with_content() + return resp diff --git a/resend/emails/_received_email.py b/resend/emails/_received_email.py index 37380d4..dabd478 100644 --- a/resend/emails/_received_email.py +++ b/resend/emails/_received_email.py @@ -13,6 +13,7 @@ class ReceivedEmailAttachment(TypedDict): content_type (str): The content type of the attachment. content_disposition (str): The content disposition of the attachment. content_id (NotRequired[str]): The content ID for inline attachments. + size (NotRequired[int]): The size of the attachment in bytes. """ id: str @@ -35,6 +36,10 @@ class ReceivedEmailAttachment(TypedDict): """ The content ID for inline attachments. """ + size: NotRequired[int] + """ + The size of the attachment in bytes. + """ # Uses functional typed dict syntax here in order to support "from" reserved keyword @@ -46,6 +51,15 @@ class ReceivedEmailAttachment(TypedDict): ) +# For list responses (omits html, text, headers, object from full email) +_ListReceivedEmailFromParam = TypedDict( + "_ListReceivedEmailFromParam", + { + "from": str, + }, +) + + class _ReceivedEmailDefaultAttrs(_ReceivedEmailFromParam): object: str """ @@ -87,6 +101,10 @@ class _ReceivedEmailDefaultAttrs(_ReceivedEmailFromParam): """ Reply-to addresses. """ + message_id: str + """ + The message ID of the email. + """ headers: NotRequired[Dict[str, str]] """ Email headers. @@ -113,6 +131,65 @@ class ReceivedEmail(_ReceivedEmailDefaultAttrs): bcc (Optional[List[str]]): Bcc recipients. cc (Optional[List[str]]): Cc recipients. reply_to (Optional[List[str]]): Reply-to addresses. + message_id (str): The message ID of the email. headers (NotRequired[Dict[str, str]]): Email headers. attachments (List[ReceivedEmailAttachment]): List of attachments. """ + + +class _ListReceivedEmailDefaultAttrs(_ListReceivedEmailFromParam): + id: str + """ + The received email ID. + """ + to: List[str] + """ + List of recipient email addresses. + """ + created_at: str + """ + When the email was received. + """ + subject: str + """ + The subject of the email. + """ + bcc: Optional[List[str]] + """ + Bcc recipients. + """ + cc: Optional[List[str]] + """ + Cc recipients. + """ + reply_to: Optional[List[str]] + """ + Reply-to addresses. + """ + message_id: str + """ + The message ID of the email. + """ + attachments: List[ReceivedEmailAttachment] + """ + List of attachments. + """ + + +class ListReceivedEmail(_ListReceivedEmailDefaultAttrs): + """ + ListReceivedEmail type for received email items in list responses. + Omits html, text, headers, and object fields from the full email. + + Attributes: + id (str): The received email ID. + to (List[str]): List of recipient email addresses. + from (str): The sender email address. + created_at (str): When the email was received. + subject (str): The subject of the email. + bcc (Optional[List[str]]): Bcc recipients. + cc (Optional[List[str]]): Cc recipients. + reply_to (Optional[List[str]]): Reply-to addresses. + message_id (str): The message ID of the email. + attachments (List[ReceivedEmailAttachment]): List of attachments. + """ diff --git a/resend/emails/_receiving.py b/resend/emails/_receiving.py index 6b8aadc..94a109e 100644 --- a/resend/emails/_receiving.py +++ b/resend/emails/_receiving.py @@ -1,5 +1,40 @@ +from typing import Any, Dict, List, Optional, cast + +from typing_extensions import NotRequired, TypedDict + from resend import request -from resend.emails._received_email import ReceivedEmail +from resend.emails._received_email import ListReceivedEmail, ReceivedEmail +from resend.pagination_helper import PaginationHelper + + +class _ListParams(TypedDict): + limit: NotRequired[int] + """ + The maximum number of emails to return. Maximum 100, minimum 1. + """ + after: NotRequired[str] + """ + Return emails after this cursor for pagination. + """ + before: NotRequired[str] + """ + Return emails before this cursor for pagination. + """ + + +class _ListResponse(TypedDict): + object: str + """ + The object type: "list" + """ + data: List[ListReceivedEmail] + """ + The list of received email objects. + """ + has_more: bool + """ + Whether there are more emails available for pagination. + """ class Receiving: @@ -7,6 +42,26 @@ class Receiving: Receiving class that provides methods for retrieving received (inbound) emails. """ + class ListParams(_ListParams): + """ + ListParams is the class that wraps the parameters for the list method. + + Attributes: + limit (NotRequired[int]): The maximum number of emails to return. Maximum 100, minimum 1. + after (NotRequired[str]): Return emails after this cursor for pagination. + before (NotRequired[str]): Return emails before this cursor for pagination. + """ + + class ListResponse(_ListResponse): + """ + ListResponse is the type that wraps the response for listing received emails. + + Attributes: + object (str): The object type: "list" + data (List[ListReceivedEmail]): The list of received email objects. + has_more (bool): Whether there are more emails available for pagination. + """ + @classmethod def get(cls, email_id: str) -> ReceivedEmail: """ @@ -26,3 +81,25 @@ def get(cls, email_id: str) -> ReceivedEmail: verb="get", ).perform_with_content() return resp + + @classmethod + def list(cls, params: Optional[ListParams] = None) -> ListResponse: + """ + Retrieve a list of received emails. + see more: https://resend.com/docs/api-reference/emails/list-received-emails + + Args: + params (Optional[ListParams]): The list parameters for pagination + + Returns: + ListResponse: A paginated list of received email objects + """ + base_path = "/emails/receiving" + query_params = cast(Dict[Any, Any], params) if params else None + path = PaginationHelper.build_paginated_path(base_path, query_params) + resp = request.Request[Receiving.ListResponse]( + path=path, + params={}, + verb="get", + ).perform_with_content() + return resp diff --git a/tests/attachments_test.py b/tests/attachments_test.py new file mode 100644 index 0000000..213ff61 --- /dev/null +++ b/tests/attachments_test.py @@ -0,0 +1,176 @@ +import resend +from resend.exceptions import NoContentError +from tests.conftest import ResendBaseTest + +# flake8: noqa + + +class TestResendAttachments(ResendBaseTest): + + def test_receiving_get_attachment(self) -> None: + self.set_mock_json( + { + "object": "attachment", + "data": { + "id": "2a0c9ce0-3112-4728-976e-47ddcd16a318", + "filename": "avatar.png", + "content_type": "image/png", + "content_disposition": "inline", + "content_id": "img001", + "download_url": "https://inbound-cdn.resend.com/4ef9a417-02e9-4d39-ad75-9611e0fcc33c/attachments/2a0c9ce0-3112-4728-976e-47ddcd16a318?some-params=example&signature=sig-123", + "expires_at": "2025-10-17T14:29:41.521Z", + }, + } + ) + + attachment: resend.ReceivedEmailAttachmentDetails = ( + resend.Attachments.Receiving.get( + email_id="4ef9a417-02e9-4d39-ad75-9611e0fcc33c", + attachment_id="2a0c9ce0-3112-4728-976e-47ddcd16a318", + ) + ) + assert attachment["id"] == "2a0c9ce0-3112-4728-976e-47ddcd16a318" + assert attachment["object"] == "attachment" + assert attachment["filename"] == "avatar.png" + assert attachment["content_type"] == "image/png" + assert attachment["content_disposition"] == "inline" + assert attachment["content_id"] == "img001" + assert "https://inbound-cdn.resend.com" in attachment["download_url"] + assert attachment["expires_at"] == "2025-10-17T14:29:41.521Z" + + def test_receiving_get_attachment_without_content_id(self) -> None: + self.set_mock_json( + { + "object": "attachment", + "data": { + "id": "3b1c9ce0-4223-5839-a87f-58eecd27b429", + "filename": "document.pdf", + "content_type": "application/pdf", + "content_disposition": "attachment", + "download_url": "https://inbound-cdn.resend.com/test-email/attachments/test-attachment", + "expires_at": "2025-10-18T10:00:00.000Z", + }, + } + ) + + attachment: resend.ReceivedEmailAttachmentDetails = ( + resend.Attachments.Receiving.get( + email_id="test-email-id", + attachment_id="3b1c9ce0-4223-5839-a87f-58eecd27b429", + ) + ) + assert attachment["id"] == "3b1c9ce0-4223-5839-a87f-58eecd27b429" + assert attachment["filename"] == "document.pdf" + assert attachment["content_type"] == "application/pdf" + assert attachment["content_disposition"] == "attachment" + assert "content_id" not in attachment or attachment.get("content_id") is None + + def test_should_receiving_get_attachment_raise_exception_when_no_content( + self, + ) -> None: + self.set_mock_json(None) + with self.assertRaises(NoContentError): + _ = resend.Attachments.Receiving.get( + email_id="4ef9a417-02e9-4d39-ad75-9611e0fcc33c", + attachment_id="2a0c9ce0-3112-4728-976e-47ddcd16a318", + ) + + def test_receiving_list_attachments(self) -> None: + self.set_mock_json( + { + "object": "list", + "has_more": False, + "data": [ + { + "id": "2a0c9ce0-3112-4728-976e-47ddcd16a318", + "filename": "avatar.png", + "content_type": "image/png", + "content_disposition": "inline", + "content_id": "img001", + "size": 1024, + }, + { + "id": "3b1d0df1-4223-5839-a87f-58eecd27b429", + "filename": "document.pdf", + "content_type": "application/pdf", + "content_disposition": "attachment", + "size": 2048, + }, + ], + } + ) + + list_params: resend.Attachments.Receiving.ListParams = { + "email_id": "4ef9a417-02e9-4d39-ad75-9611e0fcc33c", + } + attachments: resend.Attachments.Receiving.ListResponse = ( + resend.Attachments.Receiving.list(params=list_params) + ) + + assert attachments["object"] == "list" + assert attachments["has_more"] == False + assert len(attachments["data"]) == 2 + assert attachments["data"][0]["id"] == "2a0c9ce0-3112-4728-976e-47ddcd16a318" + assert attachments["data"][0]["filename"] == "avatar.png" + assert attachments["data"][0]["size"] == 1024 + assert attachments["data"][1]["id"] == "3b1d0df1-4223-5839-a87f-58eecd27b429" + assert attachments["data"][1]["filename"] == "document.pdf" + + def test_receiving_list_attachments_with_pagination(self) -> None: + self.set_mock_json( + { + "object": "list", + "has_more": True, + "data": [ + { + "id": "2a0c9ce0-3112-4728-976e-47ddcd16a318", + "filename": "avatar.png", + "content_type": "image/png", + "content_disposition": "inline", + "size": 1024, + }, + ], + } + ) + + list_params: resend.Attachments.Receiving.ListParams = { + "email_id": "4ef9a417-02e9-4d39-ad75-9611e0fcc33c", + "limit": 1, + } + attachments: resend.Attachments.Receiving.ListResponse = ( + resend.Attachments.Receiving.list(params=list_params) + ) + + assert attachments["object"] == "list" + assert attachments["has_more"] == True + assert len(attachments["data"]) == 1 + + def test_receiving_list_attachments_empty(self) -> None: + self.set_mock_json( + { + "object": "list", + "has_more": False, + "data": [], + } + ) + + list_params: resend.Attachments.Receiving.ListParams = { + "email_id": "4ef9a417-02e9-4d39-ad75-9611e0fcc33c", + } + attachments: resend.Attachments.Receiving.ListResponse = ( + resend.Attachments.Receiving.list(params=list_params) + ) + + assert attachments["object"] == "list" + assert len(attachments["data"]) == 0 + assert attachments["has_more"] == False + + def test_should_receiving_list_attachments_raise_exception_when_no_content( + self, + ) -> None: + self.set_mock_json(None) + list_params: resend.Attachments.Receiving.ListParams = { + "email_id": "4ef9a417-02e9-4d39-ad75-9611e0fcc33c", + } + with self.assertRaises(NoContentError): + _ = resend.Attachments.Receiving.list(params=list_params) diff --git a/tests/emails_test.py b/tests/emails_test.py index 9b2e73e..f8afa38 100644 --- a/tests/emails_test.py +++ b/tests/emails_test.py @@ -337,3 +337,108 @@ def test_should_receiving_get_raise_exception_when_no_content(self) -> None: _ = resend.Emails.Receiving.get( email_id="67d9bcdb-5a02-42d7-8da9-0d6feea18cff", ) + + def test_receiving_list(self) -> None: + self.set_mock_json( + { + "object": "list", + "has_more": True, + "data": [ + { + "id": "a39999a6-88e3-48b1-888b-beaabcde1b33", + "to": ["recipient@example.com"], + "from": "sender@example.com", + "created_at": "2025-10-09 14:37:40.951732+00", + "subject": "Hello World", + "bcc": [], + "cc": [], + "reply_to": [], + "message_id": "<111-222-333@email.provider.example.com>", + "attachments": [ + { + "filename": "example.txt", + "content_type": "text/plain", + "content_id": None, + "content_disposition": "attachment", + "id": "47e999c7-c89c-4999-bf32-aaaaa1c3ff21", + "size": 13, + } + ], + }, + { + "id": "b49999a6-99e3-59b1-999b-ceaabcde2c44", + "to": ["another@example.com"], + "from": "sender2@example.com", + "created_at": "2025-10-10 10:20:30.123456+00", + "subject": "Test Email", + "bcc": None, + "cc": ["cc@example.com"], + "reply_to": None, + "message_id": "<222-333-444@email.provider.example.com>", + "attachments": [], + }, + ], + } + ) + + emails: resend.Emails.Receiving.ListResponse = resend.Emails.Receiving.list() + assert emails["object"] == "list" + assert emails["has_more"] == True + assert len(emails["data"]) == 2 + assert emails["data"][0]["id"] == "a39999a6-88e3-48b1-888b-beaabcde1b33" + assert emails["data"][0]["subject"] == "Hello World" + assert len(emails["data"][0]["attachments"]) == 1 + assert emails["data"][0]["attachments"][0]["size"] == 13 + assert emails["data"][1]["id"] == "b49999a6-99e3-59b1-999b-ceaabcde2c44" + + def test_receiving_list_with_params(self) -> None: + self.set_mock_json( + { + "object": "list", + "has_more": False, + "data": [ + { + "id": "a39999a6-88e3-48b1-888b-beaabcde1b33", + "to": ["recipient@example.com"], + "from": "sender@example.com", + "created_at": "2025-10-09 14:37:40.951732+00", + "subject": "Hello World", + "bcc": None, + "cc": None, + "reply_to": None, + "message_id": "<111-222-333@email.provider.example.com>", + "attachments": [], + } + ], + } + ) + + list_params: resend.Emails.Receiving.ListParams = { + "limit": 10, + "after": "cursor123", + } + emails: resend.Emails.Receiving.ListResponse = resend.Emails.Receiving.list( + params=list_params + ) + assert emails["object"] == "list" + assert len(emails["data"]) == 1 + assert emails["has_more"] == False + + def test_receiving_list_empty(self) -> None: + self.set_mock_json( + { + "object": "list", + "has_more": False, + "data": [], + } + ) + + emails: resend.Emails.Receiving.ListResponse = resend.Emails.Receiving.list() + assert emails["object"] == "list" + assert len(emails["data"]) == 0 + assert emails["has_more"] == False + + def test_should_receiving_list_raise_exception_when_no_content(self) -> None: + self.set_mock_json(None) + with self.assertRaises(NoContentError): + _ = resend.Emails.Receiving.list() From 0ccc259bd2ba72d7310bd0cd1cb9b83266d6ac27 Mon Sep 17 00:00:00 2001 From: Derich Pacheco Date: Mon, 20 Oct 2025 14:19:17 -0300 Subject: [PATCH 3/5] chore: tweaks to typing --- examples/receiving_email.py | 29 +++++------ resend/__init__.py | 12 +++-- .../_received_email_attachment_details.py | 50 ------------------- resend/attachments/_receiving.py | 27 +++------- resend/emails/_received_email.py | 44 ++++++++++++++++ tests/attachments_test.py | 36 +++++++------ tests/emails_test.py | 9 ++-- 7 files changed, 95 insertions(+), 112 deletions(-) delete mode 100644 resend/attachments/_received_email_attachment_details.py diff --git a/examples/receiving_email.py b/examples/receiving_email.py index 1c948c0..016a3d7 100644 --- a/examples/receiving_email.py +++ b/examples/receiving_email.py @@ -1,6 +1,8 @@ import os import resend +# Import types for type hints +from resend import AttachmentsReceiving, EmailsReceiving if not os.environ["RESEND_API_KEY"]: raise EnvironmentError("RESEND_API_KEY is missing") @@ -19,13 +21,15 @@ print(f"Object type: {received_email['object']}") print("\n--- Email Content ---") -if received_email.get("html"): - print(f"HTML: {received_email['html'][:100]}...") # Show first 100 chars +html_content = received_email.get("html") +if html_content: + print(f"HTML: {html_content[:100]}...") # Show first 100 chars else: print("HTML: None") -if received_email.get("text"): - print(f"Text: {received_email['text'][:100]}...") # Show first 100 chars +text_content = received_email.get("text") +if text_content: + print(f"Text: {text_content[:100]}...") # Show first 100 chars else: print("Text: None") @@ -59,7 +63,7 @@ # List all received emails print("\n--- Listing All Received Emails ---") -all_emails: resend.Emails.Receiving.ListResponse = resend.Emails.Receiving.list() +all_emails: EmailsReceiving.ListResponse = resend.Emails.Receiving.list() print(f"Total emails in this batch: {len(all_emails['data'])}") print(f"Has more emails: {all_emails['has_more']}") @@ -76,10 +80,10 @@ # List with pagination print("\n--- Listing Received Emails with Pagination ---") -list_params: resend.Emails.Receiving.ListParams = { +list_params: EmailsReceiving.ListParams = { "limit": 5, } -paginated_emails: resend.Emails.Receiving.ListResponse = resend.Emails.Receiving.list( +paginated_emails: EmailsReceiving.ListResponse = resend.Emails.Receiving.list( params=list_params ) @@ -90,11 +94,11 @@ if paginated_emails["data"] and paginated_emails["has_more"]: last_email_id = paginated_emails["data"][-1]["id"] print(f"\n--- Getting Next Page (after {last_email_id}) ---") - next_page_params: resend.Emails.Receiving.ListParams = { + next_page_params: EmailsReceiving.ListParams = { "limit": 5, "after": last_email_id, } - next_page: resend.Emails.Receiving.ListResponse = resend.Emails.Receiving.list( + next_page: EmailsReceiving.ListResponse = resend.Emails.Receiving.list( params=next_page_params ) print(f"Next page has {len(next_page['data'])} emails") @@ -106,11 +110,8 @@ # List all attachments for a received email print("\n--- Listing All Attachments ---") -list_attachments_params: resend.Attachments.Receiving.ListParams = { - "email_id": email_id, -} -all_attachments: resend.Attachments.Receiving.ListResponse = ( - resend.Attachments.Receiving.list(params=list_attachments_params) +all_attachments: AttachmentsReceiving.ListResponse = resend.Attachments.Receiving.list( + email_id=email_id ) print(f"Total attachments: {len(all_attachments['data'])}") diff --git a/resend/__init__.py b/resend/__init__.py index e7a85e6..5ab85b3 100644 --- a/resend/__init__.py +++ b/resend/__init__.py @@ -3,8 +3,7 @@ from .api_keys._api_key import ApiKey from .api_keys._api_keys import ApiKeys from .attachments._attachments import Attachments -from .attachments._received_email_attachment_details import \ - ReceivedEmailAttachmentDetails +from .attachments._receiving import Receiving as AttachmentsReceiving from .audiences._audience import Audience from .audiences._audiences import Audiences from .broadcasts._broadcast import Broadcast @@ -18,8 +17,9 @@ from .emails._email import Email from .emails._emails import Emails from .emails._received_email import (ListReceivedEmail, ReceivedEmail, - ReceivedEmailAttachment) -from .emails._receiving import Receiving + ReceivedEmailAttachment, + ReceivedEmailAttachmentDetails) +from .emails._receiving import Receiving as EmailsReceiving from .emails._tag import Tag from .http_client import HTTPClient from .http_client_requests import RequestsClient @@ -59,11 +59,13 @@ "Tag", "Broadcast", "BatchValidationError", - "Receiving", "ReceivedEmail", "ReceivedEmailAttachment", "ReceivedEmailAttachmentDetails", "ListReceivedEmail", + # Receiving types (for type hints) + "EmailsReceiving", + "AttachmentsReceiving", # Default HTTP Client "RequestsClient", ] diff --git a/resend/attachments/_received_email_attachment_details.py b/resend/attachments/_received_email_attachment_details.py deleted file mode 100644 index 485a6ba..0000000 --- a/resend/attachments/_received_email_attachment_details.py +++ /dev/null @@ -1,50 +0,0 @@ -from typing_extensions import NotRequired, TypedDict - - -class ReceivedEmailAttachmentDetails(TypedDict): - """ - ReceivedEmailAttachmentDetails type that wraps a received email attachment with download details. - - Attributes: - object (str): The object type. - id (str): The attachment ID. - filename (str): The filename of the attachment. - content_type (str): The content type of the attachment. - content_disposition (str): The content disposition of the attachment. - content_id (NotRequired[str]): The content ID for inline attachments. - download_url (str): The URL to download the attachment. - expires_at (str): When the download URL expires. - """ - - object: str - """ - The object type. - """ - id: str - """ - The attachment ID. - """ - filename: str - """ - The filename of the attachment. - """ - content_type: str - """ - The content type of the attachment. - """ - content_disposition: str - """ - The content disposition of the attachment. - """ - content_id: NotRequired[str] - """ - The content ID for inline attachments. - """ - download_url: str - """ - The URL to download the attachment. - """ - expires_at: str - """ - When the download URL expires. - """ diff --git a/resend/attachments/_receiving.py b/resend/attachments/_receiving.py index 7b96aae..eb67eb2 100644 --- a/resend/attachments/_receiving.py +++ b/resend/attachments/_receiving.py @@ -3,24 +3,18 @@ from typing_extensions import NotRequired, TypedDict from resend import request -from resend.attachments._received_email_attachment_details import ( - ReceivedEmailAttachmentDetails, -) -from resend.emails._received_email import ReceivedEmailAttachment +from resend.emails._received_email import (ReceivedEmailAttachment, + ReceivedEmailAttachmentDetails) from resend.pagination_helper import PaginationHelper -# Internal wrapper type for API response +# Internal wrapper type for get attachment API response class _GetAttachmentResponse(TypedDict): object: str data: ReceivedEmailAttachmentDetails class _ListParams(TypedDict): - email_id: str - """ - The ID of the received email. - """ limit: NotRequired[int] """ The maximum number of attachments to return. Maximum 100, minimum 1. @@ -60,7 +54,6 @@ class ListParams(_ListParams): ListParams is the class that wraps the parameters for the list method. Attributes: - email_id (str): The ID of the received email. limit (NotRequired[int]): The maximum number of attachments to return. Maximum 100, minimum 1. after (NotRequired[str]): Return attachments after this cursor for pagination. before (NotRequired[str]): Return attachments before this cursor for pagination. @@ -99,27 +92,21 @@ def get(cls, email_id: str, attachment_id: str) -> ReceivedEmailAttachmentDetail return resp["data"] @classmethod - def list(cls, params: ListParams) -> ListResponse: + def list(cls, email_id: str, params: Optional[ListParams] = None) -> ListResponse: """ Retrieve a list of attachments from a received email. see more: https://resend.com/docs/api-reference/attachments/list-attachments Args: - params (ListParams): The list parameters including email_id and optional pagination + email_id (str): The ID of the received email + params (Optional[ListParams]): The list parameters for pagination Returns: ListResponse: A paginated list of attachment objects """ - email_id = params["email_id"] base_path = f"/emails/receiving/{email_id}/attachments" - - # Extract pagination params only (exclude email_id) - pagination_params = { - k: v for k, v in params.items() if k in ["limit", "after", "before"] - } - query_params = cast(Dict[Any, Any], pagination_params) if pagination_params else None + query_params = cast(Dict[Any, Any], params) if params else None path = PaginationHelper.build_paginated_path(base_path, query_params) - resp = request.Request[Receiving.ListResponse]( path=path, params={}, diff --git a/resend/emails/_received_email.py b/resend/emails/_received_email.py index dabd478..6bbd656 100644 --- a/resend/emails/_received_email.py +++ b/resend/emails/_received_email.py @@ -42,6 +42,50 @@ class ReceivedEmailAttachment(TypedDict): """ +class ReceivedEmailAttachmentDetails(TypedDict): + """ + ReceivedEmailAttachmentDetails type that wraps a received email attachment with download details. + + Attributes: + id (str): The attachment ID. + filename (str): The filename of the attachment. + content_type (str): The content type of the attachment. + content_disposition (str): The content disposition of the attachment. + content_id (NotRequired[str]): The content ID for inline attachments. + download_url (str): The URL to download the attachment. + expires_at (str): When the download URL expires. + """ + + id: str + """ + The attachment ID. + """ + filename: str + """ + The filename of the attachment. + """ + content_type: str + """ + The content type of the attachment. + """ + content_disposition: str + """ + The content disposition of the attachment. + """ + content_id: NotRequired[str] + """ + The content ID for inline attachments. + """ + download_url: str + """ + The URL to download the attachment. + """ + expires_at: str + """ + When the download URL expires. + """ + + # Uses functional typed dict syntax here in order to support "from" reserved keyword _ReceivedEmailFromParam = TypedDict( "_ReceivedEmailFromParam", diff --git a/tests/attachments_test.py b/tests/attachments_test.py index 213ff61..a117848 100644 --- a/tests/attachments_test.py +++ b/tests/attachments_test.py @@ -1,4 +1,5 @@ import resend +from resend import AttachmentsReceiving from resend.exceptions import NoContentError from tests.conftest import ResendBaseTest @@ -30,7 +31,6 @@ def test_receiving_get_attachment(self) -> None: ) ) assert attachment["id"] == "2a0c9ce0-3112-4728-976e-47ddcd16a318" - assert attachment["object"] == "attachment" assert attachment["filename"] == "avatar.png" assert attachment["content_type"] == "image/png" assert attachment["content_disposition"] == "inline" @@ -100,11 +100,10 @@ def test_receiving_list_attachments(self) -> None: } ) - list_params: resend.Attachments.Receiving.ListParams = { - "email_id": "4ef9a417-02e9-4d39-ad75-9611e0fcc33c", - } - attachments: resend.Attachments.Receiving.ListResponse = ( - resend.Attachments.Receiving.list(params=list_params) + attachments: AttachmentsReceiving.ListResponse = ( + resend.Attachments.Receiving.list( + email_id="4ef9a417-02e9-4d39-ad75-9611e0fcc33c" + ) ) assert attachments["object"] == "list" @@ -133,12 +132,13 @@ def test_receiving_list_attachments_with_pagination(self) -> None: } ) - list_params: resend.Attachments.Receiving.ListParams = { - "email_id": "4ef9a417-02e9-4d39-ad75-9611e0fcc33c", + list_params: AttachmentsReceiving.ListParams = { "limit": 1, } - attachments: resend.Attachments.Receiving.ListResponse = ( - resend.Attachments.Receiving.list(params=list_params) + attachments: AttachmentsReceiving.ListResponse = ( + resend.Attachments.Receiving.list( + email_id="4ef9a417-02e9-4d39-ad75-9611e0fcc33c", params=list_params + ) ) assert attachments["object"] == "list" @@ -154,11 +154,10 @@ def test_receiving_list_attachments_empty(self) -> None: } ) - list_params: resend.Attachments.Receiving.ListParams = { - "email_id": "4ef9a417-02e9-4d39-ad75-9611e0fcc33c", - } - attachments: resend.Attachments.Receiving.ListResponse = ( - resend.Attachments.Receiving.list(params=list_params) + attachments: AttachmentsReceiving.ListResponse = ( + resend.Attachments.Receiving.list( + email_id="4ef9a417-02e9-4d39-ad75-9611e0fcc33c" + ) ) assert attachments["object"] == "list" @@ -169,8 +168,7 @@ def test_should_receiving_list_attachments_raise_exception_when_no_content( self, ) -> None: self.set_mock_json(None) - list_params: resend.Attachments.Receiving.ListParams = { - "email_id": "4ef9a417-02e9-4d39-ad75-9611e0fcc33c", - } with self.assertRaises(NoContentError): - _ = resend.Attachments.Receiving.list(params=list_params) + _ = resend.Attachments.Receiving.list( + email_id="4ef9a417-02e9-4d39-ad75-9611e0fcc33c" + ) diff --git a/tests/emails_test.py b/tests/emails_test.py index f8afa38..3281363 100644 --- a/tests/emails_test.py +++ b/tests/emails_test.py @@ -1,6 +1,7 @@ from unittest.mock import MagicMock import resend +from resend import EmailsReceiving from resend.exceptions import NoContentError, ResendError from tests.conftest import ResendBaseTest @@ -381,7 +382,7 @@ def test_receiving_list(self) -> None: } ) - emails: resend.Emails.Receiving.ListResponse = resend.Emails.Receiving.list() + emails: EmailsReceiving.ListResponse = resend.Emails.Receiving.list() assert emails["object"] == "list" assert emails["has_more"] == True assert len(emails["data"]) == 2 @@ -413,11 +414,11 @@ def test_receiving_list_with_params(self) -> None: } ) - list_params: resend.Emails.Receiving.ListParams = { + list_params: EmailsReceiving.ListParams = { "limit": 10, "after": "cursor123", } - emails: resend.Emails.Receiving.ListResponse = resend.Emails.Receiving.list( + emails: EmailsReceiving.ListResponse = resend.Emails.Receiving.list( params=list_params ) assert emails["object"] == "list" @@ -433,7 +434,7 @@ def test_receiving_list_empty(self) -> None: } ) - emails: resend.Emails.Receiving.ListResponse = resend.Emails.Receiving.list() + emails: EmailsReceiving.ListResponse = resend.Emails.Receiving.list() assert emails["object"] == "list" assert len(emails["data"]) == 0 assert emails["has_more"] == False From 13cc34b9fd3a75bca863318d6fc3bfb9e2b764bb Mon Sep 17 00:00:00 2001 From: Derich Pacheco Date: Mon, 20 Oct 2025 14:35:55 -0300 Subject: [PATCH 4/5] examples: tweaks --- examples/receiving_email.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/examples/receiving_email.py b/examples/receiving_email.py index 016a3d7..4a4fe08 100644 --- a/examples/receiving_email.py +++ b/examples/receiving_email.py @@ -1,7 +1,8 @@ import os import resend -# Import types for type hints + +# Type imports from resend import AttachmentsReceiving, EmailsReceiving if not os.environ["RESEND_API_KEY"]: @@ -104,10 +105,6 @@ print(f"Next page has {len(next_page['data'])} emails") print(f"Next page has more: {next_page['has_more']}") -print("\n" + "=" * 60) -print("ATTACHMENTS EXAMPLES") -print("=" * 60) - # List all attachments for a received email print("\n--- Listing All Attachments ---") all_attachments: AttachmentsReceiving.ListResponse = resend.Attachments.Receiving.list( From ef340a7cc55d74729d1e8638388baff59075e50f Mon Sep 17 00:00:00 2001 From: Derich Pacheco Date: Sun, 26 Oct 2025 20:41:56 -0300 Subject: [PATCH 5/5] chore: address pr review comments --- examples/receiving_email.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/examples/receiving_email.py b/examples/receiving_email.py index 4a4fe08..d9fdb4a 100644 --- a/examples/receiving_email.py +++ b/examples/receiving_email.py @@ -24,13 +24,13 @@ print("\n--- Email Content ---") html_content = received_email.get("html") if html_content: - print(f"HTML: {html_content[:100]}...") # Show first 100 chars + print(f"HTML: {html_content[:100]}...") else: print("HTML: None") text_content = received_email.get("text") if text_content: - print(f"Text: {text_content[:100]}...") # Show first 100 chars + print(f"Text: {text_content[:100]}...") else: print("Text: None") @@ -62,7 +62,6 @@ else: print("No attachments") -# List all received emails print("\n--- Listing All Received Emails ---") all_emails: EmailsReceiving.ListResponse = resend.Emails.Receiving.list() @@ -79,7 +78,6 @@ print(f" Created: {email['created_at']}") print(f" Attachments: {len(email['attachments'])}") -# List with pagination print("\n--- Listing Received Emails with Pagination ---") list_params: EmailsReceiving.ListParams = { "limit": 5, @@ -91,7 +89,6 @@ print(f"Retrieved {len(paginated_emails['data'])} emails (limited to 5)") print(f"Has more: {paginated_emails['has_more']}") -# Example with cursor-based pagination if paginated_emails["data"] and paginated_emails["has_more"]: last_email_id = paginated_emails["data"][-1]["id"] print(f"\n--- Getting Next Page (after {last_email_id}) ---") @@ -105,7 +102,6 @@ print(f"Next page has {len(next_page['data'])} emails") print(f"Next page has more: {next_page['has_more']}") -# List all attachments for a received email print("\n--- Listing All Attachments ---") all_attachments: AttachmentsReceiving.ListResponse = resend.Attachments.Receiving.list( email_id=email_id @@ -122,7 +118,6 @@ print(f" Content Type: {att['content_type']}") print(f" Size: {att.get('size', 'N/A')} bytes") -# Retrieve a specific attachment from a received email if received_email["attachments"] and len(received_email["attachments"]) > 0: first_attachment = received_email["attachments"][0] attachment_id = first_attachment["id"]