diff --git a/examples/inboxes.py b/examples/inboxes.py index ef7abc1..7cafc04 100644 --- a/examples/inboxes.py +++ b/examples/inboxes.py @@ -132,7 +132,11 @@ "content": "UmVmdW5kIHJlY2VpcHQ=", "filename": "receipt.txt", "content_type": "text/plain", - } + }, + { + "path": "https://example.com/invoice.pdf", + "filename": "invoice.pdf", + }, ], } draft: resend.Inboxes.Drafts.CreateResponse = resend.Inboxes.Drafts.create( diff --git a/resend/inboxes/drafts/_inbox_drafts.py b/resend/inboxes/drafts/_inbox_drafts.py index 0ecb346..833db29 100644 --- a/resend/inboxes/drafts/_inbox_drafts.py +++ b/resend/inboxes/drafts/_inbox_drafts.py @@ -23,9 +23,7 @@ class InboxDrafts: class AttachmentParams(TypedDict): """ - AttachmentParams is a file to store on a draft. Each one needs - ``content``, ``filename`` and ``content_type``. A ``path`` or - ``content_id`` returns a 422. + AttachmentParams is a file to store on a draft, sent inline. Attributes: content (str): The content of the file, encoded in base64. @@ -46,6 +44,34 @@ class AttachmentParams(TypedDict): The MIME type of the file, for example ``application/pdf``. """ + class PathAttachmentParams(TypedDict): + """ + PathAttachmentParams is a file to store on a draft, downloaded from a URL + when the request is made. + + Attributes: + path (str): The URL to download the file from. + filename (str): The name of the file. + content_type (NotRequired[str]): The MIME type of the file. + """ + + path: str + """ + The URL to download the file from, starting with ``http`` or ``https``. + A file that cannot be downloaded returns a 422. + """ + filename: str + """ + The name of the file. + """ + content_type: NotRequired[str] + """ + The MIME type of the file. Defaults to the content type of the + downloaded file. + """ + + DraftAttachmentParams = Union[AttachmentParams, PathAttachmentParams] + class ListParams(TypedDict): """ ListParams are the pagination parameters for listing drafts. @@ -111,7 +137,7 @@ class CreateParams(TypedDict): subject (NotRequired[Optional[str]]): The subject. html (NotRequired[Optional[str]]): The HTML body. text (NotRequired[Optional[str]]): The plain text body. - attachments (NotRequired[Optional[List[AttachmentParams]]]): Files to store on the draft. + attachments (NotRequired[Optional[List[DraftAttachmentParams]]]): Files to store on the draft. thread_id (NotRequired[str]): The Thread ID to reply in. reply_to_email_id (NotRequired[str]): The Email ID to reply to. """ @@ -146,12 +172,13 @@ class CreateParams(TypedDict): """ The plain text body. """ - attachments: NotRequired[Optional[List["InboxDrafts.AttachmentParams"]]] + attachments: NotRequired[Optional[List["InboxDrafts.DraftAttachmentParams"]]] """ - Files to store on the draft. Each one needs ``content``, ``filename`` and - ``content_type``. A ``path`` or ``content_id`` returns a 422. The files on - a draft can total at most 25 MB. An empty file or a blocked file type - returns a 422. + Files to store on the draft. Each one needs a ``filename`` and either + ``content`` with a ``content_type``, or a ``path`` URL that is downloaded + when the request is made. The files on a draft, downloaded ones included, + can total at most 25 MB. An empty file, a blocked file type, or a ``path`` + that cannot be downloaded returns a 422. """ thread_id: NotRequired[str] """ @@ -220,7 +247,7 @@ class UpdateParams(TypedDict): subject (NotRequired[Optional[str]]): The subject. html (NotRequired[Optional[str]]): The HTML body. text (NotRequired[Optional[str]]): The plain text body. - attachments (NotRequired[Optional[List[AttachmentParams]]]): Files that replace + attachments (NotRequired[Optional[List[DraftAttachmentParams]]]): Files that replace every attachment on the draft. """ @@ -258,13 +285,14 @@ class UpdateParams(TypedDict): """ The plain text body. Pass None to clear. """ - attachments: NotRequired[Optional[List["InboxDrafts.AttachmentParams"]]] + attachments: NotRequired[Optional[List["InboxDrafts.DraftAttachmentParams"]]] """ - Files to store on the draft. Each one needs ``content``, ``filename`` and - ``content_type``. A ``path`` or ``content_id`` returns a 422. The files on - a draft can total at most 25 MB. An empty file or a blocked file type - returns a 422. Sending the field replaces every attachment on the draft. - Send ``[]`` or None to remove them all. + Files to store on the draft. Each one needs a ``filename`` and either + ``content`` with a ``content_type``, or a ``path`` URL that is downloaded + when the request is made. The files on a draft, downloaded ones included, + can total at most 25 MB. An empty file, a blocked file type, or a ``path`` + that cannot be downloaded returns a 422. Sending the field replaces every + attachment on the draft. Send ``[]`` or None to remove them all. """ class UpdateResponse(BaseResponse): @@ -394,8 +422,8 @@ def create( ``to`` is addressed to the sender of the email it replies to, or to its Reply-To when set. For an email the inbox sent, it defaults to that email's ``to`` recipients. Send ``to: []`` to leave it empty. Attachments are - stored on the draft, so each one needs ``content``, ``filename`` and - ``content_type``. Only one unsent reply draft can target an email, so a + stored on the draft, so a ``path`` file is downloaded when the request + is made. Only one unsent reply draft can target an email, so a reply draft request for an email that already has one returns the ID of the existing draft. That request ignores every other field, except ``attachments``, which replaces the attachments of the existing draft. A @@ -550,8 +578,8 @@ async def create_async( ``to`` is addressed to the sender of the email it replies to, or to its Reply-To when set. For an email the inbox sent, it defaults to that email's ``to`` recipients. Send ``to: []`` to leave it empty. Attachments are - stored on the draft, so each one needs ``content``, ``filename`` and - ``content_type``. Only one unsent reply draft can target an email, so a + stored on the draft, so a ``path`` file is downloaded when the request + is made. Only one unsent reply draft can target an email, so a reply draft request for an email that already has one returns the ID of the existing draft. That request ignores every other field, except ``attachments``, which replaces the attachments of the existing draft. A diff --git a/resend/version.py b/resend/version.py index b77813e..09e91aa 100644 --- a/resend/version.py +++ b/resend/version.py @@ -1,4 +1,4 @@ -__version__ = "2.50.1" +__version__ = "2.51.0" def get_version() -> str: diff --git a/tests/inboxes_test.py b/tests/inboxes_test.py index 93cc449..c0d1b2a 100644 --- a/tests/inboxes_test.py +++ b/tests/inboxes_test.py @@ -720,6 +720,20 @@ def test_should_send_inbox_drafts_raise_exception_when_no_content( _ = resend.Inboxes.Drafts.send(INBOX_ID, DRAFT_ID) +def _invalid_draft_attachments() -> List[resend.Inboxes.Drafts.DraftAttachmentParams]: + """Checked by mypy: each entry must stay a type error.""" + return [ + {"filename": "note.txt"}, # type: ignore[list-item] + {"content": "aGVsbG8=", "filename": "note.txt"}, # type: ignore[list-item] + { # type: ignore[list-item] + "content": "aGVsbG8=", + "path": "https://example.com/invoice.pdf", + "filename": "note.txt", + "content_type": "text/plain", + }, + ] + + class TestInboxesRequest(TestCase): def setUp(self) -> None: resend.api_key = "re_123" @@ -1044,11 +1058,11 @@ def test_drafts_paths_and_bodies(self) -> None: {"to": None}, ) - attachment: resend.Inboxes.Drafts.AttachmentParams = { - "content": "aGVsbG8=", - "filename": "note.txt", - "content_type": "text/plain", - } + attachment = resend.InboxDrafts.AttachmentParams( + content="aGVsbG8=", + filename="note.txt", + content_type="text/plain", + ) resend.Inboxes.Drafts.create( {"inbox_id": INBOX_ID, "to": "ada@example.org", "attachments": [attachment]} ) @@ -1057,6 +1071,29 @@ def test_drafts_paths_and_bodies(self) -> None: "attachments": [attachment], } + path_attachment: resend.Inboxes.Drafts.DraftAttachmentParams = { + "path": "https://example.com/invoice.pdf", + "filename": "invoice.pdf", + } + resend.Inboxes.Drafts.create( + {"inbox_id": INBOX_ID, "attachments": [path_attachment]} + ) + assert self.last_request()["json"] == {"attachments": [path_attachment]} + + typed_path_attachment: resend.Inboxes.Drafts.DraftAttachmentParams = { + "path": "https://example.com/invoice.pdf", + "filename": "invoice.pdf", + "content_type": "application/pdf", + } + resend.Inboxes.Drafts.update( + { + "inbox_id": INBOX_ID, + "id": DRAFT_ID, + "attachments": [typed_path_attachment], + } + ) + assert self.last_request()["json"] == {"attachments": [typed_path_attachment]} + resend.Inboxes.Drafts.update( {"inbox_id": INBOX_ID, "id": DRAFT_ID, "attachments": []} )