Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 112 additions & 6 deletions src/main/java/com/resend/services/contacts/Contacts.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,13 @@ public Contacts(final String apiKey) {
public CreateContactResponseSuccess create(CreateContactOptions createContactOptions) throws ResendException {
String segmentId = createContactOptions.getSegmentId() != null ? createContactOptions.getSegmentId() : createContactOptions.getAudienceId();
String payload = super.resendMapper.writeValue(createContactOptions);
AbstractHttpResponse<String> response = httpClient.perform("/segments/" + segmentId + "/contacts" , super.apiKey, HttpMethod.POST, payload, MediaType.get("application/json"));

// Use /contacts for global contacts (when no segment ID is provided)
String endpoint = (segmentId == null || segmentId.isEmpty())
? "/contacts"
: "/segments/" + segmentId + "/contacts";

AbstractHttpResponse<String> response = httpClient.perform(endpoint, super.apiKey, HttpMethod.POST, payload, MediaType.get("application/json"));

if (!response.isSuccessful()) {
throw new ResendException(response.getCode(), response.getBody());
Expand Down Expand Up @@ -83,13 +89,51 @@ public ListContactsResponseSuccess list(String segmentId, ListParams params) thr
return resendMapper.readValue(responseBody, ListContactsResponseSuccess.class);
}

/**
* Retrieves a list of global contacts (not associated with any segment).
*
* @return A ListContactsResponseSuccess containing the list of global contacts.
* @throws ResendException If an error occurs during the contacts list retrieval process.
*/
public ListContactsResponseSuccess list() throws ResendException {
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Oct 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rule violated: API Key Permission Check SDK Methods

Per the API Key Permission Check SDK Methods guideline, these new global contact endpoints (list/get/remove/update against /contacts) introduce additional Resend API operations. Please confirm production API keys have the necessary permissions for these calls to prevent authorization failures post-deploy.

Prompt for AI agents
Address the following comment on src/main/java/com/resend/services/contacts/Contacts.java at line 98:

<comment>Per the API Key Permission Check SDK Methods guideline, these new global contact endpoints (list/get/remove/update against /contacts) introduce additional Resend API operations. Please confirm production API keys have the necessary permissions for these calls to prevent authorization failures post-deploy.</comment>

<file context>
@@ -83,13 +89,51 @@ public ListContactsResponseSuccess list(String segmentId, ListParams params) thr
+     * @return A ListContactsResponseSuccess containing the list of global contacts.
+     * @throws ResendException If an error occurs during the contacts list retrieval process.
+     */
+    public ListContactsResponseSuccess list() throws ResendException {
+        AbstractHttpResponse&lt;String&gt; response = this.httpClient.perform(&quot;/contacts&quot;, super.apiKey, HttpMethod.GET, null, MediaType.get(&quot;application/json&quot;));
+
</file context>
Fix with Cubic

AbstractHttpResponse<String> response = this.httpClient.perform("/contacts", super.apiKey, HttpMethod.GET, null, MediaType.get("application/json"));

if (!response.isSuccessful()) {
throw new ResendException(response.getCode(), response.getBody());
}

String responseBody = response.getBody();

return resendMapper.readValue(responseBody, ListContactsResponseSuccess.class);
}

/**
* Retrieves a paginated list of global contacts (not associated with any segment).
*
* @param params The params used to customize the list.
* @return A ListContactsResponseSuccess containing the paginated list of global contacts.
* @throws ResendException If an error occurs during the contacts list retrieval process.
*/
public ListContactsResponseSuccess list(ListParams params) throws ResendException {
String pathWithQuery = "/contacts" + URLHelper.parse(params);
AbstractHttpResponse<String> response = this.httpClient.perform(pathWithQuery, super.apiKey, HttpMethod.GET, null, MediaType.get("application/json"));

if (!response.isSuccessful()) {
throw new ResendException(response.getCode(), response.getBody());
}

String responseBody = response.getBody();

return resendMapper.readValue(responseBody, ListContactsResponseSuccess.class);
}

/**
* Retrieves a contact by its unique identifier.
*
* @param params The object containing:
* – {@code audienceId}: the audience to which the contact belongs
* – Either {@code id}: the contacts id
* or {@code email}: the contacts email address
* – Either {@code id}: the contact's id
* or {@code email}: the contact's email address
* @return The retrieved contact details.
* @throws ResendException If an error occurs while retrieving the contact.
*/
Expand All @@ -104,7 +148,35 @@ public GetContactResponseSuccess get(GetContactOptions params) throws ResendExce
String contactIdentifier = (id != null && !id.isEmpty()) ? id : email;
String segmentId = params.getSegmentId() != null ? params.getSegmentId() : params.getAudienceId();

AbstractHttpResponse<String> response = this.httpClient.perform("/segments/" + segmentId + "/contacts/" + contactIdentifier, super.apiKey, HttpMethod.GET, null, MediaType.get("application/json"));
// Use /contacts for global contacts (when no segment ID is provided)
String endpoint = (segmentId == null || segmentId.isEmpty())
? "/contacts/" + contactIdentifier
: "/segments/" + segmentId + "/contacts/" + contactIdentifier;

AbstractHttpResponse<String> response = this.httpClient.perform(endpoint, super.apiKey, HttpMethod.GET, null, MediaType.get("application/json"));

if (!response.isSuccessful()) {
throw new ResendException(response.getCode(), response.getBody());
}

String responseBody = response.getBody();

return resendMapper.readValue(responseBody, GetContactResponseSuccess.class);
}

/**
* Retrieves a global contact by its unique identifier (not associated with any segment).
*
* @param contactIdOrEmail The contact's id or email address.
* @return The retrieved contact details.
* @throws ResendException If an error occurs while retrieving the contact.
*/
public GetContactResponseSuccess get(String contactIdOrEmail) throws ResendException {
if (contactIdOrEmail == null || contactIdOrEmail.isEmpty()) {
throw new IllegalArgumentException("Contact id or email must be provided");
}

AbstractHttpResponse<String> response = this.httpClient.perform("/contacts/" + contactIdOrEmail, super.apiKey, HttpMethod.GET, null, MediaType.get("application/json"));

if (!response.isSuccessful()) {
throw new ResendException(response.getCode(), response.getBody());
Expand All @@ -131,7 +203,36 @@ public RemoveContactResponseSuccess remove(RemoveContactOptions params) throws R
String pathParameter = params.getId() != null ? params.getId() : params.getEmail();
String segmentId = params.getSegmentId() != null ? params.getSegmentId() : params.getAudienceId();

AbstractHttpResponse<String> response = httpClient.perform("/segments/" + segmentId + "/contacts/" + pathParameter, super.apiKey, HttpMethod.DELETE, "", null);
// Use /contacts for global contacts (when no segment ID is provided)
String endpoint = (segmentId == null || segmentId.isEmpty())
? "/contacts/" + pathParameter
: "/segments/" + segmentId + "/contacts/" + pathParameter;

AbstractHttpResponse<String> response = httpClient.perform(endpoint, super.apiKey, HttpMethod.DELETE, "", null);

if (!response.isSuccessful()) {
throw new ResendException(response.getCode(), response.getBody());
}

String responseBody = response.getBody();

return resendMapper.readValue(responseBody, RemoveContactResponseSuccess.class);
}

/**
* Deletes a global contact based on the provided contact ID.
* Note: Global contacts can only be removed by ID, not by email.
*
* @param contactId The contact's id.
* @return The RemoveContactsResponseSuccess with the details of the removed contact.
* @throws ResendException If an error occurs during the contact deletion process.
*/
public RemoveContactResponseSuccess remove(String contactId) throws ResendException {
if (contactId == null || contactId.isEmpty()) {
throw new IllegalArgumentException("Contact id must be provided");
}

AbstractHttpResponse<String> response = httpClient.perform("/contacts/" + contactId, super.apiKey, HttpMethod.DELETE, "", null);

if (!response.isSuccessful()) {
throw new ResendException(response.getCode(), response.getBody());
Expand All @@ -158,8 +259,13 @@ public UpdateContactResponseSuccess update(UpdateContactOptions params) throws R
String pathParameter = params.getId() != null ? params.getId() : params.getEmail();
String segmentId = params.getSegmentId() != null ? params.getSegmentId() : params.getAudienceId();

// Use /contacts for global contacts (when no segment ID is provided)
String endpoint = (segmentId == null || segmentId.isEmpty())
? "/contacts/" + pathParameter
: "/segments/" + segmentId + "/contacts/" + pathParameter;

String payload = super.resendMapper.writeValue(params);
AbstractHttpResponse<String> response = httpClient.perform("/segments/" + segmentId + "/contacts/" + pathParameter, super.apiKey, HttpMethod.PATCH, payload, MediaType.get("application/json"));
AbstractHttpResponse<String> response = httpClient.perform(endpoint, super.apiKey, HttpMethod.PATCH, payload, MediaType.get("application/json"));

if (!response.isSuccessful()) {
throw new ResendException(response.getCode(), response.getBody());
Expand Down
128 changes: 128 additions & 0 deletions src/test/java/com/resend/services/contacts/ContactsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -203,4 +203,132 @@ public void testUpdateTopics_Success() throws ResendException {
assertEquals(expectedResponse.getId(), res.getId());
verify(contacts, times(1)).updateTopics(options);
}

// Global contacts tests (without segment ID)

@Test
public void testCreateGlobalContact_Success() throws ResendException {
CreateContactResponseSuccess expectedContact = ContactsUtil.createContactResponseSuccess();
CreateContactOptions param = CreateContactOptions.builder()
.email("user@example.com")
.firstName("John")
.lastName("Doe")
.build();

when(contacts.create(param))
.thenReturn(expectedContact);

CreateContactResponseSuccess createdContact = contacts.create(param);

assertEquals(createdContact, expectedContact);
verify(contacts, times(1)).create(param);
}

@Test
public void testListGlobalContacts_Success() throws ResendException {
ListContactsResponseSuccess expectedResponse = ContactsUtil.createContactsListResponse();

when(contacts.list())
.thenReturn(expectedResponse);

ListContactsResponseSuccess res = contacts.list();

assertNotNull(res);
assertEquals(expectedResponse.getData().size(), res.getData().size());
assertEquals(expectedResponse.getObject(), res.getObject());
}

@Test
public void testListGlobalContactsWithPagination_Success() throws ResendException {
ListParams params = ListParams.builder()
.limit(3).build();

ListContactsResponseSuccess expectedResponse = ContactsUtil.createContactsListResponse();

when(contacts.list(params))
.thenReturn(expectedResponse);

ListContactsResponseSuccess res = contacts.list(params);

assertNotNull(res);
assertEquals(params.getLimit(), res.getData().size());
assertEquals(expectedResponse.getObject(), res.getObject());
}

@Test
public void testGetGlobalContactById_Success() throws ResendException {
String contactId = "e169aa45-1ecf-4183-9955-b1499d5701d3";
GetContactResponseSuccess expected = ContactsUtil.getContactResponseSuccess();

when(contacts.get(contactId)).thenReturn(expected);

GetContactResponseSuccess res = contacts.get(contactId);

assertNotNull(res);
assertEquals(expected, res);
verify(contacts, times(1)).get(contactId);
}

@Test
public void testGetGlobalContactByEmail_Success() throws ResendException {
String contactEmail = "user@example.com";
GetContactResponseSuccess expected = ContactsUtil.getContactResponseSuccess();

when(contacts.get(contactEmail)).thenReturn(expected);

GetContactResponseSuccess res = contacts.get(contactEmail);

assertNotNull(res);
assertEquals(expected, res);
verify(contacts, times(1)).get(contactEmail);
}

@Test
public void testRemoveGlobalContactById_Success() throws ResendException {
String contactId = "e169aa45-1ecf-4183-9955-b1499d5701d3";
RemoveContactResponseSuccess removed = ContactsUtil.removeContactResponseSuccess();

when(contacts.remove(contactId))
.thenReturn(removed);

RemoveContactResponseSuccess res = contacts.remove(contactId);

assertEquals(removed, res);
verify(contacts, times(1)).remove(contactId);
}

@Test
public void testUpdateGlobalContact_Success() throws ResendException {
UpdateContactOptions params = UpdateContactOptions.builder()
.id("e169aa45-1ecf-4183-9955-b1499d5701d3")
.firstName("Jane")
.lastName("Smith")
.build();
UpdateContactResponseSuccess expectedResponse = ContactsUtil.updateContactResponseSuccess();

when(contacts.update(params))
.thenReturn(expectedResponse);

UpdateContactResponseSuccess res = contacts.update(params);

assertNotNull(res);
assertEquals(expectedResponse.getId(), res.getId());
assertEquals(expectedResponse.getObject(), res.getObject());
}

@Test
public void testGetContactWithoutSegmentId_Success() throws ResendException {
GetContactOptions params = GetContactOptions.builder()
.id("e169aa45-1ecf-4183-9955-b1499d5701d3")
.build();
GetContactResponseSuccess expected = ContactsUtil.getContactResponseSuccess();

when(contacts.get(params)).thenReturn(expected);

GetContactResponseSuccess res = contacts.get(params);

assertNotNull(res);
assertEquals(expected, res);
verify(contacts, times(1)).get(params);
}
}