From fe234f01cd5c595cf0650a6d2135ab64266c5b45 Mon Sep 17 00:00:00 2001 From: mayankbohradev Date: Thu, 9 Jul 2026 12:26:11 +0530 Subject: [PATCH] fix: encode contact email path identifiers --- resend/contacts/_contacts.py | 31 ++++++---- resend/contacts/_topics.py | 13 +++-- resend/contacts/segments/_contact_segments.py | 19 +++++-- tests/contact_topics_async_test.py | 40 +++++++++++++ tests/contact_topics_test.py | 38 +++++++++++++ tests/contacts_async_test.py | 53 +++++++++++++++++ tests/contacts_segments_async_test.py | 47 +++++++++++++++ tests/contacts_segments_test.py | 47 +++++++++++++++ tests/contacts_test.py | 57 +++++++++++++++++-- 9 files changed, 318 insertions(+), 27 deletions(-) diff --git a/resend/contacts/_contacts.py b/resend/contacts/_contacts.py index 5517455..bfa51a6 100644 --- a/resend/contacts/_contacts.py +++ b/resend/contacts/_contacts.py @@ -1,4 +1,5 @@ from typing import Any, Dict, List, Optional, cast +from urllib.parse import quote from typing_extensions import NotRequired, TypedDict @@ -238,14 +239,15 @@ def update(cls, params: UpdateParams) -> UpdateContactResponse: contact_identifier = ( params.get("email") if params.get("email") is not None else params.get("id") ) + encoded_contact_identifier = quote(cast(str, contact_identifier), safe="") audience_id = params.get("audience_id") if audience_id: # Audience-specific contact (no properties support) - path = f"/audiences/{audience_id}/contacts/{contact_identifier}" + path = f"/audiences/{audience_id}/contacts/{encoded_contact_identifier}" else: # Global contact (supports properties) - path = f"/contacts/{contact_identifier}" + path = f"/contacts/{encoded_contact_identifier}" resp = request.Request[Contacts.UpdateContactResponse]( path=path, params=cast(Dict[Any, Any], params), verb="patch" @@ -316,12 +318,13 @@ def get( if contact_identifier is None: raise ValueError("id or email must be provided") + encoded_contact_identifier = quote(contact_identifier, safe="") if audience_id: # Audience-specific contact - path = f"/audiences/{audience_id}/contacts/{contact_identifier}" + path = f"/audiences/{audience_id}/contacts/{encoded_contact_identifier}" else: # Global contact - path = f"/contacts/{contact_identifier}" + path = f"/contacts/{encoded_contact_identifier}" resp = request.Request[Contact]( path=path, params={}, verb="get" @@ -353,12 +356,13 @@ def remove( if contact_identifier is None: raise ValueError("id or email must be provided") + encoded_contact_identifier = quote(contact_identifier, safe="") if audience_id: # Audience-specific contact - path = f"/audiences/{audience_id}/contacts/{contact_identifier}" + path = f"/audiences/{audience_id}/contacts/{encoded_contact_identifier}" else: # Global contact - path = f"/contacts/{contact_identifier}" + path = f"/contacts/{encoded_contact_identifier}" resp = request.Request[Contacts.RemoveContactResponse]( path=path, params={}, verb="delete" @@ -414,12 +418,13 @@ async def update_async(cls, params: UpdateParams) -> UpdateContactResponse: contact_identifier = ( params.get("email") if params.get("email") is not None else params.get("id") ) + encoded_contact_identifier = quote(cast(str, contact_identifier), safe="") audience_id = params.get("audience_id") if audience_id: - path = f"/audiences/{audience_id}/contacts/{contact_identifier}" + path = f"/audiences/{audience_id}/contacts/{encoded_contact_identifier}" else: - path = f"/contacts/{contact_identifier}" + path = f"/contacts/{encoded_contact_identifier}" resp = await AsyncRequest[Contacts.UpdateContactResponse]( path=path, params=cast(Dict[Any, Any], params), verb="patch" @@ -486,10 +491,11 @@ async def get_async( if contact_identifier is None: raise ValueError("id or email must be provided") + encoded_contact_identifier = quote(contact_identifier, safe="") if audience_id: - path = f"/audiences/{audience_id}/contacts/{contact_identifier}" + path = f"/audiences/{audience_id}/contacts/{encoded_contact_identifier}" else: - path = f"/contacts/{contact_identifier}" + path = f"/contacts/{encoded_contact_identifier}" resp = await AsyncRequest[Contact]( path=path, params={}, verb="get" @@ -520,10 +526,11 @@ async def remove_async( if contact_identifier is None: raise ValueError("id or email must be provided") + encoded_contact_identifier = quote(contact_identifier, safe="") if audience_id: - path = f"/audiences/{audience_id}/contacts/{contact_identifier}" + path = f"/audiences/{audience_id}/contacts/{encoded_contact_identifier}" else: - path = f"/contacts/{contact_identifier}" + path = f"/contacts/{encoded_contact_identifier}" resp = await AsyncRequest[Contacts.RemoveContactResponse]( path=path, params={}, verb="delete" diff --git a/resend/contacts/_topics.py b/resend/contacts/_topics.py index 2321885..d2bc24d 100644 --- a/resend/contacts/_topics.py +++ b/resend/contacts/_topics.py @@ -1,4 +1,5 @@ from typing import Any, Dict, List, Optional, Union, cast +from urllib.parse import quote from typing_extensions import NotRequired, TypedDict @@ -140,7 +141,8 @@ def list( if contact is None: raise ValueError("contact_id or email must be provided") - base_path = f"/contacts/{contact}/topics" + encoded_contact = quote(contact, safe="") + base_path = f"/contacts/{encoded_contact}/topics" query_params = cast(Dict[Any, Any], params) if params else None path = PaginationHelper.build_paginated_path(base_path, query_params) resp = request.Request[_ListResponse]( @@ -172,7 +174,8 @@ def update(cls, params: UpdateParams) -> UpdateResponse: contact = ( params.get("id") if params.get("id") is not None else params.get("email") ) - path = f"/contacts/{contact}/topics" + encoded_contact = quote(cast(str, contact), safe="") + path = f"/contacts/{encoded_contact}/topics" # Send the topics array directly as the request body (not wrapped in an object) # The Request class accepts Union[Dict, List] as params @@ -211,7 +214,8 @@ async def list_async( if contact is None: raise ValueError("contact_id or email must be provided") - base_path = f"/contacts/{contact}/topics" + encoded_contact = quote(contact, safe="") + base_path = f"/contacts/{encoded_contact}/topics" query_params = cast(Dict[Any, Any], params) if params else None path = PaginationHelper.build_paginated_path(base_path, query_params) resp = await AsyncRequest[_ListResponse]( @@ -240,7 +244,8 @@ async def update_async(cls, params: UpdateParams) -> UpdateResponse: contact = ( params.get("id") if params.get("id") is not None else params.get("email") ) - path = f"/contacts/{contact}/topics" + encoded_contact = quote(cast(str, contact), safe="") + path = f"/contacts/{encoded_contact}/topics" request_body: Union[Dict[str, Any], List[Dict[str, Any]]] = cast( List[Dict[str, Any]], params["topics"] diff --git a/resend/contacts/segments/_contact_segments.py b/resend/contacts/segments/_contact_segments.py index ce564a0..0f7fa84 100644 --- a/resend/contacts/segments/_contact_segments.py +++ b/resend/contacts/segments/_contact_segments.py @@ -1,4 +1,5 @@ from typing import Any, Dict, List, Optional, cast +from urllib.parse import quote from typing_extensions import NotRequired, TypedDict @@ -185,7 +186,8 @@ def add(cls, params: AddParams) -> AddContactSegmentResponse: raise ValueError("Either contact_id or email must be provided") segment_id = params["segment_id"] - path = f"/contacts/{contact_identifier}/segments/{segment_id}" + encoded_contact_identifier = quote(contact_identifier, safe="") + path = f"/contacts/{encoded_contact_identifier}/segments/{segment_id}" resp = request.Request[ContactSegments.AddContactSegmentResponse]( path=path, params=cast(Dict[Any, Any], params), verb="post" ).perform_with_content() @@ -210,7 +212,8 @@ def remove(cls, params: RemoveParams) -> RemoveContactSegmentResponse: raise ValueError("Either contact_id or email must be provided") segment_id = params["segment_id"] - path = f"/contacts/{contact_identifier}/segments/{segment_id}" + encoded_contact_identifier = quote(contact_identifier, safe="") + path = f"/contacts/{encoded_contact_identifier}/segments/{segment_id}" resp = request.Request[ContactSegments.RemoveContactSegmentResponse]( path=path, params={}, verb="delete" ).perform_with_content() @@ -237,7 +240,8 @@ def list( if not contact_identifier: raise ValueError("Either contact_id or email must be provided") - base_path = f"/contacts/{contact_identifier}/segments" + encoded_contact_identifier = quote(contact_identifier, safe="") + base_path = f"/contacts/{encoded_contact_identifier}/segments" query_params = cast(Dict[Any, Any], pagination) if pagination else None path = PaginationHelper.build_paginated_path(base_path, query_params) resp = request.Request[ContactSegments.ListContactSegmentsResponse]( @@ -264,7 +268,8 @@ async def add_async(cls, params: AddParams) -> AddContactSegmentResponse: raise ValueError("Either contact_id or email must be provided") segment_id = params["segment_id"] - path = f"/contacts/{contact_identifier}/segments/{segment_id}" + encoded_contact_identifier = quote(contact_identifier, safe="") + path = f"/contacts/{encoded_contact_identifier}/segments/{segment_id}" resp = await AsyncRequest[ContactSegments.AddContactSegmentResponse]( path=path, params=cast(Dict[Any, Any], params), verb="post" ).perform_with_content() @@ -289,7 +294,8 @@ async def remove_async(cls, params: RemoveParams) -> RemoveContactSegmentRespons raise ValueError("Either contact_id or email must be provided") segment_id = params["segment_id"] - path = f"/contacts/{contact_identifier}/segments/{segment_id}" + encoded_contact_identifier = quote(contact_identifier, safe="") + path = f"/contacts/{encoded_contact_identifier}/segments/{segment_id}" resp = await AsyncRequest[ContactSegments.RemoveContactSegmentResponse]( path=path, params={}, verb="delete" ).perform_with_content() @@ -316,7 +322,8 @@ async def list_async( if not contact_identifier: raise ValueError("Either contact_id or email must be provided") - base_path = f"/contacts/{contact_identifier}/segments" + encoded_contact_identifier = quote(contact_identifier, safe="") + base_path = f"/contacts/{encoded_contact_identifier}/segments" query_params = cast(Dict[Any, Any], pagination) if pagination else None path = PaginationHelper.build_paginated_path(base_path, query_params) resp = await AsyncRequest[ContactSegments.ListContactSegmentsResponse]( diff --git a/tests/contact_topics_async_test.py b/tests/contact_topics_async_test.py index 8076e18..2d680f1 100644 --- a/tests/contact_topics_async_test.py +++ b/tests/contact_topics_async_test.py @@ -59,6 +59,24 @@ async def test_contact_topics_list_async_by_email(self) -> None: assert response["has_more"] is False assert response["data"][0]["id"] == "topic_123" + async def test_contact_topics_list_async_encodes_email_identifier(self) -> None: + self.set_mock_json( + { + "object": "list", + "has_more": False, + "data": [], + } + ) + + response: ContactsTopics.ListResponse = await resend.Contacts.Topics.list_async( + email="team/a?b@example.com" + ) + assert response["has_more"] is False + assert ( + self.mock.call_args.kwargs["url"] + == "https://api.resend.com/contacts/team%2Fa%3Fb%40example.com/topics" + ) + async def test_contact_topics_list_async_raises_when_no_contact_identifier( self, ) -> None: @@ -113,6 +131,28 @@ async def test_contact_topics_update_async_by_email(self) -> None: ) assert response["id"] == "cont_456" + async def test_contact_topics_update_async_encodes_email_identifier(self) -> None: + self.set_mock_json( + { + "id": "cont_456", + } + ) + + params: ContactsTopics.UpdateParams = { + "email": "team/a?b@example.com", + "topics": [ + {"id": "topic_1", "subscription": "opt_in"}, + ], + } + response: ContactsTopics.UpdateResponse = ( + await resend.Contacts.Topics.update_async(params) + ) + assert response["id"] == "cont_456" + assert ( + self.mock.call_args.kwargs["url"] + == "https://api.resend.com/contacts/team%2Fa%3Fb%40example.com/topics" + ) + async def test_contact_topics_update_async_raises_when_no_contact_identifier( self, ) -> None: diff --git a/tests/contact_topics_test.py b/tests/contact_topics_test.py index 494cbfc..b494639 100644 --- a/tests/contact_topics_test.py +++ b/tests/contact_topics_test.py @@ -55,6 +55,24 @@ def test_contact_topics_list_by_email(self) -> None: assert response["has_more"] is False assert response["data"][0]["id"] == "topic_123" + def test_contact_topics_list_encodes_email_identifier(self) -> None: + self.set_mock_json( + { + "object": "list", + "has_more": False, + "data": [], + } + ) + + response: ContactsTopics.ListResponse = resend.Contacts.Topics.list( + email="team/a?b@example.com" + ) + assert response["has_more"] is False + assert ( + self.mock.call_args.kwargs["url"] + == "https://api.resend.com/contacts/team%2Fa%3Fb%40example.com/topics" + ) + def test_contact_topics_list_with_pagination(self) -> None: self.set_mock_json( { @@ -136,6 +154,26 @@ def test_contact_topics_update_by_email(self) -> None: response: ContactsTopics.UpdateResponse = resend.Contacts.Topics.update(params) assert response["id"] == "cont_456" + def test_contact_topics_update_encodes_email_identifier(self) -> None: + self.set_mock_json( + { + "id": "cont_456", + } + ) + + params: ContactsTopics.UpdateParams = { + "email": "team/a?b@example.com", + "topics": [ + {"id": "topic_1", "subscription": "opt_in"}, + ], + } + response: ContactsTopics.UpdateResponse = resend.Contacts.Topics.update(params) + assert response["id"] == "cont_456" + assert ( + self.mock.call_args.kwargs["url"] + == "https://api.resend.com/contacts/team%2Fa%3Fb%40example.com/topics" + ) + def test_contact_topics_update_raises_when_no_contact_identifier(self) -> None: resend.api_key = "re_123" diff --git a/tests/contacts_async_test.py b/tests/contacts_async_test.py index c24f758..d503935 100644 --- a/tests/contacts_async_test.py +++ b/tests/contacts_async_test.py @@ -58,6 +58,25 @@ async def test_contacts_update_async(self) -> None: contact = await resend.Contacts.update_async(params) assert contact["id"] == "479e3145-dd38-476b-932c-529ceb705947" + async def test_contacts_update_async_encodes_email_identifier(self) -> None: + self.set_mock_json( + { + "object": "contact", + "id": "global-contact-123", + } + ) + + params: resend.Contacts.UpdateParams = { + "email": "team/a?b@example.com", + "first_name": "Updated Global", + } + contact = await resend.Contacts.update_async(params) + assert contact["id"] == "global-contact-123" + assert ( + self.mock.call_args.kwargs["url"] + == "https://api.resend.com/contacts/team%2Fa%3Fb%40example.com" + ) + async def test_contacts_update_async_missing_required_params(self) -> None: params: resend.Contacts.UpdateParams = { "audience_id": "48c269ed-9873-4d60-bdd9-cd7e6fc0b9b8", @@ -131,6 +150,24 @@ async def test_contacts_get_async_by_email(self) -> None: assert contact["created_at"] == "2023-10-06T23:47:56.678Z" assert contact["unsubscribed"] is False + async def test_contacts_get_async_encodes_email_identifier(self) -> None: + self.set_mock_json( + { + "object": "contact", + "id": "e169aa45-1ecf-4183-9955-b1499d5701d3", + "email": "team/a?b@example.com", + "created_at": "2023-10-06T23:47:56.678Z", + "unsubscribed": False, + } + ) + + contact = await resend.Contacts.get_async(email="team/a?b@example.com") + assert contact["email"] == "team/a?b@example.com" + assert ( + self.mock.call_args.kwargs["url"] + == "https://api.resend.com/contacts/team%2Fa%3Fb%40example.com" + ) + async def test_contacts_get_async_raises(self) -> None: resend.api_key = "re_123" @@ -193,6 +230,22 @@ async def test_contacts_remove_async_by_email(self) -> None: assert rmed["id"] == "520784e2-887d-4c25-b53c-4ad46ad38100" assert rmed["deleted"] is True + async def test_contacts_remove_async_encodes_email_identifier(self) -> None: + self.set_mock_json( + { + "object": "contact", + "id": "520784e2-887d-4c25-b53c-4ad46ad38100", + "deleted": True, + } + ) + + rmed = await resend.Contacts.remove_async(email="team/a?b@example.com") + assert rmed["deleted"] is True + assert ( + self.mock.call_args.kwargs["url"] + == "https://api.resend.com/contacts/team%2Fa%3Fb%40example.com" + ) + async def test_should_remove_contacts_async_by_email_raise_exception_when_no_content( self, ) -> None: diff --git a/tests/contacts_segments_async_test.py b/tests/contacts_segments_async_test.py index c3990dd..7482156 100644 --- a/tests/contacts_segments_async_test.py +++ b/tests/contacts_segments_async_test.py @@ -30,6 +30,20 @@ async def test_contact_segments_add_async_with_email(self) -> None: response = await resend.Contacts.Segments.add_async(params) assert response["id"] == "contact-segment-id-456" + async def test_contact_segments_add_async_encodes_email_identifier(self) -> None: + self.set_mock_json({"id": "contact-segment-id-456"}) + + params: resend.ContactSegments.AddParams = { + "segment_id": "segment-123", + "email": "team/a?b@example.com", + } + response = await resend.Contacts.Segments.add_async(params) + assert response["id"] == "contact-segment-id-456" + assert ( + self.mock.call_args.kwargs["url"] + == "https://api.resend.com/contacts/team%2Fa%3Fb%40example.com/segments/segment-123" + ) + async def test_contact_segments_add_async_raises_without_identifier(self) -> None: params: resend.ContactSegments.AddParams = { "segment_id": "segment-123", @@ -60,6 +74,20 @@ async def test_contact_segments_remove_async(self) -> None: assert response["id"] == "contact-segment-id-123" assert response["deleted"] is True + async def test_contact_segments_remove_async_encodes_email_identifier(self) -> None: + self.set_mock_json({"id": "contact-segment-id-456", "deleted": True}) + + params: resend.ContactSegments.RemoveParams = { + "segment_id": "segment-123", + "email": "team/a?b@example.com", + } + response = await resend.Contacts.Segments.remove_async(params) + assert response["deleted"] is True + assert ( + self.mock.call_args.kwargs["url"] + == "https://api.resend.com/contacts/team%2Fa%3Fb%40example.com/segments/segment-123" + ) + async def test_contact_segments_remove_async_raises_without_identifier( self, ) -> None: @@ -113,6 +141,25 @@ async def test_contact_segments_list_async(self) -> None: assert response["data"][0]["id"] == "segment-1" assert response["data"][1]["id"] == "segment-2" + async def test_contact_segments_list_async_encodes_email_identifier(self) -> None: + self.set_mock_json( + { + "object": "list", + "has_more": False, + "data": [], + } + ) + + params: resend.ContactSegments.ListParams = { + "email": "team/a?b@example.com", + } + response = await resend.Contacts.Segments.list_async(params) + assert response["has_more"] is False + assert ( + self.mock.call_args.kwargs["url"] + == "https://api.resend.com/contacts/team%2Fa%3Fb%40example.com/segments" + ) + async def test_contact_segments_list_async_raises_without_identifier(self) -> None: params: resend.ContactSegments.ListParams = {} with pytest.raises(ValueError) as exc_info: diff --git a/tests/contacts_segments_test.py b/tests/contacts_segments_test.py index 3a3c9cd..a103d3a 100644 --- a/tests/contacts_segments_test.py +++ b/tests/contacts_segments_test.py @@ -26,6 +26,20 @@ def test_contact_segments_add_with_email(self) -> None: response = resend.Contacts.Segments.add(params) assert response["id"] == "contact-segment-id-456" + def test_contact_segments_add_encodes_email_identifier(self) -> None: + self.set_mock_json({"id": "contact-segment-id-456"}) + + params: resend.ContactSegments.AddParams = { + "segment_id": "segment-123", + "email": "team/a?b@example.com", + } + response = resend.Contacts.Segments.add(params) + assert response["id"] == "contact-segment-id-456" + assert ( + self.mock.call_args.kwargs["url"] + == "https://api.resend.com/contacts/team%2Fa%3Fb%40example.com/segments/segment-123" + ) + def test_contact_segments_add_raises_without_identifier(self) -> None: params: resend.ContactSegments.AddParams = { "segment_id": "segment-123", @@ -56,6 +70,20 @@ def test_contact_segments_remove_with_email(self) -> None: assert response["id"] == "contact-segment-id-456" assert response["deleted"] is True + def test_contact_segments_remove_encodes_email_identifier(self) -> None: + self.set_mock_json({"id": "contact-segment-id-456", "deleted": True}) + + params: resend.ContactSegments.RemoveParams = { + "segment_id": "segment-123", + "email": "team/a?b@example.com", + } + response = resend.Contacts.Segments.remove(params) + assert response["deleted"] is True + assert ( + self.mock.call_args.kwargs["url"] + == "https://api.resend.com/contacts/team%2Fa%3Fb%40example.com/segments/segment-123" + ) + def test_contact_segments_remove_raises_without_identifier(self) -> None: params: resend.ContactSegments.RemoveParams = { "segment_id": "segment-123", @@ -120,6 +148,25 @@ def test_contact_segments_list_with_email(self) -> None: assert len(response["data"]) == 1 assert response["data"][0]["id"] == "segment-3" + def test_contact_segments_list_encodes_email_identifier(self) -> None: + self.set_mock_json( + { + "object": "list", + "has_more": False, + "data": [], + } + ) + + params: resend.ContactSegments.ListParams = { + "email": "team/a?b@example.com", + } + response = resend.Contacts.Segments.list(params) + assert response["has_more"] is False + assert ( + self.mock.call_args.kwargs["url"] + == "https://api.resend.com/contacts/team%2Fa%3Fb%40example.com/segments" + ) + def test_contact_segments_list_with_pagination(self) -> None: self.set_mock_json( { diff --git a/tests/contacts_test.py b/tests/contacts_test.py index c0f965c..ebb1f49 100644 --- a/tests/contacts_test.py +++ b/tests/contacts_test.py @@ -122,6 +122,24 @@ def test_contacts_get_by_email(self) -> None: assert contact["created_at"] == "2023-10-06T23:47:56.678Z" assert contact["unsubscribed"] is False + def test_contacts_get_encodes_email_identifier(self) -> None: + self.set_mock_json( + { + "object": "contact", + "id": "e169aa45-1ecf-4183-9955-b1499d5701d3", + "email": "team/a?b@example.com", + "created_at": "2023-10-06T23:47:56.678Z", + "unsubscribed": False, + } + ) + + contact = resend.Contacts.get(email="team/a?b@example.com") + assert contact["email"] == "team/a?b@example.com" + assert ( + self.mock.call_args.kwargs["url"] + == "https://api.resend.com/contacts/team%2Fa%3Fb%40example.com" + ) + def test_contacts_get_raises(self) -> None: resend.api_key = "re_123" @@ -330,6 +348,25 @@ def test_contacts_update_global(self) -> None: contact = resend.Contacts.update(params) assert contact["id"] == "global-contact-123" + def test_contacts_update_encodes_email_identifier(self) -> None: + self.set_mock_json( + { + "object": "contact", + "id": "global-contact-123", + } + ) + + params: resend.Contacts.UpdateParams = { + "email": "team/a?b@example.com", + "first_name": "Updated Global", + } + contact = resend.Contacts.update(params) + assert contact["id"] == "global-contact-123" + assert ( + self.mock.call_args.kwargs["url"] + == "https://api.resend.com/contacts/team%2Fa%3Fb%40example.com" + ) + def test_contacts_get_global(self) -> None: self.set_mock_json( { @@ -349,9 +386,6 @@ def test_contacts_get_global(self) -> None: assert contact["email"] == "global@example.com" assert contact.get("properties") == {"tier": "premium"} - # Note: Global contacts only accept UUID identifiers, not emails - # The API returns "The `id` must be a valid UUID" for email identifiers - def test_contacts_list_global(self) -> None: self.set_mock_json( { @@ -459,5 +493,18 @@ def test_contacts_remove_global_by_id(self) -> None: assert rmed["contact"] == "global-contact-789" assert rmed["deleted"] is True - # Note: Global contacts only accept UUID identifiers for remove operations - # Email-based removal would fail with "The `id` must be a valid UUID" error + def test_contacts_remove_encodes_email_identifier(self) -> None: + self.set_mock_json( + { + "object": "contact", + "contact": "global-contact-789", + "deleted": True, + } + ) + + rmed = resend.Contacts.remove(email="team/a?b@example.com") + assert rmed["deleted"] is True + assert ( + self.mock.call_args.kwargs["url"] + == "https://api.resend.com/contacts/team%2Fa%3Fb%40example.com" + )