From 892b3ed7ff90168008f4bb9bbe5ef3551f8bd092 Mon Sep 17 00:00:00 2001 From: Felipe Freitag Date: Mon, 12 Oct 2026 14:05:31 -0300 Subject: [PATCH 01/30] feat(inboxes): add the inbox request and response models --- .../model/CreateInboxDraftOptions.java | 321 ++++++++++++ .../CreateInboxDraftResponseSuccess.java | 51 ++ .../model/CreateInboxLabelOptions.java | 92 ++++ .../CreateInboxLabelResponseSuccess.java | 51 ++ .../inboxes/model/CreateInboxOptions.java | 142 +++++ .../model/CreateInboxResponseSuccess.java | 51 ++ .../model/ForwardInboxThreadEmailOptions.java | 322 ++++++++++++ ...orwardInboxThreadEmailResponseSuccess.java | 37 ++ .../model/GetInboxAgentResponseSuccess.java | 29 ++ .../model/GetInboxDraftResponseSuccess.java | 39 ++ .../model/GetInboxResponseSuccess.java | 34 ++ .../GetInboxThreadEmailResponseSuccess.java | 39 ++ .../model/GetInboxThreadResponseSuccess.java | 31 ++ .../resend/services/inboxes/model/Inbox.java | 177 +++++++ .../services/inboxes/model/InboxAgent.java | 81 +++ .../inboxes/model/InboxAgentAction.java | 62 +++ .../services/inboxes/model/InboxDraft.java | 221 ++++++++ .../inboxes/model/InboxDraftListItem.java | 165 ++++++ .../services/inboxes/model/InboxFolder.java | 58 +++ .../services/inboxes/model/InboxLabel.java | 79 +++ .../services/inboxes/model/InboxListItem.java | 107 ++++ .../services/inboxes/model/InboxThread.java | 207 ++++++++ .../inboxes/model/InboxThreadEmail.java | 221 ++++++++ .../model/InboxThreadEmailAttachment.java | 65 +++ .../inboxes/model/InboxThreadLabel.java | 65 +++ .../model/InboxThreadSearchResult.java | 69 +++ .../inboxes/model/InboxThreadSummary.java | 109 ++++ .../model/ListInboxDraftsResponseSuccess.java | 67 +++ .../model/ListInboxLabelsResponseSuccess.java | 53 ++ .../ListInboxThreadEmailsResponseSuccess.java | 67 +++ .../inboxes/model/ListInboxThreadsParams.java | 250 +++++++++ .../ListInboxThreadsResponseSuccess.java | 67 +++ .../model/ListInboxesResponseSuccess.java | 67 +++ .../inboxes/model/MoveInboxThreadFolder.java | 56 ++ .../RemoveInboxDraftResponseSuccess.java | 65 +++ .../RemoveInboxLabelResponseSuccess.java | 65 +++ .../model/RemoveInboxResponseSuccess.java | 65 +++ .../RemoveInboxThreadResponseSuccess.java | 65 +++ .../model/ReplyInboxThreadEmailOptions.java | 297 +++++++++++ .../ReplyInboxThreadEmailResponseSuccess.java | 37 ++ .../model/SearchInboxThreadsParams.java | 489 ++++++++++++++++++ .../SearchInboxThreadsResponseSuccess.java | 67 +++ .../model/SendInboxDraftResponseSuccess.java | 37 ++ .../model/UpdateInboxAgentOptions.java | 145 ++++++ .../UpdateInboxAgentResponseSuccess.java | 51 ++ .../model/UpdateInboxDraftOptions.java | 222 ++++++++ .../UpdateInboxDraftResponseSuccess.java | 39 ++ .../model/UpdateInboxLabelOptions.java | 92 ++++ .../UpdateInboxLabelResponseSuccess.java | 51 ++ .../inboxes/model/UpdateInboxOptions.java | 92 ++++ .../model/UpdateInboxResponseSuccess.java | 51 ++ .../model/UpdateInboxThreadOptions.java | 198 +++++++ .../UpdateInboxThreadResponseSuccess.java | 31 ++ 53 files changed, 5711 insertions(+) create mode 100644 src/main/java/com/resend/services/inboxes/model/CreateInboxDraftOptions.java create mode 100644 src/main/java/com/resend/services/inboxes/model/CreateInboxDraftResponseSuccess.java create mode 100644 src/main/java/com/resend/services/inboxes/model/CreateInboxLabelOptions.java create mode 100644 src/main/java/com/resend/services/inboxes/model/CreateInboxLabelResponseSuccess.java create mode 100644 src/main/java/com/resend/services/inboxes/model/CreateInboxOptions.java create mode 100644 src/main/java/com/resend/services/inboxes/model/CreateInboxResponseSuccess.java create mode 100644 src/main/java/com/resend/services/inboxes/model/ForwardInboxThreadEmailOptions.java create mode 100644 src/main/java/com/resend/services/inboxes/model/ForwardInboxThreadEmailResponseSuccess.java create mode 100644 src/main/java/com/resend/services/inboxes/model/GetInboxAgentResponseSuccess.java create mode 100644 src/main/java/com/resend/services/inboxes/model/GetInboxDraftResponseSuccess.java create mode 100644 src/main/java/com/resend/services/inboxes/model/GetInboxResponseSuccess.java create mode 100644 src/main/java/com/resend/services/inboxes/model/GetInboxThreadEmailResponseSuccess.java create mode 100644 src/main/java/com/resend/services/inboxes/model/GetInboxThreadResponseSuccess.java create mode 100644 src/main/java/com/resend/services/inboxes/model/Inbox.java create mode 100644 src/main/java/com/resend/services/inboxes/model/InboxAgent.java create mode 100644 src/main/java/com/resend/services/inboxes/model/InboxAgentAction.java create mode 100644 src/main/java/com/resend/services/inboxes/model/InboxDraft.java create mode 100644 src/main/java/com/resend/services/inboxes/model/InboxDraftListItem.java create mode 100644 src/main/java/com/resend/services/inboxes/model/InboxFolder.java create mode 100644 src/main/java/com/resend/services/inboxes/model/InboxLabel.java create mode 100644 src/main/java/com/resend/services/inboxes/model/InboxListItem.java create mode 100644 src/main/java/com/resend/services/inboxes/model/InboxThread.java create mode 100644 src/main/java/com/resend/services/inboxes/model/InboxThreadEmail.java create mode 100644 src/main/java/com/resend/services/inboxes/model/InboxThreadEmailAttachment.java create mode 100644 src/main/java/com/resend/services/inboxes/model/InboxThreadLabel.java create mode 100644 src/main/java/com/resend/services/inboxes/model/InboxThreadSearchResult.java create mode 100644 src/main/java/com/resend/services/inboxes/model/InboxThreadSummary.java create mode 100644 src/main/java/com/resend/services/inboxes/model/ListInboxDraftsResponseSuccess.java create mode 100644 src/main/java/com/resend/services/inboxes/model/ListInboxLabelsResponseSuccess.java create mode 100644 src/main/java/com/resend/services/inboxes/model/ListInboxThreadEmailsResponseSuccess.java create mode 100644 src/main/java/com/resend/services/inboxes/model/ListInboxThreadsParams.java create mode 100644 src/main/java/com/resend/services/inboxes/model/ListInboxThreadsResponseSuccess.java create mode 100644 src/main/java/com/resend/services/inboxes/model/ListInboxesResponseSuccess.java create mode 100644 src/main/java/com/resend/services/inboxes/model/MoveInboxThreadFolder.java create mode 100644 src/main/java/com/resend/services/inboxes/model/RemoveInboxDraftResponseSuccess.java create mode 100644 src/main/java/com/resend/services/inboxes/model/RemoveInboxLabelResponseSuccess.java create mode 100644 src/main/java/com/resend/services/inboxes/model/RemoveInboxResponseSuccess.java create mode 100644 src/main/java/com/resend/services/inboxes/model/RemoveInboxThreadResponseSuccess.java create mode 100644 src/main/java/com/resend/services/inboxes/model/ReplyInboxThreadEmailOptions.java create mode 100644 src/main/java/com/resend/services/inboxes/model/ReplyInboxThreadEmailResponseSuccess.java create mode 100644 src/main/java/com/resend/services/inboxes/model/SearchInboxThreadsParams.java create mode 100644 src/main/java/com/resend/services/inboxes/model/SearchInboxThreadsResponseSuccess.java create mode 100644 src/main/java/com/resend/services/inboxes/model/SendInboxDraftResponseSuccess.java create mode 100644 src/main/java/com/resend/services/inboxes/model/UpdateInboxAgentOptions.java create mode 100644 src/main/java/com/resend/services/inboxes/model/UpdateInboxAgentResponseSuccess.java create mode 100644 src/main/java/com/resend/services/inboxes/model/UpdateInboxDraftOptions.java create mode 100644 src/main/java/com/resend/services/inboxes/model/UpdateInboxDraftResponseSuccess.java create mode 100644 src/main/java/com/resend/services/inboxes/model/UpdateInboxLabelOptions.java create mode 100644 src/main/java/com/resend/services/inboxes/model/UpdateInboxLabelResponseSuccess.java create mode 100644 src/main/java/com/resend/services/inboxes/model/UpdateInboxOptions.java create mode 100644 src/main/java/com/resend/services/inboxes/model/UpdateInboxResponseSuccess.java create mode 100644 src/main/java/com/resend/services/inboxes/model/UpdateInboxThreadOptions.java create mode 100644 src/main/java/com/resend/services/inboxes/model/UpdateInboxThreadResponseSuccess.java diff --git a/src/main/java/com/resend/services/inboxes/model/CreateInboxDraftOptions.java b/src/main/java/com/resend/services/inboxes/model/CreateInboxDraftOptions.java new file mode 100644 index 0000000..918d814 --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/model/CreateInboxDraftOptions.java @@ -0,0 +1,321 @@ +package com.resend.services.inboxes.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Represents a request to create a draft. At least one of {@code to}, {@code cc}, {@code bcc}, {@code subject}, {@code text} or {@code html} must be non-empty. The combined recipients of {@code to}, {@code cc} and {@code bcc} cannot exceed 50. {@code thread_id} and {@code reply_to_email_id} must be sent together or not at all. + */ +public class CreateInboxDraftOptions { + + @JsonProperty("to") + private final List to; + + @JsonProperty("cc") + private final List cc; + + @JsonProperty("bcc") + private final List bcc; + + @JsonProperty("subject") + private final String subject; + + @JsonProperty("html") + private final String html; + + @JsonProperty("text") + private final String text; + + @JsonProperty("thread_id") + private final String threadId; + + @JsonProperty("reply_to_email_id") + private final String replyToEmailId; + + private CreateInboxDraftOptions(Builder builder) { + this.to = builder.to; + this.cc = builder.cc; + this.bcc = builder.bcc; + this.subject = builder.subject; + this.html = builder.html; + this.text = builder.text; + this.threadId = builder.threadId; + this.replyToEmailId = builder.replyToEmailId; + } + + /** + * Get the recipients. + * + * @return The recipients. + */ + public List getTo() { + return to; + } + + /** + * Get the CC recipients. + * + * @return The CC recipients. + */ + public List getCc() { + return cc; + } + + /** + * Get the BCC recipients. + * + * @return The BCC recipients. + */ + public List getBcc() { + return bcc; + } + + /** + * Get the subject. + * + * @return The subject. + */ + public String getSubject() { + return subject; + } + + /** + * Get the HTML body. + * + * @return The HTML body. + */ + public String getHtml() { + return html; + } + + /** + * Get the plain text body. + * + * @return The plain text body. + */ + public String getText() { + return text; + } + + /** + * Get the Thread ID to reply in. + * + * @return The Thread ID to reply in. + */ + public String getThreadId() { + return threadId; + } + + /** + * Get the Email ID to reply to, as returned in {@code data[].id} by the list thread emails endpoint. + * + * @return The Email ID to reply to, as returned in {@code data[].id} by the list thread emails endpoint. + */ + public String getReplyToEmailId() { + return replyToEmailId; + } + + /** + * Create a new builder instance for constructing CreateInboxDraftOptions objects. + * + * @return A new builder instance. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Builder class for constructing CreateInboxDraftOptions objects. + */ + public static class Builder { + private List to; + private List cc; + private List bcc; + private String subject; + private String html; + private String text; + private String threadId; + private String replyToEmailId; + + /** + * Creates a new Builder instance. + */ + public Builder() { + } + + /** + * Set the recipients. Each value can include a friendly name, for example {@code Ada Lovelace }. A reply draft without {@code to} is addressed to the sender of the email it replies to, or to its Reply-To when set. Send an empty list to leave it empty. + * + * @param to The recipients. + * @return The builder instance. + */ + public Builder to(String... to) { + this.to = new ArrayList<>(Arrays.asList(to)); + return this; + } + + /** + * Set the recipients. Each value can include a friendly name, for example {@code Ada Lovelace }. A reply draft without {@code to} is addressed to the sender of the email it replies to, or to its Reply-To when set. Send an empty list to leave it empty. + * + * @param to The recipients. + * @return The builder instance. + */ + public Builder to(List to) { + this.to = to == null ? null : new ArrayList<>(to); + return this; + } + + /** + * Add one value to the recipients. + * + * @param recipient The value to add. + * @return The builder instance. + */ + public Builder addTo(String recipient) { + if (this.to == null) { + this.to = new ArrayList<>(); + } + this.to.add(recipient); + return this; + } + + /** + * Set the CC recipients. + * + * @param cc The CC recipients. + * @return The builder instance. + */ + public Builder cc(String... cc) { + this.cc = new ArrayList<>(Arrays.asList(cc)); + return this; + } + + /** + * Set the CC recipients. + * + * @param cc The CC recipients. + * @return The builder instance. + */ + public Builder cc(List cc) { + this.cc = cc == null ? null : new ArrayList<>(cc); + return this; + } + + /** + * Add one value to the CC recipients. + * + * @param recipient The value to add. + * @return The builder instance. + */ + public Builder addCc(String recipient) { + if (this.cc == null) { + this.cc = new ArrayList<>(); + } + this.cc.add(recipient); + return this; + } + + /** + * Set the BCC recipients. + * + * @param bcc The BCC recipients. + * @return The builder instance. + */ + public Builder bcc(String... bcc) { + this.bcc = new ArrayList<>(Arrays.asList(bcc)); + return this; + } + + /** + * Set the BCC recipients. + * + * @param bcc The BCC recipients. + * @return The builder instance. + */ + public Builder bcc(List bcc) { + this.bcc = bcc == null ? null : new ArrayList<>(bcc); + return this; + } + + /** + * Add one value to the BCC recipients. + * + * @param recipient The value to add. + * @return The builder instance. + */ + public Builder addBcc(String recipient) { + if (this.bcc == null) { + this.bcc = new ArrayList<>(); + } + this.bcc.add(recipient); + return this; + } + + /** + * Set the subject. Max 2000 characters. A string that is only whitespace, or that contains a line break, is rejected. + * + * @param subject The subject. + * @return The builder instance. + */ + public Builder subject(String subject) { + this.subject = subject; + return this; + } + + /** + * Set the HTML body. + * + * @param html The HTML body. + * @return The builder instance. + */ + public Builder html(String html) { + this.html = html; + return this; + } + + /** + * Set the plain text body. + * + * @param text The plain text body. + * @return The builder instance. + */ + public Builder text(String text) { + this.text = text; + return this; + } + + /** + * Set the Thread ID to reply in. Must be sent with {@code reply_to_email_id}. + * + * @param threadId The Thread ID to reply in. + * @return The builder instance. + */ + public Builder threadId(String threadId) { + this.threadId = threadId; + return this; + } + + /** + * Set the Email ID to reply to, as returned in {@code data[].id} by the list thread emails endpoint. Must be sent with {@code thread_id}. + * + * @param replyToEmailId The Email ID to reply to, as returned in {@code data[].id} by the list thread emails endpoint. + * @return The builder instance. + */ + public Builder replyToEmailId(String replyToEmailId) { + this.replyToEmailId = replyToEmailId; + return this; + } + + /** + * Build a new CreateInboxDraftOptions object. + * + * @return A new CreateInboxDraftOptions object. + */ + public CreateInboxDraftOptions build() { + return new CreateInboxDraftOptions(this); + } + } +} diff --git a/src/main/java/com/resend/services/inboxes/model/CreateInboxDraftResponseSuccess.java b/src/main/java/com/resend/services/inboxes/model/CreateInboxDraftResponseSuccess.java new file mode 100644 index 0000000..00b8fdf --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/model/CreateInboxDraftResponseSuccess.java @@ -0,0 +1,51 @@ +package com.resend.services.inboxes.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Represents a successful response for creating a draft. + */ +public class CreateInboxDraftResponseSuccess { + + @JsonProperty("object") + private String object; + + @JsonProperty("id") + private String id; + + /** + * Default constructor + */ + public CreateInboxDraftResponseSuccess() { + + } + + /** + * Constructs a CreateInboxDraftResponseSuccess. + * + * @param object The object type. Always {@code inbox_draft}. + * @param id The ID of the draft. + */ + public CreateInboxDraftResponseSuccess(String object, String id) { + this.object = object; + this.id = id; + } + + /** + * Get the object type. Always {@code inbox_draft}. + * + * @return The object type. Always {@code inbox_draft}. + */ + public String getObject() { + return object; + } + + /** + * Get the ID of the draft. + * + * @return The ID of the draft. + */ + public String getId() { + return id; + } +} diff --git a/src/main/java/com/resend/services/inboxes/model/CreateInboxLabelOptions.java b/src/main/java/com/resend/services/inboxes/model/CreateInboxLabelOptions.java new file mode 100644 index 0000000..309fc70 --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/model/CreateInboxLabelOptions.java @@ -0,0 +1,92 @@ +package com.resend.services.inboxes.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Represents a request to create a label on an inbox. + */ +public class CreateInboxLabelOptions { + + @JsonProperty("name") + private final String name; + + @JsonProperty("color") + private final String color; + + private CreateInboxLabelOptions(Builder builder) { + this.name = builder.name; + this.color = builder.color; + } + + /** + * Get the name of the label. + * + * @return The name of the label. + */ + public String getName() { + return name; + } + + /** + * Get the color of the label, as a {@code #RRGGBB} hex color. + * + * @return The color of the label, as a {@code #RRGGBB} hex color. + */ + public String getColor() { + return color; + } + + /** + * Create a new builder instance for constructing CreateInboxLabelOptions objects. + * + * @return A new builder instance. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Builder class for constructing CreateInboxLabelOptions objects. + */ + public static class Builder { + private String name; + private String color; + + /** + * Creates a new Builder instance. + */ + public Builder() { + } + + /** + * Set the name of the label. Max 64 characters. Leading and trailing whitespace is trimmed. Must not match another label on the inbox, compared without case. + * + * @param name The name of the label. + * @return The builder instance. + */ + public Builder name(String name) { + this.name = name; + return this; + } + + /** + * Set the color of the label, as a {@code #RRGGBB} hex color. It is stored in uppercase. When omitted, a random color from the dashboard palette is used. + * + * @param color The color of the label, as a {@code #RRGGBB} hex color. + * @return The builder instance. + */ + public Builder color(String color) { + this.color = color; + return this; + } + + /** + * Build a new CreateInboxLabelOptions object. + * + * @return A new CreateInboxLabelOptions object. + */ + public CreateInboxLabelOptions build() { + return new CreateInboxLabelOptions(this); + } + } +} diff --git a/src/main/java/com/resend/services/inboxes/model/CreateInboxLabelResponseSuccess.java b/src/main/java/com/resend/services/inboxes/model/CreateInboxLabelResponseSuccess.java new file mode 100644 index 0000000..c9cdc81 --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/model/CreateInboxLabelResponseSuccess.java @@ -0,0 +1,51 @@ +package com.resend.services.inboxes.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Represents a successful response for creating an inbox label. + */ +public class CreateInboxLabelResponseSuccess { + + @JsonProperty("object") + private String object; + + @JsonProperty("id") + private String id; + + /** + * Default constructor + */ + public CreateInboxLabelResponseSuccess() { + + } + + /** + * Constructs a CreateInboxLabelResponseSuccess. + * + * @param object The object type. Always {@code inbox_label}. + * @param id The ID of the label. + */ + public CreateInboxLabelResponseSuccess(String object, String id) { + this.object = object; + this.id = id; + } + + /** + * Get the object type. Always {@code inbox_label}. + * + * @return The object type. Always {@code inbox_label}. + */ + public String getObject() { + return object; + } + + /** + * Get the ID of the label. + * + * @return The ID of the label. + */ + public String getId() { + return id; + } +} diff --git a/src/main/java/com/resend/services/inboxes/model/CreateInboxOptions.java b/src/main/java/com/resend/services/inboxes/model/CreateInboxOptions.java new file mode 100644 index 0000000..767fac4 --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/model/CreateInboxOptions.java @@ -0,0 +1,142 @@ +package com.resend.services.inboxes.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Represents a request to create an inbox. + */ +public class CreateInboxOptions { + + @JsonProperty("email_address") + private final String emailAddress; + + @JsonProperty("name") + private final String name; + + @JsonProperty("from_name") + private final String fromName; + + @JsonProperty("forwarding") + private final Boolean forwarding; + + private CreateInboxOptions(Builder builder) { + this.emailAddress = builder.emailAddress; + this.name = builder.name; + this.fromName = builder.fromName; + this.forwarding = builder.forwarding; + } + + /** + * Get the inbox email address on one of your domains. + * + * @return The inbox email address on one of your domains. + */ + public String getEmailAddress() { + return emailAddress; + } + + /** + * Get the internal name for the inbox. + * + * @return The internal name for the inbox. + */ + public String getName() { + return name; + } + + /** + * Get the name recipients see when mail is sent from this inbox. + * + * @return The name recipients see when mail is sent from this inbox. + */ + public String getFromName() { + return fromName; + } + + /** + * Get whether Resend provisions a receiving address, so you can receive mail without adding an MX record. + * + * @return Whether Resend provisions a receiving address, so you can receive mail without adding an MX record. + */ + public Boolean getForwarding() { + return forwarding; + } + + /** + * Create a new builder instance for constructing CreateInboxOptions objects. + * + * @return A new builder instance. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Builder class for constructing CreateInboxOptions objects. + */ + public static class Builder { + private String emailAddress; + private String name; + private String fromName; + private Boolean forwarding; + + /** + * Creates a new Builder instance. + */ + public Builder() { + } + + /** + * Set the inbox email address on one of your domains. The API trims and lowercases it. + * + * @param emailAddress The inbox email address on one of your domains. + * @return The builder instance. + */ + public Builder emailAddress(String emailAddress) { + this.emailAddress = emailAddress; + return this; + } + + /** + * Set the internal name for the inbox. Recipients do not see it. Max 64 characters. Defaults to {@code email_address}. + * + * @param name The internal name for the inbox. + * @return The builder instance. + */ + public Builder name(String name) { + this.name = name; + return this; + } + + /** + * Set the name recipients see when mail is sent from this inbox. A plain name, not a {@code Name } address. Max 78 characters. An empty string leaves it unset. + * + * @param fromName The name recipients see when mail is sent from this inbox. + * @return The builder instance. + */ + public Builder fromName(String fromName) { + this.fromName = fromName; + return this; + } + + /** + * Set whether Resend provisions a receiving address, so you can receive mail without adding an MX record. The address is returned in {@code receiving_address}. Defaults to {@code false}. + * + * @param forwarding Whether Resend provisions a receiving address, so you can receive mail without adding an MX record. + * @return The builder instance. + */ + public Builder forwarding(Boolean forwarding) { + this.forwarding = forwarding; + return this; + } + + /** + * Build a new CreateInboxOptions object. + * + * @return A new CreateInboxOptions object. + */ + public CreateInboxOptions build() { + return new CreateInboxOptions(this); + } + } +} diff --git a/src/main/java/com/resend/services/inboxes/model/CreateInboxResponseSuccess.java b/src/main/java/com/resend/services/inboxes/model/CreateInboxResponseSuccess.java new file mode 100644 index 0000000..cb7de44 --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/model/CreateInboxResponseSuccess.java @@ -0,0 +1,51 @@ +package com.resend.services.inboxes.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Represents a successful response for creating an inbox. + */ +public class CreateInboxResponseSuccess { + + @JsonProperty("object") + private String object; + + @JsonProperty("id") + private String id; + + /** + * Default constructor + */ + public CreateInboxResponseSuccess() { + + } + + /** + * Constructs a CreateInboxResponseSuccess. + * + * @param object The object type. Always {@code inbox}. + * @param id The ID of the inbox. + */ + public CreateInboxResponseSuccess(String object, String id) { + this.object = object; + this.id = id; + } + + /** + * Get the object type. Always {@code inbox}. + * + * @return The object type. Always {@code inbox}. + */ + public String getObject() { + return object; + } + + /** + * Get the ID of the inbox. + * + * @return The ID of the inbox. + */ + public String getId() { + return id; + } +} diff --git a/src/main/java/com/resend/services/inboxes/model/ForwardInboxThreadEmailOptions.java b/src/main/java/com/resend/services/inboxes/model/ForwardInboxThreadEmailOptions.java new file mode 100644 index 0000000..5ad891e --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/model/ForwardInboxThreadEmailOptions.java @@ -0,0 +1,322 @@ +package com.resend.services.inboxes.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.resend.services.emails.model.Attachment; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Represents a request to forward a thread email. {@code to}, {@code cc} and {@code bcc} combined cannot exceed 50 recipients. + */ +public class ForwardInboxThreadEmailOptions { + + @JsonProperty("to") + private final List to; + + @JsonProperty("cc") + private final List cc; + + @JsonProperty("bcc") + private final List bcc; + + @JsonProperty("html") + private final String html; + + @JsonProperty("text") + private final String text; + + @JsonProperty("subject") + private final String subject; + + @JsonProperty("attachments") + private final List attachments; + + private ForwardInboxThreadEmailOptions(Builder builder) { + this.to = builder.to; + this.cc = builder.cc; + this.bcc = builder.bcc; + this.html = builder.html; + this.text = builder.text; + this.subject = builder.subject; + this.attachments = builder.attachments; + } + + /** + * Get the recipients. + * + * @return The recipients. + */ + public List getTo() { + return to; + } + + /** + * Get the CC recipients. + * + * @return The CC recipients. + */ + public List getCc() { + return cc; + } + + /** + * Get the BCC recipients. + * + * @return The BCC recipients. + */ + public List getBcc() { + return bcc; + } + + /** + * Get the HTML note sent above the quoted original. + * + * @return The HTML note sent above the quoted original. + */ + public String getHtml() { + return html; + } + + /** + * Get the plain-text note sent above the quoted original. + * + * @return The plain-text note sent above the quoted original. + */ + public String getText() { + return text; + } + + /** + * Get the subject of the forward. + * + * @return The subject of the forward. + */ + public String getSubject() { + return subject; + } + + /** + * Get the files to attach, as on {@code POST /emails}. + * + * @return The files to attach, as on {@code POST /emails}. + */ + public List getAttachments() { + return attachments; + } + + /** + * Create a new builder instance for constructing ForwardInboxThreadEmailOptions objects. + * + * @return A new builder instance. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Builder class for constructing ForwardInboxThreadEmailOptions objects. + */ + public static class Builder { + private List to; + private List cc; + private List bcc; + private String html; + private String text; + private String subject; + private List attachments; + + /** + * Creates a new Builder instance. + */ + public Builder() { + } + + /** + * Set the recipients. Must name at least one address. + * + * @param to The recipients. + * @return The builder instance. + */ + public Builder to(String... to) { + this.to = new ArrayList<>(Arrays.asList(to)); + return this; + } + + /** + * Set the recipients. Must name at least one address. + * + * @param to The recipients. + * @return The builder instance. + */ + public Builder to(List to) { + this.to = to == null ? null : new ArrayList<>(to); + return this; + } + + /** + * Add one value to the recipients. + * + * @param recipient The value to add. + * @return The builder instance. + */ + public Builder addTo(String recipient) { + if (this.to == null) { + this.to = new ArrayList<>(); + } + this.to.add(recipient); + return this; + } + + /** + * Set the CC recipients. Counts toward the 50-recipient limit. + * + * @param cc The CC recipients. + * @return The builder instance. + */ + public Builder cc(String... cc) { + this.cc = new ArrayList<>(Arrays.asList(cc)); + return this; + } + + /** + * Set the CC recipients. Counts toward the 50-recipient limit. + * + * @param cc The CC recipients. + * @return The builder instance. + */ + public Builder cc(List cc) { + this.cc = cc == null ? null : new ArrayList<>(cc); + return this; + } + + /** + * Add one value to the CC recipients. + * + * @param recipient The value to add. + * @return The builder instance. + */ + public Builder addCc(String recipient) { + if (this.cc == null) { + this.cc = new ArrayList<>(); + } + this.cc.add(recipient); + return this; + } + + /** + * Set the BCC recipients. Counts toward the 50-recipient limit. + * + * @param bcc The BCC recipients. + * @return The builder instance. + */ + public Builder bcc(String... bcc) { + this.bcc = new ArrayList<>(Arrays.asList(bcc)); + return this; + } + + /** + * Set the BCC recipients. Counts toward the 50-recipient limit. + * + * @param bcc The BCC recipients. + * @return The builder instance. + */ + public Builder bcc(List bcc) { + this.bcc = bcc == null ? null : new ArrayList<>(bcc); + return this; + } + + /** + * Add one value to the BCC recipients. + * + * @param recipient The value to add. + * @return The builder instance. + */ + public Builder addBcc(String recipient) { + if (this.bcc == null) { + this.bcc = new ArrayList<>(); + } + this.bcc.add(recipient); + return this; + } + + /** + * Set the HTML note sent above the quoted original. + * + * @param html The HTML note sent above the quoted original. + * @return The builder instance. + */ + public Builder html(String html) { + this.html = html; + return this; + } + + /** + * Set the plain-text note sent above the quoted original. + * + * @param text The plain-text note sent above the quoted original. + * @return The builder instance. + */ + public Builder text(String text) { + this.text = text; + return this; + } + + /** + * Set the subject of the forward. Cannot contain a line break. When omitted, the thread subject is used, prefixed with {@code Fwd:} if it does not already start with it. + * + * @param subject The subject of the forward. + * @return The builder instance. + */ + public Builder subject(String subject) { + this.subject = subject; + return this; + } + + /** + * Set the files to attach, as on {@code POST /emails}. Each one needs {@code content} or {@code path}. They share one limit with the attachments of the original message, which the forward also sends: up to 10 files and at most 25 MB of {@code content}. + * + * @param attachments The files to attach, as on {@code POST /emails}. + * @return The builder instance. + */ + public Builder attachments(Attachment... attachments) { + this.attachments = new ArrayList<>(Arrays.asList(attachments)); + return this; + } + + /** + * Set the files to attach, as on {@code POST /emails}. Each one needs {@code content} or {@code path}. They share one limit with the attachments of the original message, which the forward also sends: up to 10 files and at most 25 MB of {@code content}. + * + * @param attachments The files to attach, as on {@code POST /emails}. + * @return The builder instance. + */ + public Builder attachments(List attachments) { + this.attachments = attachments == null ? null : new ArrayList<>(attachments); + return this; + } + + /** + * Add one value to the files to attach, as on {@code POST /emails}. + * + * @param attachment The value to add. + * @return The builder instance. + */ + public Builder addAttachment(Attachment attachment) { + if (this.attachments == null) { + this.attachments = new ArrayList<>(); + } + this.attachments.add(attachment); + return this; + } + + /** + * Build a new ForwardInboxThreadEmailOptions object. + * + * @return A new ForwardInboxThreadEmailOptions object. + */ + public ForwardInboxThreadEmailOptions build() { + return new ForwardInboxThreadEmailOptions(this); + } + } +} diff --git a/src/main/java/com/resend/services/inboxes/model/ForwardInboxThreadEmailResponseSuccess.java b/src/main/java/com/resend/services/inboxes/model/ForwardInboxThreadEmailResponseSuccess.java new file mode 100644 index 0000000..ad45fb7 --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/model/ForwardInboxThreadEmailResponseSuccess.java @@ -0,0 +1,37 @@ +package com.resend.services.inboxes.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Represents a successful response for forwarding a thread email. + */ +public class ForwardInboxThreadEmailResponseSuccess { + + @JsonProperty("id") + private String id; + + /** + * Default constructor + */ + public ForwardInboxThreadEmailResponseSuccess() { + + } + + /** + * Constructs a ForwardInboxThreadEmailResponseSuccess. + * + * @param id The ID of the sent email. Use it with the emails endpoints. + */ + public ForwardInboxThreadEmailResponseSuccess(String id) { + this.id = id; + } + + /** + * Get the ID of the sent email. Use it with the emails endpoints. + * + * @return The ID of the sent email. Use it with the emails endpoints. + */ + public String getId() { + return id; + } +} diff --git a/src/main/java/com/resend/services/inboxes/model/GetInboxAgentResponseSuccess.java b/src/main/java/com/resend/services/inboxes/model/GetInboxAgentResponseSuccess.java new file mode 100644 index 0000000..e3c9c62 --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/model/GetInboxAgentResponseSuccess.java @@ -0,0 +1,29 @@ +package com.resend.services.inboxes.model; + +import java.util.List; + +/** + * Represents a successful response for retrieving the agent settings of an inbox. + * Extends the InboxAgent class. + */ +public class GetInboxAgentResponseSuccess extends InboxAgent { + + /** + * Default constructor + */ + public GetInboxAgentResponseSuccess() { + + } + + /** + * Constructs a GetInboxAgentResponseSuccess. + * + * @param object The object type. Always {@code inbox_agent}. + * @param instructions The instructions the agent follows when it handles the threads of the inbox. Null when no instructions are set. + * @param tone The tone the agent writes in, such as {@code friendly and concise}. Null when no tone is set. + * @param enabledActions The actions the agent can take. Empty when no actions are enabled. + */ + public GetInboxAgentResponseSuccess(String object, String instructions, String tone, List enabledActions) { + super(object, instructions, tone, enabledActions); + } +} diff --git a/src/main/java/com/resend/services/inboxes/model/GetInboxDraftResponseSuccess.java b/src/main/java/com/resend/services/inboxes/model/GetInboxDraftResponseSuccess.java new file mode 100644 index 0000000..1d66e38 --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/model/GetInboxDraftResponseSuccess.java @@ -0,0 +1,39 @@ +package com.resend.services.inboxes.model; + +import java.util.List; + +/** + * Represents a successful response for retrieving a draft. + * Extends the InboxDraft class. + */ +public class GetInboxDraftResponseSuccess extends InboxDraft { + + /** + * Default constructor + */ + public GetInboxDraftResponseSuccess() { + + } + + /** + * Constructs a GetInboxDraftResponseSuccess. + * + * @param object The object type. Always {@code inbox_draft}. + * @param id The ID of the draft. + * @param type The draft type. {@code reply} when the draft replies to an email in an existing thread. {@code standalone} when the draft starts a new thread. + * @param to The recipients. {@code null} or empty when no recipients are set. + * @param cc The CC recipients. + * @param bcc The BCC recipients. + * @param subject The subject. + * @param html The HTML body. + * @param text The plain text body. + * @param threadId The Thread ID when the draft is a reply. {@code null} otherwise. + * @param replyToEmailId The Email ID the draft replies to. {@code null} otherwise. + * @param emailId The ID of the outbound email queued when the draft was sent. {@code null} until the draft is sent. + * @param createdAt The timestamp when the draft was created. + * @param updatedAt The timestamp when the draft was last saved. + */ + public GetInboxDraftResponseSuccess(String object, String id, String type, List to, List cc, List bcc, String subject, String html, String text, String threadId, String replyToEmailId, String emailId, String createdAt, String updatedAt) { + super(object, id, type, to, cc, bcc, subject, html, text, threadId, replyToEmailId, emailId, createdAt, updatedAt); + } +} diff --git a/src/main/java/com/resend/services/inboxes/model/GetInboxResponseSuccess.java b/src/main/java/com/resend/services/inboxes/model/GetInboxResponseSuccess.java new file mode 100644 index 0000000..970f89b --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/model/GetInboxResponseSuccess.java @@ -0,0 +1,34 @@ +package com.resend.services.inboxes.model; + +/** + * Represents a successful response for retrieving an inbox. + * Extends the Inbox class. + */ +public class GetInboxResponseSuccess extends Inbox { + + /** + * Default constructor + */ + public GetInboxResponseSuccess() { + + } + + /** + * Constructs a GetInboxResponseSuccess. + * + * @param object The object type. Always {@code inbox}. + * @param id The ID of the inbox. + * @param name The internal name for the inbox. Recipients do not see it. Falls back to the inbox email address. + * @param emailAddress The address of the inbox. + * @param domainId The ID of the domain that owns the address. + * @param receivingAddress The address to forward mail to when forwarding is enabled. {@code null} otherwise. + * @param fromName The name recipients see when mail is sent from this inbox. {@code null} when not set. + * @param unread The number of unread threads in the Inbox folder. + * @param drafts The number of unsent drafts. + * @param lastReceived The ISO 8601 timestamp of the newest received email in the Inbox folder. {@code null} when the folder has no received email. + * @param createdAt The ISO 8601 timestamp when the inbox was created. + */ + public GetInboxResponseSuccess(String object, String id, String name, String emailAddress, String domainId, String receivingAddress, String fromName, Integer unread, Integer drafts, String lastReceived, String createdAt) { + super(object, id, name, emailAddress, domainId, receivingAddress, fromName, unread, drafts, lastReceived, createdAt); + } +} diff --git a/src/main/java/com/resend/services/inboxes/model/GetInboxThreadEmailResponseSuccess.java b/src/main/java/com/resend/services/inboxes/model/GetInboxThreadEmailResponseSuccess.java new file mode 100644 index 0000000..7ba224b --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/model/GetInboxThreadEmailResponseSuccess.java @@ -0,0 +1,39 @@ +package com.resend.services.inboxes.model; + +import java.util.List; + +/** + * Represents a successful response for retrieving a thread email. + * Extends the InboxThreadEmail class. + */ +public class GetInboxThreadEmailResponseSuccess extends InboxThreadEmail { + + /** + * Default constructor + */ + public GetInboxThreadEmailResponseSuccess() { + + } + + /** + * Constructs a GetInboxThreadEmailResponseSuccess. + * + * @param id The ID of the message. + * @param direction Whether the inbox received the message ({@code inbound}) or sent it ({@code outbound}). A message the inbox sent to itself is {@code inbound}. + * @param from The sender email address. Includes the display name when the sender has one. + * @param to The recipients of the message. For a received message, these are the addresses it was delivered to, not the full {@code To} header. + * @param cc The CC recipients of the message. + * @param bcc The BCC recipients of the message. Always empty for a received message. + * @param replyTo The Reply-To addresses of the message. + * @param subject The subject of the message. + * @param messageId The Message-ID header of the message. + * @param html The HTML body. + * @param text The plain-text body. + * @param attachments The attachments of the message. + * @param read Whether the message has been read. + * @param receivedAt The timestamp when the inbox received the message, or when the inbox sent it. + */ + public GetInboxThreadEmailResponseSuccess(String id, String direction, String from, List to, List cc, List bcc, List replyTo, String subject, String messageId, String html, String text, List attachments, Boolean read, String receivedAt) { + super(id, direction, from, to, cc, bcc, replyTo, subject, messageId, html, text, attachments, read, receivedAt); + } +} diff --git a/src/main/java/com/resend/services/inboxes/model/GetInboxThreadResponseSuccess.java b/src/main/java/com/resend/services/inboxes/model/GetInboxThreadResponseSuccess.java new file mode 100644 index 0000000..6955b49 --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/model/GetInboxThreadResponseSuccess.java @@ -0,0 +1,31 @@ +package com.resend.services.inboxes.model; + +import java.util.List; + +/** + * Represents a successful response for retrieving a thread. + * Extends the InboxThreadSummary class. + */ +public class GetInboxThreadResponseSuccess extends InboxThreadSummary { + + /** + * Default constructor + */ + public GetInboxThreadResponseSuccess() { + + } + + /** + * Constructs a GetInboxThreadResponseSuccess. + * + * @param object The object type. Always {@code inbox_thread}. + * @param id The ID of the thread. + * @param subject The subject of the thread. + * @param folder The folder the thread is in. + * @param labels The labels attached to the thread. + * @param read True only when every email in the thread is read. + */ + public GetInboxThreadResponseSuccess(String object, String id, String subject, String folder, List labels, Boolean read) { + super(object, id, subject, folder, labels, read); + } +} diff --git a/src/main/java/com/resend/services/inboxes/model/Inbox.java b/src/main/java/com/resend/services/inboxes/model/Inbox.java new file mode 100644 index 0000000..51983a9 --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/model/Inbox.java @@ -0,0 +1,177 @@ +package com.resend.services.inboxes.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Represents an inbox. + */ +public class Inbox { + + @JsonProperty("object") + private String object; + + @JsonProperty("id") + private String id; + + @JsonProperty("name") + private String name; + + @JsonProperty("email_address") + private String emailAddress; + + @JsonProperty("domain_id") + private String domainId; + + @JsonProperty("receiving_address") + private String receivingAddress; + + @JsonProperty("from_name") + private String fromName; + + @JsonProperty("unread") + private Integer unread; + + @JsonProperty("drafts") + private Integer drafts; + + @JsonProperty("last_received") + private String lastReceived; + + @JsonProperty("created_at") + private String createdAt; + + /** + * Default constructor + */ + public Inbox() { + + } + + /** + * Constructs a Inbox. + * + * @param object The object type. Always {@code inbox}. + * @param id The ID of the inbox. + * @param name The internal name for the inbox. Recipients do not see it. Falls back to the inbox email address. + * @param emailAddress The address of the inbox. + * @param domainId The ID of the domain that owns the address. + * @param receivingAddress The address to forward mail to when forwarding is enabled. {@code null} otherwise. + * @param fromName The name recipients see when mail is sent from this inbox. {@code null} when not set. + * @param unread The number of unread threads in the Inbox folder. + * @param drafts The number of unsent drafts. + * @param lastReceived The ISO 8601 timestamp of the newest received email in the Inbox folder. {@code null} when the folder has no received email. + * @param createdAt The ISO 8601 timestamp when the inbox was created. + */ + public Inbox(String object, String id, String name, String emailAddress, String domainId, String receivingAddress, String fromName, Integer unread, Integer drafts, String lastReceived, String createdAt) { + this.object = object; + this.id = id; + this.name = name; + this.emailAddress = emailAddress; + this.domainId = domainId; + this.receivingAddress = receivingAddress; + this.fromName = fromName; + this.unread = unread; + this.drafts = drafts; + this.lastReceived = lastReceived; + this.createdAt = createdAt; + } + + /** + * Get the object type. Always {@code inbox}. + * + * @return The object type. Always {@code inbox}. + */ + public String getObject() { + return object; + } + + /** + * Get the ID of the inbox. + * + * @return The ID of the inbox. + */ + public String getId() { + return id; + } + + /** + * Get the internal name for the inbox. Recipients do not see it. Falls back to the inbox email address. + * + * @return The internal name for the inbox. Recipients do not see it. Falls back to the inbox email address. + */ + public String getName() { + return name; + } + + /** + * Get the address of the inbox. + * + * @return The address of the inbox. + */ + public String getEmailAddress() { + return emailAddress; + } + + /** + * Get the ID of the domain that owns the address. + * + * @return The ID of the domain that owns the address. + */ + public String getDomainId() { + return domainId; + } + + /** + * Get the address to forward mail to when forwarding is enabled. {@code null} otherwise. + * + * @return The address to forward mail to when forwarding is enabled. {@code null} otherwise. + */ + public String getReceivingAddress() { + return receivingAddress; + } + + /** + * Get the name recipients see when mail is sent from this inbox. {@code null} when not set. + * + * @return The name recipients see when mail is sent from this inbox. {@code null} when not set. + */ + public String getFromName() { + return fromName; + } + + /** + * Get the number of unread threads in the Inbox folder. + * + * @return The number of unread threads in the Inbox folder. + */ + public Integer getUnread() { + return unread; + } + + /** + * Get the number of unsent drafts. + * + * @return The number of unsent drafts. + */ + public Integer getDrafts() { + return drafts; + } + + /** + * Get the ISO 8601 timestamp of the newest received email in the Inbox folder. {@code null} when the folder has no received email. + * + * @return The ISO 8601 timestamp of the newest received email in the Inbox folder. {@code null} when the folder has no received email. + */ + public String getLastReceived() { + return lastReceived; + } + + /** + * Get the ISO 8601 timestamp when the inbox was created. + * + * @return The ISO 8601 timestamp when the inbox was created. + */ + public String getCreatedAt() { + return createdAt; + } +} diff --git a/src/main/java/com/resend/services/inboxes/model/InboxAgent.java b/src/main/java/com/resend/services/inboxes/model/InboxAgent.java new file mode 100644 index 0000000..84003f2 --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/model/InboxAgent.java @@ -0,0 +1,81 @@ +package com.resend.services.inboxes.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +/** + * Represents the agent settings of an inbox. + */ +public class InboxAgent { + + @JsonProperty("object") + private String object; + + @JsonProperty("instructions") + private String instructions; + + @JsonProperty("tone") + private String tone; + + @JsonProperty("enabled_actions") + private List enabledActions; + + /** + * Default constructor + */ + public InboxAgent() { + + } + + /** + * Constructs a InboxAgent. + * + * @param object The object type. Always {@code inbox_agent}. + * @param instructions The instructions the agent follows when it handles the threads of the inbox. Null when no instructions are set. + * @param tone The tone the agent writes in, such as {@code friendly and concise}. Null when no tone is set. + * @param enabledActions The actions the agent can take. Empty when no actions are enabled. + */ + public InboxAgent(String object, String instructions, String tone, List enabledActions) { + this.object = object; + this.instructions = instructions; + this.tone = tone; + this.enabledActions = enabledActions; + } + + /** + * Get the object type. Always {@code inbox_agent}. + * + * @return The object type. Always {@code inbox_agent}. + */ + public String getObject() { + return object; + } + + /** + * Get the instructions the agent follows when it handles the threads of the inbox. Null when no instructions are set. + * + * @return The instructions the agent follows when it handles the threads of the inbox. Null when no instructions are set. + */ + public String getInstructions() { + return instructions; + } + + /** + * Get the tone the agent writes in, such as {@code friendly and concise}. Null when no tone is set. + * + * @return The tone the agent writes in, such as {@code friendly and concise}. Null when no tone is set. + */ + public String getTone() { + return tone; + } + + /** + * Get the actions the agent can take. Empty when no actions are enabled. + * + * @return The actions the agent can take. Empty when no actions are enabled. + */ + public List getEnabledActions() { + return enabledActions; + } +} diff --git a/src/main/java/com/resend/services/inboxes/model/InboxAgentAction.java b/src/main/java/com/resend/services/inboxes/model/InboxAgentAction.java new file mode 100644 index 0000000..b9eca81 --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/model/InboxAgentAction.java @@ -0,0 +1,62 @@ +package com.resend.services.inboxes.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * Represents an action the inbox agent can take. + */ +public enum InboxAgentAction { + /** The agent drafts replies. */ + DRAFT_REPLY("draft_reply"), + /** The agent forwards threads. */ + FORWARD_THREAD("forward_thread"), + /** The agent adds labels to threads. */ + ADD_LABELS("add_labels"), + /** The agent assigns threads. */ + ASSIGN_THREAD("assign_thread"), + /** The agent marks threads as spam. */ + MARK_AS_SPAM("mark_as_spam"), + /** The agent archives threads. */ + ARCHIVE_THREAD("archive_thread"), + /** The agent deletes threads. */ + DELETE_THREAD("delete_thread"); + + private final String value; + + InboxAgentAction(String value) { + this.value = value; + } + + /** + * Returns the string value of the action. + * + * @return The action value. + */ + @JsonValue + public String getValue() { + return value; + } + + /** + * Creates a InboxAgentAction from a string value. + * + * @param value The string value. + * @return The corresponding InboxAgentAction. + * @throws IllegalArgumentException If the value is unknown. + */ + @JsonCreator + public static InboxAgentAction fromValue(String value) { + for (InboxAgentAction item : InboxAgentAction.values()) { + if (item.value.equals(value)) { + return item; + } + } + throw new IllegalArgumentException("Unknown action: " + value); + } + + @Override + public String toString() { + return value; + } +} diff --git a/src/main/java/com/resend/services/inboxes/model/InboxDraft.java b/src/main/java/com/resend/services/inboxes/model/InboxDraft.java new file mode 100644 index 0000000..c8f3f52 --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/model/InboxDraft.java @@ -0,0 +1,221 @@ +package com.resend.services.inboxes.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +/** + * Represents a draft on an inbox. + */ +public class InboxDraft { + + @JsonProperty("object") + private String object; + + @JsonProperty("id") + private String id; + + @JsonProperty("type") + private String type; + + @JsonProperty("to") + private List to; + + @JsonProperty("cc") + private List cc; + + @JsonProperty("bcc") + private List bcc; + + @JsonProperty("subject") + private String subject; + + @JsonProperty("html") + private String html; + + @JsonProperty("text") + private String text; + + @JsonProperty("thread_id") + private String threadId; + + @JsonProperty("reply_to_email_id") + private String replyToEmailId; + + @JsonProperty("email_id") + private String emailId; + + @JsonProperty("created_at") + private String createdAt; + + @JsonProperty("updated_at") + private String updatedAt; + + /** + * Default constructor + */ + public InboxDraft() { + + } + + /** + * Constructs a InboxDraft. + * + * @param object The object type. Always {@code inbox_draft}. + * @param id The ID of the draft. + * @param type The draft type. {@code reply} when the draft replies to an email in an existing thread. {@code standalone} when the draft starts a new thread. + * @param to The recipients. {@code null} or empty when no recipients are set. + * @param cc The CC recipients. + * @param bcc The BCC recipients. + * @param subject The subject. + * @param html The HTML body. + * @param text The plain text body. + * @param threadId The Thread ID when the draft is a reply. {@code null} otherwise. + * @param replyToEmailId The Email ID the draft replies to. {@code null} otherwise. + * @param emailId The ID of the outbound email queued when the draft was sent. {@code null} until the draft is sent. + * @param createdAt The timestamp when the draft was created. + * @param updatedAt The timestamp when the draft was last saved. + */ + public InboxDraft(String object, String id, String type, List to, List cc, List bcc, String subject, String html, String text, String threadId, String replyToEmailId, String emailId, String createdAt, String updatedAt) { + this.object = object; + this.id = id; + this.type = type; + this.to = to; + this.cc = cc; + this.bcc = bcc; + this.subject = subject; + this.html = html; + this.text = text; + this.threadId = threadId; + this.replyToEmailId = replyToEmailId; + this.emailId = emailId; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + /** + * Get the object type. Always {@code inbox_draft}. + * + * @return The object type. Always {@code inbox_draft}. + */ + public String getObject() { + return object; + } + + /** + * Get the ID of the draft. + * + * @return The ID of the draft. + */ + public String getId() { + return id; + } + + /** + * Get the draft type. {@code reply} when the draft replies to an email in an existing thread. {@code standalone} when the draft starts a new thread. + * + * @return The draft type. {@code reply} when the draft replies to an email in an existing thread. {@code standalone} when the draft starts a new thread. + */ + public String getType() { + return type; + } + + /** + * Get the recipients. {@code null} or empty when no recipients are set. + * + * @return The recipients. {@code null} or empty when no recipients are set. + */ + public List getTo() { + return to; + } + + /** + * Get the CC recipients. + * + * @return The CC recipients. + */ + public List getCc() { + return cc; + } + + /** + * Get the BCC recipients. + * + * @return The BCC recipients. + */ + public List getBcc() { + return bcc; + } + + /** + * Get the subject. + * + * @return The subject. + */ + public String getSubject() { + return subject; + } + + /** + * Get the HTML body. + * + * @return The HTML body. + */ + public String getHtml() { + return html; + } + + /** + * Get the plain text body. + * + * @return The plain text body. + */ + public String getText() { + return text; + } + + /** + * Get the Thread ID when the draft is a reply. {@code null} otherwise. + * + * @return The Thread ID when the draft is a reply. {@code null} otherwise. + */ + public String getThreadId() { + return threadId; + } + + /** + * Get the Email ID the draft replies to. {@code null} otherwise. + * + * @return The Email ID the draft replies to. {@code null} otherwise. + */ + public String getReplyToEmailId() { + return replyToEmailId; + } + + /** + * Get the ID of the outbound email queued when the draft was sent. {@code null} until the draft is sent. + * + * @return The ID of the outbound email queued when the draft was sent. {@code null} until the draft is sent. + */ + public String getEmailId() { + return emailId; + } + + /** + * Get the timestamp when the draft was created. + * + * @return The timestamp when the draft was created. + */ + public String getCreatedAt() { + return createdAt; + } + + /** + * Get the timestamp when the draft was last saved. + * + * @return The timestamp when the draft was last saved. + */ + public String getUpdatedAt() { + return updatedAt; + } +} diff --git a/src/main/java/com/resend/services/inboxes/model/InboxDraftListItem.java b/src/main/java/com/resend/services/inboxes/model/InboxDraftListItem.java new file mode 100644 index 0000000..bed35c3 --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/model/InboxDraftListItem.java @@ -0,0 +1,165 @@ +package com.resend.services.inboxes.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +/** + * Represents a draft in a list of drafts. + */ +public class InboxDraftListItem { + + @JsonProperty("id") + private String id; + + @JsonProperty("type") + private String type; + + @JsonProperty("to") + private List to; + + @JsonProperty("cc") + private List cc; + + @JsonProperty("bcc") + private List bcc; + + @JsonProperty("subject") + private String subject; + + @JsonProperty("snippet") + private String snippet; + + @JsonProperty("thread_id") + private String threadId; + + @JsonProperty("reply_to_email_id") + private String replyToEmailId; + + @JsonProperty("updated_at") + private String updatedAt; + + /** + * Default constructor + */ + public InboxDraftListItem() { + + } + + /** + * Constructs a InboxDraftListItem. + * + * @param id The ID of the draft. + * @param type The draft type. {@code reply} when the draft replies to an email in an existing thread. {@code standalone} when the draft starts a new thread. + * @param to The recipients. {@code null} or empty when no recipients are set. + * @param cc The CC recipients. + * @param bcc The BCC recipients. + * @param subject The subject. + * @param snippet A short preview of the body. + * @param threadId The Thread ID when the draft is a reply. {@code null} otherwise. + * @param replyToEmailId The Email ID the draft replies to. {@code null} otherwise. + * @param updatedAt The timestamp when the draft was last saved. + */ + public InboxDraftListItem(String id, String type, List to, List cc, List bcc, String subject, String snippet, String threadId, String replyToEmailId, String updatedAt) { + this.id = id; + this.type = type; + this.to = to; + this.cc = cc; + this.bcc = bcc; + this.subject = subject; + this.snippet = snippet; + this.threadId = threadId; + this.replyToEmailId = replyToEmailId; + this.updatedAt = updatedAt; + } + + /** + * Get the ID of the draft. + * + * @return The ID of the draft. + */ + public String getId() { + return id; + } + + /** + * Get the draft type. {@code reply} when the draft replies to an email in an existing thread. {@code standalone} when the draft starts a new thread. + * + * @return The draft type. {@code reply} when the draft replies to an email in an existing thread. {@code standalone} when the draft starts a new thread. + */ + public String getType() { + return type; + } + + /** + * Get the recipients. {@code null} or empty when no recipients are set. + * + * @return The recipients. {@code null} or empty when no recipients are set. + */ + public List getTo() { + return to; + } + + /** + * Get the CC recipients. + * + * @return The CC recipients. + */ + public List getCc() { + return cc; + } + + /** + * Get the BCC recipients. + * + * @return The BCC recipients. + */ + public List getBcc() { + return bcc; + } + + /** + * Get the subject. + * + * @return The subject. + */ + public String getSubject() { + return subject; + } + + /** + * Get a short preview of the body. + * + * @return A short preview of the body. + */ + public String getSnippet() { + return snippet; + } + + /** + * Get the Thread ID when the draft is a reply. {@code null} otherwise. + * + * @return The Thread ID when the draft is a reply. {@code null} otherwise. + */ + public String getThreadId() { + return threadId; + } + + /** + * Get the Email ID the draft replies to. {@code null} otherwise. + * + * @return The Email ID the draft replies to. {@code null} otherwise. + */ + public String getReplyToEmailId() { + return replyToEmailId; + } + + /** + * Get the timestamp when the draft was last saved. + * + * @return The timestamp when the draft was last saved. + */ + public String getUpdatedAt() { + return updatedAt; + } +} diff --git a/src/main/java/com/resend/services/inboxes/model/InboxFolder.java b/src/main/java/com/resend/services/inboxes/model/InboxFolder.java new file mode 100644 index 0000000..986d9b3 --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/model/InboxFolder.java @@ -0,0 +1,58 @@ +package com.resend.services.inboxes.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * Represents a folder of an inbox. + */ +public enum InboxFolder { + /** Threads with at least one received email. */ + INBOX("inbox"), + /** Threads moved to the archive. */ + ARCHIVE("archive"), + /** Threads moved to spam. */ + SPAM("spam"), + /** Threads whose emails are all outbound. Assigned automatically. */ + SENT("sent"), + /** Threads scheduled for deletion in 30 days. */ + TRASH("trash"); + + private final String value; + + InboxFolder(String value) { + this.value = value; + } + + /** + * Returns the string value of the folder. + * + * @return The folder value. + */ + @JsonValue + public String getValue() { + return value; + } + + /** + * Creates a InboxFolder from a string value. + * + * @param value The string value. + * @return The corresponding InboxFolder. + * @throws IllegalArgumentException If the value is unknown. + */ + @JsonCreator + public static InboxFolder fromValue(String value) { + for (InboxFolder item : InboxFolder.values()) { + if (item.value.equals(value)) { + return item; + } + } + throw new IllegalArgumentException("Unknown folder: " + value); + } + + @Override + public String toString() { + return value; + } +} diff --git a/src/main/java/com/resend/services/inboxes/model/InboxLabel.java b/src/main/java/com/resend/services/inboxes/model/InboxLabel.java new file mode 100644 index 0000000..3773606 --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/model/InboxLabel.java @@ -0,0 +1,79 @@ +package com.resend.services.inboxes.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Represents a label on an inbox. + */ +public class InboxLabel { + + @JsonProperty("id") + private String id; + + @JsonProperty("name") + private String name; + + @JsonProperty("color") + private String color; + + @JsonProperty("created_at") + private String createdAt; + + /** + * Default constructor + */ + public InboxLabel() { + + } + + /** + * Constructs a InboxLabel. + * + * @param id The ID of the label. + * @param name The name of the label. + * @param color The color of the label, as a {@code #RRGGBB} hex color in uppercase. + * @param createdAt The timestamp when the label was created. + */ + public InboxLabel(String id, String name, String color, String createdAt) { + this.id = id; + this.name = name; + this.color = color; + this.createdAt = createdAt; + } + + /** + * Get the ID of the label. + * + * @return The ID of the label. + */ + public String getId() { + return id; + } + + /** + * Get the name of the label. + * + * @return The name of the label. + */ + public String getName() { + return name; + } + + /** + * Get the color of the label, as a {@code #RRGGBB} hex color in uppercase. + * + * @return The color of the label, as a {@code #RRGGBB} hex color in uppercase. + */ + public String getColor() { + return color; + } + + /** + * Get the timestamp when the label was created. + * + * @return The timestamp when the label was created. + */ + public String getCreatedAt() { + return createdAt; + } +} diff --git a/src/main/java/com/resend/services/inboxes/model/InboxListItem.java b/src/main/java/com/resend/services/inboxes/model/InboxListItem.java new file mode 100644 index 0000000..f22c591 --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/model/InboxListItem.java @@ -0,0 +1,107 @@ +package com.resend.services.inboxes.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Represents an inbox in a list of inboxes. The list items carry fewer fields than a single inbox. + */ +public class InboxListItem { + + @JsonProperty("id") + private String id; + + @JsonProperty("name") + private String name; + + @JsonProperty("email_address") + private String emailAddress; + + @JsonProperty("from_name") + private String fromName; + + @JsonProperty("unread") + private Integer unread; + + @JsonProperty("last_received") + private String lastReceived; + + /** + * Default constructor + */ + public InboxListItem() { + + } + + /** + * Constructs a InboxListItem. + * + * @param id The ID of the inbox. + * @param name The internal name for the inbox. Recipients do not see it. + * @param emailAddress The address of the inbox. + * @param fromName The name recipients see when mail is sent from this inbox. {@code null} when not set. + * @param unread The number of unread threads in the Inbox folder. + * @param lastReceived The ISO 8601 timestamp of the newest received email in the Inbox folder. {@code null} when the folder has no received email. + */ + public InboxListItem(String id, String name, String emailAddress, String fromName, Integer unread, String lastReceived) { + this.id = id; + this.name = name; + this.emailAddress = emailAddress; + this.fromName = fromName; + this.unread = unread; + this.lastReceived = lastReceived; + } + + /** + * Get the ID of the inbox. + * + * @return The ID of the inbox. + */ + public String getId() { + return id; + } + + /** + * Get the internal name for the inbox. Recipients do not see it. + * + * @return The internal name for the inbox. Recipients do not see it. + */ + public String getName() { + return name; + } + + /** + * Get the address of the inbox. + * + * @return The address of the inbox. + */ + public String getEmailAddress() { + return emailAddress; + } + + /** + * Get the name recipients see when mail is sent from this inbox. {@code null} when not set. + * + * @return The name recipients see when mail is sent from this inbox. {@code null} when not set. + */ + public String getFromName() { + return fromName; + } + + /** + * Get the number of unread threads in the Inbox folder. + * + * @return The number of unread threads in the Inbox folder. + */ + public Integer getUnread() { + return unread; + } + + /** + * Get the ISO 8601 timestamp of the newest received email in the Inbox folder. {@code null} when the folder has no received email. + * + * @return The ISO 8601 timestamp of the newest received email in the Inbox folder. {@code null} when the folder has no received email. + */ + public String getLastReceived() { + return lastReceived; + } +} diff --git a/src/main/java/com/resend/services/inboxes/model/InboxThread.java b/src/main/java/com/resend/services/inboxes/model/InboxThread.java new file mode 100644 index 0000000..1efbc10 --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/model/InboxThread.java @@ -0,0 +1,207 @@ +package com.resend.services.inboxes.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +/** + * Represents a thread as it appears in the list of an inbox's threads. + */ +public class InboxThread { + + @JsonProperty("id") + private String id; + + @JsonProperty("subject") + private String subject; + + @JsonProperty("from") + private String from; + + @JsonProperty("to") + private List to; + + @JsonProperty("cc") + private List cc; + + @JsonProperty("bcc") + private List bcc; + + @JsonProperty("labels") + private List labels; + + @JsonProperty("message_count") + private Integer messageCount; + + @JsonProperty("has_attachment") + private Boolean hasAttachment; + + @JsonProperty("has_draft") + private Boolean hasDraft; + + @JsonProperty("read") + private Boolean read; + + @JsonProperty("received_at") + private String receivedAt; + + @JsonProperty("folder") + private String folder; + + /** + * Default constructor + */ + public InboxThread() { + + } + + /** + * Constructs a InboxThread. + * + * @param id The ID of the thread. + * @param subject The subject of the thread. + * @param from The sender email address. Can include the display name. + * @param to The recipients of the most recent email. For a received email, these are the addresses it was delivered to, not the full {@code To} header. + * @param cc The CC recipients of the most recent email. + * @param bcc The BCC recipients of the most recent email. Always empty for a received email. + * @param labels The labels attached to the thread. + * @param messageCount The number of emails in the thread. + * @param hasAttachment Whether the most recent email in the thread has an attachment. + * @param hasDraft Whether the thread has an unsent draft. + * @param read True only when every email in the thread is read. + * @param receivedAt The timestamp when the thread was last active. + * @param folder The folder the thread is in. Every thread is in exactly one folder. + */ + public InboxThread(String id, String subject, String from, List to, List cc, List bcc, List labels, Integer messageCount, Boolean hasAttachment, Boolean hasDraft, Boolean read, String receivedAt, String folder) { + this.id = id; + this.subject = subject; + this.from = from; + this.to = to; + this.cc = cc; + this.bcc = bcc; + this.labels = labels; + this.messageCount = messageCount; + this.hasAttachment = hasAttachment; + this.hasDraft = hasDraft; + this.read = read; + this.receivedAt = receivedAt; + this.folder = folder; + } + + /** + * Get the ID of the thread. + * + * @return The ID of the thread. + */ + public String getId() { + return id; + } + + /** + * Get the subject of the thread. + * + * @return The subject of the thread. + */ + public String getSubject() { + return subject; + } + + /** + * Get the sender email address. Can include the display name. + * + * @return The sender email address. Can include the display name. + */ + public String getFrom() { + return from; + } + + /** + * Get the recipients of the most recent email. For a received email, these are the addresses it was delivered to, not the full {@code To} header. + * + * @return The recipients of the most recent email. For a received email, these are the addresses it was delivered to, not the full {@code To} header. + */ + public List getTo() { + return to; + } + + /** + * Get the CC recipients of the most recent email. + * + * @return The CC recipients of the most recent email. + */ + public List getCc() { + return cc; + } + + /** + * Get the BCC recipients of the most recent email. Always empty for a received email. + * + * @return The BCC recipients of the most recent email. Always empty for a received email. + */ + public List getBcc() { + return bcc; + } + + /** + * Get the labels attached to the thread. + * + * @return The labels attached to the thread. + */ + public List getLabels() { + return labels; + } + + /** + * Get the number of emails in the thread. + * + * @return The number of emails in the thread. + */ + public Integer getMessageCount() { + return messageCount; + } + + /** + * Get whether the most recent email in the thread has an attachment. + * + * @return Whether the most recent email in the thread has an attachment. + */ + public Boolean getHasAttachment() { + return hasAttachment; + } + + /** + * Get whether the thread has an unsent draft. + * + * @return Whether the thread has an unsent draft. + */ + public Boolean getHasDraft() { + return hasDraft; + } + + /** + * Get true only when every email in the thread is read. + * + * @return True only when every email in the thread is read. + */ + public Boolean getRead() { + return read; + } + + /** + * Get the timestamp when the thread was last active. + * + * @return The timestamp when the thread was last active. + */ + public String getReceivedAt() { + return receivedAt; + } + + /** + * Get the folder the thread is in. Every thread is in exactly one folder. + * + * @return The folder the thread is in. Every thread is in exactly one folder. + */ + public String getFolder() { + return folder; + } +} diff --git a/src/main/java/com/resend/services/inboxes/model/InboxThreadEmail.java b/src/main/java/com/resend/services/inboxes/model/InboxThreadEmail.java new file mode 100644 index 0000000..03c9e59 --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/model/InboxThreadEmail.java @@ -0,0 +1,221 @@ +package com.resend.services.inboxes.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +/** + * Represents a message in a thread. + */ +public class InboxThreadEmail { + + @JsonProperty("id") + private String id; + + @JsonProperty("direction") + private String direction; + + @JsonProperty("from") + private String from; + + @JsonProperty("to") + private List to; + + @JsonProperty("cc") + private List cc; + + @JsonProperty("bcc") + private List bcc; + + @JsonProperty("reply_to") + private List replyTo; + + @JsonProperty("subject") + private String subject; + + @JsonProperty("message_id") + private String messageId; + + @JsonProperty("html") + private String html; + + @JsonProperty("text") + private String text; + + @JsonProperty("attachments") + private List attachments; + + @JsonProperty("read") + private Boolean read; + + @JsonProperty("received_at") + private String receivedAt; + + /** + * Default constructor + */ + public InboxThreadEmail() { + + } + + /** + * Constructs a InboxThreadEmail. + * + * @param id The ID of the message. + * @param direction Whether the inbox received the message ({@code inbound}) or sent it ({@code outbound}). A message the inbox sent to itself is {@code inbound}. + * @param from The sender email address. Includes the display name when the sender has one. + * @param to The recipients of the message. For a received message, these are the addresses it was delivered to, not the full {@code To} header. + * @param cc The CC recipients of the message. + * @param bcc The BCC recipients of the message. Always empty for a received message. + * @param replyTo The Reply-To addresses of the message. + * @param subject The subject of the message. + * @param messageId The Message-ID header of the message. + * @param html The HTML body. + * @param text The plain-text body. + * @param attachments The attachments of the message. + * @param read Whether the message has been read. + * @param receivedAt The timestamp when the inbox received the message, or when the inbox sent it. + */ + public InboxThreadEmail(String id, String direction, String from, List to, List cc, List bcc, List replyTo, String subject, String messageId, String html, String text, List attachments, Boolean read, String receivedAt) { + this.id = id; + this.direction = direction; + this.from = from; + this.to = to; + this.cc = cc; + this.bcc = bcc; + this.replyTo = replyTo; + this.subject = subject; + this.messageId = messageId; + this.html = html; + this.text = text; + this.attachments = attachments; + this.read = read; + this.receivedAt = receivedAt; + } + + /** + * Get the ID of the message. + * + * @return The ID of the message. + */ + public String getId() { + return id; + } + + /** + * Get whether the inbox received the message ({@code inbound}) or sent it ({@code outbound}). A message the inbox sent to itself is {@code inbound}. + * + * @return Whether the inbox received the message ({@code inbound}) or sent it ({@code outbound}). A message the inbox sent to itself is {@code inbound}. + */ + public String getDirection() { + return direction; + } + + /** + * Get the sender email address. Includes the display name when the sender has one. + * + * @return The sender email address. Includes the display name when the sender has one. + */ + public String getFrom() { + return from; + } + + /** + * Get the recipients of the message. For a received message, these are the addresses it was delivered to, not the full {@code To} header. + * + * @return The recipients of the message. For a received message, these are the addresses it was delivered to, not the full {@code To} header. + */ + public List getTo() { + return to; + } + + /** + * Get the CC recipients of the message. + * + * @return The CC recipients of the message. + */ + public List getCc() { + return cc; + } + + /** + * Get the BCC recipients of the message. Always empty for a received message. + * + * @return The BCC recipients of the message. Always empty for a received message. + */ + public List getBcc() { + return bcc; + } + + /** + * Get the Reply-To addresses of the message. + * + * @return The Reply-To addresses of the message. + */ + public List getReplyTo() { + return replyTo; + } + + /** + * Get the subject of the message. + * + * @return The subject of the message. + */ + public String getSubject() { + return subject; + } + + /** + * Get the Message-ID header of the message. + * + * @return The Message-ID header of the message. + */ + public String getMessageId() { + return messageId; + } + + /** + * Get the HTML body. + * + * @return The HTML body. + */ + public String getHtml() { + return html; + } + + /** + * Get the plain-text body. + * + * @return The plain-text body. + */ + public String getText() { + return text; + } + + /** + * Get the attachments of the message. + * + * @return The attachments of the message. + */ + public List getAttachments() { + return attachments; + } + + /** + * Get whether the message has been read. + * + * @return Whether the message has been read. + */ + public Boolean getRead() { + return read; + } + + /** + * Get the timestamp when the inbox received the message, or when the inbox sent it. + * + * @return The timestamp when the inbox received the message, or when the inbox sent it. + */ + public String getReceivedAt() { + return receivedAt; + } +} diff --git a/src/main/java/com/resend/services/inboxes/model/InboxThreadEmailAttachment.java b/src/main/java/com/resend/services/inboxes/model/InboxThreadEmailAttachment.java new file mode 100644 index 0000000..251326f --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/model/InboxThreadEmailAttachment.java @@ -0,0 +1,65 @@ +package com.resend.services.inboxes.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Represents an attachment of a thread email. + */ +public class InboxThreadEmailAttachment { + + @JsonProperty("id") + private String id; + + @JsonProperty("filename") + private String filename; + + @JsonProperty("size") + private Integer size; + + /** + * Default constructor + */ + public InboxThreadEmailAttachment() { + + } + + /** + * Constructs a InboxThreadEmailAttachment. + * + * @param id The ID of the attachment. + * @param filename The filename of the attachment. + * @param size The size of the attachment in bytes. + */ + public InboxThreadEmailAttachment(String id, String filename, Integer size) { + this.id = id; + this.filename = filename; + this.size = size; + } + + /** + * Get the ID of the attachment. + * + * @return The ID of the attachment. + */ + public String getId() { + return id; + } + + /** + * Get the filename of the attachment. + * + * @return The filename of the attachment. + */ + public String getFilename() { + return filename; + } + + /** + * Get the size of the attachment in bytes. + * + * @return The size of the attachment in bytes. + */ + public Integer getSize() { + return size; + } +} diff --git a/src/main/java/com/resend/services/inboxes/model/InboxThreadLabel.java b/src/main/java/com/resend/services/inboxes/model/InboxThreadLabel.java new file mode 100644 index 0000000..a0325b0 --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/model/InboxThreadLabel.java @@ -0,0 +1,65 @@ +package com.resend.services.inboxes.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Represents a label attached to a thread. + */ +public class InboxThreadLabel { + + @JsonProperty("id") + private String id; + + @JsonProperty("name") + private String name; + + @JsonProperty("color") + private String color; + + /** + * Default constructor + */ + public InboxThreadLabel() { + + } + + /** + * Constructs a InboxThreadLabel. + * + * @param id The ID of the label. + * @param name The name of the label. + * @param color The color of the label, as a {@code #RRGGBB} hex color in uppercase. + */ + public InboxThreadLabel(String id, String name, String color) { + this.id = id; + this.name = name; + this.color = color; + } + + /** + * Get the ID of the label. + * + * @return The ID of the label. + */ + public String getId() { + return id; + } + + /** + * Get the name of the label. + * + * @return The name of the label. + */ + public String getName() { + return name; + } + + /** + * Get the color of the label, as a {@code #RRGGBB} hex color in uppercase. + * + * @return The color of the label, as a {@code #RRGGBB} hex color in uppercase. + */ + public String getColor() { + return color; + } +} diff --git a/src/main/java/com/resend/services/inboxes/model/InboxThreadSearchResult.java b/src/main/java/com/resend/services/inboxes/model/InboxThreadSearchResult.java new file mode 100644 index 0000000..6235cb5 --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/model/InboxThreadSearchResult.java @@ -0,0 +1,69 @@ +package com.resend.services.inboxes.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; +import java.util.Map; + +/** + * Represents a thread that matched a search. + * Extends the InboxThread class. + */ +public class InboxThreadSearchResult extends InboxThread { + + @JsonProperty("matched_email_id") + private String matchedEmailId; + + @JsonProperty("highlights") + private Map> highlights; + + /** + * Default constructor + */ + public InboxThreadSearchResult() { + + } + + /** + * Constructs a InboxThreadSearchResult. + * + * @param id The ID of the thread. + * @param subject The subject of the thread. + * @param from The sender email address. Can include the display name. + * @param to The recipients of the most recent email. For a received email, these are the addresses it was delivered to, not the full {@code To} header. + * @param cc The CC recipients of the most recent email. + * @param bcc The BCC recipients of the most recent email. Always empty for a received email. + * @param labels The labels attached to the thread. + * @param messageCount The number of emails in the thread. + * @param hasAttachment Whether the most recent email in the thread has an attachment. + * @param hasDraft Whether the thread has an unsent draft. + * @param read True only when every email in the thread is read. + * @param receivedAt The timestamp when the thread was last active. + * @param folder The folder the thread is in. Every thread is in exactly one folder. + * @param matchedEmailId The email in the thread that matched the search. The get, reply and forward thread email endpoints accept it as {@code email_id}, but it is not equal to the {@code id} those endpoints return. Null when the search index returns no matched email for the thread. + * @param highlights Where {@code query} matched the email in {@code matched_email_id}, as text fragments with each match wrapped in {@code **}, keyed by {@code subject}, {@code body}, {@code from}, {@code to}, {@code cc}, {@code bcc} or {@code attachments}. Only keys with a match are present. Empty when there is no {@code query}. + */ + public InboxThreadSearchResult(String id, String subject, String from, List to, List cc, List bcc, List labels, Integer messageCount, Boolean hasAttachment, Boolean hasDraft, Boolean read, String receivedAt, String folder, String matchedEmailId, Map> highlights) { + super(id, subject, from, to, cc, bcc, labels, messageCount, hasAttachment, hasDraft, read, receivedAt, folder); + this.matchedEmailId = matchedEmailId; + this.highlights = highlights; + } + + /** + * Get the email in the thread that matched the search. The get, reply and forward thread email endpoints accept it as {@code email_id}, but it is not equal to the {@code id} those endpoints return. Null when the search index returns no matched email for the thread. + * + * @return The email in the thread that matched the search. The get, reply and forward thread email endpoints accept it as {@code email_id}, but it is not equal to the {@code id} those endpoints return. Null when the search index returns no matched email for the thread. + */ + public String getMatchedEmailId() { + return matchedEmailId; + } + + /** + * Get where {@code query} matched the email in {@code matched_email_id}, as text fragments with each match wrapped in {@code **}, keyed by {@code subject}, {@code body}, {@code from}, {@code to}, {@code cc}, {@code bcc} or {@code attachments}. Only keys with a match are present. Empty when there is no {@code query}. + * + * @return Where {@code query} matched the email in {@code matched_email_id}, as text fragments with each match wrapped in {@code **}, keyed by {@code subject}, {@code body}, {@code from}, {@code to}, {@code cc}, {@code bcc} or {@code attachments}. Only keys with a match are present. Empty when there is no {@code query}. + */ + public Map> getHighlights() { + return highlights; + } +} diff --git a/src/main/java/com/resend/services/inboxes/model/InboxThreadSummary.java b/src/main/java/com/resend/services/inboxes/model/InboxThreadSummary.java new file mode 100644 index 0000000..19a2dc6 --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/model/InboxThreadSummary.java @@ -0,0 +1,109 @@ +package com.resend.services.inboxes.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +/** + * Represents the summary of a single thread. + */ +public class InboxThreadSummary { + + @JsonProperty("object") + private String object; + + @JsonProperty("id") + private String id; + + @JsonProperty("subject") + private String subject; + + @JsonProperty("folder") + private String folder; + + @JsonProperty("labels") + private List labels; + + @JsonProperty("read") + private Boolean read; + + /** + * Default constructor + */ + public InboxThreadSummary() { + + } + + /** + * Constructs a InboxThreadSummary. + * + * @param object The object type. Always {@code inbox_thread}. + * @param id The ID of the thread. + * @param subject The subject of the thread. + * @param folder The folder the thread is in. + * @param labels The labels attached to the thread. + * @param read True only when every email in the thread is read. + */ + public InboxThreadSummary(String object, String id, String subject, String folder, List labels, Boolean read) { + this.object = object; + this.id = id; + this.subject = subject; + this.folder = folder; + this.labels = labels; + this.read = read; + } + + /** + * Get the object type. Always {@code inbox_thread}. + * + * @return The object type. Always {@code inbox_thread}. + */ + public String getObject() { + return object; + } + + /** + * Get the ID of the thread. + * + * @return The ID of the thread. + */ + public String getId() { + return id; + } + + /** + * Get the subject of the thread. + * + * @return The subject of the thread. + */ + public String getSubject() { + return subject; + } + + /** + * Get the folder the thread is in. + * + * @return The folder the thread is in. + */ + public String getFolder() { + return folder; + } + + /** + * Get the labels attached to the thread. + * + * @return The labels attached to the thread. + */ + public List getLabels() { + return labels; + } + + /** + * Get true only when every email in the thread is read. + * + * @return True only when every email in the thread is read. + */ + public Boolean getRead() { + return read; + } +} diff --git a/src/main/java/com/resend/services/inboxes/model/ListInboxDraftsResponseSuccess.java b/src/main/java/com/resend/services/inboxes/model/ListInboxDraftsResponseSuccess.java new file mode 100644 index 0000000..d5dbb10 --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/model/ListInboxDraftsResponseSuccess.java @@ -0,0 +1,67 @@ +package com.resend.services.inboxes.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +/** + * Represents a successful response for listing the drafts of an inbox. + */ +public class ListInboxDraftsResponseSuccess { + + @JsonProperty("object") + private String object; + + @JsonProperty("has_more") + private Boolean hasMore; + + @JsonProperty("data") + private List data; + + /** + * Default constructor + */ + public ListInboxDraftsResponseSuccess() { + + } + + /** + * Constructs a ListInboxDraftsResponseSuccess. + * + * @param object The object type. Always {@code list}. + * @param hasMore Whether there are more drafts available for pagination. + * @param data The unsent drafts on this page, most recently updated first. + */ + public ListInboxDraftsResponseSuccess(String object, Boolean hasMore, List data) { + this.object = object; + this.hasMore = hasMore; + this.data = data; + } + + /** + * Get the object type. Always {@code list}. + * + * @return The object type. Always {@code list}. + */ + public String getObject() { + return object; + } + + /** + * Get whether there are more drafts available for pagination. + * + * @return Whether there are more drafts available for pagination. + */ + public Boolean hasMore() { + return hasMore; + } + + /** + * Get the unsent drafts on this page, most recently updated first. + * + * @return The unsent drafts on this page, most recently updated first. + */ + public List getData() { + return data; + } +} diff --git a/src/main/java/com/resend/services/inboxes/model/ListInboxLabelsResponseSuccess.java b/src/main/java/com/resend/services/inboxes/model/ListInboxLabelsResponseSuccess.java new file mode 100644 index 0000000..79de957 --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/model/ListInboxLabelsResponseSuccess.java @@ -0,0 +1,53 @@ +package com.resend.services.inboxes.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +/** + * Represents a successful response for listing the labels of an inbox. + */ +public class ListInboxLabelsResponseSuccess { + + @JsonProperty("object") + private String object; + + @JsonProperty("data") + private List data; + + /** + * Default constructor + */ + public ListInboxLabelsResponseSuccess() { + + } + + /** + * Constructs a ListInboxLabelsResponseSuccess. + * + * @param object The object type. Always {@code list}. + * @param data The labels on the inbox, oldest first. + */ + public ListInboxLabelsResponseSuccess(String object, List data) { + this.object = object; + this.data = data; + } + + /** + * Get the object type. Always {@code list}. + * + * @return The object type. Always {@code list}. + */ + public String getObject() { + return object; + } + + /** + * Get the labels on the inbox, oldest first. + * + * @return The labels on the inbox, oldest first. + */ + public List getData() { + return data; + } +} diff --git a/src/main/java/com/resend/services/inboxes/model/ListInboxThreadEmailsResponseSuccess.java b/src/main/java/com/resend/services/inboxes/model/ListInboxThreadEmailsResponseSuccess.java new file mode 100644 index 0000000..4d809eb --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/model/ListInboxThreadEmailsResponseSuccess.java @@ -0,0 +1,67 @@ +package com.resend.services.inboxes.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +/** + * Represents a successful response for listing the emails of a thread. + */ +public class ListInboxThreadEmailsResponseSuccess { + + @JsonProperty("object") + private String object; + + @JsonProperty("has_more") + private Boolean hasMore; + + @JsonProperty("data") + private List data; + + /** + * Default constructor + */ + public ListInboxThreadEmailsResponseSuccess() { + + } + + /** + * Constructs a ListInboxThreadEmailsResponseSuccess. + * + * @param object The object type. Always {@code list}. + * @param hasMore Whether there are more emails available for pagination. + * @param data The messages of the thread on this page, oldest first. + */ + public ListInboxThreadEmailsResponseSuccess(String object, Boolean hasMore, List data) { + this.object = object; + this.hasMore = hasMore; + this.data = data; + } + + /** + * Get the object type. Always {@code list}. + * + * @return The object type. Always {@code list}. + */ + public String getObject() { + return object; + } + + /** + * Get whether there are more emails available for pagination. + * + * @return Whether there are more emails available for pagination. + */ + public Boolean hasMore() { + return hasMore; + } + + /** + * Get the messages of the thread on this page, oldest first. + * + * @return The messages of the thread on this page, oldest first. + */ + public List getData() { + return data; + } +} diff --git a/src/main/java/com/resend/services/inboxes/model/ListInboxThreadsParams.java b/src/main/java/com/resend/services/inboxes/model/ListInboxThreadsParams.java new file mode 100644 index 0000000..9865cbd --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/model/ListInboxThreadsParams.java @@ -0,0 +1,250 @@ +package com.resend.services.inboxes.model; + +import com.resend.core.helper.URLHelper; +import com.resend.core.net.ListParams; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Represents parameters for listing the threads of an inbox with filtering and pagination. + * The {@code after} and {@code before} cursors take a thread ID, and only one of them can be passed. + */ +public class ListInboxThreadsParams { + + private final List folders; + private final List labels; + private final Boolean read; + private final Integer limit; + private final String after; + private final String before; + + private ListInboxThreadsParams(Builder builder) { + this.folders = builder.folders; + this.labels = builder.labels; + this.read = builder.read; + this.limit = builder.limit; + this.after = builder.after; + this.before = builder.before; + } + + /** + * Get the folders to list. + * + * @return The folders filter. + */ + public List getFolders() { + return folders; + } + + /** + * Get the label IDs to filter by. + * + * @return The label IDs filter. + */ + public List getLabels() { + return labels; + } + + /** + * Get the read filter. + * + * @return The read filter. + */ + public Boolean getRead() { + return read; + } + + /** + * Get the maximum number of results. + * + * @return The limit. + */ + public Integer getLimit() { + return limit; + } + + /** + * Get the cursor for pagination (after). + * + * @return The after cursor. + */ + public String getAfter() { + return after; + } + + /** + * Get the cursor for pagination (before). + * + * @return The before cursor. + */ + public String getBefore() { + return before; + } + + /** + * Converts the parameters to a query string. + * + * @return A query string starting with "?" if parameters exist, or an empty string otherwise. + */ + public String toQueryString() { + ListParams base = ListParams.builder() + .limit(limit) + .after(after) + .before(before) + .build(); + return URLHelper.parse(base, filters(folders, labels, read)); + } + + static Map filters(List folders, List labels, Boolean read) { + Map extras = new LinkedHashMap<>(); + if (folders != null && !folders.isEmpty()) { + List values = new ArrayList<>(); + for (InboxFolder folder : folders) { + values.add(folder.getValue()); + } + extras.put("folders", String.join(",", values)); + } + if (labels != null && !labels.isEmpty()) { + extras.put("labels", String.join(",", labels)); + } + if (read != null) { + extras.put("read", read.toString()); + } + return extras; + } + + /** + * Creates a new builder instance for ListInboxThreadsParams. + * + * @return A new Builder instance. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Builder class for constructing ListInboxThreadsParams objects. + */ + public static class Builder { + private List folders; + private List labels; + private Boolean read; + private Integer limit; + private String after; + private String before; + + /** + * Creates a new Builder instance. + */ + public Builder() { + } + + /** + * Sets the folders to list. A thread is returned when any of its emails is in one of the folders. + * Defaults to {@code inbox}, or to {@code inbox,archive,sent} when {@code labels} is set. + * {@code spam} and {@code trash} are only included when you name them. + * + * @param folders The folders. + * @return The builder instance. + */ + public Builder folders(InboxFolder... folders) { + this.folders = new ArrayList<>(Arrays.asList(folders)); + return this; + } + + /** + * Sets the folders to list. A thread is returned when any of its emails is in one of the folders. + * Defaults to {@code inbox}, or to {@code inbox,archive,sent} when {@code labels} is set. + * {@code spam} and {@code trash} are only included when you name them. + * + * @param folders The folders. + * @return The builder instance. + */ + public Builder folders(List folders) { + this.folders = folders == null ? null : new ArrayList<>(folders); + return this; + } + + /** + * Sets the label IDs. Returns threads with any of these labels. Up to 50 IDs. + * An ID that does not exist in the inbox returns a 404. + * + * @param labels The label IDs. + * @return The builder instance. + */ + public Builder labels(String... labels) { + this.labels = new ArrayList<>(Arrays.asList(labels)); + return this; + } + + /** + * Sets the label IDs. Returns threads with any of these labels. Up to 50 IDs. + * An ID that does not exist in the inbox returns a 404. + * + * @param labels The label IDs. + * @return The builder instance. + */ + public Builder labels(List labels) { + this.labels = labels == null ? null : new ArrayList<>(labels); + return this; + } + + /** + * Sets the read filter. {@code true} returns threads where every email is read. + * {@code false} returns threads with at least one unread email. + * + * @param read The read filter. + * @return The builder instance. + */ + public Builder read(Boolean read) { + this.read = read; + return this; + } + + /** + * Sets the maximum number of results. + * + * @param limit The limit. + * @return The builder instance. + */ + public Builder limit(Integer limit) { + this.limit = limit; + return this; + } + + /** + * Sets the after cursor for pagination. Takes a thread ID. + * + * @param after The after cursor. + * @return The builder instance. + */ + public Builder after(String after) { + this.after = after; + return this; + } + + /** + * Sets the before cursor for pagination. Takes a thread ID. + * + * @param before The before cursor. + * @return The builder instance. + */ + public Builder before(String before) { + this.before = before; + return this; + } + + /** + * Builds a new ListInboxThreadsParams instance. + * + * @return A new ListInboxThreadsParams. + */ + public ListInboxThreadsParams build() { + return new ListInboxThreadsParams(this); + } + } +} diff --git a/src/main/java/com/resend/services/inboxes/model/ListInboxThreadsResponseSuccess.java b/src/main/java/com/resend/services/inboxes/model/ListInboxThreadsResponseSuccess.java new file mode 100644 index 0000000..25d14a1 --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/model/ListInboxThreadsResponseSuccess.java @@ -0,0 +1,67 @@ +package com.resend.services.inboxes.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +/** + * Represents a successful response for listing the threads of an inbox. + */ +public class ListInboxThreadsResponseSuccess { + + @JsonProperty("object") + private String object; + + @JsonProperty("has_more") + private Boolean hasMore; + + @JsonProperty("data") + private List data; + + /** + * Default constructor + */ + public ListInboxThreadsResponseSuccess() { + + } + + /** + * Constructs a ListInboxThreadsResponseSuccess. + * + * @param object The object type. Always {@code list}. + * @param hasMore Whether there are more threads available for pagination. + * @param data The matching threads, most recently active first. + */ + public ListInboxThreadsResponseSuccess(String object, Boolean hasMore, List data) { + this.object = object; + this.hasMore = hasMore; + this.data = data; + } + + /** + * Get the object type. Always {@code list}. + * + * @return The object type. Always {@code list}. + */ + public String getObject() { + return object; + } + + /** + * Get whether there are more threads available for pagination. + * + * @return Whether there are more threads available for pagination. + */ + public Boolean hasMore() { + return hasMore; + } + + /** + * Get the matching threads, most recently active first. + * + * @return The matching threads, most recently active first. + */ + public List getData() { + return data; + } +} diff --git a/src/main/java/com/resend/services/inboxes/model/ListInboxesResponseSuccess.java b/src/main/java/com/resend/services/inboxes/model/ListInboxesResponseSuccess.java new file mode 100644 index 0000000..43fafd0 --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/model/ListInboxesResponseSuccess.java @@ -0,0 +1,67 @@ +package com.resend.services.inboxes.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +/** + * Represents a successful response for listing inboxes. + */ +public class ListInboxesResponseSuccess { + + @JsonProperty("object") + private String object; + + @JsonProperty("has_more") + private Boolean hasMore; + + @JsonProperty("data") + private List data; + + /** + * Default constructor + */ + public ListInboxesResponseSuccess() { + + } + + /** + * Constructs a ListInboxesResponseSuccess. + * + * @param object The object type. Always {@code list}. + * @param hasMore Whether there are more inboxes available for pagination. + * @param data The list of inboxes, newest first. + */ + public ListInboxesResponseSuccess(String object, Boolean hasMore, List data) { + this.object = object; + this.hasMore = hasMore; + this.data = data; + } + + /** + * Get the object type. Always {@code list}. + * + * @return The object type. Always {@code list}. + */ + public String getObject() { + return object; + } + + /** + * Get whether there are more inboxes available for pagination. + * + * @return Whether there are more inboxes available for pagination. + */ + public Boolean hasMore() { + return hasMore; + } + + /** + * Get the list of inboxes, newest first. + * + * @return The list of inboxes, newest first. + */ + public List getData() { + return data; + } +} diff --git a/src/main/java/com/resend/services/inboxes/model/MoveInboxThreadFolder.java b/src/main/java/com/resend/services/inboxes/model/MoveInboxThreadFolder.java new file mode 100644 index 0000000..43527b4 --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/model/MoveInboxThreadFolder.java @@ -0,0 +1,56 @@ +package com.resend.services.inboxes.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * Represents a folder a thread can be moved to. {@code sent} is assigned automatically and cannot be set. + */ +public enum MoveInboxThreadFolder { + /** Moves the thread to the inbox. */ + INBOX("inbox"), + /** Moves the thread to the archive. */ + ARCHIVE("archive"), + /** Moves the thread to spam. */ + SPAM("spam"), + /** Moves the thread to trash and schedules it for deletion in 30 days. */ + TRASH("trash"); + + private final String value; + + MoveInboxThreadFolder(String value) { + this.value = value; + } + + /** + * Returns the string value of the folder. + * + * @return The folder value. + */ + @JsonValue + public String getValue() { + return value; + } + + /** + * Creates a MoveInboxThreadFolder from a string value. + * + * @param value The string value. + * @return The corresponding MoveInboxThreadFolder. + * @throws IllegalArgumentException If the value is unknown. + */ + @JsonCreator + public static MoveInboxThreadFolder fromValue(String value) { + for (MoveInboxThreadFolder item : MoveInboxThreadFolder.values()) { + if (item.value.equals(value)) { + return item; + } + } + throw new IllegalArgumentException("Unknown folder: " + value); + } + + @Override + public String toString() { + return value; + } +} diff --git a/src/main/java/com/resend/services/inboxes/model/RemoveInboxDraftResponseSuccess.java b/src/main/java/com/resend/services/inboxes/model/RemoveInboxDraftResponseSuccess.java new file mode 100644 index 0000000..35cdfc1 --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/model/RemoveInboxDraftResponseSuccess.java @@ -0,0 +1,65 @@ +package com.resend.services.inboxes.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Represents a successful response for removing a draft. + */ +public class RemoveInboxDraftResponseSuccess { + + @JsonProperty("object") + private String object; + + @JsonProperty("id") + private String id; + + @JsonProperty("deleted") + private Boolean deleted; + + /** + * Default constructor + */ + public RemoveInboxDraftResponseSuccess() { + + } + + /** + * Constructs a RemoveInboxDraftResponseSuccess. + * + * @param object The object type. Always {@code inbox_draft}. + * @param id The ID of the draft. + * @param deleted Whether the draft was deleted. + */ + public RemoveInboxDraftResponseSuccess(String object, String id, Boolean deleted) { + this.object = object; + this.id = id; + this.deleted = deleted; + } + + /** + * Get the object type. Always {@code inbox_draft}. + * + * @return The object type. Always {@code inbox_draft}. + */ + public String getObject() { + return object; + } + + /** + * Get the ID of the draft. + * + * @return The ID of the draft. + */ + public String getId() { + return id; + } + + /** + * Get whether the draft was deleted. + * + * @return Whether the draft was deleted. + */ + public Boolean getDeleted() { + return deleted; + } +} diff --git a/src/main/java/com/resend/services/inboxes/model/RemoveInboxLabelResponseSuccess.java b/src/main/java/com/resend/services/inboxes/model/RemoveInboxLabelResponseSuccess.java new file mode 100644 index 0000000..6a589e3 --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/model/RemoveInboxLabelResponseSuccess.java @@ -0,0 +1,65 @@ +package com.resend.services.inboxes.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Represents a successful response for removing an inbox label. + */ +public class RemoveInboxLabelResponseSuccess { + + @JsonProperty("object") + private String object; + + @JsonProperty("id") + private String id; + + @JsonProperty("deleted") + private Boolean deleted; + + /** + * Default constructor + */ + public RemoveInboxLabelResponseSuccess() { + + } + + /** + * Constructs a RemoveInboxLabelResponseSuccess. + * + * @param object The object type. Always {@code inbox_label}. + * @param id The ID of the removed label. + * @param deleted Whether the label was deleted. + */ + public RemoveInboxLabelResponseSuccess(String object, String id, Boolean deleted) { + this.object = object; + this.id = id; + this.deleted = deleted; + } + + /** + * Get the object type. Always {@code inbox_label}. + * + * @return The object type. Always {@code inbox_label}. + */ + public String getObject() { + return object; + } + + /** + * Get the ID of the removed label. + * + * @return The ID of the removed label. + */ + public String getId() { + return id; + } + + /** + * Get whether the label was deleted. + * + * @return Whether the label was deleted. + */ + public Boolean getDeleted() { + return deleted; + } +} diff --git a/src/main/java/com/resend/services/inboxes/model/RemoveInboxResponseSuccess.java b/src/main/java/com/resend/services/inboxes/model/RemoveInboxResponseSuccess.java new file mode 100644 index 0000000..561a827 --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/model/RemoveInboxResponseSuccess.java @@ -0,0 +1,65 @@ +package com.resend.services.inboxes.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Represents a successful response for removing an inbox. + */ +public class RemoveInboxResponseSuccess { + + @JsonProperty("object") + private String object; + + @JsonProperty("id") + private String id; + + @JsonProperty("deleted") + private Boolean deleted; + + /** + * Default constructor + */ + public RemoveInboxResponseSuccess() { + + } + + /** + * Constructs a RemoveInboxResponseSuccess. + * + * @param object The object type. Always {@code inbox}. + * @param id The ID of the inbox. + * @param deleted Whether the inbox was deleted. + */ + public RemoveInboxResponseSuccess(String object, String id, Boolean deleted) { + this.object = object; + this.id = id; + this.deleted = deleted; + } + + /** + * Get the object type. Always {@code inbox}. + * + * @return The object type. Always {@code inbox}. + */ + public String getObject() { + return object; + } + + /** + * Get the ID of the inbox. + * + * @return The ID of the inbox. + */ + public String getId() { + return id; + } + + /** + * Get whether the inbox was deleted. + * + * @return Whether the inbox was deleted. + */ + public Boolean getDeleted() { + return deleted; + } +} diff --git a/src/main/java/com/resend/services/inboxes/model/RemoveInboxThreadResponseSuccess.java b/src/main/java/com/resend/services/inboxes/model/RemoveInboxThreadResponseSuccess.java new file mode 100644 index 0000000..d1b43db --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/model/RemoveInboxThreadResponseSuccess.java @@ -0,0 +1,65 @@ +package com.resend.services.inboxes.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Represents a successful response for removing a thread. + */ +public class RemoveInboxThreadResponseSuccess { + + @JsonProperty("object") + private String object; + + @JsonProperty("id") + private String id; + + @JsonProperty("deleted") + private Boolean deleted; + + /** + * Default constructor + */ + public RemoveInboxThreadResponseSuccess() { + + } + + /** + * Constructs a RemoveInboxThreadResponseSuccess. + * + * @param object The object type. Always {@code inbox_thread}. + * @param id The ID of the thread. + * @param deleted True when this call moved the thread to {@code trash}. False when the thread was already there. + */ + public RemoveInboxThreadResponseSuccess(String object, String id, Boolean deleted) { + this.object = object; + this.id = id; + this.deleted = deleted; + } + + /** + * Get the object type. Always {@code inbox_thread}. + * + * @return The object type. Always {@code inbox_thread}. + */ + public String getObject() { + return object; + } + + /** + * Get the ID of the thread. + * + * @return The ID of the thread. + */ + public String getId() { + return id; + } + + /** + * Get true when this call moved the thread to {@code trash}. False when the thread was already there. + * + * @return True when this call moved the thread to {@code trash}. False when the thread was already there. + */ + public Boolean getDeleted() { + return deleted; + } +} diff --git a/src/main/java/com/resend/services/inboxes/model/ReplyInboxThreadEmailOptions.java b/src/main/java/com/resend/services/inboxes/model/ReplyInboxThreadEmailOptions.java new file mode 100644 index 0000000..f83f4fb --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/model/ReplyInboxThreadEmailOptions.java @@ -0,0 +1,297 @@ +package com.resend.services.inboxes.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.resend.services.emails.model.Attachment; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Represents a request to reply to a thread email. At least one of {@code html} or {@code text} must contain more than whitespace. + */ +public class ReplyInboxThreadEmailOptions { + + @JsonProperty("cc") + private final List cc; + + @JsonProperty("bcc") + private final List bcc; + + @JsonProperty("html") + private final String html; + + @JsonProperty("text") + private final String text; + + @JsonProperty("subject") + private final String subject; + + @JsonProperty("reply_all") + private final Boolean replyAll; + + @JsonProperty("attachments") + private final List attachments; + + private ReplyInboxThreadEmailOptions(Builder builder) { + this.cc = builder.cc; + this.bcc = builder.bcc; + this.html = builder.html; + this.text = builder.text; + this.subject = builder.subject; + this.replyAll = builder.replyAll; + this.attachments = builder.attachments; + } + + /** + * Get the CC recipients. + * + * @return The CC recipients. + */ + public List getCc() { + return cc; + } + + /** + * Get the BCC recipients. + * + * @return The BCC recipients. + */ + public List getBcc() { + return bcc; + } + + /** + * Get the HTML body of the reply. + * + * @return The HTML body of the reply. + */ + public String getHtml() { + return html; + } + + /** + * Get the plain-text body of the reply. + * + * @return The plain-text body of the reply. + */ + public String getText() { + return text; + } + + /** + * Get the subject of the reply. + * + * @return The subject of the reply. + */ + public String getSubject() { + return subject; + } + + /** + * Get whether to reply to everyone on the original message. + * + * @return Whether to reply to everyone on the original message. + */ + public Boolean getReplyAll() { + return replyAll; + } + + /** + * Get the files to attach, as on {@code POST /emails}. + * + * @return The files to attach, as on {@code POST /emails}. + */ + public List getAttachments() { + return attachments; + } + + /** + * Create a new builder instance for constructing ReplyInboxThreadEmailOptions objects. + * + * @return A new builder instance. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Builder class for constructing ReplyInboxThreadEmailOptions objects. + */ + public static class Builder { + private List cc; + private List bcc; + private String html; + private String text; + private String subject; + private Boolean replyAll; + private List attachments; + + /** + * Creates a new Builder instance. + */ + public Builder() { + } + + /** + * Set the CC recipients. Without {@code reply_all}, the reply copies only these addresses. With {@code reply_all}, these addresses are merged with the copied recipients, without duplicates. + * + * @param cc The CC recipients. + * @return The builder instance. + */ + public Builder cc(String... cc) { + this.cc = new ArrayList<>(Arrays.asList(cc)); + return this; + } + + /** + * Set the CC recipients. Without {@code reply_all}, the reply copies only these addresses. With {@code reply_all}, these addresses are merged with the copied recipients, without duplicates. + * + * @param cc The CC recipients. + * @return The builder instance. + */ + public Builder cc(List cc) { + this.cc = cc == null ? null : new ArrayList<>(cc); + return this; + } + + /** + * Add one value to the CC recipients. + * + * @param recipient The value to add. + * @return The builder instance. + */ + public Builder addCc(String recipient) { + if (this.cc == null) { + this.cc = new ArrayList<>(); + } + this.cc.add(recipient); + return this; + } + + /** + * Set the BCC recipients. BCC recipients of the original message are never copied. + * + * @param bcc The BCC recipients. + * @return The builder instance. + */ + public Builder bcc(String... bcc) { + this.bcc = new ArrayList<>(Arrays.asList(bcc)); + return this; + } + + /** + * Set the BCC recipients. BCC recipients of the original message are never copied. + * + * @param bcc The BCC recipients. + * @return The builder instance. + */ + public Builder bcc(List bcc) { + this.bcc = bcc == null ? null : new ArrayList<>(bcc); + return this; + } + + /** + * Add one value to the BCC recipients. + * + * @param recipient The value to add. + * @return The builder instance. + */ + public Builder addBcc(String recipient) { + if (this.bcc == null) { + this.bcc = new ArrayList<>(); + } + this.bcc.add(recipient); + return this; + } + + /** + * Set the HTML body of the reply. + * + * @param html The HTML body of the reply. + * @return The builder instance. + */ + public Builder html(String html) { + this.html = html; + return this; + } + + /** + * Set the plain-text body of the reply. + * + * @param text The plain-text body of the reply. + * @return The builder instance. + */ + public Builder text(String text) { + this.text = text; + return this; + } + + /** + * Set the subject of the reply. Cannot contain a line break. When omitted, the thread subject is used, prefixed with {@code Re:} if it does not already start with it. + * + * @param subject The subject of the reply. + * @return The builder instance. + */ + public Builder subject(String subject) { + this.subject = subject; + return this; + } + + /** + * Set whether to reply to everyone on the original message. The other {@code to} and {@code cc} recipients of the message are copied in {@code cc}. The inbox address is always excluded. Defaults to {@code false}. + * + * @param replyAll Whether to reply to everyone on the original message. + * @return The builder instance. + */ + public Builder replyAll(Boolean replyAll) { + this.replyAll = replyAll; + return this; + } + + /** + * Set the files to attach, as on {@code POST /emails}. Each one needs {@code content} or {@code path}. Up to 10 files. The {@code content} of all files can total at most 25 MB. + * + * @param attachments The files to attach, as on {@code POST /emails}. + * @return The builder instance. + */ + public Builder attachments(Attachment... attachments) { + this.attachments = new ArrayList<>(Arrays.asList(attachments)); + return this; + } + + /** + * Set the files to attach, as on {@code POST /emails}. Each one needs {@code content} or {@code path}. Up to 10 files. The {@code content} of all files can total at most 25 MB. + * + * @param attachments The files to attach, as on {@code POST /emails}. + * @return The builder instance. + */ + public Builder attachments(List attachments) { + this.attachments = attachments == null ? null : new ArrayList<>(attachments); + return this; + } + + /** + * Add one value to the files to attach, as on {@code POST /emails}. + * + * @param attachment The value to add. + * @return The builder instance. + */ + public Builder addAttachment(Attachment attachment) { + if (this.attachments == null) { + this.attachments = new ArrayList<>(); + } + this.attachments.add(attachment); + return this; + } + + /** + * Build a new ReplyInboxThreadEmailOptions object. + * + * @return A new ReplyInboxThreadEmailOptions object. + */ + public ReplyInboxThreadEmailOptions build() { + return new ReplyInboxThreadEmailOptions(this); + } + } +} diff --git a/src/main/java/com/resend/services/inboxes/model/ReplyInboxThreadEmailResponseSuccess.java b/src/main/java/com/resend/services/inboxes/model/ReplyInboxThreadEmailResponseSuccess.java new file mode 100644 index 0000000..59b8054 --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/model/ReplyInboxThreadEmailResponseSuccess.java @@ -0,0 +1,37 @@ +package com.resend.services.inboxes.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Represents a successful response for replying to a thread email. + */ +public class ReplyInboxThreadEmailResponseSuccess { + + @JsonProperty("id") + private String id; + + /** + * Default constructor + */ + public ReplyInboxThreadEmailResponseSuccess() { + + } + + /** + * Constructs a ReplyInboxThreadEmailResponseSuccess. + * + * @param id The ID of the sent email. Use it with the emails endpoints. + */ + public ReplyInboxThreadEmailResponseSuccess(String id) { + this.id = id; + } + + /** + * Get the ID of the sent email. Use it with the emails endpoints. + * + * @return The ID of the sent email. Use it with the emails endpoints. + */ + public String getId() { + return id; + } +} diff --git a/src/main/java/com/resend/services/inboxes/model/SearchInboxThreadsParams.java b/src/main/java/com/resend/services/inboxes/model/SearchInboxThreadsParams.java new file mode 100644 index 0000000..74c7898 --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/model/SearchInboxThreadsParams.java @@ -0,0 +1,489 @@ +package com.resend.services.inboxes.model; + +import com.resend.core.helper.URLHelper; +import com.resend.core.net.ListParams; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Represents parameters for searching the threads of an inbox. + * Different parameters combine, so a thread must match all of them. {@code folders}, {@code labels}, and + * {@code read} apply to the thread. {@code query}, {@code from}, {@code to}, {@code cc}, {@code bcc}, + * {@code has_attachment}, {@code start_date}, and {@code end_date} must all match the same email in the thread. + */ +public class SearchInboxThreadsParams { + + private final String query; + private final List from; + private final List to; + private final List cc; + private final List bcc; + private final Boolean hasAttachment; + private final String startDate; + private final String endDate; + private final List folders; + private final List labels; + private final Boolean read; + private final Integer limit; + private final String after; + private final String before; + + private SearchInboxThreadsParams(Builder builder) { + this.query = builder.query; + this.from = builder.from; + this.to = builder.to; + this.cc = builder.cc; + this.bcc = builder.bcc; + this.hasAttachment = builder.hasAttachment; + this.startDate = builder.startDate; + this.endDate = builder.endDate; + this.folders = builder.folders; + this.labels = builder.labels; + this.read = builder.read; + this.limit = builder.limit; + this.after = builder.after; + this.before = builder.before; + } + + /** + * Get the free text to search for. + * + * @return The query. + */ + public String getQuery() { + return query; + } + + /** + * Get the senders to match. + * + * @return The senders filter. + */ + public List getFrom() { + return from; + } + + /** + * Get the recipients to match. + * + * @return The recipients filter. + */ + public List getTo() { + return to; + } + + /** + * Get the CC recipients to match. + * + * @return The CC recipients filter. + */ + public List getCc() { + return cc; + } + + /** + * Get the BCC recipients to match. + * + * @return The BCC recipients filter. + */ + public List getBcc() { + return bcc; + } + + /** + * Get the attachment filter. + * + * @return The attachment filter. + */ + public Boolean getHasAttachment() { + return hasAttachment; + } + + /** + * Get the start date filter. + * + * @return The start date. + */ + public String getStartDate() { + return startDate; + } + + /** + * Get the end date filter. + * + * @return The end date. + */ + public String getEndDate() { + return endDate; + } + + /** + * Get the folders to search. + * + * @return The folders filter. + */ + public List getFolders() { + return folders; + } + + /** + * Get the label IDs to filter by. + * + * @return The label IDs filter. + */ + public List getLabels() { + return labels; + } + + /** + * Get the read filter. + * + * @return The read filter. + */ + public Boolean getRead() { + return read; + } + + /** + * Get the maximum number of results. + * + * @return The limit. + */ + public Integer getLimit() { + return limit; + } + + /** + * Get the cursor for pagination (after). + * + * @return The after cursor. + */ + public String getAfter() { + return after; + } + + /** + * Get the cursor for pagination (before). + * + * @return The before cursor. + */ + public String getBefore() { + return before; + } + + /** + * Converts the parameters to a query string. + * + * @return A query string starting with "?" if parameters exist, or an empty string otherwise. + */ + public String toQueryString() { + ListParams base = ListParams.builder() + .limit(limit) + .after(after) + .before(before) + .build(); + + Map extras = new LinkedHashMap<>(); + extras.put("query", query); + putList(extras, "from", from); + putList(extras, "to", to); + putList(extras, "cc", cc); + putList(extras, "bcc", bcc); + if (hasAttachment != null) { + extras.put("has_attachment", hasAttachment.toString()); + } + extras.put("start_date", startDate); + extras.put("end_date", endDate); + extras.putAll(ListInboxThreadsParams.filters(folders, labels, read)); + + return URLHelper.parse(base, extras); + } + + private static void putList(Map extras, String key, List values) { + if (values != null && !values.isEmpty()) { + extras.put(key, String.join(",", values)); + } + } + + /** + * Creates a new builder instance for SearchInboxThreadsParams. + * + * @return A new Builder instance. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Builder class for constructing SearchInboxThreadsParams objects. + */ + public static class Builder { + private String query; + private List from; + private List to; + private List cc; + private List bcc; + private Boolean hasAttachment; + private String startDate; + private String endDate; + private List folders; + private List labels; + private Boolean read; + private Integer limit; + private String after; + private String before; + + /** + * Creates a new Builder instance. + */ + public Builder() { + } + + /** + * Sets the free text to search for in the subject, body, sender, recipients, and attachment names. + * Every word must match. Wrap words in double quotes to match an exact phrase. Start a word or quoted + * phrase with {@code -} to exclude it. Max 256 characters and 32 words. + * + * @param query The query. + * @return The builder instance. + */ + public Builder query(String query) { + this.query = query; + return this; + } + + /** + * Sets the senders. Each value matches part of the sender's address or name, without case. + * Returns threads that match any of them. Up to 20 values. + * + * @param from The senders. + * @return The builder instance. + */ + public Builder from(String... from) { + this.from = new ArrayList<>(Arrays.asList(from)); + return this; + } + + /** + * Sets the senders. Each value matches part of the sender's address or name, without case. + * Returns threads that match any of them. Up to 20 values. + * + * @param from The senders. + * @return The builder instance. + */ + public Builder from(List from) { + this.from = from == null ? null : new ArrayList<>(from); + return this; + } + + /** + * Sets the recipients, matched the same way as {@code from}. Up to 20 values. + * + * @param to The recipients. + * @return The builder instance. + */ + public Builder to(String... to) { + this.to = new ArrayList<>(Arrays.asList(to)); + return this; + } + + /** + * Sets the recipients, matched the same way as {@code from}. Up to 20 values. + * + * @param to The recipients. + * @return The builder instance. + */ + public Builder to(List to) { + this.to = to == null ? null : new ArrayList<>(to); + return this; + } + + /** + * Sets the CC recipients, matched the same way as {@code from}. Up to 20 values. + * + * @param cc The CC recipients. + * @return The builder instance. + */ + public Builder cc(String... cc) { + this.cc = new ArrayList<>(Arrays.asList(cc)); + return this; + } + + /** + * Sets the CC recipients, matched the same way as {@code from}. Up to 20 values. + * + * @param cc The CC recipients. + * @return The builder instance. + */ + public Builder cc(List cc) { + this.cc = cc == null ? null : new ArrayList<>(cc); + return this; + } + + /** + * Sets the BCC recipients, matched the same way as {@code from}. Up to 20 values. + * + * @param bcc The BCC recipients. + * @return The builder instance. + */ + public Builder bcc(String... bcc) { + this.bcc = new ArrayList<>(Arrays.asList(bcc)); + return this; + } + + /** + * Sets the BCC recipients, matched the same way as {@code from}. Up to 20 values. + * + * @param bcc The BCC recipients. + * @return The builder instance. + */ + public Builder bcc(List bcc) { + this.bcc = bcc == null ? null : new ArrayList<>(bcc); + return this; + } + + /** + * Sets the attachment filter. {@code true} matches emails with an attachment. + * {@code false} matches emails without one. + * + * @param hasAttachment The attachment filter. + * @return The builder instance. + */ + public Builder hasAttachment(Boolean hasAttachment) { + this.hasAttachment = hasAttachment; + return this; + } + + /** + * Sets the start date. Matches emails sent on or after this date. Accepts a date like + * {@code 2026-09-01} or an ISO 8601 timestamp, in UTC unless the timestamp has an offset. + * + * @param startDate The start date. + * @return The builder instance. + */ + public Builder startDate(String startDate) { + this.startDate = startDate; + return this; + } + + /** + * Sets the end date. Matches emails sent on or before this date. Same formats as {@code start_date}. + * A date covers that whole day. A timestamp is used as given. + * + * @param endDate The end date. + * @return The builder instance. + */ + public Builder endDate(String endDate) { + this.endDate = endDate; + return this; + } + + /** + * Sets the folders to search. A thread is returned when any of its emails is in one of the folders. + * Defaults to {@code inbox}, or to {@code inbox,archive,sent} when {@code labels} is set. + * {@code spam} and {@code trash} are only included when you name them. + * + * @param folders The folders. + * @return The builder instance. + */ + public Builder folders(InboxFolder... folders) { + this.folders = new ArrayList<>(Arrays.asList(folders)); + return this; + } + + /** + * Sets the folders to search. A thread is returned when any of its emails is in one of the folders. + * Defaults to {@code inbox}, or to {@code inbox,archive,sent} when {@code labels} is set. + * {@code spam} and {@code trash} are only included when you name them. + * + * @param folders The folders. + * @return The builder instance. + */ + public Builder folders(List folders) { + this.folders = folders == null ? null : new ArrayList<>(folders); + return this; + } + + /** + * Sets the label IDs. Returns threads with any of these labels. Up to 50 IDs. + * An ID that does not exist in the inbox returns a 404. + * + * @param labels The label IDs. + * @return The builder instance. + */ + public Builder labels(String... labels) { + this.labels = new ArrayList<>(Arrays.asList(labels)); + return this; + } + + /** + * Sets the label IDs. Returns threads with any of these labels. Up to 50 IDs. + * An ID that does not exist in the inbox returns a 404. + * + * @param labels The label IDs. + * @return The builder instance. + */ + public Builder labels(List labels) { + this.labels = labels == null ? null : new ArrayList<>(labels); + return this; + } + + /** + * Sets the read filter. {@code true} returns threads where every email is read. + * {@code false} returns threads with at least one unread email. + * + * @param read The read filter. + * @return The builder instance. + */ + public Builder read(Boolean read) { + this.read = read; + return this; + } + + /** + * Sets the maximum number of results. + * + * @param limit The limit. + * @return The builder instance. + */ + public Builder limit(Integer limit) { + this.limit = limit; + return this; + } + + /** + * Sets the after cursor for pagination. Takes a thread ID. + * + * @param after The after cursor. + * @return The builder instance. + */ + public Builder after(String after) { + this.after = after; + return this; + } + + /** + * Sets the before cursor for pagination. Takes a thread ID. + * + * @param before The before cursor. + * @return The builder instance. + */ + public Builder before(String before) { + this.before = before; + return this; + } + + /** + * Builds a new SearchInboxThreadsParams instance. + * + * @return A new SearchInboxThreadsParams. + */ + public SearchInboxThreadsParams build() { + return new SearchInboxThreadsParams(this); + } + } +} diff --git a/src/main/java/com/resend/services/inboxes/model/SearchInboxThreadsResponseSuccess.java b/src/main/java/com/resend/services/inboxes/model/SearchInboxThreadsResponseSuccess.java new file mode 100644 index 0000000..851e5e2 --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/model/SearchInboxThreadsResponseSuccess.java @@ -0,0 +1,67 @@ +package com.resend.services.inboxes.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +/** + * Represents a successful response for searching the threads of an inbox. + */ +public class SearchInboxThreadsResponseSuccess { + + @JsonProperty("object") + private String object; + + @JsonProperty("has_more") + private Boolean hasMore; + + @JsonProperty("data") + private List data; + + /** + * Default constructor + */ + public SearchInboxThreadsResponseSuccess() { + + } + + /** + * Constructs a SearchInboxThreadsResponseSuccess. + * + * @param object The object type. Always {@code list}. + * @param hasMore Whether there are more threads available for pagination. + * @param data The matching threads, most recently active first. + */ + public SearchInboxThreadsResponseSuccess(String object, Boolean hasMore, List data) { + this.object = object; + this.hasMore = hasMore; + this.data = data; + } + + /** + * Get the object type. Always {@code list}. + * + * @return The object type. Always {@code list}. + */ + public String getObject() { + return object; + } + + /** + * Get whether there are more threads available for pagination. + * + * @return Whether there are more threads available for pagination. + */ + public Boolean hasMore() { + return hasMore; + } + + /** + * Get the matching threads, most recently active first. + * + * @return The matching threads, most recently active first. + */ + public List getData() { + return data; + } +} diff --git a/src/main/java/com/resend/services/inboxes/model/SendInboxDraftResponseSuccess.java b/src/main/java/com/resend/services/inboxes/model/SendInboxDraftResponseSuccess.java new file mode 100644 index 0000000..757c538 --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/model/SendInboxDraftResponseSuccess.java @@ -0,0 +1,37 @@ +package com.resend.services.inboxes.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Represents a successful response for sending a draft. + */ +public class SendInboxDraftResponseSuccess { + + @JsonProperty("id") + private String id; + + /** + * Default constructor + */ + public SendInboxDraftResponseSuccess() { + + } + + /** + * Constructs a SendInboxDraftResponseSuccess. + * + * @param id The ID of the sent email. Use it with the emails endpoints. + */ + public SendInboxDraftResponseSuccess(String id) { + this.id = id; + } + + /** + * Get the ID of the sent email. Use it with the emails endpoints. + * + * @return The ID of the sent email. Use it with the emails endpoints. + */ + public String getId() { + return id; + } +} diff --git a/src/main/java/com/resend/services/inboxes/model/UpdateInboxAgentOptions.java b/src/main/java/com/resend/services/inboxes/model/UpdateInboxAgentOptions.java new file mode 100644 index 0000000..f1d00db --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/model/UpdateInboxAgentOptions.java @@ -0,0 +1,145 @@ +package com.resend.services.inboxes.model; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonInclude; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Represents a request to update the agent settings of an inbox. + * At least one of {@code instructions}, {@code tone}, or {@code enabled_actions} must be provided. + * Omitted fields keep their current value. + */ +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.NONE, getterVisibility = JsonAutoDetect.Visibility.NONE, isGetterVisibility = JsonAutoDetect.Visibility.NONE) +public class UpdateInboxAgentOptions { + + private final Map fields; + + private UpdateInboxAgentOptions(Builder builder) { + this.fields = Collections.unmodifiableMap(new LinkedHashMap<>(builder.fields)); + } + + @JsonAnyGetter + @JsonInclude(content = JsonInclude.Include.ALWAYS) + private Map getFields() { + return fields; + } + + /** + * Get the instructions the agent follows when it handles the threads of the inbox. + * + * @return The instructions, or null when they are not set or are cleared. + */ + public String getInstructions() { + return (String) fields.get("instructions"); + } + + /** + * Get the tone the agent writes in. + * + * @return The tone, or null when it is not set or is cleared. + */ + public String getTone() { + return (String) fields.get("tone"); + } + + /** + * Get the actions the agent can take. + * + * @return The enabled actions, or null when they are not set. + */ + @SuppressWarnings("unchecked") + public List getEnabledActions() { + return (List) fields.get("enabled_actions"); + } + + /** + * Create a new builder instance for constructing UpdateInboxAgentOptions objects. + * + * @return A new builder instance. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Builder class for constructing UpdateInboxAgentOptions objects. + */ + public static class Builder { + private final Map fields = new LinkedHashMap<>(); + + /** + * Creates a new Builder instance. + */ + public Builder() { + } + + /** + * Set the instructions the agent follows when it handles the threads of the inbox. + * Max 4000 characters. Leading and trailing whitespace is trimmed, and the trimmed value must not be empty. + * Pass null to clear the instructions. + * + * @param instructions The instructions, or null to clear them. + * @return The builder instance. + */ + public Builder instructions(String instructions) { + fields.put("instructions", instructions); + return this; + } + + /** + * Set the tone the agent writes in, such as {@code friendly and concise}. + * Max 64 characters. Leading and trailing whitespace is trimmed, and the trimmed value must not be empty. + * Pass null to clear the tone. + * + * @param tone The tone, or null to clear it. + * @return The builder instance. + */ + public Builder tone(String tone) { + fields.put("tone", tone); + return this; + } + + /** + * Set the actions the agent can take. Replaces the whole set. Pass no actions to disable all actions. + * + * @param enabledActions The enabled actions. + * @return The builder instance. + */ + public Builder enabledActions(InboxAgentAction... enabledActions) { + fields.put("enabled_actions", new ArrayList<>(Arrays.asList(enabledActions))); + return this; + } + + /** + * Set the actions the agent can take. Replaces the whole set. Pass an empty list to disable all actions. + * Pass null to leave the actions unchanged. + * + * @param enabledActions The enabled actions. + * @return The builder instance. + */ + public Builder enabledActions(List enabledActions) { + if (enabledActions == null) { + fields.remove("enabled_actions"); + } else { + fields.put("enabled_actions", new ArrayList<>(enabledActions)); + } + return this; + } + + /** + * Build a new UpdateInboxAgentOptions object. + * + * @return A new UpdateInboxAgentOptions object. + */ + public UpdateInboxAgentOptions build() { + return new UpdateInboxAgentOptions(this); + } + } +} diff --git a/src/main/java/com/resend/services/inboxes/model/UpdateInboxAgentResponseSuccess.java b/src/main/java/com/resend/services/inboxes/model/UpdateInboxAgentResponseSuccess.java new file mode 100644 index 0000000..0f689ba --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/model/UpdateInboxAgentResponseSuccess.java @@ -0,0 +1,51 @@ +package com.resend.services.inboxes.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Represents a successful response for updating the agent settings of an inbox. + */ +public class UpdateInboxAgentResponseSuccess { + + @JsonProperty("object") + private String object; + + @JsonProperty("id") + private String id; + + /** + * Default constructor + */ + public UpdateInboxAgentResponseSuccess() { + + } + + /** + * Constructs a UpdateInboxAgentResponseSuccess. + * + * @param object The object type. Always {@code inbox_agent}. + * @param id The ID of the inbox agent. + */ + public UpdateInboxAgentResponseSuccess(String object, String id) { + this.object = object; + this.id = id; + } + + /** + * Get the object type. Always {@code inbox_agent}. + * + * @return The object type. Always {@code inbox_agent}. + */ + public String getObject() { + return object; + } + + /** + * Get the ID of the inbox agent. + * + * @return The ID of the inbox agent. + */ + public String getId() { + return id; + } +} diff --git a/src/main/java/com/resend/services/inboxes/model/UpdateInboxDraftOptions.java b/src/main/java/com/resend/services/inboxes/model/UpdateInboxDraftOptions.java new file mode 100644 index 0000000..a11bbac --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/model/UpdateInboxDraftOptions.java @@ -0,0 +1,222 @@ +package com.resend.services.inboxes.model; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonInclude; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Represents a request to update a draft. + * Send at least one of {@code to}, {@code cc}, {@code bcc}, {@code subject}, {@code text} or {@code html}. + * Omitted fields are left unchanged. Setting {@code text} or {@code html} replaces the whole body, + * so the body part you omit becomes {@code null}. The merged draft must contain at least one non-empty field, + * and its combined recipients cannot exceed 50. + */ +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.NONE, getterVisibility = JsonAutoDetect.Visibility.NONE, isGetterVisibility = JsonAutoDetect.Visibility.NONE) +public class UpdateInboxDraftOptions { + + private final Map fields; + + private UpdateInboxDraftOptions(Builder builder) { + this.fields = Collections.unmodifiableMap(new LinkedHashMap<>(builder.fields)); + } + + @JsonAnyGetter + @JsonInclude(content = JsonInclude.Include.ALWAYS) + private Map getFields() { + return fields; + } + + /** + * Get the recipients. + * + * @return The recipients, or null when they are not set or are cleared. + */ + @SuppressWarnings("unchecked") + public List getTo() { + return (List) fields.get("to"); + } + + /** + * Get the CC recipients. + * + * @return The CC recipients, or null when they are not set or are cleared. + */ + @SuppressWarnings("unchecked") + public List getCc() { + return (List) fields.get("cc"); + } + + /** + * Get the BCC recipients. + * + * @return The BCC recipients, or null when they are not set or are cleared. + */ + @SuppressWarnings("unchecked") + public List getBcc() { + return (List) fields.get("bcc"); + } + + /** + * Get the subject. + * + * @return The subject, or null when it is not set or is cleared. + */ + public String getSubject() { + return (String) fields.get("subject"); + } + + /** + * Get the HTML body. + * + * @return The HTML body, or null when it is not set or is cleared. + */ + public String getHtml() { + return (String) fields.get("html"); + } + + /** + * Get the plain text body. + * + * @return The plain text body, or null when it is not set or is cleared. + */ + public String getText() { + return (String) fields.get("text"); + } + + /** + * Create a new builder instance for constructing UpdateInboxDraftOptions objects. + * + * @return A new builder instance. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Builder class for constructing UpdateInboxDraftOptions objects. + */ + public static class Builder { + private final Map fields = new LinkedHashMap<>(); + + /** + * Creates a new Builder instance. + */ + public Builder() { + } + + /** + * Set the recipients. Each value can include a friendly name. + * + * @param to The recipients. + * @return The builder instance. + */ + public Builder to(String... to) { + fields.put("to", new ArrayList<>(Arrays.asList(to))); + return this; + } + + /** + * Set the recipients. Each value can include a friendly name. Pass null to clear. + * + * @param to The recipients, or null to clear them. + * @return The builder instance. + */ + public Builder to(List to) { + fields.put("to", to == null ? null : new ArrayList<>(to)); + return this; + } + + /** + * Set the CC recipients. + * + * @param cc The CC recipients. + * @return The builder instance. + */ + public Builder cc(String... cc) { + fields.put("cc", new ArrayList<>(Arrays.asList(cc))); + return this; + } + + /** + * Set the CC recipients. Pass null or an empty list to clear. + * + * @param cc The CC recipients, or null to clear them. + * @return The builder instance. + */ + public Builder cc(List cc) { + fields.put("cc", cc == null ? null : new ArrayList<>(cc)); + return this; + } + + /** + * Set the BCC recipients. + * + * @param bcc The BCC recipients. + * @return The builder instance. + */ + public Builder bcc(String... bcc) { + fields.put("bcc", new ArrayList<>(Arrays.asList(bcc))); + return this; + } + + /** + * Set the BCC recipients. Pass null or an empty list to clear. + * + * @param bcc The BCC recipients, or null to clear them. + * @return The builder instance. + */ + public Builder bcc(List bcc) { + fields.put("bcc", bcc == null ? null : new ArrayList<>(bcc)); + return this; + } + + /** + * Set the subject. Max 2000 characters. Must not contain a line break. Pass null to clear. + * + * @param subject The subject, or null to clear it. + * @return The builder instance. + */ + public Builder subject(String subject) { + fields.put("subject", subject); + return this; + } + + /** + * Set the HTML body. Pass null to clear. + * + * @param html The HTML body, or null to clear it. + * @return The builder instance. + */ + public Builder html(String html) { + fields.put("html", html); + return this; + } + + /** + * Set the plain text body. Pass null to clear. + * + * @param text The plain text body, or null to clear it. + * @return The builder instance. + */ + public Builder text(String text) { + fields.put("text", text); + return this; + } + + /** + * Build a new UpdateInboxDraftOptions object. + * + * @return A new UpdateInboxDraftOptions object. + */ + public UpdateInboxDraftOptions build() { + return new UpdateInboxDraftOptions(this); + } + } +} diff --git a/src/main/java/com/resend/services/inboxes/model/UpdateInboxDraftResponseSuccess.java b/src/main/java/com/resend/services/inboxes/model/UpdateInboxDraftResponseSuccess.java new file mode 100644 index 0000000..9963e19 --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/model/UpdateInboxDraftResponseSuccess.java @@ -0,0 +1,39 @@ +package com.resend.services.inboxes.model; + +import java.util.List; + +/** + * Represents a successful response for updating a draft. + * Extends the InboxDraft class. + */ +public class UpdateInboxDraftResponseSuccess extends InboxDraft { + + /** + * Default constructor + */ + public UpdateInboxDraftResponseSuccess() { + + } + + /** + * Constructs a UpdateInboxDraftResponseSuccess. + * + * @param object The object type. Always {@code inbox_draft}. + * @param id The ID of the draft. + * @param type The draft type. {@code reply} when the draft replies to an email in an existing thread. {@code standalone} when the draft starts a new thread. + * @param to The recipients. {@code null} or empty when no recipients are set. + * @param cc The CC recipients. + * @param bcc The BCC recipients. + * @param subject The subject. + * @param html The HTML body. + * @param text The plain text body. + * @param threadId The Thread ID when the draft is a reply. {@code null} otherwise. + * @param replyToEmailId The Email ID the draft replies to. {@code null} otherwise. + * @param emailId The ID of the outbound email queued when the draft was sent. {@code null} until the draft is sent. + * @param createdAt The timestamp when the draft was created. + * @param updatedAt The timestamp when the draft was last saved. + */ + public UpdateInboxDraftResponseSuccess(String object, String id, String type, List to, List cc, List bcc, String subject, String html, String text, String threadId, String replyToEmailId, String emailId, String createdAt, String updatedAt) { + super(object, id, type, to, cc, bcc, subject, html, text, threadId, replyToEmailId, emailId, createdAt, updatedAt); + } +} diff --git a/src/main/java/com/resend/services/inboxes/model/UpdateInboxLabelOptions.java b/src/main/java/com/resend/services/inboxes/model/UpdateInboxLabelOptions.java new file mode 100644 index 0000000..60dc105 --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/model/UpdateInboxLabelOptions.java @@ -0,0 +1,92 @@ +package com.resend.services.inboxes.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Represents a request to update an inbox label. Omitted fields keep their current value. + */ +public class UpdateInboxLabelOptions { + + @JsonProperty("name") + private final String name; + + @JsonProperty("color") + private final String color; + + private UpdateInboxLabelOptions(Builder builder) { + this.name = builder.name; + this.color = builder.color; + } + + /** + * Get the new name for the label. + * + * @return The new name for the label. + */ + public String getName() { + return name; + } + + /** + * Get the new color for the label, as a {@code #RRGGBB} hex color. + * + * @return The new color for the label, as a {@code #RRGGBB} hex color. + */ + public String getColor() { + return color; + } + + /** + * Create a new builder instance for constructing UpdateInboxLabelOptions objects. + * + * @return A new builder instance. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Builder class for constructing UpdateInboxLabelOptions objects. + */ + public static class Builder { + private String name; + private String color; + + /** + * Creates a new Builder instance. + */ + public Builder() { + } + + /** + * Set the new name for the label. Max 64 characters. Leading and trailing whitespace is trimmed. Must not match another label on the inbox, compared without case. + * + * @param name The new name for the label. + * @return The builder instance. + */ + public Builder name(String name) { + this.name = name; + return this; + } + + /** + * Set the new color for the label, as a {@code #RRGGBB} hex color. It is stored in uppercase. + * + * @param color The new color for the label, as a {@code #RRGGBB} hex color. + * @return The builder instance. + */ + public Builder color(String color) { + this.color = color; + return this; + } + + /** + * Build a new UpdateInboxLabelOptions object. + * + * @return A new UpdateInboxLabelOptions object. + */ + public UpdateInboxLabelOptions build() { + return new UpdateInboxLabelOptions(this); + } + } +} diff --git a/src/main/java/com/resend/services/inboxes/model/UpdateInboxLabelResponseSuccess.java b/src/main/java/com/resend/services/inboxes/model/UpdateInboxLabelResponseSuccess.java new file mode 100644 index 0000000..2f76828 --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/model/UpdateInboxLabelResponseSuccess.java @@ -0,0 +1,51 @@ +package com.resend.services.inboxes.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Represents a successful response for updating an inbox label. + */ +public class UpdateInboxLabelResponseSuccess { + + @JsonProperty("object") + private String object; + + @JsonProperty("id") + private String id; + + /** + * Default constructor + */ + public UpdateInboxLabelResponseSuccess() { + + } + + /** + * Constructs a UpdateInboxLabelResponseSuccess. + * + * @param object The object type. Always {@code inbox_label}. + * @param id The ID of the updated label. + */ + public UpdateInboxLabelResponseSuccess(String object, String id) { + this.object = object; + this.id = id; + } + + /** + * Get the object type. Always {@code inbox_label}. + * + * @return The object type. Always {@code inbox_label}. + */ + public String getObject() { + return object; + } + + /** + * Get the ID of the updated label. + * + * @return The ID of the updated label. + */ + public String getId() { + return id; + } +} diff --git a/src/main/java/com/resend/services/inboxes/model/UpdateInboxOptions.java b/src/main/java/com/resend/services/inboxes/model/UpdateInboxOptions.java new file mode 100644 index 0000000..ad61198 --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/model/UpdateInboxOptions.java @@ -0,0 +1,92 @@ +package com.resend.services.inboxes.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Represents a request to update an inbox. Provide at least one of {@code name} or {@code from_name}. + */ +public class UpdateInboxOptions { + + @JsonProperty("name") + private final String name; + + @JsonProperty("from_name") + private final String fromName; + + private UpdateInboxOptions(Builder builder) { + this.name = builder.name; + this.fromName = builder.fromName; + } + + /** + * Get the new internal name for the inbox. + * + * @return The new internal name for the inbox. + */ + public String getName() { + return name; + } + + /** + * Get the name recipients see when mail is sent from this inbox. + * + * @return The name recipients see when mail is sent from this inbox. + */ + public String getFromName() { + return fromName; + } + + /** + * Create a new builder instance for constructing UpdateInboxOptions objects. + * + * @return A new builder instance. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Builder class for constructing UpdateInboxOptions objects. + */ + public static class Builder { + private String name; + private String fromName; + + /** + * Creates a new Builder instance. + */ + public Builder() { + } + + /** + * Set the new internal name for the inbox. Recipients do not see it. Between 1 and 64 characters after trimming. + * + * @param name The new internal name for the inbox. + * @return The builder instance. + */ + public Builder name(String name) { + this.name = name; + return this; + } + + /** + * Set the name recipients see when mail is sent from this inbox. A plain name, not a {@code Name } address. Max 78 characters. An empty string removes it. + * + * @param fromName The name recipients see when mail is sent from this inbox. + * @return The builder instance. + */ + public Builder fromName(String fromName) { + this.fromName = fromName; + return this; + } + + /** + * Build a new UpdateInboxOptions object. + * + * @return A new UpdateInboxOptions object. + */ + public UpdateInboxOptions build() { + return new UpdateInboxOptions(this); + } + } +} diff --git a/src/main/java/com/resend/services/inboxes/model/UpdateInboxResponseSuccess.java b/src/main/java/com/resend/services/inboxes/model/UpdateInboxResponseSuccess.java new file mode 100644 index 0000000..94fb9f1 --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/model/UpdateInboxResponseSuccess.java @@ -0,0 +1,51 @@ +package com.resend.services.inboxes.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Represents a successful response for updating an inbox. + */ +public class UpdateInboxResponseSuccess { + + @JsonProperty("object") + private String object; + + @JsonProperty("id") + private String id; + + /** + * Default constructor + */ + public UpdateInboxResponseSuccess() { + + } + + /** + * Constructs a UpdateInboxResponseSuccess. + * + * @param object The object type. Always {@code inbox}. + * @param id The ID of the inbox. + */ + public UpdateInboxResponseSuccess(String object, String id) { + this.object = object; + this.id = id; + } + + /** + * Get the object type. Always {@code inbox}. + * + * @return The object type. Always {@code inbox}. + */ + public String getObject() { + return object; + } + + /** + * Get the ID of the inbox. + * + * @return The ID of the inbox. + */ + public String getId() { + return id; + } +} diff --git a/src/main/java/com/resend/services/inboxes/model/UpdateInboxThreadOptions.java b/src/main/java/com/resend/services/inboxes/model/UpdateInboxThreadOptions.java new file mode 100644 index 0000000..1b34f20 --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/model/UpdateInboxThreadOptions.java @@ -0,0 +1,198 @@ +package com.resend.services.inboxes.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Represents a request to update a thread. Provide at least one of {@code read}, {@code folder}, {@code label_id}, {@code add_label_ids}, or {@code remove_label_ids}. A label ID cannot be both added and removed in the same request. + */ +public class UpdateInboxThreadOptions { + + @JsonProperty("read") + private final Boolean read; + + @JsonProperty("folder") + private final MoveInboxThreadFolder folder; + + @Deprecated + @JsonProperty("label_id") + private final String labelId; + + @JsonProperty("add_label_ids") + private final List addLabelIds; + + @JsonProperty("remove_label_ids") + private final List removeLabelIds; + + private UpdateInboxThreadOptions(Builder builder) { + this.read = builder.read; + this.folder = builder.folder; + this.labelId = builder.labelId; + this.addLabelIds = builder.addLabelIds; + this.removeLabelIds = builder.removeLabelIds; + } + + /** + * Get whether every email in the thread is marked read or unread. + * + * @return Whether every email in the thread is marked read or unread. + */ + public Boolean getRead() { + return read; + } + + /** + * Get the folder to move the thread to. + * + * @return The folder to move the thread to. + */ + public MoveInboxThreadFolder getFolder() { + return folder; + } + + /** + * Get the label to apply to the thread, the same as one entry in {@code add_label_ids}. + * + * @return The label to apply to the thread, the same as one entry in {@code add_label_ids}. + * @deprecated Use {@link Builder#addLabelIds(String...)} instead. + */ + @Deprecated + public String getLabelId() { + return labelId; + } + + /** + * Get the label IDs to apply to the thread. + * + * @return The label IDs to apply to the thread. + */ + public List getAddLabelIds() { + return addLabelIds; + } + + /** + * Get the label IDs to remove from the thread. + * + * @return The label IDs to remove from the thread. + */ + public List getRemoveLabelIds() { + return removeLabelIds; + } + + /** + * Create a new builder instance for constructing UpdateInboxThreadOptions objects. + * + * @return A new builder instance. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Builder class for constructing UpdateInboxThreadOptions objects. + */ + public static class Builder { + private Boolean read; + private MoveInboxThreadFolder folder; + private String labelId; + private List addLabelIds; + private List removeLabelIds; + + /** + * Creates a new Builder instance. + */ + public Builder() { + } + + /** + * Set whether every email in the thread is marked read or unread. + * + * @param read Whether every email in the thread is marked read or unread. + * @return The builder instance. + */ + public Builder read(Boolean read) { + this.read = read; + return this; + } + + /** + * Set the folder to move the thread to. Moving a thread out of {@code trash} restores it and clears its deletion deadline. + * + * @param folder The folder to move the thread to. + * @return The builder instance. + */ + public Builder folder(MoveInboxThreadFolder folder) { + this.folder = folder; + return this; + } + + /** + * Set the label to apply to the thread, the same as one entry in {@code add_label_ids}. A label ID that does not exist in the inbox returns a 404. + * + * @param labelId The label to apply to the thread, the same as one entry in {@code add_label_ids}. + * @return The builder instance. + * @deprecated Use {@link Builder#addLabelIds(String...)} instead. + */ + @Deprecated + public Builder labelId(String labelId) { + this.labelId = labelId; + return this; + } + + /** + * Set the label IDs to apply to the thread. Between 1 and 10 IDs. A label ID that does not exist in the inbox returns a 404. + * + * @param addLabelIds The label IDs to apply to the thread. + * @return The builder instance. + */ + public Builder addLabelIds(String... addLabelIds) { + this.addLabelIds = new ArrayList<>(Arrays.asList(addLabelIds)); + return this; + } + + /** + * Set the label IDs to apply to the thread. Between 1 and 10 IDs. A label ID that does not exist in the inbox returns a 404. + * + * @param addLabelIds The label IDs to apply to the thread. + * @return The builder instance. + */ + public Builder addLabelIds(List addLabelIds) { + this.addLabelIds = addLabelIds == null ? null : new ArrayList<>(addLabelIds); + return this; + } + + /** + * Set the label IDs to remove from the thread. Between 1 and 10 IDs. A label ID that does not exist in the inbox returns a 404. + * + * @param removeLabelIds The label IDs to remove from the thread. + * @return The builder instance. + */ + public Builder removeLabelIds(String... removeLabelIds) { + this.removeLabelIds = new ArrayList<>(Arrays.asList(removeLabelIds)); + return this; + } + + /** + * Set the label IDs to remove from the thread. Between 1 and 10 IDs. A label ID that does not exist in the inbox returns a 404. + * + * @param removeLabelIds The label IDs to remove from the thread. + * @return The builder instance. + */ + public Builder removeLabelIds(List removeLabelIds) { + this.removeLabelIds = removeLabelIds == null ? null : new ArrayList<>(removeLabelIds); + return this; + } + + /** + * Build a new UpdateInboxThreadOptions object. + * + * @return A new UpdateInboxThreadOptions object. + */ + public UpdateInboxThreadOptions build() { + return new UpdateInboxThreadOptions(this); + } + } +} diff --git a/src/main/java/com/resend/services/inboxes/model/UpdateInboxThreadResponseSuccess.java b/src/main/java/com/resend/services/inboxes/model/UpdateInboxThreadResponseSuccess.java new file mode 100644 index 0000000..ca7141b --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/model/UpdateInboxThreadResponseSuccess.java @@ -0,0 +1,31 @@ +package com.resend.services.inboxes.model; + +import java.util.List; + +/** + * Represents a successful response for updating a thread. + * Extends the InboxThreadSummary class. + */ +public class UpdateInboxThreadResponseSuccess extends InboxThreadSummary { + + /** + * Default constructor + */ + public UpdateInboxThreadResponseSuccess() { + + } + + /** + * Constructs a UpdateInboxThreadResponseSuccess. + * + * @param object The object type. Always {@code inbox_thread}. + * @param id The ID of the thread. + * @param subject The subject of the thread. + * @param folder The folder the thread is in. + * @param labels The labels attached to the thread. + * @param read True only when every email in the thread is read. + */ + public UpdateInboxThreadResponseSuccess(String object, String id, String subject, String folder, List labels, Boolean read) { + super(object, id, subject, folder, labels, read); + } +} From 10dfd927fbf88cabdce7c1590e838e9c91b8bbc3 Mon Sep 17 00:00:00 2001 From: Felipe Freitag Date: Mon, 12 Oct 2026 14:05:31 -0300 Subject: [PATCH 02/30] feat(inboxes): add the inboxes service with agent, labels, threads, thread emails and drafts --- src/main/java/com/resend/Resend.java | 10 ++ .../resend/services/inboxes/InboxAgent.java | 61 +++++++ .../resend/services/inboxes/InboxDrafts.java | 137 ++++++++++++++++ .../resend/services/inboxes/InboxLabels.java | 90 +++++++++++ .../services/inboxes/InboxThreadEmails.java | 121 ++++++++++++++ .../resend/services/inboxes/InboxThreads.java | 145 +++++++++++++++++ .../com/resend/services/inboxes/Inboxes.java | 151 ++++++++++++++++++ 7 files changed, 715 insertions(+) create mode 100644 src/main/java/com/resend/services/inboxes/InboxAgent.java create mode 100644 src/main/java/com/resend/services/inboxes/InboxDrafts.java create mode 100644 src/main/java/com/resend/services/inboxes/InboxLabels.java create mode 100644 src/main/java/com/resend/services/inboxes/InboxThreadEmails.java create mode 100644 src/main/java/com/resend/services/inboxes/InboxThreads.java create mode 100644 src/main/java/com/resend/services/inboxes/Inboxes.java diff --git a/src/main/java/com/resend/Resend.java b/src/main/java/com/resend/Resend.java index 1e802d4..2a693a4 100644 --- a/src/main/java/com/resend/Resend.java +++ b/src/main/java/com/resend/Resend.java @@ -11,6 +11,7 @@ import com.resend.services.contactproperties.ContactProperties; import com.resend.services.domains.Domains; import com.resend.services.emails.Emails; +import com.resend.services.inboxes.Inboxes; import com.resend.services.segments.Segments; import com.resend.services.suppressions.Suppressions; import com.resend.services.webhooks.Webhooks; @@ -248,6 +249,15 @@ public Usage usage() { return new Usage(apiKey, httpClient); } + /** + * Returns an Inboxes object that can be used to interact with the Inboxes service. + * + * @return An Inboxes object. + */ + public Inboxes inboxes() { + return new Inboxes(apiKey, httpClient); + } + /** * Builds a {@link Resend} instance with a custom configuration. * diff --git a/src/main/java/com/resend/services/inboxes/InboxAgent.java b/src/main/java/com/resend/services/inboxes/InboxAgent.java new file mode 100644 index 0000000..00a476e --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/InboxAgent.java @@ -0,0 +1,61 @@ +package com.resend.services.inboxes; + +import com.resend.core.exception.ResendException; +import com.resend.core.net.HttpMethod; +import com.resend.core.net.IHttpClient; +import com.resend.core.service.BaseService; +import com.resend.services.inboxes.model.GetInboxAgentResponseSuccess; +import com.resend.services.inboxes.model.UpdateInboxAgentOptions; +import com.resend.services.inboxes.model.UpdateInboxAgentResponseSuccess; +import okhttp3.MediaType; + +/** + * Represents the Resend Inbox Agent module. + */ +public class InboxAgent extends BaseService { + + /** + * Constructs an instance of the {@code InboxAgent} class. + * + * @param apiKey The apiKey used for authentication. + */ + public InboxAgent(final String apiKey) { + super(apiKey); + } + + /** + * Constructs an instance of the {@code InboxAgent} class that sends requests through the given HTTP client. + * + * @param apiKey The apiKey used for authentication. + * @param httpClient The HTTP client to use. + */ + public InboxAgent(final String apiKey, final IHttpClient httpClient) { + super(apiKey, httpClient); + } + + /** + * Retrieves the agent settings of an inbox. An inbox without a configured agent returns empty settings: + * {@code instructions} and {@code tone} are null and {@code enabled_actions} is an empty list. + * + * @param inboxId The inbox ID. An email address is not accepted. + * @return The agent settings of the inbox. + * @throws ResendException If an error occurs while retrieving the agent settings. + */ + public GetInboxAgentResponseSuccess get(String inboxId) throws ResendException { + return execute("/inboxes/" + inboxId + "/agent", HttpMethod.GET, null, MediaType.get("application/json"), GetInboxAgentResponseSuccess.class); + } + + /** + * Updates the agent settings of an inbox. The agent is created when the inbox has none. + * Omitted fields keep their current value. + * + * @param inboxId The inbox ID. An email address is not accepted. + * @param updateInboxAgentOptions The settings to update. + * @return The ID of the inbox agent. + * @throws ResendException If an error occurs while updating the agent settings. + */ + public UpdateInboxAgentResponseSuccess update(String inboxId, UpdateInboxAgentOptions updateInboxAgentOptions) throws ResendException { + String payload = super.resendMapper.writeValue(updateInboxAgentOptions); + return execute("/inboxes/" + inboxId + "/agent", HttpMethod.PATCH, payload, MediaType.get("application/json"), UpdateInboxAgentResponseSuccess.class); + } +} diff --git a/src/main/java/com/resend/services/inboxes/InboxDrafts.java b/src/main/java/com/resend/services/inboxes/InboxDrafts.java new file mode 100644 index 0000000..4cd89b5 --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/InboxDrafts.java @@ -0,0 +1,137 @@ +package com.resend.services.inboxes; + +import com.resend.core.exception.ResendException; +import com.resend.core.helper.URLHelper; +import com.resend.core.net.HttpMethod; +import com.resend.core.net.IHttpClient; +import com.resend.core.net.ListParams; +import com.resend.core.service.BaseService; +import com.resend.services.inboxes.model.CreateInboxDraftOptions; +import com.resend.services.inboxes.model.CreateInboxDraftResponseSuccess; +import com.resend.services.inboxes.model.GetInboxDraftResponseSuccess; +import com.resend.services.inboxes.model.ListInboxDraftsResponseSuccess; +import com.resend.services.inboxes.model.RemoveInboxDraftResponseSuccess; +import com.resend.services.inboxes.model.SendInboxDraftResponseSuccess; +import com.resend.services.inboxes.model.UpdateInboxDraftOptions; +import com.resend.services.inboxes.model.UpdateInboxDraftResponseSuccess; +import okhttp3.MediaType; + +/** + * Represents the Resend Inbox Drafts module. + */ +public class InboxDrafts extends BaseService { + + /** + * Constructs an instance of the {@code InboxDrafts} class. + * + * @param apiKey The apiKey used for authentication. + */ + public InboxDrafts(final String apiKey) { + super(apiKey); + } + + /** + * Constructs an instance of the {@code InboxDrafts} class that sends requests through the given HTTP client. + * + * @param apiKey The apiKey used for authentication. + * @param httpClient The HTTP client to use. + */ + public InboxDrafts(final String apiKey, final IHttpClient httpClient) { + super(apiKey, httpClient); + } + + /** + * Retrieves the unsent drafts on an inbox, most recently updated first. + * + * @param inboxId The inbox ID. An email address is not accepted. + * @return A ListInboxDraftsResponseSuccess containing the drafts. + * @throws ResendException If an error occurs while retrieving the drafts. + */ + public ListInboxDraftsResponseSuccess list(String inboxId) throws ResendException { + return list(inboxId, null); + } + + /** + * Retrieves a page of the unsent drafts on an inbox, most recently updated first. + * The {@code after} and {@code before} cursors take a draft ID from the current page. + * A cursor that is not an unsent draft on this inbox returns a 404. + * + * @param inboxId The inbox ID. An email address is not accepted. + * @param params The params used to paginate the list. + * @return A ListInboxDraftsResponseSuccess containing the drafts. + * @throws ResendException If an error occurs while retrieving the drafts. + */ + public ListInboxDraftsResponseSuccess list(String inboxId, ListParams params) throws ResendException { + return execute("/inboxes/" + inboxId + "/drafts" + URLHelper.parse(params), HttpMethod.GET, null, MediaType.get("application/json"), ListInboxDraftsResponseSuccess.class); + } + + /** + * Creates an unsent draft on an inbox. Omit {@code thread_id} and {@code reply_to_email_id} for a draft that + * starts a new thread. Pass both together for a reply draft in an existing thread. 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. + * + * @param inboxId The inbox ID. An email address is not accepted. + * @param createInboxDraftOptions The draft content. + * @return The ID of the draft. + * @throws ResendException If an error occurs while creating the draft. + */ + public CreateInboxDraftResponseSuccess create(String inboxId, CreateInboxDraftOptions createInboxDraftOptions) throws ResendException { + String payload = super.resendMapper.writeValue(createInboxDraftOptions); + return execute("/inboxes/" + inboxId + "/drafts", HttpMethod.POST, payload, MediaType.get("application/json"), CreateInboxDraftResponseSuccess.class); + } + + /** + * Retrieves a single draft. + * + * @param inboxId The inbox ID. An email address is not accepted. + * @param draftId The draft ID. + * @return The draft. + * @throws ResendException If an error occurs while retrieving the draft. + */ + public GetInboxDraftResponseSuccess get(String inboxId, String draftId) throws ResendException { + return execute("/inboxes/" + inboxId + "/drafts/" + draftId, HttpMethod.GET, null, MediaType.get("application/json"), GetInboxDraftResponseSuccess.class); + } + + /** + * Updates the content of an unsent draft. Omitted fields are left unchanged. A request that sets + * {@code text} or {@code html} replaces the whole body. The body part you omit from that request becomes + * {@code null}. A draft that was already sent, or that changed during the request, returns a 409. + * + * @param inboxId The inbox ID. An email address is not accepted. + * @param draftId The draft ID. + * @param updateInboxDraftOptions The fields to update. + * @return The updated draft. + * @throws ResendException If an error occurs while updating the draft. + */ + public UpdateInboxDraftResponseSuccess update(String inboxId, String draftId, UpdateInboxDraftOptions updateInboxDraftOptions) throws ResendException { + String payload = super.resendMapper.writeValue(updateInboxDraftOptions); + return execute("/inboxes/" + inboxId + "/drafts/" + draftId, HttpMethod.PATCH, payload, MediaType.get("application/json"), UpdateInboxDraftResponseSuccess.class); + } + + /** + * Discards an unsent draft. A draft that was already sent, or that changed during the request, returns a 409. + * + * @param inboxId The inbox ID. An email address is not accepted. + * @param draftId The draft ID. + * @return The RemoveInboxDraftResponseSuccess with the details of the removed draft. + * @throws ResendException If an error occurs while removing the draft. + */ + public RemoveInboxDraftResponseSuccess remove(String inboxId, String draftId) throws ResendException { + return execute("/inboxes/" + inboxId + "/drafts/" + draftId, HttpMethod.DELETE, null, MediaType.get("application/json"), RemoveInboxDraftResponseSuccess.class); + } + + /** + * Sends a draft from the inbox address. A reply draft is sent in its thread. A draft without a thread starts + * a new thread. The draft must have at least one {@code to} recipient, or the request returns a 422. + * A draft that was already sent returns a 409. + * + * @param inboxId The inbox ID. An email address is not accepted. + * @param draftId The draft ID. + * @return The ID of the sent email. + * @throws ResendException If an error occurs while sending the draft. + */ + public SendInboxDraftResponseSuccess send(String inboxId, String draftId) throws ResendException { + return execute("/inboxes/" + inboxId + "/drafts/" + draftId + "/send", HttpMethod.POST, "", MediaType.get("application/json"), SendInboxDraftResponseSuccess.class); + } +} diff --git a/src/main/java/com/resend/services/inboxes/InboxLabels.java b/src/main/java/com/resend/services/inboxes/InboxLabels.java new file mode 100644 index 0000000..e4ffcdc --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/InboxLabels.java @@ -0,0 +1,90 @@ +package com.resend.services.inboxes; + +import com.resend.core.exception.ResendException; +import com.resend.core.net.HttpMethod; +import com.resend.core.net.IHttpClient; +import com.resend.core.service.BaseService; +import com.resend.services.inboxes.model.CreateInboxLabelOptions; +import com.resend.services.inboxes.model.CreateInboxLabelResponseSuccess; +import com.resend.services.inboxes.model.ListInboxLabelsResponseSuccess; +import com.resend.services.inboxes.model.RemoveInboxLabelResponseSuccess; +import com.resend.services.inboxes.model.UpdateInboxLabelOptions; +import com.resend.services.inboxes.model.UpdateInboxLabelResponseSuccess; +import okhttp3.MediaType; + +/** + * Represents the Resend Inbox Labels module. + */ +public class InboxLabels extends BaseService { + + /** + * Constructs an instance of the {@code InboxLabels} class. + * + * @param apiKey The apiKey used for authentication. + */ + public InboxLabels(final String apiKey) { + super(apiKey); + } + + /** + * Constructs an instance of the {@code InboxLabels} class that sends requests through the given HTTP client. + * + * @param apiKey The apiKey used for authentication. + * @param httpClient The HTTP client to use. + */ + public InboxLabels(final String apiKey, final IHttpClient httpClient) { + super(apiKey, httpClient); + } + + /** + * Retrieves every label on an inbox, oldest first. The list is not paginated. + * + * @param inboxId The inbox ID. An email address is not accepted. + * @return A ListInboxLabelsResponseSuccess containing the labels. + * @throws ResendException If an error occurs while retrieving the labels. + */ + public ListInboxLabelsResponseSuccess list(String inboxId) throws ResendException { + return execute("/inboxes/" + inboxId + "/labels", HttpMethod.GET, null, MediaType.get("application/json"), ListInboxLabelsResponseSuccess.class); + } + + /** + * Creates a label on an inbox. An inbox can have at most 100 labels. Label names are unique per inbox, + * compared without case. A duplicate name or a full inbox returns a 422. + * + * @param inboxId The inbox ID. An email address is not accepted. + * @param createInboxLabelOptions The label details. + * @return The ID of the created label. + * @throws ResendException If an error occurs while creating the label. + */ + public CreateInboxLabelResponseSuccess create(String inboxId, CreateInboxLabelOptions createInboxLabelOptions) throws ResendException { + String payload = super.resendMapper.writeValue(createInboxLabelOptions); + return execute("/inboxes/" + inboxId + "/labels", HttpMethod.POST, payload, MediaType.get("application/json"), CreateInboxLabelResponseSuccess.class); + } + + /** + * Renames or recolors a label. Omitted fields keep their current value. A new name must not match another + * label on the inbox, compared without case, or the request returns a 422. + * + * @param inboxId The inbox ID. An email address is not accepted. + * @param labelId The label ID. + * @param updateInboxLabelOptions The fields to update. + * @return The ID of the updated label. + * @throws ResendException If an error occurs while updating the label. + */ + public UpdateInboxLabelResponseSuccess update(String inboxId, String labelId, UpdateInboxLabelOptions updateInboxLabelOptions) throws ResendException { + String payload = super.resendMapper.writeValue(updateInboxLabelOptions); + return execute("/inboxes/" + inboxId + "/labels/" + labelId, HttpMethod.PATCH, payload, MediaType.get("application/json"), UpdateInboxLabelResponseSuccess.class); + } + + /** + * Deletes a label and removes it from every thread that carries it. + * + * @param inboxId The inbox ID. An email address is not accepted. + * @param labelId The label ID. + * @return The RemoveInboxLabelResponseSuccess with the details of the removed label. + * @throws ResendException If an error occurs while removing the label. + */ + public RemoveInboxLabelResponseSuccess remove(String inboxId, String labelId) throws ResendException { + return execute("/inboxes/" + inboxId + "/labels/" + labelId, HttpMethod.DELETE, null, MediaType.get("application/json"), RemoveInboxLabelResponseSuccess.class); + } +} diff --git a/src/main/java/com/resend/services/inboxes/InboxThreadEmails.java b/src/main/java/com/resend/services/inboxes/InboxThreadEmails.java new file mode 100644 index 0000000..5eab7b0 --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/InboxThreadEmails.java @@ -0,0 +1,121 @@ +package com.resend.services.inboxes; + +import com.resend.core.exception.ResendException; +import com.resend.core.helper.URLHelper; +import com.resend.core.net.HttpMethod; +import com.resend.core.net.IHttpClient; +import com.resend.core.net.ListParams; +import com.resend.core.service.BaseService; +import com.resend.services.inboxes.model.ForwardInboxThreadEmailOptions; +import com.resend.services.inboxes.model.ForwardInboxThreadEmailResponseSuccess; +import com.resend.services.inboxes.model.GetInboxThreadEmailResponseSuccess; +import com.resend.services.inboxes.model.ListInboxThreadEmailsResponseSuccess; +import com.resend.services.inboxes.model.ReplyInboxThreadEmailOptions; +import com.resend.services.inboxes.model.ReplyInboxThreadEmailResponseSuccess; +import okhttp3.MediaType; + +/** + * Represents the Resend Inbox Thread Emails module. + */ +public class InboxThreadEmails extends BaseService { + + /** + * Constructs an instance of the {@code InboxThreadEmails} class. + * + * @param apiKey The apiKey used for authentication. + */ + public InboxThreadEmails(final String apiKey) { + super(apiKey); + } + + /** + * Constructs an instance of the {@code InboxThreadEmails} class that sends requests through the given HTTP client. + * + * @param apiKey The apiKey used for authentication. + * @param httpClient The HTTP client to use. + */ + public InboxThreadEmails(final String apiKey, final IHttpClient httpClient) { + super(apiKey, httpClient); + } + + /** + * Retrieves the messages of a thread, oldest first. + * + * @param inboxId The inbox ID. An email address is not accepted. + * @param threadId The thread ID. + * @return A ListInboxThreadEmailsResponseSuccess containing the messages. + * @throws ResendException If an error occurs while retrieving the messages. + */ + public ListInboxThreadEmailsResponseSuccess list(String inboxId, String threadId) throws ResendException { + return list(inboxId, threadId, null); + } + + /** + * Retrieves a page of the messages of a thread, oldest first. Pass the {@code id} of the last message as + * {@code after} to get the next, newer page. Pass the {@code id} of the first message as {@code before} + * to get the previous, older page. You cannot combine {@code after} and {@code before}. + * + * @param inboxId The inbox ID. An email address is not accepted. + * @param threadId The thread ID. + * @param params The params used to paginate the list. + * @return A ListInboxThreadEmailsResponseSuccess containing the messages. + * @throws ResendException If an error occurs while retrieving the messages. + */ + public ListInboxThreadEmailsResponseSuccess list(String inboxId, String threadId, ListParams params) throws ResendException { + String path = "/inboxes/" + inboxId + "/threads/" + threadId + "/emails" + URLHelper.parse(params); + return execute(path, HttpMethod.GET, null, MediaType.get("application/json"), ListInboxThreadEmailsResponseSuccess.class); + } + + /** + * Retrieves a single message of a thread. + * + * @param inboxId The inbox ID. An email address is not accepted. + * @param threadId The thread ID. + * @param emailId The email ID, as returned in {@code data[].id} by the list thread emails endpoint. + * @return The message. + * @throws ResendException If an error occurs while retrieving the message. + */ + public GetInboxThreadEmailResponseSuccess get(String inboxId, String threadId, String emailId) throws ResendException { + String path = "/inboxes/" + inboxId + "/threads/" + threadId + "/emails/" + emailId; + return execute(path, HttpMethod.GET, null, MediaType.get("application/json"), GetInboxThreadEmailResponseSuccess.class); + } + + /** + * Sends a reply from the inbox address right away, with no stored draft. The {@code to} recipients come from + * the message you reply to. For a message the inbox received, the reply goes to its {@code Reply-To} + * addresses, or to its sender when it has no {@code Reply-To}. For a message the inbox sent, the reply goes + * to that message's {@code to} recipients. The reply threads under the message through the + * {@code In-Reply-To} and {@code References} headers. + * + * @param inboxId The inbox ID. An email address is not accepted. + * @param threadId The thread ID. + * @param emailId The email ID of the message to reply to. + * @param replyInboxThreadEmailOptions The reply content. + * @return The ID of the sent email. + * @throws ResendException If an error occurs while sending the reply. + */ + public ReplyInboxThreadEmailResponseSuccess reply(String inboxId, String threadId, String emailId, ReplyInboxThreadEmailOptions replyInboxThreadEmailOptions) throws ResendException { + String path = "/inboxes/" + inboxId + "/threads/" + threadId + "/emails/" + emailId + "/reply"; + String payload = super.resendMapper.writeValue(replyInboxThreadEmailOptions); + return execute(path, HttpMethod.POST, payload, MediaType.get("application/json"), ReplyInboxThreadEmailResponseSuccess.class); + } + + /** + * Forwards a message from the inbox address right away, with no stored draft. The original message is quoted + * under a forwarded-message banner, below the optional {@code html} or {@code text} note. The attachments of + * the original message are sent with the forward, together with any attachments you add. The request fails + * when they exceed 10 files or 25 MB of {@code content} in total. + * + * @param inboxId The inbox ID. An email address is not accepted. + * @param threadId The thread ID. + * @param emailId The email ID of the message to forward. + * @param forwardInboxThreadEmailOptions The forward recipients and content. + * @return The ID of the sent email. + * @throws ResendException If an error occurs while sending the forward. + */ + public ForwardInboxThreadEmailResponseSuccess forward(String inboxId, String threadId, String emailId, ForwardInboxThreadEmailOptions forwardInboxThreadEmailOptions) throws ResendException { + String path = "/inboxes/" + inboxId + "/threads/" + threadId + "/emails/" + emailId + "/forward"; + String payload = super.resendMapper.writeValue(forwardInboxThreadEmailOptions); + return execute(path, HttpMethod.POST, payload, MediaType.get("application/json"), ForwardInboxThreadEmailResponseSuccess.class); + } +} diff --git a/src/main/java/com/resend/services/inboxes/InboxThreads.java b/src/main/java/com/resend/services/inboxes/InboxThreads.java new file mode 100644 index 0000000..96436eb --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/InboxThreads.java @@ -0,0 +1,145 @@ +package com.resend.services.inboxes; + +import com.resend.core.exception.ResendException; +import com.resend.core.net.HttpMethod; +import com.resend.core.net.IHttpClient; +import com.resend.core.service.BaseService; +import com.resend.services.inboxes.model.GetInboxThreadResponseSuccess; +import com.resend.services.inboxes.model.ListInboxThreadsParams; +import com.resend.services.inboxes.model.ListInboxThreadsResponseSuccess; +import com.resend.services.inboxes.model.RemoveInboxThreadResponseSuccess; +import com.resend.services.inboxes.model.SearchInboxThreadsParams; +import com.resend.services.inboxes.model.SearchInboxThreadsResponseSuccess; +import com.resend.services.inboxes.model.UpdateInboxThreadOptions; +import com.resend.services.inboxes.model.UpdateInboxThreadResponseSuccess; +import okhttp3.MediaType; + +/** + * Represents the Resend Inbox Threads module. + */ +public class InboxThreads extends BaseService { + + /** + * Constructs an instance of the {@code InboxThreads} class. + * + * @param apiKey The apiKey used for authentication. + */ + public InboxThreads(final String apiKey) { + super(apiKey); + } + + /** + * Constructs an instance of the {@code InboxThreads} class that sends requests through the given HTTP client. + * + * @param apiKey The apiKey used for authentication. + * @param httpClient The HTTP client to use. + */ + public InboxThreads(final String apiKey, final IHttpClient httpClient) { + super(apiKey, httpClient); + } + + /** + * Returns an InboxThreadEmails object that can be used to read, reply to, and forward the emails of a thread. + * + * @return An InboxThreadEmails object. + */ + public InboxThreadEmails emails() { + return new InboxThreadEmails(super.apiKey, super.httpClient); + } + + /** + * Retrieves the threads in an inbox, most recently active first. + * To find threads by text, sender, recipient, attachment, or date, use {@link #search(String, SearchInboxThreadsParams)}. + * + * @param inboxId The inbox ID. An email address is not accepted. + * @return A ListInboxThreadsResponseSuccess containing the threads. + * @throws ResendException If an error occurs while retrieving the threads. + */ + public ListInboxThreadsResponseSuccess list(String inboxId) throws ResendException { + return list(inboxId, null); + } + + /** + * Retrieves the threads in an inbox, most recently active first, with filtering and pagination. + * The {@code after} and {@code before} cursors take a thread ID, and only one of them can be passed. + * To find threads by text, sender, recipient, attachment, or date, use {@link #search(String, SearchInboxThreadsParams)}. + * + * @param inboxId The inbox ID. An email address is not accepted. + * @param params The params used to filter and paginate the list. + * @return A ListInboxThreadsResponseSuccess containing the threads. + * @throws ResendException If an error occurs while retrieving the threads. + */ + public ListInboxThreadsResponseSuccess list(String inboxId, ListInboxThreadsParams params) throws ResendException { + String query = params == null ? "" : params.toQueryString(); + return execute("/inboxes/" + inboxId + "/threads" + query, HttpMethod.GET, null, MediaType.get("application/json"), ListInboxThreadsResponseSuccess.class); + } + + /** + * Searches the threads of an inbox, newest activity first. + * + * @param inboxId The inbox ID. An email address is not accepted. + * @return A SearchInboxThreadsResponseSuccess containing the matching threads. + * @throws ResendException If an error occurs while searching the threads. + */ + public SearchInboxThreadsResponseSuccess search(String inboxId) throws ResendException { + return search(inboxId, null); + } + + /** + * Finds threads by the text of their emails, who sent or received them, whether they have attachments, + * and their dates. Results are ordered by latest activity, newest first, and paginate like the list threads + * endpoint. Search can be up to about a minute behind new emails. When more than 10,000 threads match, + * search covers the 10,000 with the newest matching email. + * + * @param inboxId The inbox ID. An email address is not accepted. + * @param params The search filters and pagination. + * @return A SearchInboxThreadsResponseSuccess containing the matching threads. + * @throws ResendException If an error occurs while searching the threads. + */ + public SearchInboxThreadsResponseSuccess search(String inboxId, SearchInboxThreadsParams params) throws ResendException { + String query = params == null ? "" : params.toQueryString(); + return execute("/inboxes/" + inboxId + "/threads/search" + query, HttpMethod.GET, null, MediaType.get("application/json"), SearchInboxThreadsResponseSuccess.class); + } + + /** + * Retrieves the summary of a thread. The emails in the thread are listed with {@link InboxThreadEmails#list(String, String)}. + * + * @param inboxId The inbox ID. An email address is not accepted. + * @param threadId The thread ID. + * @return The thread summary. + * @throws ResendException If an error occurs while retrieving the thread. + */ + public GetInboxThreadResponseSuccess get(String inboxId, String threadId) throws ResendException { + return execute("/inboxes/" + inboxId + "/threads/" + threadId, HttpMethod.GET, null, MediaType.get("application/json"), GetInboxThreadResponseSuccess.class); + } + + /** + * Marks a thread read or unread, moves it to a folder, or adds and removes labels. + * A label ID cannot be both added and removed in the same request. + * Moving a thread out of {@code trash} restores it and clears its deletion deadline. + * + * @param inboxId The inbox ID. An email address is not accepted. + * @param threadId The thread ID. + * @param updateInboxThreadOptions The changes to apply. + * @return The updated thread summary. + * @throws ResendException If an error occurs while updating the thread. + */ + public UpdateInboxThreadResponseSuccess update(String inboxId, String threadId, UpdateInboxThreadOptions updateInboxThreadOptions) throws ResendException { + String payload = super.resendMapper.writeValue(updateInboxThreadOptions); + return execute("/inboxes/" + inboxId + "/threads/" + threadId, HttpMethod.PATCH, payload, MediaType.get("application/json"), UpdateInboxThreadResponseSuccess.class); + } + + /** + * Moves a thread to {@code trash} and schedules it for deletion in 30 days. This is reversible. + * To restore the thread, update its folder to {@code inbox}, {@code archive}, or {@code spam}, + * which also clears the deletion deadline. + * + * @param inboxId The inbox ID. An email address is not accepted. + * @param threadId The thread ID. + * @return The RemoveInboxThreadResponseSuccess with the details of the removed thread. + * @throws ResendException If an error occurs while removing the thread. + */ + public RemoveInboxThreadResponseSuccess remove(String inboxId, String threadId) throws ResendException { + return execute("/inboxes/" + inboxId + "/threads/" + threadId, HttpMethod.DELETE, null, MediaType.get("application/json"), RemoveInboxThreadResponseSuccess.class); + } +} diff --git a/src/main/java/com/resend/services/inboxes/Inboxes.java b/src/main/java/com/resend/services/inboxes/Inboxes.java new file mode 100644 index 0000000..efbbd63 --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/Inboxes.java @@ -0,0 +1,151 @@ +package com.resend.services.inboxes; + +import com.resend.core.exception.ResendException; +import com.resend.core.helper.URLHelper; +import com.resend.core.net.HttpMethod; +import com.resend.core.net.IHttpClient; +import com.resend.core.net.ListParams; +import com.resend.core.service.BaseService; +import com.resend.services.inboxes.model.CreateInboxOptions; +import com.resend.services.inboxes.model.CreateInboxResponseSuccess; +import com.resend.services.inboxes.model.GetInboxResponseSuccess; +import com.resend.services.inboxes.model.ListInboxesResponseSuccess; +import com.resend.services.inboxes.model.RemoveInboxResponseSuccess; +import com.resend.services.inboxes.model.UpdateInboxOptions; +import com.resend.services.inboxes.model.UpdateInboxResponseSuccess; +import okhttp3.MediaType; + +/** + * Represents the Resend Inboxes module. + */ +public class Inboxes extends BaseService { + + /** + * Constructs an instance of the {@code Inboxes} class. + * + * @param apiKey The apiKey used for authentication. + */ + public Inboxes(final String apiKey) { + super(apiKey); + } + + /** + * Constructs an instance of the {@code Inboxes} class that sends requests through the given HTTP client. + * + * @param apiKey The apiKey used for authentication. + * @param httpClient The HTTP client to use. + */ + public Inboxes(final String apiKey, final IHttpClient httpClient) { + super(apiKey, httpClient); + } + + /** + * Returns an InboxAgent object that can be used to read and update the agent settings of an inbox. + * + * @return An InboxAgent object. + */ + public InboxAgent agent() { + return new InboxAgent(super.apiKey, super.httpClient); + } + + /** + * Returns an InboxLabels object that can be used to manage the labels of an inbox. + * + * @return An InboxLabels object. + */ + public InboxLabels labels() { + return new InboxLabels(super.apiKey, super.httpClient); + } + + /** + * Returns an InboxThreads object that can be used to manage the threads of an inbox. + * + * @return An InboxThreads object. + */ + public InboxThreads threads() { + return new InboxThreads(super.apiKey, super.httpClient); + } + + /** + * Returns an InboxDrafts object that can be used to manage the drafts of an inbox. + * + * @return An InboxDrafts object. + */ + public InboxDrafts drafts() { + return new InboxDrafts(super.apiKey, super.httpClient); + } + + /** + * Creates an inbox that sends and receives email at an address on one of your domains. + * A domain that does not meet the inbox rules returns a 422. An address that is already in use returns a 409. + * A team that has reached the inbox limit of its plan returns a 403. + * + * @param createInboxOptions The inbox details. + * @return The ID of the created inbox. + * @throws ResendException If an error occurs while creating the inbox. + */ + public CreateInboxResponseSuccess create(CreateInboxOptions createInboxOptions) throws ResendException { + String payload = super.resendMapper.writeValue(createInboxOptions); + return execute("/inboxes", HttpMethod.POST, payload, MediaType.get("application/json"), CreateInboxResponseSuccess.class); + } + + /** + * Retrieves the inboxes of the team, newest first. + * The list items carry fewer fields than a single inbox. Retrieve an inbox to get all its fields. + * + * @return A ListInboxesResponseSuccess containing the list of inboxes. + * @throws ResendException If an error occurs while retrieving the inboxes. + */ + public ListInboxesResponseSuccess list() throws ResendException { + return list(null); + } + + /** + * Retrieves a paginated list of the inboxes of the team, newest first. + * The list items carry fewer fields than a single inbox. Retrieve an inbox to get all its fields. + * + * @param params The params used to customize the list. + * @return A ListInboxesResponseSuccess containing the list of inboxes. + * @throws ResendException If an error occurs while retrieving the inboxes. + */ + public ListInboxesResponseSuccess list(ListParams params) throws ResendException { + return execute("/inboxes" + URLHelper.parse(params), HttpMethod.GET, null, MediaType.get("application/json"), ListInboxesResponseSuccess.class); + } + + /** + * Retrieves a single inbox by its ID or its email address. + * A value that contains {@code @} must be a valid email address, and the match is case-insensitive. + * + * @param inboxIdOrEmail The inbox ID or the inbox email address. + * @return The inbox. + * @throws ResendException If an error occurs while retrieving the inbox. + */ + public GetInboxResponseSuccess get(String inboxIdOrEmail) throws ResendException { + return execute("/inboxes/" + inboxIdOrEmail, HttpMethod.GET, null, MediaType.get("application/json"), GetInboxResponseSuccess.class); + } + + /** + * Updates the internal name of an inbox or the name recipients see when mail is sent from it. + * At least one of {@code name} or {@code from_name} is required. + * + * @param inboxId The inbox ID. An email address is not accepted. + * @param updateInboxOptions The fields to update. + * @return The ID of the updated inbox. + * @throws ResendException If an error occurs while updating the inbox. + */ + public UpdateInboxResponseSuccess update(String inboxId, UpdateInboxOptions updateInboxOptions) throws ResendException { + String payload = super.resendMapper.writeValue(updateInboxOptions); + return execute("/inboxes/" + inboxId, HttpMethod.PATCH, payload, MediaType.get("application/json"), UpdateInboxResponseSuccess.class); + } + + /** + * Removes an inbox. An inbox that does not exist or was already removed returns a 404. + * + * @param inboxId The inbox ID. An email address is not accepted. + * @return The RemoveInboxResponseSuccess with the details of the removed inbox. + * @throws ResendException If an error occurs while removing the inbox. + */ + public RemoveInboxResponseSuccess remove(String inboxId) throws ResendException { + return execute("/inboxes/" + inboxId, HttpMethod.DELETE, null, MediaType.get("application/json"), RemoveInboxResponseSuccess.class); + } +} From 1b3c11e27a911283c10f1e8cb060465ce04c33a6 Mon Sep 17 00:00:00 2001 From: Felipe Freitag Date: Mon, 12 Oct 2026 14:05:31 -0300 Subject: [PATCH 03/30] test(inboxes): cover every inbox operation --- .../services/inboxes/InboxAgentTest.java | 91 ++++++++++ .../services/inboxes/InboxDraftsTest.java | 157 ++++++++++++++++ .../services/inboxes/InboxLabelsTest.java | 96 ++++++++++ .../inboxes/InboxThreadEmailsTest.java | 107 +++++++++++ .../services/inboxes/InboxThreadsTest.java | 167 ++++++++++++++++++ .../resend/services/inboxes/InboxesTest.java | 141 +++++++++++++++ .../com/resend/services/util/InboxesUtil.java | 67 +++++++ 7 files changed, 826 insertions(+) create mode 100644 src/test/java/com/resend/services/inboxes/InboxAgentTest.java create mode 100644 src/test/java/com/resend/services/inboxes/InboxDraftsTest.java create mode 100644 src/test/java/com/resend/services/inboxes/InboxLabelsTest.java create mode 100644 src/test/java/com/resend/services/inboxes/InboxThreadEmailsTest.java create mode 100644 src/test/java/com/resend/services/inboxes/InboxThreadsTest.java create mode 100644 src/test/java/com/resend/services/inboxes/InboxesTest.java create mode 100644 src/test/java/com/resend/services/util/InboxesUtil.java diff --git a/src/test/java/com/resend/services/inboxes/InboxAgentTest.java b/src/test/java/com/resend/services/inboxes/InboxAgentTest.java new file mode 100644 index 0000000..a20ffe8 --- /dev/null +++ b/src/test/java/com/resend/services/inboxes/InboxAgentTest.java @@ -0,0 +1,91 @@ +package com.resend.services.inboxes; + +import com.resend.core.exception.ResendException; +import com.resend.core.net.AbstractHttpResponse; +import com.resend.core.net.HttpMethod; +import com.resend.core.net.IHttpClient; +import com.resend.services.inboxes.model.*; +import okhttp3.MediaType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.Arrays; +import java.util.Collections; + +import static com.resend.services.util.InboxesUtil.INBOX_ID; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.when; + +@SuppressWarnings("unchecked") +public class InboxAgentTest { + + private static final String AGENT_PATH = "/inboxes/" + INBOX_ID + "/agent"; + private static final String UPDATE_RESPONSE_JSON = + "{\"object\":\"inbox_agent\",\"id\":\"a1f84a4e-6f2b-4f0a-9c1d-8a2e5b3c7d90\"}"; + + @Mock + private IHttpClient httpClient; + + private InboxAgent agent; + + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + agent = new Inboxes("test-api-key", httpClient).agent(); + } + + @Test + public void testGetAgent_Success() throws ResendException { + String json = "{\"object\":\"inbox_agent\",\"instructions\":null,\"tone\":\"friendly and concise\"," + + "\"enabled_actions\":[\"draft_reply\",\"add_labels\"]}"; + when(httpClient.perform(eq(AGENT_PATH), anyString(), eq(HttpMethod.GET), isNull(), any(MediaType.class))) + .thenReturn(new AbstractHttpResponse<>(200, json, true)); + + GetInboxAgentResponseSuccess response = agent.get(INBOX_ID); + + assertEquals("inbox_agent", response.getObject()); + assertNull(response.getInstructions()); + assertEquals("friendly and concise", response.getTone()); + assertEquals(Arrays.asList("draft_reply", "add_labels"), response.getEnabledActions()); + } + + @Test + public void testUpdateAgent_SendsOnlySetFields() throws ResendException { + UpdateInboxAgentOptions options = UpdateInboxAgentOptions.builder() + .tone("friendly and concise") + .enabledActions(InboxAgentAction.DRAFT_REPLY, InboxAgentAction.ADD_LABELS) + .build(); + when(httpClient.perform(eq(AGENT_PATH), anyString(), eq(HttpMethod.PATCH), + eq("{\"tone\":\"friendly and concise\",\"enabled_actions\":[\"draft_reply\",\"add_labels\"]}"), any(MediaType.class))) + .thenReturn(new AbstractHttpResponse<>(200, UPDATE_RESPONSE_JSON, true)); + + UpdateInboxAgentResponseSuccess response = agent.update(INBOX_ID, options); + + assertEquals("inbox_agent", response.getObject()); + assertEquals("a1f84a4e-6f2b-4f0a-9c1d-8a2e5b3c7d90", response.getId()); + } + + @Test + public void testUpdateAgent_SendsNullToClear() throws ResendException { + UpdateInboxAgentOptions options = UpdateInboxAgentOptions.builder() + .instructions(null) + .enabledActions(Collections.emptyList()) + .build(); + when(httpClient.perform(eq(AGENT_PATH), anyString(), eq(HttpMethod.PATCH), + eq("{\"instructions\":null,\"enabled_actions\":[]}"), any(MediaType.class))) + .thenReturn(new AbstractHttpResponse<>(200, UPDATE_RESPONSE_JSON, true)); + + assertNotNull(agent.update(INBOX_ID, options)); + assertNull(options.getInstructions()); + assertTrue(options.getEnabledActions().isEmpty()); + } + + @Test + public void testAgentAction_UnknownValue_Throws() { + assertEquals(InboxAgentAction.MARK_AS_SPAM, InboxAgentAction.fromValue("mark_as_spam")); + assertThrows(IllegalArgumentException.class, () -> InboxAgentAction.fromValue("send_email")); + } +} diff --git a/src/test/java/com/resend/services/inboxes/InboxDraftsTest.java b/src/test/java/com/resend/services/inboxes/InboxDraftsTest.java new file mode 100644 index 0000000..fba9174 --- /dev/null +++ b/src/test/java/com/resend/services/inboxes/InboxDraftsTest.java @@ -0,0 +1,157 @@ +package com.resend.services.inboxes; + +import com.resend.core.exception.ResendException; +import com.resend.core.net.AbstractHttpResponse; +import com.resend.core.net.HttpMethod; +import com.resend.core.net.IHttpClient; +import com.resend.core.net.ListParams; +import com.resend.services.inboxes.model.*; +import com.resend.services.util.InboxesUtil; +import okhttp3.MediaType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.Collections; + +import static com.resend.services.util.InboxesUtil.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.when; + +@SuppressWarnings("unchecked") +public class InboxDraftsTest { + + private static final String DRAFTS_PATH = "/inboxes/" + INBOX_ID + "/drafts"; + private static final String DRAFT_JSON = "{\"object\":\"inbox_draft\",\"id\":\"" + DRAFT_ID + "\",\"type\":\"reply\"," + + "\"to\":[\"ada@example.org\"],\"cc\":[],\"bcc\":[],\"subject\":\"Re: Refund for order 1041\"," + + "\"html\":null,\"text\":\"Refund issued for order 1041.\",\"thread_id\":\"" + THREAD_ID + "\"," + + "\"reply_to_email_id\":\"" + EMAIL_ID + "\",\"email_id\":null," + + "\"created_at\":\"2026-08-05T14:12:04.110Z\",\"updated_at\":\"2026-08-05T14:12:04.110Z\"}"; + + @Mock + private IHttpClient httpClient; + + private InboxDrafts drafts; + + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + drafts = new Inboxes("test-api-key", httpClient).drafts(); + } + + @Test + public void testListDrafts_Success() throws ResendException { + String json = "{\"object\":\"list\",\"has_more\":false,\"data\":[{\"id\":\"" + DRAFT_ID + "\",\"type\":\"standalone\"," + + "\"to\":null,\"cc\":[],\"bcc\":[],\"subject\":null,\"snippet\":\"Refund issued\",\"thread_id\":null," + + "\"reply_to_email_id\":null,\"updated_at\":\"2026-08-05T14:12:04.110Z\"}]}"; + when(httpClient.perform(eq(DRAFTS_PATH + "?limit=1"), anyString(), eq(HttpMethod.GET), isNull(), any(MediaType.class))) + .thenReturn(new AbstractHttpResponse<>(200, json, true)); + + ListInboxDraftsResponseSuccess response = drafts.list(INBOX_ID, ListParams.builder().limit(1).build()); + + InboxDraftListItem item = response.getData().get(0); + assertEquals(DRAFT_ID, item.getId()); + assertEquals("standalone", item.getType()); + assertNull(item.getTo()); + assertEquals("Refund issued", item.getSnippet()); + assertNull(item.getThreadId()); + } + + @Test + public void testCreateReplyDraft_Success() throws ResendException { + String payload = "{\"text\":\"Refund issued for order 1041.\",\"thread_id\":\"" + THREAD_ID + "\"," + + "\"reply_to_email_id\":\"" + EMAIL_ID + "\"}"; + when(httpClient.perform(eq(DRAFTS_PATH), anyString(), eq(HttpMethod.POST), eq(payload), any(MediaType.class))) + .thenReturn(new AbstractHttpResponse<>(201, "{\"object\":\"inbox_draft\",\"id\":\"" + DRAFT_ID + "\"}", true)); + + CreateInboxDraftResponseSuccess response = drafts.create(INBOX_ID, InboxesUtil.createReplyDraftRequest()); + + assertEquals("inbox_draft", response.getObject()); + assertEquals(DRAFT_ID, response.getId()); + } + + @Test + public void testCreateDraft_EmptyToIsSent() throws ResendException { + CreateInboxDraftOptions options = CreateInboxDraftOptions.builder() + .to(Collections.emptyList()) + .subject("Hello") + .build(); + when(httpClient.perform(eq(DRAFTS_PATH), anyString(), eq(HttpMethod.POST), eq("{\"to\":[],\"subject\":\"Hello\"}"), any(MediaType.class))) + .thenReturn(new AbstractHttpResponse<>(201, "{\"object\":\"inbox_draft\",\"id\":\"" + DRAFT_ID + "\"}", true)); + + assertEquals(DRAFT_ID, drafts.create(INBOX_ID, options).getId()); + } + + @Test + public void testGetDraft_Success() throws ResendException { + when(httpClient.perform(eq(DRAFTS_PATH + "/" + DRAFT_ID), anyString(), eq(HttpMethod.GET), isNull(), any(MediaType.class))) + .thenReturn(new AbstractHttpResponse<>(200, DRAFT_JSON, true)); + + GetInboxDraftResponseSuccess response = drafts.get(INBOX_ID, DRAFT_ID); + + assertEquals("reply", response.getType()); + assertEquals(Collections.singletonList("ada@example.org"), response.getTo()); + assertNull(response.getHtml()); + assertEquals(THREAD_ID, response.getThreadId()); + assertEquals(EMAIL_ID, response.getReplyToEmailId()); + assertNull(response.getEmailId()); + } + + @Test + public void testUpdateDraft_SendsNullToClear() throws ResendException { + UpdateInboxDraftOptions options = UpdateInboxDraftOptions.builder() + .subject(null) + .cc((java.util.List) null) + .text("Updated body.") + .build(); + when(httpClient.perform(eq(DRAFTS_PATH + "/" + DRAFT_ID), anyString(), eq(HttpMethod.PATCH), + eq("{\"subject\":null,\"cc\":null,\"text\":\"Updated body.\"}"), any(MediaType.class))) + .thenReturn(new AbstractHttpResponse<>(200, DRAFT_JSON, true)); + + UpdateInboxDraftResponseSuccess response = drafts.update(INBOX_ID, DRAFT_ID, options); + + assertEquals(DRAFT_ID, response.getId()); + } + + @Test + public void testUpdateDraft_OmitsUnsetFields() throws ResendException { + UpdateInboxDraftOptions options = UpdateInboxDraftOptions.builder().to("ada@example.org").build(); + when(httpClient.perform(eq(DRAFTS_PATH + "/" + DRAFT_ID), anyString(), eq(HttpMethod.PATCH), + eq("{\"to\":[\"ada@example.org\"]}"), any(MediaType.class))) + .thenReturn(new AbstractHttpResponse<>(200, DRAFT_JSON, true)); + + assertNotNull(drafts.update(INBOX_ID, DRAFT_ID, options)); + assertEquals(Collections.singletonList("ada@example.org"), options.getTo()); + } + + @Test + public void testRemoveDraft_Success() throws ResendException { + when(httpClient.perform(eq(DRAFTS_PATH + "/" + DRAFT_ID), anyString(), eq(HttpMethod.DELETE), isNull(), any(MediaType.class))) + .thenReturn(new AbstractHttpResponse<>(200, "{\"object\":\"inbox_draft\",\"id\":\"" + DRAFT_ID + "\",\"deleted\":true}", true)); + + RemoveInboxDraftResponseSuccess response = drafts.remove(INBOX_ID, DRAFT_ID); + + assertTrue(response.getDeleted()); + } + + @Test + public void testSendDraft_Success() throws ResendException { + when(httpClient.perform(eq(DRAFTS_PATH + "/" + DRAFT_ID + "/send"), anyString(), eq(HttpMethod.POST), eq(""), any(MediaType.class))) + .thenReturn(new AbstractHttpResponse<>(200, "{\"id\":\"" + SENT_EMAIL_ID + "\"}", true)); + + SendInboxDraftResponseSuccess response = drafts.send(INBOX_ID, DRAFT_ID); + + assertEquals(SENT_EMAIL_ID, response.getId()); + } + + @Test + public void testSendDraft_AlreadySent_ThrowsResendException() throws ResendException { + when(httpClient.perform(eq(DRAFTS_PATH + "/" + DRAFT_ID + "/send"), anyString(), eq(HttpMethod.POST), eq(""), any(MediaType.class))) + .thenReturn(new AbstractHttpResponse<>(409, "{\"name\":\"conflict\",\"message\":\"Draft already sent\"}", false)); + + ResendException ex = assertThrows(ResendException.class, () -> drafts.send(INBOX_ID, DRAFT_ID)); + assertEquals(409, (int) ex.getStatusCode()); + } +} diff --git a/src/test/java/com/resend/services/inboxes/InboxLabelsTest.java b/src/test/java/com/resend/services/inboxes/InboxLabelsTest.java new file mode 100644 index 0000000..6daf7d0 --- /dev/null +++ b/src/test/java/com/resend/services/inboxes/InboxLabelsTest.java @@ -0,0 +1,96 @@ +package com.resend.services.inboxes; + +import com.resend.core.exception.ResendException; +import com.resend.core.net.AbstractHttpResponse; +import com.resend.core.net.HttpMethod; +import com.resend.core.net.IHttpClient; +import com.resend.services.inboxes.model.*; +import com.resend.services.util.InboxesUtil; +import okhttp3.MediaType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import static com.resend.services.util.InboxesUtil.INBOX_ID; +import static com.resend.services.util.InboxesUtil.LABEL_ID; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.when; + +@SuppressWarnings("unchecked") +public class InboxLabelsTest { + + private static final String LABELS_PATH = "/inboxes/" + INBOX_ID + "/labels"; + + @Mock + private IHttpClient httpClient; + + private InboxLabels labels; + + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + labels = new Inboxes("test-api-key", httpClient).labels(); + } + + @Test + public void testListLabels_Success() throws ResendException { + String json = "{\"object\":\"list\",\"data\":[{\"id\":\"" + LABEL_ID + "\",\"name\":\"Urgent\"," + + "\"color\":\"#E93D82\",\"created_at\":\"2026-08-05T14:07:42.881Z\"}]}"; + when(httpClient.perform(eq(LABELS_PATH), anyString(), eq(HttpMethod.GET), isNull(), any(MediaType.class))) + .thenReturn(new AbstractHttpResponse<>(200, json, true)); + + ListInboxLabelsResponseSuccess response = labels.list(INBOX_ID); + + assertEquals("list", response.getObject()); + InboxLabel label = response.getData().get(0); + assertEquals(LABEL_ID, label.getId()); + assertEquals("Urgent", label.getName()); + assertEquals("#E93D82", label.getColor()); + assertEquals("2026-08-05T14:07:42.881Z", label.getCreatedAt()); + } + + @Test + public void testCreateLabel_Success() throws ResendException { + when(httpClient.perform(eq(LABELS_PATH), anyString(), eq(HttpMethod.POST), + eq("{\"name\":\"Urgent\",\"color\":\"#E93D82\"}"), any(MediaType.class))) + .thenReturn(new AbstractHttpResponse<>(201, "{\"object\":\"inbox_label\",\"id\":\"" + LABEL_ID + "\"}", true)); + + CreateInboxLabelResponseSuccess response = labels.create(INBOX_ID, InboxesUtil.createLabelRequest()); + + assertEquals("inbox_label", response.getObject()); + assertEquals(LABEL_ID, response.getId()); + } + + @Test + public void testCreateLabel_Duplicate_ThrowsResendException() throws ResendException { + when(httpClient.perform(eq(LABELS_PATH), anyString(), eq(HttpMethod.POST), anyString(), any(MediaType.class))) + .thenReturn(new AbstractHttpResponse<>(422, "{\"name\":\"validation_error\",\"message\":\"Duplicate label\"}", false)); + + ResendException ex = assertThrows(ResendException.class, () -> labels.create(INBOX_ID, InboxesUtil.createLabelRequest())); + assertEquals(422, (int) ex.getStatusCode()); + } + + @Test + public void testUpdateLabel_Success() throws ResendException { + when(httpClient.perform(eq(LABELS_PATH + "/" + LABEL_ID), anyString(), eq(HttpMethod.PATCH), + eq("{\"color\":\"#F76B15\"}"), any(MediaType.class))) + .thenReturn(new AbstractHttpResponse<>(200, "{\"object\":\"inbox_label\",\"id\":\"" + LABEL_ID + "\"}", true)); + + UpdateInboxLabelResponseSuccess response = labels.update(INBOX_ID, LABEL_ID, InboxesUtil.updateLabelRequest()); + + assertEquals(LABEL_ID, response.getId()); + } + + @Test + public void testRemoveLabel_Success() throws ResendException { + when(httpClient.perform(eq(LABELS_PATH + "/" + LABEL_ID), anyString(), eq(HttpMethod.DELETE), isNull(), any(MediaType.class))) + .thenReturn(new AbstractHttpResponse<>(200, "{\"object\":\"inbox_label\",\"id\":\"" + LABEL_ID + "\",\"deleted\":true}", true)); + + RemoveInboxLabelResponseSuccess response = labels.remove(INBOX_ID, LABEL_ID); + + assertEquals(LABEL_ID, response.getId()); + assertTrue(response.getDeleted()); + } +} diff --git a/src/test/java/com/resend/services/inboxes/InboxThreadEmailsTest.java b/src/test/java/com/resend/services/inboxes/InboxThreadEmailsTest.java new file mode 100644 index 0000000..a184287 --- /dev/null +++ b/src/test/java/com/resend/services/inboxes/InboxThreadEmailsTest.java @@ -0,0 +1,107 @@ +package com.resend.services.inboxes; + +import com.resend.core.exception.ResendException; +import com.resend.core.net.AbstractHttpResponse; +import com.resend.core.net.HttpMethod; +import com.resend.core.net.IHttpClient; +import com.resend.core.net.ListParams; +import com.resend.services.inboxes.model.*; +import com.resend.services.util.InboxesUtil; +import okhttp3.MediaType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.Collections; + +import static com.resend.services.util.InboxesUtil.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.when; + +@SuppressWarnings("unchecked") +public class InboxThreadEmailsTest { + + private static final String EMAILS_PATH = "/inboxes/" + INBOX_ID + "/threads/" + THREAD_ID + "/emails"; + private static final String EMAIL_JSON = "{\"id\":\"" + EMAIL_ID + "\",\"direction\":\"inbound\"," + + "\"from\":\"Ada Lovelace \",\"to\":[\"support@acme.com\"],\"cc\":[],\"bcc\":[]," + + "\"reply_to\":[\"replies@example.org\"],\"subject\":\"Refund for order 1041\"," + + "\"message_id\":\"<1041@example.org>\",\"html\":\"

Could I get a refund?

\"," + + "\"text\":\"Could I get a refund?\",\"attachments\":[{\"id\":\"9d4f2b81-6c3a-4e7d-8b12-0a5c6d7e8f90\"," + + "\"filename\":\"receipt.pdf\",\"size\":20841}],\"read\":false,\"received_at\":\"2026-08-05T14:03:11.229Z\"}"; + + @Mock + private IHttpClient httpClient; + + private InboxThreadEmails emails; + + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + emails = new Inboxes("test-api-key", httpClient).threads().emails(); + } + + @Test + public void testListThreadEmails_Success() throws ResendException { + when(httpClient.perform(eq(EMAILS_PATH + "?limit=2&before=" + EMAIL_ID), anyString(), eq(HttpMethod.GET), isNull(), any(MediaType.class))) + .thenReturn(new AbstractHttpResponse<>(200, "{\"object\":\"list\",\"has_more\":false,\"data\":[" + EMAIL_JSON + "]}", true)); + + ListInboxThreadEmailsResponseSuccess response = emails.list(INBOX_ID, THREAD_ID, ListParams.builder().limit(2).before(EMAIL_ID).build()); + + assertFalse(response.hasMore()); + InboxThreadEmail email = response.getData().get(0); + assertEquals("inbound", email.getDirection()); + assertEquals(Collections.singletonList("replies@example.org"), email.getReplyTo()); + assertEquals("<1041@example.org>", email.getMessageId()); + assertEquals("receipt.pdf", email.getAttachments().get(0).getFilename()); + assertEquals(20841, (int) email.getAttachments().get(0).getSize()); + } + + @Test + public void testGetThreadEmail_Success() throws ResendException { + when(httpClient.perform(eq(EMAILS_PATH + "/" + EMAIL_ID), anyString(), eq(HttpMethod.GET), isNull(), any(MediaType.class))) + .thenReturn(new AbstractHttpResponse<>(200, EMAIL_JSON, true)); + + GetInboxThreadEmailResponseSuccess response = emails.get(INBOX_ID, THREAD_ID, EMAIL_ID); + + assertEquals(EMAIL_ID, response.getId()); + assertEquals("Ada Lovelace ", response.getFrom()); + assertEquals("Could I get a refund?", response.getText()); + assertFalse(response.getRead()); + assertEquals("2026-08-05T14:03:11.229Z", response.getReceivedAt()); + } + + @Test + public void testReply_Success() throws ResendException { + String payload = "{\"cc\":[\"billing@acme.com\"],\"text\":\"Refund issued for order 1041.\",\"reply_all\":true," + + "\"attachments\":[{\"filename\":\"receipt.txt\",\"content\":\"cmVjZWlwdA==\"}]}"; + when(httpClient.perform(eq(EMAILS_PATH + "/" + EMAIL_ID + "/reply"), anyString(), eq(HttpMethod.POST), eq(payload), any(MediaType.class))) + .thenReturn(new AbstractHttpResponse<>(200, "{\"id\":\"" + SENT_EMAIL_ID + "\"}", true)); + + ReplyInboxThreadEmailResponseSuccess response = emails.reply(INBOX_ID, THREAD_ID, EMAIL_ID, InboxesUtil.replyRequest()); + + assertEquals(SENT_EMAIL_ID, response.getId()); + } + + @Test + public void testForward_Success() throws ResendException { + String payload = "{\"to\":[\"colleague@example.org\"],\"text\":\"Flagging this refund request for you.\"}"; + when(httpClient.perform(eq(EMAILS_PATH + "/" + EMAIL_ID + "/forward"), anyString(), eq(HttpMethod.POST), eq(payload), any(MediaType.class))) + .thenReturn(new AbstractHttpResponse<>(200, "{\"id\":\"" + SENT_EMAIL_ID + "\"}", true)); + + ForwardInboxThreadEmailResponseSuccess response = emails.forward(INBOX_ID, THREAD_ID, EMAIL_ID, InboxesUtil.forwardRequest()); + + assertEquals(SENT_EMAIL_ID, response.getId()); + } + + @Test + public void testReply_ValidationError_ThrowsResendException() throws ResendException { + when(httpClient.perform(eq(EMAILS_PATH + "/" + EMAIL_ID + "/reply"), anyString(), eq(HttpMethod.POST), anyString(), any(MediaType.class))) + .thenReturn(new AbstractHttpResponse<>(422, "{\"name\":\"validation_error\",\"message\":\"Missing body\"}", false)); + + ReplyInboxThreadEmailOptions options = ReplyInboxThreadEmailOptions.builder().subject("Re: hi").build(); + ResendException ex = assertThrows(ResendException.class, () -> emails.reply(INBOX_ID, THREAD_ID, EMAIL_ID, options)); + assertEquals(422, (int) ex.getStatusCode()); + } +} diff --git a/src/test/java/com/resend/services/inboxes/InboxThreadsTest.java b/src/test/java/com/resend/services/inboxes/InboxThreadsTest.java new file mode 100644 index 0000000..90d5876 --- /dev/null +++ b/src/test/java/com/resend/services/inboxes/InboxThreadsTest.java @@ -0,0 +1,167 @@ +package com.resend.services.inboxes; + +import com.resend.core.exception.ResendException; +import com.resend.core.net.AbstractHttpResponse; +import com.resend.core.net.HttpMethod; +import com.resend.core.net.IHttpClient; +import com.resend.services.inboxes.model.*; +import okhttp3.MediaType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.Collections; + +import static com.resend.services.util.InboxesUtil.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.when; + +@SuppressWarnings("unchecked") +public class InboxThreadsTest { + + private static final String THREADS_PATH = "/inboxes/" + INBOX_ID + "/threads"; + private static final String THREAD_JSON = "{\"id\":\"" + THREAD_ID + "\",\"subject\":\"Refund for order 1041\"," + + "\"from\":\"ada@example.org\",\"to\":[\"support@example.com\"],\"cc\":[],\"bcc\":[]," + + "\"labels\":[{\"id\":\"" + LABEL_ID + "\",\"name\":\"Urgent\",\"color\":\"#E93D82\"}]," + + "\"message_count\":2,\"has_attachment\":true,\"has_draft\":false,\"read\":false," + + "\"received_at\":\"2026-08-05T14:03:11.229Z\",\"folder\":\"inbox\""; + private static final String SUMMARY_JSON = "{\"object\":\"inbox_thread\",\"id\":\"" + THREAD_ID + "\"," + + "\"subject\":\"Refund for order 1041\",\"folder\":\"archive\"," + + "\"labels\":[{\"id\":\"" + LABEL_ID + "\",\"name\":\"Urgent\",\"color\":\"#E93D82\"}],\"read\":true}"; + + @Mock + private IHttpClient httpClient; + + private InboxThreads threads; + + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + threads = new Inboxes("test-api-key", httpClient).threads(); + } + + @Test + public void testListThreads_WithFilters() throws ResendException { + ListInboxThreadsParams params = ListInboxThreadsParams.builder() + .folders(InboxFolder.INBOX, InboxFolder.SENT) + .labels(LABEL_ID, OTHER_LABEL_ID) + .read(false) + .limit(5) + .after(THREAD_ID) + .build(); + String path = THREADS_PATH + "?limit=5&after=" + THREAD_ID + "&folders=inbox%2Csent&labels=" + LABEL_ID + "%2C" + OTHER_LABEL_ID + "&read=false"; + when(httpClient.perform(eq(path), anyString(), eq(HttpMethod.GET), isNull(), any(MediaType.class))) + .thenReturn(new AbstractHttpResponse<>(200, "{\"object\":\"list\",\"has_more\":true,\"data\":[" + THREAD_JSON + "}]}", true)); + + ListInboxThreadsResponseSuccess response = threads.list(INBOX_ID, params); + + assertTrue(response.hasMore()); + InboxThread thread = response.getData().get(0); + assertEquals(THREAD_ID, thread.getId()); + assertEquals("ada@example.org", thread.getFrom()); + assertEquals(Collections.singletonList("support@example.com"), thread.getTo()); + assertEquals("#E93D82", thread.getLabels().get(0).getColor()); + assertEquals(2, (int) thread.getMessageCount()); + assertTrue(thread.getHasAttachment()); + assertFalse(thread.getHasDraft()); + assertFalse(thread.getRead()); + assertEquals("inbox", thread.getFolder()); + } + + @Test + public void testListThreads_NoParams() throws ResendException { + when(httpClient.perform(eq(THREADS_PATH), anyString(), eq(HttpMethod.GET), isNull(), any(MediaType.class))) + .thenReturn(new AbstractHttpResponse<>(200, "{\"object\":\"list\",\"has_more\":false,\"data\":[]}", true)); + + assertFalse(threads.list(INBOX_ID).hasMore()); + } + + @Test + public void testSearchThreads_Success() throws ResendException { + SearchInboxThreadsParams params = SearchInboxThreadsParams.builder() + .query("invoice -draft") + .from("isabella@example.com", "carolina@example.com") + .hasAttachment(true) + .startDate("2026-09-01") + .endDate("2026-09-30") + .folders(InboxFolder.INBOX) + .limit(10) + .build(); + String path = THREADS_PATH + "/search?limit=10&query=invoice+-draft&from=isabella%40example.com%2Ccarolina%40example.com" + + "&has_attachment=true&start_date=2026-09-01&end_date=2026-09-30&folders=inbox"; + String json = "{\"object\":\"list\",\"has_more\":false,\"data\":[" + THREAD_JSON + "," + + "\"matched_email_id\":\"e8a3f1c6-2b9d-4e5a-a7c4-9d1b6f3e2a80\"," + + "\"highlights\":{\"subject\":[\"September **invoice**\"],\"attachments\":[\"**invoice**-2026-09.pdf\"]}}]}"; + when(httpClient.perform(eq(path), anyString(), eq(HttpMethod.GET), isNull(), any(MediaType.class))) + .thenReturn(new AbstractHttpResponse<>(200, json, true)); + + SearchInboxThreadsResponseSuccess response = threads.search(INBOX_ID, params); + + InboxThreadSearchResult result = response.getData().get(0); + assertEquals(THREAD_ID, result.getId()); + assertEquals("e8a3f1c6-2b9d-4e5a-a7c4-9d1b6f3e2a80", result.getMatchedEmailId()); + assertEquals(Collections.singletonList("September **invoice**"), result.getHighlights().get("subject")); + assertFalse(result.getHighlights().containsKey("body")); + } + + @Test + public void testGetThread_Success() throws ResendException { + when(httpClient.perform(eq(THREADS_PATH + "/" + THREAD_ID), anyString(), eq(HttpMethod.GET), isNull(), any(MediaType.class))) + .thenReturn(new AbstractHttpResponse<>(200, SUMMARY_JSON, true)); + + GetInboxThreadResponseSuccess response = threads.get(INBOX_ID, THREAD_ID); + + assertEquals("inbox_thread", response.getObject()); + assertEquals("archive", response.getFolder()); + assertEquals(LABEL_ID, response.getLabels().get(0).getId()); + assertTrue(response.getRead()); + } + + @Test + public void testUpdateThread_AddAndRemoveLabels() throws ResendException { + UpdateInboxThreadOptions options = UpdateInboxThreadOptions.builder() + .read(true) + .folder(MoveInboxThreadFolder.ARCHIVE) + .addLabelIds(LABEL_ID) + .removeLabelIds(OTHER_LABEL_ID) + .build(); + String payload = "{\"read\":true,\"folder\":\"archive\",\"add_label_ids\":[\"" + LABEL_ID + "\"]," + + "\"remove_label_ids\":[\"" + OTHER_LABEL_ID + "\"]}"; + when(httpClient.perform(eq(THREADS_PATH + "/" + THREAD_ID), anyString(), eq(HttpMethod.PATCH), eq(payload), any(MediaType.class))) + .thenReturn(new AbstractHttpResponse<>(200, SUMMARY_JSON, true)); + + UpdateInboxThreadResponseSuccess response = threads.update(INBOX_ID, THREAD_ID, options); + + assertEquals(THREAD_ID, response.getId()); + } + + @Test + @SuppressWarnings("deprecation") + public void testUpdateThread_DeprecatedLabelId() throws ResendException { + UpdateInboxThreadOptions options = UpdateInboxThreadOptions.builder().labelId(LABEL_ID).build(); + when(httpClient.perform(eq(THREADS_PATH + "/" + THREAD_ID), anyString(), eq(HttpMethod.PATCH), + eq("{\"label_id\":\"" + LABEL_ID + "\"}"), any(MediaType.class))) + .thenReturn(new AbstractHttpResponse<>(200, SUMMARY_JSON, true)); + + assertNotNull(threads.update(INBOX_ID, THREAD_ID, options)); + } + + @Test + public void testRemoveThread_Success() throws ResendException { + when(httpClient.perform(eq(THREADS_PATH + "/" + THREAD_ID), anyString(), eq(HttpMethod.DELETE), isNull(), any(MediaType.class))) + .thenReturn(new AbstractHttpResponse<>(200, "{\"object\":\"inbox_thread\",\"id\":\"" + THREAD_ID + "\",\"deleted\":false}", true)); + + RemoveInboxThreadResponseSuccess response = threads.remove(INBOX_ID, THREAD_ID); + + assertEquals(THREAD_ID, response.getId()); + assertFalse(response.getDeleted()); + } + + @Test + public void testFolder_UnknownValue_Throws() { + assertEquals(InboxFolder.TRASH, InboxFolder.fromValue("trash")); + assertThrows(IllegalArgumentException.class, () -> MoveInboxThreadFolder.fromValue("sent")); + } +} diff --git a/src/test/java/com/resend/services/inboxes/InboxesTest.java b/src/test/java/com/resend/services/inboxes/InboxesTest.java new file mode 100644 index 0000000..d328de4 --- /dev/null +++ b/src/test/java/com/resend/services/inboxes/InboxesTest.java @@ -0,0 +1,141 @@ +package com.resend.services.inboxes; + +import com.resend.core.exception.ResendException; +import com.resend.core.net.AbstractHttpResponse; +import com.resend.core.net.HttpMethod; +import com.resend.core.net.IHttpClient; +import com.resend.core.net.ListParams; +import com.resend.services.inboxes.model.*; +import com.resend.services.util.InboxesUtil; +import okhttp3.MediaType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import static com.resend.services.util.InboxesUtil.INBOX_ID; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.when; + +@SuppressWarnings("unchecked") +public class InboxesTest { + + private static final String INBOX_JSON = "{\"object\":\"inbox\",\"id\":\"" + INBOX_ID + "\"," + + "\"name\":\"Customer Support\",\"email_address\":\"support@acme.com\"," + + "\"domain_id\":\"d91cd9bd-1176-453e-8fc1-35364d380206\",\"receiving_address\":null," + + "\"from_name\":\"Ada from Support\",\"unread\":3,\"drafts\":2," + + "\"last_received\":\"2026-08-05T14:03:11.229Z\",\"created_at\":\"2026-08-01T09:12:44.512Z\"}"; + + @Mock + private IHttpClient httpClient; + + private Inboxes inboxes; + + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + inboxes = new Inboxes("test-api-key", httpClient); + } + + @Test + public void testCreateInbox_Success() throws ResendException { + String payload = "{\"email_address\":\"support@acme.com\",\"name\":\"Customer Support\"," + + "\"from_name\":\"Ada from Support\",\"forwarding\":true}"; + when(httpClient.perform(eq("/inboxes"), anyString(), eq(HttpMethod.POST), eq(payload), any(MediaType.class))) + .thenReturn(new AbstractHttpResponse<>(201, "{\"object\":\"inbox\",\"id\":\"" + INBOX_ID + "\"}", true)); + + CreateInboxResponseSuccess response = inboxes.create(InboxesUtil.createInboxRequest()); + + assertEquals("inbox", response.getObject()); + assertEquals(INBOX_ID, response.getId()); + } + + @Test + public void testCreateInbox_PlanLimit_ThrowsResendException() throws ResendException { + when(httpClient.perform(eq("/inboxes"), anyString(), eq(HttpMethod.POST), anyString(), any(MediaType.class))) + .thenReturn(new AbstractHttpResponse<>(403, + "{\"name\":\"validation_error\",\"message\":\"You have reached the inbox limit of your plan\"}", false)); + + ResendException ex = assertThrows(ResendException.class, () -> inboxes.create(InboxesUtil.createInboxRequest())); + assertEquals(403, (int) ex.getStatusCode()); + } + + @Test + public void testListInboxes_Success() throws ResendException { + String json = "{\"object\":\"list\",\"has_more\":false,\"data\":[{\"id\":\"" + INBOX_ID + "\"," + + "\"name\":null,\"email_address\":\"support@acme.com\",\"from_name\":null,\"unread\":3," + + "\"last_received\":null}]}"; + when(httpClient.perform(eq("/inboxes?limit=10&after=" + INBOX_ID), anyString(), eq(HttpMethod.GET), isNull(), any(MediaType.class))) + .thenReturn(new AbstractHttpResponse<>(200, json, true)); + + ListInboxesResponseSuccess response = inboxes.list(ListParams.builder().limit(10).after(INBOX_ID).build()); + + assertEquals("list", response.getObject()); + assertFalse(response.hasMore()); + assertEquals(1, response.getData().size()); + InboxListItem item = response.getData().get(0); + assertEquals(INBOX_ID, item.getId()); + assertNull(item.getName()); + assertEquals("support@acme.com", item.getEmailAddress()); + assertEquals(3, (int) item.getUnread()); + assertNull(item.getLastReceived()); + } + + @Test + public void testListInboxes_NoParams() throws ResendException { + when(httpClient.perform(eq("/inboxes"), anyString(), eq(HttpMethod.GET), isNull(), any(MediaType.class))) + .thenReturn(new AbstractHttpResponse<>(200, "{\"object\":\"list\",\"has_more\":false,\"data\":[]}", true)); + + assertTrue(inboxes.list().getData().isEmpty()); + } + + @Test + public void testGetInboxByEmail_Success() throws ResendException { + when(httpClient.perform(eq("/inboxes/support@acme.com"), anyString(), eq(HttpMethod.GET), isNull(), any(MediaType.class))) + .thenReturn(new AbstractHttpResponse<>(200, INBOX_JSON, true)); + + GetInboxResponseSuccess response = inboxes.get("support@acme.com"); + + assertEquals("inbox", response.getObject()); + assertEquals(INBOX_ID, response.getId()); + assertEquals("Customer Support", response.getName()); + assertEquals("d91cd9bd-1176-453e-8fc1-35364d380206", response.getDomainId()); + assertNull(response.getReceivingAddress()); + assertEquals("Ada from Support", response.getFromName()); + assertEquals(3, (int) response.getUnread()); + assertEquals(2, (int) response.getDrafts()); + assertEquals("2026-08-05T14:03:11.229Z", response.getLastReceived()); + assertEquals("2026-08-01T09:12:44.512Z", response.getCreatedAt()); + } + + @Test + public void testUpdateInbox_Success() throws ResendException { + when(httpClient.perform(eq("/inboxes/" + INBOX_ID), anyString(), eq(HttpMethod.PATCH), eq("{\"from_name\":\"Ada from Support\"}"), any(MediaType.class))) + .thenReturn(new AbstractHttpResponse<>(200, "{\"object\":\"inbox\",\"id\":\"" + INBOX_ID + "\"}", true)); + + UpdateInboxResponseSuccess response = inboxes.update(INBOX_ID, InboxesUtil.updateInboxRequest()); + + assertEquals(INBOX_ID, response.getId()); + } + + @Test + public void testRemoveInbox_Success() throws ResendException { + when(httpClient.perform(eq("/inboxes/" + INBOX_ID), anyString(), eq(HttpMethod.DELETE), isNull(), any(MediaType.class))) + .thenReturn(new AbstractHttpResponse<>(200, "{\"object\":\"inbox\",\"id\":\"" + INBOX_ID + "\",\"deleted\":true}", true)); + + RemoveInboxResponseSuccess response = inboxes.remove(INBOX_ID); + + assertEquals(INBOX_ID, response.getId()); + assertTrue(response.getDeleted()); + } + + @Test + public void testRemoveInbox_NotFound_ThrowsResendException() throws ResendException { + when(httpClient.perform(eq("/inboxes/" + INBOX_ID), anyString(), eq(HttpMethod.DELETE), isNull(), any(MediaType.class))) + .thenReturn(new AbstractHttpResponse<>(404, "{\"name\":\"not_found\",\"message\":\"Inbox not found\"}", false)); + + ResendException ex = assertThrows(ResendException.class, () -> inboxes.remove(INBOX_ID)); + assertEquals(404, (int) ex.getStatusCode()); + } +} diff --git a/src/test/java/com/resend/services/util/InboxesUtil.java b/src/test/java/com/resend/services/util/InboxesUtil.java new file mode 100644 index 0000000..afd68e3 --- /dev/null +++ b/src/test/java/com/resend/services/util/InboxesUtil.java @@ -0,0 +1,67 @@ +package com.resend.services.util; + +import com.resend.services.emails.model.Attachment; +import com.resend.services.inboxes.model.*; + +public class InboxesUtil { + + public static final String INBOX_ID = "b3e2b2b6-3f0e-4c8e-9ad3-2f43a1e2c7f1"; + public static final String LABEL_ID = "7f9c1d2e-4a6b-4c3d-8e15-9b0a7c6d5e34"; + public static final String OTHER_LABEL_ID = "2b6d0f4e-8a1c-4e3b-9d57-1c0e8a6f4b21"; + public static final String THREAD_ID = "4d8e2a1c-9b3f-4c6d-8a21-3e5f7c9c0d12"; + public static final String EMAIL_ID = "5b1a9f47-2c8d-4e6f-9a03-1d2e3f4a5b60"; + public static final String SENT_EMAIL_ID = "6a0c8e58-3d9e-4f7a-8b14-2e3f4a5b6c71"; + public static final String DRAFT_ID = "c3a1e8b4-2d5f-4a7c-9e10-6b8d7f5a4c32"; + + public static CreateInboxOptions createInboxRequest() { + return CreateInboxOptions.builder() + .emailAddress("support@acme.com") + .name("Customer Support") + .fromName("Ada from Support") + .forwarding(true) + .build(); + } + + public static UpdateInboxOptions updateInboxRequest() { + return UpdateInboxOptions.builder() + .fromName("Ada from Support") + .build(); + } + + public static CreateInboxLabelOptions createLabelRequest() { + return CreateInboxLabelOptions.builder() + .name("Urgent") + .color("#E93D82") + .build(); + } + + public static UpdateInboxLabelOptions updateLabelRequest() { + return UpdateInboxLabelOptions.builder() + .color("#F76B15") + .build(); + } + + public static ReplyInboxThreadEmailOptions replyRequest() { + return ReplyInboxThreadEmailOptions.builder() + .text("Refund issued for order 1041.") + .cc("billing@acme.com") + .replyAll(true) + .addAttachment(Attachment.builder().fileName("receipt.txt").content("cmVjZWlwdA==").build()) + .build(); + } + + public static ForwardInboxThreadEmailOptions forwardRequest() { + return ForwardInboxThreadEmailOptions.builder() + .to("colleague@example.org") + .text("Flagging this refund request for you.") + .build(); + } + + public static CreateInboxDraftOptions createReplyDraftRequest() { + return CreateInboxDraftOptions.builder() + .text("Refund issued for order 1041.") + .threadId(THREAD_ID) + .replyToEmailId(EMAIL_ID) + .build(); + } +} From 7a02ec7b206844357d3cba78e29110508c13896b Mon Sep 17 00:00:00 2001 From: Felipe Freitag Date: Mon, 12 Oct 2026 14:05:31 -0300 Subject: [PATCH 04/30] feat(webhooks): add the inbox event types --- .../services/webhooks/model/WebhookEvent.java | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/resend/services/webhooks/model/WebhookEvent.java b/src/main/java/com/resend/services/webhooks/model/WebhookEvent.java index 85f72e1..e9e1e41 100644 --- a/src/main/java/com/resend/services/webhooks/model/WebhookEvent.java +++ b/src/main/java/com/resend/services/webhooks/model/WebhookEvent.java @@ -47,7 +47,35 @@ public enum WebhookEvent { /** Triggered when a topic is updated. */ TOPIC_UPDATED("topic.updated"), /** Triggered when a topic is deleted. */ - TOPIC_DELETED("topic.deleted"); + TOPIC_DELETED("topic.deleted"), + /** Triggered when an inbox is created. */ + INBOX_CREATED("inbox.created"), + /** Triggered when the settings of an inbox change. */ + INBOX_UPDATED("inbox.updated"), + /** Triggered when an inbox is deleted. */ + INBOX_DELETED("inbox.deleted"), + /** Triggered when the first email of a thread lands in an inbox. */ + INBOX_THREAD_CREATED("inbox.thread.created"), + /** Triggered when an inbound email is added to a thread. */ + INBOX_EMAIL_RECEIVED("inbox.email.received"), + /** Triggered when an outbound email is accepted for delivery and added to a thread. */ + INBOX_EMAIL_SENT("inbox.email.sent"), + /** Triggered when a thread moves between folders. */ + INBOX_THREAD_FOLDER_UPDATED("inbox.thread.folder.updated"), + /** Triggered when a thread is assigned or reassigned. */ + INBOX_THREAD_ASSIGNED("inbox.thread.assigned"), + /** Triggered when a thread is unassigned. */ + INBOX_THREAD_UNASSIGNED("inbox.thread.unassigned"), + /** Triggered when a label is applied to or removed from a thread. */ + INBOX_THREAD_LABELS_UPDATED("inbox.thread.labels.updated"), + /** Triggered when a draft is created. */ + INBOX_DRAFT_CREATED("inbox.draft.created"), + /** Triggered when a draft is edited. */ + INBOX_DRAFT_UPDATED("inbox.draft.updated"), + /** Triggered when a draft is sent. */ + INBOX_DRAFT_SENT("inbox.draft.sent"), + /** Triggered when a draft is discarded. */ + INBOX_DRAFT_DELETED("inbox.draft.deleted"); private final String value; From 38183d17dc013f6e548ab3de13d00c3fa697a0d1 Mon Sep 17 00:00:00 2001 From: Felipe Freitag Date: Mon, 12 Oct 2026 14:05:31 -0300 Subject: [PATCH 05/30] chore: bump to v4.29.0 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index b3848c3..b2273de 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ GROUP=com.resend -VERSION_NAME=4.28.0 +VERSION_NAME=4.29.0 POM_URL=https://github.com/resendlabs/resend-java POM_SCM_URL=https://github.com/resendlabs/resend-java.git From de3c3b695661daa08e39363be458f298c4497b88 Mon Sep 17 00:00:00 2001 From: Felipe Freitag Date: Mon, 12 Oct 2026 14:09:07 -0300 Subject: [PATCH 06/30] docs(inboxes): reword boolean getter descriptions --- .../java/com/resend/services/inboxes/model/InboxThread.java | 2 +- .../resend/services/inboxes/model/InboxThreadSearchResult.java | 2 +- .../com/resend/services/inboxes/model/InboxThreadSummary.java | 2 +- .../inboxes/model/RemoveInboxThreadResponseSuccess.java | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/resend/services/inboxes/model/InboxThread.java b/src/main/java/com/resend/services/inboxes/model/InboxThread.java index 1efbc10..cb61cf9 100644 --- a/src/main/java/com/resend/services/inboxes/model/InboxThread.java +++ b/src/main/java/com/resend/services/inboxes/model/InboxThread.java @@ -179,7 +179,7 @@ public Boolean getHasDraft() { } /** - * Get true only when every email in the thread is read. + * Get whether every email in the thread is read. * * @return True only when every email in the thread is read. */ diff --git a/src/main/java/com/resend/services/inboxes/model/InboxThreadSearchResult.java b/src/main/java/com/resend/services/inboxes/model/InboxThreadSearchResult.java index 6235cb5..eaa424e 100644 --- a/src/main/java/com/resend/services/inboxes/model/InboxThreadSearchResult.java +++ b/src/main/java/com/resend/services/inboxes/model/InboxThreadSearchResult.java @@ -59,7 +59,7 @@ public String getMatchedEmailId() { } /** - * Get where {@code query} matched the email in {@code matched_email_id}, as text fragments with each match wrapped in {@code **}, keyed by {@code subject}, {@code body}, {@code from}, {@code to}, {@code cc}, {@code bcc} or {@code attachments}. Only keys with a match are present. Empty when there is no {@code query}. + * Get the places where {@code query} matched the email in {@code matched_email_id}, as text fragments with each match wrapped in {@code **}, keyed by {@code subject}, {@code body}, {@code from}, {@code to}, {@code cc}, {@code bcc} or {@code attachments}. Only keys with a match are present. Empty when there is no {@code query}. * * @return Where {@code query} matched the email in {@code matched_email_id}, as text fragments with each match wrapped in {@code **}, keyed by {@code subject}, {@code body}, {@code from}, {@code to}, {@code cc}, {@code bcc} or {@code attachments}. Only keys with a match are present. Empty when there is no {@code query}. */ diff --git a/src/main/java/com/resend/services/inboxes/model/InboxThreadSummary.java b/src/main/java/com/resend/services/inboxes/model/InboxThreadSummary.java index 19a2dc6..1bb4134 100644 --- a/src/main/java/com/resend/services/inboxes/model/InboxThreadSummary.java +++ b/src/main/java/com/resend/services/inboxes/model/InboxThreadSummary.java @@ -99,7 +99,7 @@ public List getLabels() { } /** - * Get true only when every email in the thread is read. + * Get whether every email in the thread is read. * * @return True only when every email in the thread is read. */ diff --git a/src/main/java/com/resend/services/inboxes/model/RemoveInboxThreadResponseSuccess.java b/src/main/java/com/resend/services/inboxes/model/RemoveInboxThreadResponseSuccess.java index d1b43db..9bfc682 100644 --- a/src/main/java/com/resend/services/inboxes/model/RemoveInboxThreadResponseSuccess.java +++ b/src/main/java/com/resend/services/inboxes/model/RemoveInboxThreadResponseSuccess.java @@ -55,7 +55,7 @@ public String getId() { } /** - * Get true when this call moved the thread to {@code trash}. False when the thread was already there. + * Get whether this call moved the thread to {@code trash}. False when the thread was already there. * * @return True when this call moved the thread to {@code trash}. False when the thread was already there. */ From 52f5bd0c60ac5f98f003f191a22f2236e742d218 Mon Sep 17 00:00:00 2001 From: Felipe Freitag Date: Mon, 12 Oct 2026 14:09:07 -0300 Subject: [PATCH 07/30] fix(inboxes): encode the inbox email address in the get path --- .../java/com/resend/services/inboxes/Inboxes.java | 14 +++++++++++++- .../com/resend/services/inboxes/InboxesTest.java | 10 +++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/resend/services/inboxes/Inboxes.java b/src/main/java/com/resend/services/inboxes/Inboxes.java index efbbd63..a79de4e 100644 --- a/src/main/java/com/resend/services/inboxes/Inboxes.java +++ b/src/main/java/com/resend/services/inboxes/Inboxes.java @@ -15,6 +15,10 @@ import com.resend.services.inboxes.model.UpdateInboxResponseSuccess; import okhttp3.MediaType; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + /** * Represents the Resend Inboxes module. */ @@ -121,7 +125,7 @@ public ListInboxesResponseSuccess list(ListParams params) throws ResendException * @throws ResendException If an error occurs while retrieving the inbox. */ public GetInboxResponseSuccess get(String inboxIdOrEmail) throws ResendException { - return execute("/inboxes/" + inboxIdOrEmail, HttpMethod.GET, null, MediaType.get("application/json"), GetInboxResponseSuccess.class); + return execute("/inboxes/" + encodePathSegment(inboxIdOrEmail), HttpMethod.GET, null, MediaType.get("application/json"), GetInboxResponseSuccess.class); } /** @@ -148,4 +152,12 @@ public UpdateInboxResponseSuccess update(String inboxId, UpdateInboxOptions upda public RemoveInboxResponseSuccess remove(String inboxId) throws ResendException { return execute("/inboxes/" + inboxId, HttpMethod.DELETE, null, MediaType.get("application/json"), RemoveInboxResponseSuccess.class); } + + private static String encodePathSegment(String value) { + try { + return URLEncoder.encode(value, StandardCharsets.UTF_8.name()).replace("+", "%20"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } } diff --git a/src/test/java/com/resend/services/inboxes/InboxesTest.java b/src/test/java/com/resend/services/inboxes/InboxesTest.java index d328de4..15bb6cb 100644 --- a/src/test/java/com/resend/services/inboxes/InboxesTest.java +++ b/src/test/java/com/resend/services/inboxes/InboxesTest.java @@ -92,7 +92,7 @@ public void testListInboxes_NoParams() throws ResendException { @Test public void testGetInboxByEmail_Success() throws ResendException { - when(httpClient.perform(eq("/inboxes/support@acme.com"), anyString(), eq(HttpMethod.GET), isNull(), any(MediaType.class))) + when(httpClient.perform(eq("/inboxes/support%40acme.com"), anyString(), eq(HttpMethod.GET), isNull(), any(MediaType.class))) .thenReturn(new AbstractHttpResponse<>(200, INBOX_JSON, true)); GetInboxResponseSuccess response = inboxes.get("support@acme.com"); @@ -109,6 +109,14 @@ public void testGetInboxByEmail_Success() throws ResendException { assertEquals("2026-08-01T09:12:44.512Z", response.getCreatedAt()); } + @Test + public void testGetInboxByEmail_EncodesPercentAndPlus() throws ResendException { + when(httpClient.perform(eq("/inboxes/a%25b%2Bc%40acme.com"), anyString(), eq(HttpMethod.GET), isNull(), any(MediaType.class))) + .thenReturn(new AbstractHttpResponse<>(200, INBOX_JSON, true)); + + assertEquals(INBOX_ID, inboxes.get("a%b+c@acme.com").getId()); + } + @Test public void testUpdateInbox_Success() throws ResendException { when(httpClient.perform(eq("/inboxes/" + INBOX_ID), anyString(), eq(HttpMethod.PATCH), eq("{\"from_name\":\"Ada from Support\"}"), any(MediaType.class))) From 87aef1a60d6852b2341e7d041769720be2b4066e Mon Sep 17 00:00:00 2001 From: Felipe Freitag Date: Mon, 12 Oct 2026 14:14:52 -0300 Subject: [PATCH 08/30] feat(inboxes): validate required fields in the request builders --- .../model/CreateInboxDraftOptions.java | 24 +++++++++++++++++++ .../model/CreateInboxLabelOptions.java | 17 +++++++++++++ .../inboxes/model/CreateInboxOptions.java | 17 +++++++++++++ .../model/ForwardInboxThreadEmailOptions.java | 4 ++++ .../inboxes/model/InboxAgentAction.java | 2 +- .../services/inboxes/model/InboxFolder.java | 2 +- .../inboxes/model/ListInboxThreadsParams.java | 6 +++++ .../model/ReplyInboxThreadEmailOptions.java | 17 +++++++++++++ .../model/SearchInboxThreadsParams.java | 6 +++++ .../model/UpdateInboxAgentOptions.java | 4 ++++ .../model/UpdateInboxDraftOptions.java | 4 ++++ .../inboxes/model/UpdateInboxOptions.java | 4 ++++ .../model/UpdateInboxThreadOptions.java | 8 +++++++ .../services/inboxes/InboxAgentTest.java | 5 ++++ .../services/inboxes/InboxDraftsTest.java | 11 +++++++++ .../services/inboxes/InboxLabelsTest.java | 5 ++++ .../inboxes/InboxThreadEmailsTest.java | 12 +++++++++- .../services/inboxes/InboxThreadsTest.java | 13 ++++++++++ .../resend/services/inboxes/InboxesTest.java | 7 ++++++ 19 files changed, 165 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/resend/services/inboxes/model/CreateInboxDraftOptions.java b/src/main/java/com/resend/services/inboxes/model/CreateInboxDraftOptions.java index 918d814..ffd4b30 100644 --- a/src/main/java/com/resend/services/inboxes/model/CreateInboxDraftOptions.java +++ b/src/main/java/com/resend/services/inboxes/model/CreateInboxDraftOptions.java @@ -313,9 +313,33 @@ public Builder replyToEmailId(String replyToEmailId) { * Build a new CreateInboxDraftOptions object. * * @return A new CreateInboxDraftOptions object. + * @throws IllegalArgumentException if every content field is empty, or only one of {@code thread_id} and {@code reply_to_email_id} is set. */ public CreateInboxDraftOptions build() { + if (isEmpty(to) && isEmpty(cc) && isEmpty(bcc) && isBlank(subject) && isBlank(text) && isBlank(html)) { + throw new IllegalArgumentException("At least one of to, cc, bcc, subject, text or html must be non-empty"); + } + if ((threadId == null) != (replyToEmailId == null)) { + throw new IllegalArgumentException("thread_id and reply_to_email_id must be provided together"); + } return new CreateInboxDraftOptions(this); } + + private static boolean isBlank(String value) { + if (value == null) { + return true; + } + for (int i = 0; i < value.length(); i++) { + char c = value.charAt(i); + if (!Character.isWhitespace(c) && !Character.isSpaceChar(c)) { + return false; + } + } + return true; + } + + private static boolean isEmpty(List values) { + return values == null || values.isEmpty(); + } } } diff --git a/src/main/java/com/resend/services/inboxes/model/CreateInboxLabelOptions.java b/src/main/java/com/resend/services/inboxes/model/CreateInboxLabelOptions.java index 309fc70..2517c50 100644 --- a/src/main/java/com/resend/services/inboxes/model/CreateInboxLabelOptions.java +++ b/src/main/java/com/resend/services/inboxes/model/CreateInboxLabelOptions.java @@ -84,9 +84,26 @@ public Builder color(String color) { * Build a new CreateInboxLabelOptions object. * * @return A new CreateInboxLabelOptions object. + * @throws IllegalArgumentException if {@code name} is null or blank. */ public CreateInboxLabelOptions build() { + if (isBlank(name)) { + throw new IllegalArgumentException("name must not be null or blank"); + } return new CreateInboxLabelOptions(this); } + + private static boolean isBlank(String value) { + if (value == null) { + return true; + } + for (int i = 0; i < value.length(); i++) { + char c = value.charAt(i); + if (!Character.isWhitespace(c) && !Character.isSpaceChar(c)) { + return false; + } + } + return true; + } } } diff --git a/src/main/java/com/resend/services/inboxes/model/CreateInboxOptions.java b/src/main/java/com/resend/services/inboxes/model/CreateInboxOptions.java index 767fac4..eba051e 100644 --- a/src/main/java/com/resend/services/inboxes/model/CreateInboxOptions.java +++ b/src/main/java/com/resend/services/inboxes/model/CreateInboxOptions.java @@ -134,9 +134,26 @@ public Builder forwarding(Boolean forwarding) { * Build a new CreateInboxOptions object. * * @return A new CreateInboxOptions object. + * @throws IllegalArgumentException if {@code email_address} is null or blank. */ public CreateInboxOptions build() { + if (isBlank(emailAddress)) { + throw new IllegalArgumentException("email_address must not be null or blank"); + } return new CreateInboxOptions(this); } + + private static boolean isBlank(String value) { + if (value == null) { + return true; + } + for (int i = 0; i < value.length(); i++) { + char c = value.charAt(i); + if (!Character.isWhitespace(c) && !Character.isSpaceChar(c)) { + return false; + } + } + return true; + } } } diff --git a/src/main/java/com/resend/services/inboxes/model/ForwardInboxThreadEmailOptions.java b/src/main/java/com/resend/services/inboxes/model/ForwardInboxThreadEmailOptions.java index 5ad891e..e2df09e 100644 --- a/src/main/java/com/resend/services/inboxes/model/ForwardInboxThreadEmailOptions.java +++ b/src/main/java/com/resend/services/inboxes/model/ForwardInboxThreadEmailOptions.java @@ -314,8 +314,12 @@ public Builder addAttachment(Attachment attachment) { * Build a new ForwardInboxThreadEmailOptions object. * * @return A new ForwardInboxThreadEmailOptions object. + * @throws IllegalArgumentException if {@code to} is null or empty. */ public ForwardInboxThreadEmailOptions build() { + if (to == null || to.isEmpty()) { + throw new IllegalArgumentException("At least one to recipient must be provided"); + } return new ForwardInboxThreadEmailOptions(this); } } diff --git a/src/main/java/com/resend/services/inboxes/model/InboxAgentAction.java b/src/main/java/com/resend/services/inboxes/model/InboxAgentAction.java index b9eca81..8b250d6 100644 --- a/src/main/java/com/resend/services/inboxes/model/InboxAgentAction.java +++ b/src/main/java/com/resend/services/inboxes/model/InboxAgentAction.java @@ -39,7 +39,7 @@ public String getValue() { } /** - * Creates a InboxAgentAction from a string value. + * Creates an InboxAgentAction from a string value. * * @param value The string value. * @return The corresponding InboxAgentAction. diff --git a/src/main/java/com/resend/services/inboxes/model/InboxFolder.java b/src/main/java/com/resend/services/inboxes/model/InboxFolder.java index 986d9b3..43261b0 100644 --- a/src/main/java/com/resend/services/inboxes/model/InboxFolder.java +++ b/src/main/java/com/resend/services/inboxes/model/InboxFolder.java @@ -35,7 +35,7 @@ public String getValue() { } /** - * Creates a InboxFolder from a string value. + * Creates an InboxFolder from a string value. * * @param value The string value. * @return The corresponding InboxFolder. diff --git a/src/main/java/com/resend/services/inboxes/model/ListInboxThreadsParams.java b/src/main/java/com/resend/services/inboxes/model/ListInboxThreadsParams.java index 9865cbd..372dae5 100644 --- a/src/main/java/com/resend/services/inboxes/model/ListInboxThreadsParams.java +++ b/src/main/java/com/resend/services/inboxes/model/ListInboxThreadsParams.java @@ -242,8 +242,14 @@ public Builder before(String before) { * Builds a new ListInboxThreadsParams instance. * * @return A new ListInboxThreadsParams. + * @throws IllegalArgumentException if both {@code after} and {@code before} are set. */ public ListInboxThreadsParams build() { + boolean hasAfter = after != null && !after.isEmpty(); + boolean hasBefore = before != null && !before.isEmpty(); + if (hasAfter && hasBefore) { + throw new IllegalArgumentException("after and before cannot be used together"); + } return new ListInboxThreadsParams(this); } } diff --git a/src/main/java/com/resend/services/inboxes/model/ReplyInboxThreadEmailOptions.java b/src/main/java/com/resend/services/inboxes/model/ReplyInboxThreadEmailOptions.java index f83f4fb..3cb38ce 100644 --- a/src/main/java/com/resend/services/inboxes/model/ReplyInboxThreadEmailOptions.java +++ b/src/main/java/com/resend/services/inboxes/model/ReplyInboxThreadEmailOptions.java @@ -289,9 +289,26 @@ public Builder addAttachment(Attachment attachment) { * Build a new ReplyInboxThreadEmailOptions object. * * @return A new ReplyInboxThreadEmailOptions object. + * @throws IllegalArgumentException if both {@code html} and {@code text} are null or blank. */ public ReplyInboxThreadEmailOptions build() { + if (isBlank(html) && isBlank(text)) { + throw new IllegalArgumentException("At least one of html or text must contain more than whitespace"); + } return new ReplyInboxThreadEmailOptions(this); } + + private static boolean isBlank(String value) { + if (value == null) { + return true; + } + for (int i = 0; i < value.length(); i++) { + char c = value.charAt(i); + if (!Character.isWhitespace(c) && !Character.isSpaceChar(c)) { + return false; + } + } + return true; + } } } diff --git a/src/main/java/com/resend/services/inboxes/model/SearchInboxThreadsParams.java b/src/main/java/com/resend/services/inboxes/model/SearchInboxThreadsParams.java index 74c7898..0373662 100644 --- a/src/main/java/com/resend/services/inboxes/model/SearchInboxThreadsParams.java +++ b/src/main/java/com/resend/services/inboxes/model/SearchInboxThreadsParams.java @@ -481,8 +481,14 @@ public Builder before(String before) { * Builds a new SearchInboxThreadsParams instance. * * @return A new SearchInboxThreadsParams. + * @throws IllegalArgumentException if both {@code after} and {@code before} are set. */ public SearchInboxThreadsParams build() { + boolean hasAfter = after != null && !after.isEmpty(); + boolean hasBefore = before != null && !before.isEmpty(); + if (hasAfter && hasBefore) { + throw new IllegalArgumentException("after and before cannot be used together"); + } return new SearchInboxThreadsParams(this); } } diff --git a/src/main/java/com/resend/services/inboxes/model/UpdateInboxAgentOptions.java b/src/main/java/com/resend/services/inboxes/model/UpdateInboxAgentOptions.java index f1d00db..f70a493 100644 --- a/src/main/java/com/resend/services/inboxes/model/UpdateInboxAgentOptions.java +++ b/src/main/java/com/resend/services/inboxes/model/UpdateInboxAgentOptions.java @@ -137,8 +137,12 @@ public Builder enabledActions(List enabledActions) { * Build a new UpdateInboxAgentOptions object. * * @return A new UpdateInboxAgentOptions object. + * @throws IllegalArgumentException if no field is set. */ public UpdateInboxAgentOptions build() { + if (fields.isEmpty()) { + throw new IllegalArgumentException("At least one of instructions, tone or enabled_actions must be provided"); + } return new UpdateInboxAgentOptions(this); } } diff --git a/src/main/java/com/resend/services/inboxes/model/UpdateInboxDraftOptions.java b/src/main/java/com/resend/services/inboxes/model/UpdateInboxDraftOptions.java index a11bbac..18dd4a6 100644 --- a/src/main/java/com/resend/services/inboxes/model/UpdateInboxDraftOptions.java +++ b/src/main/java/com/resend/services/inboxes/model/UpdateInboxDraftOptions.java @@ -214,8 +214,12 @@ public Builder text(String text) { * Build a new UpdateInboxDraftOptions object. * * @return A new UpdateInboxDraftOptions object. + * @throws IllegalArgumentException if no field is set. */ public UpdateInboxDraftOptions build() { + if (fields.isEmpty()) { + throw new IllegalArgumentException("At least one of to, cc, bcc, subject, text or html must be provided"); + } return new UpdateInboxDraftOptions(this); } } diff --git a/src/main/java/com/resend/services/inboxes/model/UpdateInboxOptions.java b/src/main/java/com/resend/services/inboxes/model/UpdateInboxOptions.java index ad61198..53e07e7 100644 --- a/src/main/java/com/resend/services/inboxes/model/UpdateInboxOptions.java +++ b/src/main/java/com/resend/services/inboxes/model/UpdateInboxOptions.java @@ -84,8 +84,12 @@ public Builder fromName(String fromName) { * Build a new UpdateInboxOptions object. * * @return A new UpdateInboxOptions object. + * @throws IllegalArgumentException if neither {@code name} nor {@code from_name} is set. */ public UpdateInboxOptions build() { + if (name == null && fromName == null) { + throw new IllegalArgumentException("At least one of name or from_name must be provided"); + } return new UpdateInboxOptions(this); } } diff --git a/src/main/java/com/resend/services/inboxes/model/UpdateInboxThreadOptions.java b/src/main/java/com/resend/services/inboxes/model/UpdateInboxThreadOptions.java index 1b34f20..669f6cc 100644 --- a/src/main/java/com/resend/services/inboxes/model/UpdateInboxThreadOptions.java +++ b/src/main/java/com/resend/services/inboxes/model/UpdateInboxThreadOptions.java @@ -4,6 +4,7 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; /** @@ -190,8 +191,15 @@ public Builder removeLabelIds(List removeLabelIds) { * Build a new UpdateInboxThreadOptions object. * * @return A new UpdateInboxThreadOptions object. + * @throws IllegalArgumentException if no field is set, or a label ID is both added and removed. */ public UpdateInboxThreadOptions build() { + if (read == null && folder == null && labelId == null && addLabelIds == null && removeLabelIds == null) { + throw new IllegalArgumentException("At least one of read, folder, label_id, add_label_ids or remove_label_ids must be provided"); + } + if (addLabelIds != null && removeLabelIds != null && !Collections.disjoint(addLabelIds, removeLabelIds)) { + throw new IllegalArgumentException("A label ID cannot be both added and removed in the same request"); + } return new UpdateInboxThreadOptions(this); } } diff --git a/src/test/java/com/resend/services/inboxes/InboxAgentTest.java b/src/test/java/com/resend/services/inboxes/InboxAgentTest.java index a20ffe8..6245585 100644 --- a/src/test/java/com/resend/services/inboxes/InboxAgentTest.java +++ b/src/test/java/com/resend/services/inboxes/InboxAgentTest.java @@ -83,6 +83,11 @@ public void testUpdateAgent_SendsNullToClear() throws ResendException { assertTrue(options.getEnabledActions().isEmpty()); } + @Test + public void testUpdateAgentBuilder_RequiresAField() { + assertThrows(IllegalArgumentException.class, () -> UpdateInboxAgentOptions.builder().build()); + } + @Test public void testAgentAction_UnknownValue_Throws() { assertEquals(InboxAgentAction.MARK_AS_SPAM, InboxAgentAction.fromValue("mark_as_spam")); diff --git a/src/test/java/com/resend/services/inboxes/InboxDraftsTest.java b/src/test/java/com/resend/services/inboxes/InboxDraftsTest.java index fba9174..bd023a7 100644 --- a/src/test/java/com/resend/services/inboxes/InboxDraftsTest.java +++ b/src/test/java/com/resend/services/inboxes/InboxDraftsTest.java @@ -84,6 +84,17 @@ public void testCreateDraft_EmptyToIsSent() throws ResendException { assertEquals(DRAFT_ID, drafts.create(INBOX_ID, options).getId()); } + @Test + public void testCreateDraftBuilder_Validation() { + assertThrows(IllegalArgumentException.class, () -> CreateInboxDraftOptions.builder().to(java.util.Collections.emptyList()).build()); + assertThrows(IllegalArgumentException.class, () -> CreateInboxDraftOptions.builder().text("Hi").threadId(THREAD_ID).build()); + } + + @Test + public void testUpdateDraftBuilder_RequiresAField() { + assertThrows(IllegalArgumentException.class, () -> UpdateInboxDraftOptions.builder().build()); + } + @Test public void testGetDraft_Success() throws ResendException { when(httpClient.perform(eq(DRAFTS_PATH + "/" + DRAFT_ID), anyString(), eq(HttpMethod.GET), isNull(), any(MediaType.class))) diff --git a/src/test/java/com/resend/services/inboxes/InboxLabelsTest.java b/src/test/java/com/resend/services/inboxes/InboxLabelsTest.java index 6daf7d0..c25402b 100644 --- a/src/test/java/com/resend/services/inboxes/InboxLabelsTest.java +++ b/src/test/java/com/resend/services/inboxes/InboxLabelsTest.java @@ -72,6 +72,11 @@ public void testCreateLabel_Duplicate_ThrowsResendException() throws ResendExcep assertEquals(422, (int) ex.getStatusCode()); } + @Test + public void testCreateLabelBuilder_RequiresName() { + assertThrows(IllegalArgumentException.class, () -> CreateInboxLabelOptions.builder().color("#E93D82").build()); + } + @Test public void testUpdateLabel_Success() throws ResendException { when(httpClient.perform(eq(LABELS_PATH + "/" + LABEL_ID), anyString(), eq(HttpMethod.PATCH), diff --git a/src/test/java/com/resend/services/inboxes/InboxThreadEmailsTest.java b/src/test/java/com/resend/services/inboxes/InboxThreadEmailsTest.java index a184287..8f71f10 100644 --- a/src/test/java/com/resend/services/inboxes/InboxThreadEmailsTest.java +++ b/src/test/java/com/resend/services/inboxes/InboxThreadEmailsTest.java @@ -95,12 +95,22 @@ public void testForward_Success() throws ResendException { assertEquals(SENT_EMAIL_ID, response.getId()); } + @Test + public void testReplyBuilder_RequiresBody() { + assertThrows(IllegalArgumentException.class, () -> ReplyInboxThreadEmailOptions.builder().subject("Re: hi").text(" ").build()); + } + + @Test + public void testForwardBuilder_RequiresTo() { + assertThrows(IllegalArgumentException.class, () -> ForwardInboxThreadEmailOptions.builder().text("FYI").build()); + } + @Test public void testReply_ValidationError_ThrowsResendException() throws ResendException { when(httpClient.perform(eq(EMAILS_PATH + "/" + EMAIL_ID + "/reply"), anyString(), eq(HttpMethod.POST), anyString(), any(MediaType.class))) .thenReturn(new AbstractHttpResponse<>(422, "{\"name\":\"validation_error\",\"message\":\"Missing body\"}", false)); - ReplyInboxThreadEmailOptions options = ReplyInboxThreadEmailOptions.builder().subject("Re: hi").build(); + ReplyInboxThreadEmailOptions options = ReplyInboxThreadEmailOptions.builder().text("Thanks").build(); ResendException ex = assertThrows(ResendException.class, () -> emails.reply(INBOX_ID, THREAD_ID, EMAIL_ID, options)); assertEquals(422, (int) ex.getStatusCode()); } diff --git a/src/test/java/com/resend/services/inboxes/InboxThreadsTest.java b/src/test/java/com/resend/services/inboxes/InboxThreadsTest.java index 90d5876..cbdd924 100644 --- a/src/test/java/com/resend/services/inboxes/InboxThreadsTest.java +++ b/src/test/java/com/resend/services/inboxes/InboxThreadsTest.java @@ -159,6 +159,19 @@ public void testRemoveThread_Success() throws ResendException { assertFalse(response.getDeleted()); } + @Test + public void testUpdateThreadBuilder_Validation() { + assertThrows(IllegalArgumentException.class, () -> UpdateInboxThreadOptions.builder().build()); + assertThrows(IllegalArgumentException.class, () -> UpdateInboxThreadOptions.builder() + .addLabelIds(LABEL_ID).removeLabelIds(LABEL_ID).build()); + } + + @Test + public void testParamsBuilders_RejectBothCursors() { + assertThrows(IllegalArgumentException.class, () -> ListInboxThreadsParams.builder().after(THREAD_ID).before(THREAD_ID).build()); + assertThrows(IllegalArgumentException.class, () -> SearchInboxThreadsParams.builder().after(THREAD_ID).before(THREAD_ID).build()); + } + @Test public void testFolder_UnknownValue_Throws() { assertEquals(InboxFolder.TRASH, InboxFolder.fromValue("trash")); diff --git a/src/test/java/com/resend/services/inboxes/InboxesTest.java b/src/test/java/com/resend/services/inboxes/InboxesTest.java index 15bb6cb..a9ac447 100644 --- a/src/test/java/com/resend/services/inboxes/InboxesTest.java +++ b/src/test/java/com/resend/services/inboxes/InboxesTest.java @@ -117,6 +117,13 @@ public void testGetInboxByEmail_EncodesPercentAndPlus() throws ResendException { assertEquals(INBOX_ID, inboxes.get("a%b+c@acme.com").getId()); } + @Test + public void testBuilders_RequireFields() { + assertThrows(IllegalArgumentException.class, () -> CreateInboxOptions.builder().name("Support").build()); + assertThrows(IllegalArgumentException.class, () -> UpdateInboxOptions.builder().build()); + assertEquals("", UpdateInboxOptions.builder().fromName("").build().getFromName()); + } + @Test public void testUpdateInbox_Success() throws ResendException { when(httpClient.perform(eq("/inboxes/" + INBOX_ID), anyString(), eq(HttpMethod.PATCH), eq("{\"from_name\":\"Ada from Support\"}"), any(MediaType.class))) From be7d44de62fd8850b165382e3a9cc0d3210d1f74 Mon Sep 17 00:00:00 2001 From: Felipe Freitag Date: Mon, 12 Oct 2026 14:19:48 -0300 Subject: [PATCH 09/30] feat(inboxes): reject empty label ID lists and a blank inbox name --- .../resend/services/inboxes/model/UpdateInboxOptions.java | 5 ++++- .../services/inboxes/model/UpdateInboxThreadOptions.java | 5 ++++- .../java/com/resend/services/inboxes/InboxThreadsTest.java | 1 + src/test/java/com/resend/services/inboxes/InboxesTest.java | 1 + 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/resend/services/inboxes/model/UpdateInboxOptions.java b/src/main/java/com/resend/services/inboxes/model/UpdateInboxOptions.java index 53e07e7..b6f1159 100644 --- a/src/main/java/com/resend/services/inboxes/model/UpdateInboxOptions.java +++ b/src/main/java/com/resend/services/inboxes/model/UpdateInboxOptions.java @@ -84,12 +84,15 @@ public Builder fromName(String fromName) { * Build a new UpdateInboxOptions object. * * @return A new UpdateInboxOptions object. - * @throws IllegalArgumentException if neither {@code name} nor {@code from_name} is set. + * @throws IllegalArgumentException if neither {@code name} nor {@code from_name} is set, or {@code name} is blank. */ public UpdateInboxOptions build() { if (name == null && fromName == null) { throw new IllegalArgumentException("At least one of name or from_name must be provided"); } + if (name != null && name.trim().isEmpty()) { + throw new IllegalArgumentException("name must not be blank"); + } return new UpdateInboxOptions(this); } } diff --git a/src/main/java/com/resend/services/inboxes/model/UpdateInboxThreadOptions.java b/src/main/java/com/resend/services/inboxes/model/UpdateInboxThreadOptions.java index 669f6cc..8c5ef71 100644 --- a/src/main/java/com/resend/services/inboxes/model/UpdateInboxThreadOptions.java +++ b/src/main/java/com/resend/services/inboxes/model/UpdateInboxThreadOptions.java @@ -191,12 +191,15 @@ public Builder removeLabelIds(List removeLabelIds) { * Build a new UpdateInboxThreadOptions object. * * @return A new UpdateInboxThreadOptions object. - * @throws IllegalArgumentException if no field is set, or a label ID is both added and removed. + * @throws IllegalArgumentException if no field is set, a label ID list is empty, or a label ID is both added and removed. */ public UpdateInboxThreadOptions build() { if (read == null && folder == null && labelId == null && addLabelIds == null && removeLabelIds == null) { throw new IllegalArgumentException("At least one of read, folder, label_id, add_label_ids or remove_label_ids must be provided"); } + if ((addLabelIds != null && addLabelIds.isEmpty()) || (removeLabelIds != null && removeLabelIds.isEmpty())) { + throw new IllegalArgumentException("add_label_ids and remove_label_ids must contain at least one label ID"); + } if (addLabelIds != null && removeLabelIds != null && !Collections.disjoint(addLabelIds, removeLabelIds)) { throw new IllegalArgumentException("A label ID cannot be both added and removed in the same request"); } diff --git a/src/test/java/com/resend/services/inboxes/InboxThreadsTest.java b/src/test/java/com/resend/services/inboxes/InboxThreadsTest.java index cbdd924..e2e9f67 100644 --- a/src/test/java/com/resend/services/inboxes/InboxThreadsTest.java +++ b/src/test/java/com/resend/services/inboxes/InboxThreadsTest.java @@ -164,6 +164,7 @@ public void testUpdateThreadBuilder_Validation() { assertThrows(IllegalArgumentException.class, () -> UpdateInboxThreadOptions.builder().build()); assertThrows(IllegalArgumentException.class, () -> UpdateInboxThreadOptions.builder() .addLabelIds(LABEL_ID).removeLabelIds(LABEL_ID).build()); + assertThrows(IllegalArgumentException.class, () -> UpdateInboxThreadOptions.builder().addLabelIds().build()); } @Test diff --git a/src/test/java/com/resend/services/inboxes/InboxesTest.java b/src/test/java/com/resend/services/inboxes/InboxesTest.java index a9ac447..43c80cb 100644 --- a/src/test/java/com/resend/services/inboxes/InboxesTest.java +++ b/src/test/java/com/resend/services/inboxes/InboxesTest.java @@ -121,6 +121,7 @@ public void testGetInboxByEmail_EncodesPercentAndPlus() throws ResendException { public void testBuilders_RequireFields() { assertThrows(IllegalArgumentException.class, () -> CreateInboxOptions.builder().name("Support").build()); assertThrows(IllegalArgumentException.class, () -> UpdateInboxOptions.builder().build()); + assertThrows(IllegalArgumentException.class, () -> UpdateInboxOptions.builder().name(" ").build()); assertEquals("", UpdateInboxOptions.builder().fromName("").build().getFromName()); } From 152363e021b46fbcbae9bf314ce5a9c9b7b0e80a Mon Sep 17 00:00:00 2001 From: Felipe Freitag Date: Tue, 13 Oct 2026 09:42:08 -0300 Subject: [PATCH 10/30] feat(inboxes): remove label_id from the thread update --- .../resend/services/inboxes/InboxThreads.java | 3 +- .../model/UpdateInboxThreadOptions.java | 40 +++---------------- .../services/inboxes/InboxThreadsTest.java | 11 ----- 3 files changed, 7 insertions(+), 47 deletions(-) diff --git a/src/main/java/com/resend/services/inboxes/InboxThreads.java b/src/main/java/com/resend/services/inboxes/InboxThreads.java index 96436eb..8f81a21 100644 --- a/src/main/java/com/resend/services/inboxes/InboxThreads.java +++ b/src/main/java/com/resend/services/inboxes/InboxThreads.java @@ -115,7 +115,8 @@ public GetInboxThreadResponseSuccess get(String inboxId, String threadId) throws /** * Marks a thread read or unread, moves it to a folder, or adds and removes labels. - * A label ID cannot be both added and removed in the same request. + * A label ID cannot be both added and removed in the same request. Removing a label from a thread keeps + * the label itself. * Moving a thread out of {@code trash} restores it and clears its deletion deadline. * * @param inboxId The inbox ID. An email address is not accepted. diff --git a/src/main/java/com/resend/services/inboxes/model/UpdateInboxThreadOptions.java b/src/main/java/com/resend/services/inboxes/model/UpdateInboxThreadOptions.java index 8c5ef71..539a89e 100644 --- a/src/main/java/com/resend/services/inboxes/model/UpdateInboxThreadOptions.java +++ b/src/main/java/com/resend/services/inboxes/model/UpdateInboxThreadOptions.java @@ -8,7 +8,7 @@ import java.util.List; /** - * Represents a request to update a thread. Provide at least one of {@code read}, {@code folder}, {@code label_id}, {@code add_label_ids}, or {@code remove_label_ids}. A label ID cannot be both added and removed in the same request. + * Represents a request to update a thread. Provide at least one of {@code read}, {@code folder}, {@code add_label_ids}, or {@code remove_label_ids}. A label ID cannot be both added and removed in the same request. */ public class UpdateInboxThreadOptions { @@ -18,10 +18,6 @@ public class UpdateInboxThreadOptions { @JsonProperty("folder") private final MoveInboxThreadFolder folder; - @Deprecated - @JsonProperty("label_id") - private final String labelId; - @JsonProperty("add_label_ids") private final List addLabelIds; @@ -31,7 +27,6 @@ public class UpdateInboxThreadOptions { private UpdateInboxThreadOptions(Builder builder) { this.read = builder.read; this.folder = builder.folder; - this.labelId = builder.labelId; this.addLabelIds = builder.addLabelIds; this.removeLabelIds = builder.removeLabelIds; } @@ -54,17 +49,6 @@ public MoveInboxThreadFolder getFolder() { return folder; } - /** - * Get the label to apply to the thread, the same as one entry in {@code add_label_ids}. - * - * @return The label to apply to the thread, the same as one entry in {@code add_label_ids}. - * @deprecated Use {@link Builder#addLabelIds(String...)} instead. - */ - @Deprecated - public String getLabelId() { - return labelId; - } - /** * Get the label IDs to apply to the thread. * @@ -98,7 +82,6 @@ public static Builder builder() { public static class Builder { private Boolean read; private MoveInboxThreadFolder folder; - private String labelId; private List addLabelIds; private List removeLabelIds; @@ -130,19 +113,6 @@ public Builder folder(MoveInboxThreadFolder folder) { return this; } - /** - * Set the label to apply to the thread, the same as one entry in {@code add_label_ids}. A label ID that does not exist in the inbox returns a 404. - * - * @param labelId The label to apply to the thread, the same as one entry in {@code add_label_ids}. - * @return The builder instance. - * @deprecated Use {@link Builder#addLabelIds(String...)} instead. - */ - @Deprecated - public Builder labelId(String labelId) { - this.labelId = labelId; - return this; - } - /** * Set the label IDs to apply to the thread. Between 1 and 10 IDs. A label ID that does not exist in the inbox returns a 404. * @@ -166,7 +136,7 @@ public Builder addLabelIds(List addLabelIds) { } /** - * Set the label IDs to remove from the thread. Between 1 and 10 IDs. A label ID that does not exist in the inbox returns a 404. + * Set the label IDs to remove from the thread. Between 1 and 10 IDs. The labels themselves are kept. A label ID that does not exist in the inbox returns a 404. * * @param removeLabelIds The label IDs to remove from the thread. * @return The builder instance. @@ -177,7 +147,7 @@ public Builder removeLabelIds(String... removeLabelIds) { } /** - * Set the label IDs to remove from the thread. Between 1 and 10 IDs. A label ID that does not exist in the inbox returns a 404. + * Set the label IDs to remove from the thread. Between 1 and 10 IDs. The labels themselves are kept. A label ID that does not exist in the inbox returns a 404. * * @param removeLabelIds The label IDs to remove from the thread. * @return The builder instance. @@ -194,8 +164,8 @@ public Builder removeLabelIds(List removeLabelIds) { * @throws IllegalArgumentException if no field is set, a label ID list is empty, or a label ID is both added and removed. */ public UpdateInboxThreadOptions build() { - if (read == null && folder == null && labelId == null && addLabelIds == null && removeLabelIds == null) { - throw new IllegalArgumentException("At least one of read, folder, label_id, add_label_ids or remove_label_ids must be provided"); + if (read == null && folder == null && addLabelIds == null && removeLabelIds == null) { + throw new IllegalArgumentException("At least one of read, folder, add_label_ids or remove_label_ids must be provided"); } if ((addLabelIds != null && addLabelIds.isEmpty()) || (removeLabelIds != null && removeLabelIds.isEmpty())) { throw new IllegalArgumentException("add_label_ids and remove_label_ids must contain at least one label ID"); diff --git a/src/test/java/com/resend/services/inboxes/InboxThreadsTest.java b/src/test/java/com/resend/services/inboxes/InboxThreadsTest.java index e2e9f67..df514db 100644 --- a/src/test/java/com/resend/services/inboxes/InboxThreadsTest.java +++ b/src/test/java/com/resend/services/inboxes/InboxThreadsTest.java @@ -137,17 +137,6 @@ public void testUpdateThread_AddAndRemoveLabels() throws ResendException { assertEquals(THREAD_ID, response.getId()); } - @Test - @SuppressWarnings("deprecation") - public void testUpdateThread_DeprecatedLabelId() throws ResendException { - UpdateInboxThreadOptions options = UpdateInboxThreadOptions.builder().labelId(LABEL_ID).build(); - when(httpClient.perform(eq(THREADS_PATH + "/" + THREAD_ID), anyString(), eq(HttpMethod.PATCH), - eq("{\"label_id\":\"" + LABEL_ID + "\"}"), any(MediaType.class))) - .thenReturn(new AbstractHttpResponse<>(200, SUMMARY_JSON, true)); - - assertNotNull(threads.update(INBOX_ID, THREAD_ID, options)); - } - @Test public void testRemoveThread_Success() throws ResendException { when(httpClient.perform(eq(THREADS_PATH + "/" + THREAD_ID), anyString(), eq(HttpMethod.DELETE), isNull(), any(MediaType.class))) From f1d068488767bd0e76e95e1ae49a389bba8a8bba Mon Sep 17 00:00:00 2001 From: Felipe Freitag Date: Tue, 13 Oct 2026 09:42:08 -0300 Subject: [PATCH 11/30] feat(inboxes): add attachments to the inbox drafts --- .../resend/services/inboxes/InboxDrafts.java | 3 +- .../model/CreateInboxDraftOptions.java | 65 ++++++++++++++- .../inboxes/model/DraftAttachments.java | 35 ++++++++ .../model/GetInboxDraftResponseSuccess.java | 5 +- .../services/inboxes/model/InboxDraft.java | 16 +++- .../inboxes/model/InboxDraftAttachment.java | 79 +++++++++++++++++++ .../inboxes/model/InboxDraftListItem.java | 16 +++- .../model/UpdateInboxDraftOptions.java | 56 ++++++++++++- .../UpdateInboxDraftResponseSuccess.java | 5 +- .../services/inboxes/InboxDraftsTest.java | 45 ++++++++++- 10 files changed, 309 insertions(+), 16 deletions(-) create mode 100644 src/main/java/com/resend/services/inboxes/model/DraftAttachments.java create mode 100644 src/main/java/com/resend/services/inboxes/model/InboxDraftAttachment.java diff --git a/src/main/java/com/resend/services/inboxes/InboxDrafts.java b/src/main/java/com/resend/services/inboxes/InboxDrafts.java index 4cd89b5..40c6a24 100644 --- a/src/main/java/com/resend/services/inboxes/InboxDrafts.java +++ b/src/main/java/com/resend/services/inboxes/InboxDrafts.java @@ -95,7 +95,8 @@ public GetInboxDraftResponseSuccess get(String inboxId, String draftId) throws R /** * Updates the content of an unsent draft. Omitted fields are left unchanged. A request that sets - * {@code text} or {@code html} replaces the whole body. The body part you omit from that request becomes + * {@code text} or {@code html} replaces the whole body. + * Sending {@code attachments} replaces every attachment on the draft, and an empty list removes them all. The body part you omit from that request becomes * {@code null}. A draft that was already sent, or that changed during the request, returns a 409. * * @param inboxId The inbox ID. An email address is not accepted. diff --git a/src/main/java/com/resend/services/inboxes/model/CreateInboxDraftOptions.java b/src/main/java/com/resend/services/inboxes/model/CreateInboxDraftOptions.java index ffd4b30..7f7d811 100644 --- a/src/main/java/com/resend/services/inboxes/model/CreateInboxDraftOptions.java +++ b/src/main/java/com/resend/services/inboxes/model/CreateInboxDraftOptions.java @@ -1,13 +1,14 @@ package com.resend.services.inboxes.model; import com.fasterxml.jackson.annotation.JsonProperty; +import com.resend.services.emails.model.Attachment; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** - * Represents a request to create a draft. At least one of {@code to}, {@code cc}, {@code bcc}, {@code subject}, {@code text} or {@code html} must be non-empty. The combined recipients of {@code to}, {@code cc} and {@code bcc} cannot exceed 50. {@code thread_id} and {@code reply_to_email_id} must be sent together or not at all. + * Represents a request to create a draft. At least one of {@code to}, {@code cc}, {@code bcc}, {@code subject}, {@code text}, {@code html} or {@code attachments} must be non-empty. The combined recipients of {@code to}, {@code cc} and {@code bcc} cannot exceed 50. {@code thread_id} and {@code reply_to_email_id} must be sent together or not at all. */ public class CreateInboxDraftOptions { @@ -29,6 +30,9 @@ public class CreateInboxDraftOptions { @JsonProperty("text") private final String text; + @JsonProperty("attachments") + private final List attachments; + @JsonProperty("thread_id") private final String threadId; @@ -42,6 +46,7 @@ private CreateInboxDraftOptions(Builder builder) { this.subject = builder.subject; this.html = builder.html; this.text = builder.text; + this.attachments = builder.attachments; this.threadId = builder.threadId; this.replyToEmailId = builder.replyToEmailId; } @@ -100,6 +105,15 @@ public String getText() { return text; } + /** + * Get the files to store on the draft. + * + * @return The files to store on the draft. + */ + public List getAttachments() { + return attachments; + } + /** * Get the Thread ID to reply in. * @@ -137,6 +151,7 @@ public static class Builder { private String subject; private String html; private String text; + private List attachments; private String threadId; private String replyToEmailId; @@ -287,6 +302,46 @@ public Builder text(String text) { return this; } + /** + * Set the files to store on the draft. Each one needs {@code content}, {@code filename} and + * {@code content_type}. A {@code path} or {@code content_id} is not accepted. A draft can carry up to + * 10 files and 25 MB in total. + * + * @param attachments The files to store on the draft. + * @return The builder instance. + */ + public Builder attachments(Attachment... attachments) { + this.attachments = new ArrayList<>(Arrays.asList(attachments)); + return this; + } + + /** + * Set the files to store on the draft. Each one needs {@code content}, {@code filename} and + * {@code content_type}. A {@code path} or {@code content_id} is not accepted. A draft can carry up to + * 10 files and 25 MB in total. + * + * @param attachments The files to store on the draft. + * @return The builder instance. + */ + public Builder attachments(List attachments) { + this.attachments = attachments == null ? null : new ArrayList<>(attachments); + return this; + } + + /** + * Add one file to store on the draft. + * + * @param attachment The file to add. + * @return The builder instance. + */ + public Builder addAttachment(Attachment attachment) { + if (this.attachments == null) { + this.attachments = new ArrayList<>(); + } + this.attachments.add(attachment); + return this; + } + /** * Set the Thread ID to reply in. Must be sent with {@code reply_to_email_id}. * @@ -313,12 +368,14 @@ public Builder replyToEmailId(String replyToEmailId) { * Build a new CreateInboxDraftOptions object. * * @return A new CreateInboxDraftOptions object. - * @throws IllegalArgumentException if every content field is empty, or only one of {@code thread_id} and {@code reply_to_email_id} is set. + * @throws IllegalArgumentException if every content field is empty, an attachment lacks {@code content}, {@code filename} or {@code content_type} or has a {@code path} or {@code content_id}, or only one of {@code thread_id} and {@code reply_to_email_id} is set. */ public CreateInboxDraftOptions build() { - if (isEmpty(to) && isEmpty(cc) && isEmpty(bcc) && isBlank(subject) && isBlank(text) && isBlank(html)) { - throw new IllegalArgumentException("At least one of to, cc, bcc, subject, text or html must be non-empty"); + if (isEmpty(to) && isEmpty(cc) && isEmpty(bcc) && isBlank(subject) && isBlank(text) && isBlank(html) + && (attachments == null || attachments.isEmpty())) { + throw new IllegalArgumentException("At least one of to, cc, bcc, subject, text, html or attachments must be non-empty"); } + DraftAttachments.validate(attachments); if ((threadId == null) != (replyToEmailId == null)) { throw new IllegalArgumentException("thread_id and reply_to_email_id must be provided together"); } diff --git a/src/main/java/com/resend/services/inboxes/model/DraftAttachments.java b/src/main/java/com/resend/services/inboxes/model/DraftAttachments.java new file mode 100644 index 0000000..4626082 --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/model/DraftAttachments.java @@ -0,0 +1,35 @@ +package com.resend.services.inboxes.model; + +import com.resend.services.emails.model.Attachment; + +import java.util.List; + +final class DraftAttachments { + + private DraftAttachments() { + } + + static void validate(List attachments) { + if (attachments == null) { + return; + } + if (attachments.size() > 10) { + throw new IllegalArgumentException("A draft can carry at most 10 attachments"); + } + for (Attachment attachment : attachments) { + if (attachment == null) { + throw new IllegalArgumentException("attachments must not contain null"); + } + if (isEmpty(attachment.getContent()) || isEmpty(attachment.getFileName()) || isEmpty(attachment.getContentType())) { + throw new IllegalArgumentException("Each draft attachment needs content, filename and content_type"); + } + if (attachment.getPath() != null || attachment.getContentId() != null) { + throw new IllegalArgumentException("A draft attachment cannot have a path or content_id"); + } + } + } + + private static boolean isEmpty(String value) { + return value == null || value.isEmpty(); + } +} diff --git a/src/main/java/com/resend/services/inboxes/model/GetInboxDraftResponseSuccess.java b/src/main/java/com/resend/services/inboxes/model/GetInboxDraftResponseSuccess.java index 1d66e38..6a26998 100644 --- a/src/main/java/com/resend/services/inboxes/model/GetInboxDraftResponseSuccess.java +++ b/src/main/java/com/resend/services/inboxes/model/GetInboxDraftResponseSuccess.java @@ -32,8 +32,9 @@ public GetInboxDraftResponseSuccess() { * @param emailId The ID of the outbound email queued when the draft was sent. {@code null} until the draft is sent. * @param createdAt The timestamp when the draft was created. * @param updatedAt The timestamp when the draft was last saved. + * @param attachments The files stored on the draft. */ - public GetInboxDraftResponseSuccess(String object, String id, String type, List to, List cc, List bcc, String subject, String html, String text, String threadId, String replyToEmailId, String emailId, String createdAt, String updatedAt) { - super(object, id, type, to, cc, bcc, subject, html, text, threadId, replyToEmailId, emailId, createdAt, updatedAt); + public GetInboxDraftResponseSuccess(String object, String id, String type, List to, List cc, List bcc, String subject, String html, String text, String threadId, String replyToEmailId, String emailId, String createdAt, String updatedAt, List attachments) { + super(object, id, type, to, cc, bcc, subject, html, text, threadId, replyToEmailId, emailId, createdAt, updatedAt, attachments); } } diff --git a/src/main/java/com/resend/services/inboxes/model/InboxDraft.java b/src/main/java/com/resend/services/inboxes/model/InboxDraft.java index c8f3f52..d703707 100644 --- a/src/main/java/com/resend/services/inboxes/model/InboxDraft.java +++ b/src/main/java/com/resend/services/inboxes/model/InboxDraft.java @@ -51,6 +51,9 @@ public class InboxDraft { @JsonProperty("updated_at") private String updatedAt; + @JsonProperty("attachments") + private List attachments; + /** * Default constructor */ @@ -75,8 +78,9 @@ public InboxDraft() { * @param emailId The ID of the outbound email queued when the draft was sent. {@code null} until the draft is sent. * @param createdAt The timestamp when the draft was created. * @param updatedAt The timestamp when the draft was last saved. + * @param attachments The files stored on the draft. */ - public InboxDraft(String object, String id, String type, List to, List cc, List bcc, String subject, String html, String text, String threadId, String replyToEmailId, String emailId, String createdAt, String updatedAt) { + public InboxDraft(String object, String id, String type, List to, List cc, List bcc, String subject, String html, String text, String threadId, String replyToEmailId, String emailId, String createdAt, String updatedAt, List attachments) { this.object = object; this.id = id; this.type = type; @@ -91,6 +95,7 @@ public InboxDraft(String object, String id, String type, List to, List getAttachments() { + return attachments; + } } diff --git a/src/main/java/com/resend/services/inboxes/model/InboxDraftAttachment.java b/src/main/java/com/resend/services/inboxes/model/InboxDraftAttachment.java new file mode 100644 index 0000000..8dcd034 --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/model/InboxDraftAttachment.java @@ -0,0 +1,79 @@ +package com.resend.services.inboxes.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Represents a file stored on a draft. + */ +public class InboxDraftAttachment { + + @JsonProperty("id") + private String id; + + @JsonProperty("filename") + private String filename; + + @JsonProperty("content_type") + private String contentType; + + @JsonProperty("size") + private Integer size; + + /** + * Default constructor + */ + public InboxDraftAttachment() { + + } + + /** + * Constructs a InboxDraftAttachment. + * + * @param id The ID of the attachment. + * @param filename The filename of the attachment. + * @param contentType The MIME type of the attachment. + * @param size The size of the attachment in bytes. + */ + public InboxDraftAttachment(String id, String filename, String contentType, Integer size) { + this.id = id; + this.filename = filename; + this.contentType = contentType; + this.size = size; + } + + /** + * Get the ID of the attachment. + * + * @return The ID of the attachment. + */ + public String getId() { + return id; + } + + /** + * Get the filename of the attachment. + * + * @return The filename of the attachment. + */ + public String getFilename() { + return filename; + } + + /** + * Get the MIME type of the attachment. + * + * @return The MIME type of the attachment. + */ + public String getContentType() { + return contentType; + } + + /** + * Get the size of the attachment in bytes. + * + * @return The size of the attachment in bytes. + */ + public Integer getSize() { + return size; + } +} diff --git a/src/main/java/com/resend/services/inboxes/model/InboxDraftListItem.java b/src/main/java/com/resend/services/inboxes/model/InboxDraftListItem.java index bed35c3..b389e7d 100644 --- a/src/main/java/com/resend/services/inboxes/model/InboxDraftListItem.java +++ b/src/main/java/com/resend/services/inboxes/model/InboxDraftListItem.java @@ -39,6 +39,9 @@ public class InboxDraftListItem { @JsonProperty("updated_at") private String updatedAt; + @JsonProperty("attachments") + private List attachments; + /** * Default constructor */ @@ -59,8 +62,9 @@ public InboxDraftListItem() { * @param threadId The Thread ID when the draft is a reply. {@code null} otherwise. * @param replyToEmailId The Email ID the draft replies to. {@code null} otherwise. * @param updatedAt The timestamp when the draft was last saved. + * @param attachments The files stored on the draft. */ - public InboxDraftListItem(String id, String type, List to, List cc, List bcc, String subject, String snippet, String threadId, String replyToEmailId, String updatedAt) { + public InboxDraftListItem(String id, String type, List to, List cc, List bcc, String subject, String snippet, String threadId, String replyToEmailId, String updatedAt, List attachments) { this.id = id; this.type = type; this.to = to; @@ -71,6 +75,7 @@ public InboxDraftListItem(String id, String type, List to, List this.threadId = threadId; this.replyToEmailId = replyToEmailId; this.updatedAt = updatedAt; + this.attachments = attachments; } /** @@ -162,4 +167,13 @@ public String getReplyToEmailId() { public String getUpdatedAt() { return updatedAt; } + + /** + * Get the files stored on the draft. + * + * @return The files stored on the draft. + */ + public List getAttachments() { + return attachments; + } } diff --git a/src/main/java/com/resend/services/inboxes/model/UpdateInboxDraftOptions.java b/src/main/java/com/resend/services/inboxes/model/UpdateInboxDraftOptions.java index 18dd4a6..c410557 100644 --- a/src/main/java/com/resend/services/inboxes/model/UpdateInboxDraftOptions.java +++ b/src/main/java/com/resend/services/inboxes/model/UpdateInboxDraftOptions.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.annotation.JsonAnyGetter; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonInclude; +import com.resend.services.emails.model.Attachment; import java.util.ArrayList; import java.util.Arrays; @@ -13,7 +14,8 @@ /** * Represents a request to update a draft. - * Send at least one of {@code to}, {@code cc}, {@code bcc}, {@code subject}, {@code text} or {@code html}. + * Send at least one of {@code to}, {@code cc}, {@code bcc}, {@code subject}, {@code text}, {@code html} or + * {@code attachments}. Sending {@code attachments} replaces every attachment on the draft. * Omitted fields are left unchanged. Setting {@code text} or {@code html} replaces the whole body, * so the body part you omit becomes {@code null}. The merged draft must contain at least one non-empty field, * and its combined recipients cannot exceed 50. @@ -90,6 +92,16 @@ public String getText() { return (String) fields.get("text"); } + /** + * Get the files to store on the draft. + * + * @return The files, or null when they are not set. + */ + @SuppressWarnings("unchecked") + public List getAttachments() { + return (List) fields.get("attachments"); + } + /** * Create a new builder instance for constructing UpdateInboxDraftOptions objects. * @@ -210,17 +222,55 @@ public Builder text(String text) { return this; } + /** + * Set the files to store on the draft. Replaces every attachment on the draft. Pass no files to remove + * them all. Each one needs {@code content}, {@code filename} and {@code content_type}. A {@code path} or + * {@code content_id} is not accepted. A draft can carry up to 10 files and 25 MB in total. + * + * @param attachments The files to store on the draft. + * @return The builder instance. + */ + public Builder attachments(Attachment... attachments) { + fields.put("attachments", new ArrayList<>(Arrays.asList(attachments))); + return this; + } + + /** + * Set the files to store on the draft. Replaces every attachment on the draft. Pass an empty list to + * remove them all. Pass null to leave the attachments unchanged. Each one needs {@code content}, + * {@code filename} and {@code content_type}. A {@code path} or {@code content_id} is not accepted. + * A draft can carry up to 10 files and 25 MB in total. + * + * @param attachments The files to store on the draft. + * @return The builder instance. + */ + public Builder attachments(List attachments) { + if (attachments == null) { + fields.remove("attachments"); + } else { + fields.put("attachments", new ArrayList<>(attachments)); + } + return this; + } + /** * Build a new UpdateInboxDraftOptions object. * * @return A new UpdateInboxDraftOptions object. - * @throws IllegalArgumentException if no field is set. + * @throws IllegalArgumentException if no field is set, or an attachment lacks {@code content}, + * {@code filename} or {@code content_type} or has a {@code path} or {@code content_id}. */ public UpdateInboxDraftOptions build() { if (fields.isEmpty()) { - throw new IllegalArgumentException("At least one of to, cc, bcc, subject, text or html must be provided"); + throw new IllegalArgumentException("At least one of to, cc, bcc, subject, text, html or attachments must be provided"); } + DraftAttachments.validate(getAttachmentsField()); return new UpdateInboxDraftOptions(this); } + + @SuppressWarnings("unchecked") + private List getAttachmentsField() { + return (List) fields.get("attachments"); + } } } diff --git a/src/main/java/com/resend/services/inboxes/model/UpdateInboxDraftResponseSuccess.java b/src/main/java/com/resend/services/inboxes/model/UpdateInboxDraftResponseSuccess.java index 9963e19..7d8e221 100644 --- a/src/main/java/com/resend/services/inboxes/model/UpdateInboxDraftResponseSuccess.java +++ b/src/main/java/com/resend/services/inboxes/model/UpdateInboxDraftResponseSuccess.java @@ -32,8 +32,9 @@ public UpdateInboxDraftResponseSuccess() { * @param emailId The ID of the outbound email queued when the draft was sent. {@code null} until the draft is sent. * @param createdAt The timestamp when the draft was created. * @param updatedAt The timestamp when the draft was last saved. + * @param attachments The files stored on the draft. */ - public UpdateInboxDraftResponseSuccess(String object, String id, String type, List to, List cc, List bcc, String subject, String html, String text, String threadId, String replyToEmailId, String emailId, String createdAt, String updatedAt) { - super(object, id, type, to, cc, bcc, subject, html, text, threadId, replyToEmailId, emailId, createdAt, updatedAt); + public UpdateInboxDraftResponseSuccess(String object, String id, String type, List to, List cc, List bcc, String subject, String html, String text, String threadId, String replyToEmailId, String emailId, String createdAt, String updatedAt, List attachments) { + super(object, id, type, to, cc, bcc, subject, html, text, threadId, replyToEmailId, emailId, createdAt, updatedAt, attachments); } } diff --git a/src/test/java/com/resend/services/inboxes/InboxDraftsTest.java b/src/test/java/com/resend/services/inboxes/InboxDraftsTest.java index bd023a7..39eb4fb 100644 --- a/src/test/java/com/resend/services/inboxes/InboxDraftsTest.java +++ b/src/test/java/com/resend/services/inboxes/InboxDraftsTest.java @@ -5,6 +5,7 @@ import com.resend.core.net.HttpMethod; import com.resend.core.net.IHttpClient; import com.resend.core.net.ListParams; +import com.resend.services.emails.model.Attachment; import com.resend.services.inboxes.model.*; import com.resend.services.util.InboxesUtil; import okhttp3.MediaType; @@ -28,7 +29,9 @@ public class InboxDraftsTest { "\"to\":[\"ada@example.org\"],\"cc\":[],\"bcc\":[],\"subject\":\"Re: Refund for order 1041\"," + "\"html\":null,\"text\":\"Refund issued for order 1041.\",\"thread_id\":\"" + THREAD_ID + "\"," + "\"reply_to_email_id\":\"" + EMAIL_ID + "\",\"email_id\":null," + - "\"created_at\":\"2026-08-05T14:12:04.110Z\",\"updated_at\":\"2026-08-05T14:12:04.110Z\"}"; + "\"created_at\":\"2026-08-05T14:12:04.110Z\",\"updated_at\":\"2026-08-05T14:12:04.110Z\"," + + "\"attachments\":[{\"id\":\"5c1e9d2a-7b3f-4e8a-9c6d-2f0a1b3c4d5e\",\"filename\":\"receipt.pdf\"," + + "\"content_type\":\"application/pdf\",\"size\":20841}]}"; @Mock private IHttpClient httpClient; @@ -45,7 +48,7 @@ public void setUp() { public void testListDrafts_Success() throws ResendException { String json = "{\"object\":\"list\",\"has_more\":false,\"data\":[{\"id\":\"" + DRAFT_ID + "\",\"type\":\"standalone\"," + "\"to\":null,\"cc\":[],\"bcc\":[],\"subject\":null,\"snippet\":\"Refund issued\",\"thread_id\":null," + - "\"reply_to_email_id\":null,\"updated_at\":\"2026-08-05T14:12:04.110Z\"}]}"; + "\"reply_to_email_id\":null,\"updated_at\":\"2026-08-05T14:12:04.110Z\",\"attachments\":[]}]}"; when(httpClient.perform(eq(DRAFTS_PATH + "?limit=1"), anyString(), eq(HttpMethod.GET), isNull(), any(MediaType.class))) .thenReturn(new AbstractHttpResponse<>(200, json, true)); @@ -57,6 +60,7 @@ public void testListDrafts_Success() throws ResendException { assertNull(item.getTo()); assertEquals("Refund issued", item.getSnippet()); assertNull(item.getThreadId()); + assertTrue(item.getAttachments().isEmpty()); } @Test @@ -84,6 +88,38 @@ public void testCreateDraft_EmptyToIsSent() throws ResendException { assertEquals(DRAFT_ID, drafts.create(INBOX_ID, options).getId()); } + @Test + public void testCreateDraft_WithAttachment() throws ResendException { + CreateInboxDraftOptions options = CreateInboxDraftOptions.builder() + .addAttachment(Attachment.builder().fileName("notes.txt").content("bm90ZXM=").contentType("text/plain").build()) + .build(); + String payload = "{\"attachments\":[{\"filename\":\"notes.txt\",\"content\":\"bm90ZXM=\",\"content_type\":\"text/plain\"}]}"; + when(httpClient.perform(eq(DRAFTS_PATH), anyString(), eq(HttpMethod.POST), eq(payload), any(MediaType.class))) + .thenReturn(new AbstractHttpResponse<>(201, "{\"object\":\"inbox_draft\",\"id\":\"" + DRAFT_ID + "\"}", true)); + + assertEquals(DRAFT_ID, drafts.create(INBOX_ID, options).getId()); + } + + @Test + public void testUpdateDraft_EmptyAttachmentsRemovesThem() throws ResendException { + UpdateInboxDraftOptions options = UpdateInboxDraftOptions.builder().attachments().build(); + when(httpClient.perform(eq(DRAFTS_PATH + "/" + DRAFT_ID), anyString(), eq(HttpMethod.PATCH), + eq("{\"attachments\":[]}"), any(MediaType.class))) + .thenReturn(new AbstractHttpResponse<>(200, DRAFT_JSON, true)); + + assertNotNull(drafts.update(INBOX_ID, DRAFT_ID, options)); + assertTrue(options.getAttachments().isEmpty()); + } + + @Test + public void testDraftAttachmentBuilder_Validation() { + Attachment withPath = Attachment.builder().fileName("a.txt").path("https://example.com/a.txt").contentType("text/plain").build(); + Attachment noType = Attachment.builder().fileName("a.txt").content("YQ==").build(); + assertThrows(IllegalArgumentException.class, () -> CreateInboxDraftOptions.builder().attachments(withPath).build()); + assertThrows(IllegalArgumentException.class, () -> CreateInboxDraftOptions.builder().attachments(noType).build()); + assertThrows(IllegalArgumentException.class, () -> UpdateInboxDraftOptions.builder().attachments(noType).build()); + } + @Test public void testCreateDraftBuilder_Validation() { assertThrows(IllegalArgumentException.class, () -> CreateInboxDraftOptions.builder().to(java.util.Collections.emptyList()).build()); @@ -108,6 +144,11 @@ public void testGetDraft_Success() throws ResendException { assertEquals(THREAD_ID, response.getThreadId()); assertEquals(EMAIL_ID, response.getReplyToEmailId()); assertNull(response.getEmailId()); + InboxDraftAttachment attachment = response.getAttachments().get(0); + assertEquals("5c1e9d2a-7b3f-4e8a-9c6d-2f0a1b3c4d5e", attachment.getId()); + assertEquals("receipt.pdf", attachment.getFilename()); + assertEquals("application/pdf", attachment.getContentType()); + assertEquals(20841, (int) attachment.getSize()); } @Test From 6539d440d46468761f10165365e193c77203a593 Mon Sep 17 00:00:00 2001 From: Felipe Freitag Date: Tue, 13 Oct 2026 09:50:49 -0300 Subject: [PATCH 12/30] fix(inboxes): reject blank draft attachment fields --- .../com/resend/services/inboxes/model/DraftAttachments.java | 2 +- src/test/java/com/resend/services/inboxes/InboxDraftsTest.java | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/resend/services/inboxes/model/DraftAttachments.java b/src/main/java/com/resend/services/inboxes/model/DraftAttachments.java index 4626082..fec791b 100644 --- a/src/main/java/com/resend/services/inboxes/model/DraftAttachments.java +++ b/src/main/java/com/resend/services/inboxes/model/DraftAttachments.java @@ -30,6 +30,6 @@ static void validate(List attachments) { } private static boolean isEmpty(String value) { - return value == null || value.isEmpty(); + return value == null || value.trim().isEmpty(); } } diff --git a/src/test/java/com/resend/services/inboxes/InboxDraftsTest.java b/src/test/java/com/resend/services/inboxes/InboxDraftsTest.java index 39eb4fb..7335840 100644 --- a/src/test/java/com/resend/services/inboxes/InboxDraftsTest.java +++ b/src/test/java/com/resend/services/inboxes/InboxDraftsTest.java @@ -118,6 +118,8 @@ public void testDraftAttachmentBuilder_Validation() { assertThrows(IllegalArgumentException.class, () -> CreateInboxDraftOptions.builder().attachments(withPath).build()); assertThrows(IllegalArgumentException.class, () -> CreateInboxDraftOptions.builder().attachments(noType).build()); assertThrows(IllegalArgumentException.class, () -> UpdateInboxDraftOptions.builder().attachments(noType).build()); + Attachment blankName = Attachment.builder().fileName(" ").content("YQ==").contentType("text/plain").build(); + assertThrows(IllegalArgumentException.class, () -> CreateInboxDraftOptions.builder().attachments(blankName).build()); } @Test From b472cb1f5cc80ff95cd4593b0e6de056e360773d Mon Sep 17 00:00:00 2001 From: Felipe Freitag Date: Tue, 13 Oct 2026 10:26:35 -0300 Subject: [PATCH 13/30] fix(inboxes): drop the draft file-count check and align behavior notes with the API --- .../java/com/resend/services/inboxes/InboxDrafts.java | 7 +++++-- .../com/resend/services/inboxes/InboxThreadEmails.java | 6 ++++-- .../java/com/resend/services/inboxes/InboxThreads.java | 8 ++++++-- src/main/java/com/resend/services/inboxes/Inboxes.java | 4 +++- .../services/inboxes/model/CreateInboxDraftOptions.java | 8 ++++---- .../resend/services/inboxes/model/DraftAttachments.java | 3 --- .../services/inboxes/model/ListInboxThreadsParams.java | 1 + .../inboxes/model/ReplyInboxThreadEmailOptions.java | 2 +- .../services/inboxes/model/SearchInboxThreadsParams.java | 2 ++ .../services/inboxes/model/UpdateInboxAgentOptions.java | 4 ++-- .../services/inboxes/model/UpdateInboxDraftOptions.java | 5 +++-- .../com/resend/services/inboxes/InboxDraftsTest.java | 9 +++++++++ 12 files changed, 40 insertions(+), 19 deletions(-) diff --git a/src/main/java/com/resend/services/inboxes/InboxDrafts.java b/src/main/java/com/resend/services/inboxes/InboxDrafts.java index 40c6a24..2365069 100644 --- a/src/main/java/com/resend/services/inboxes/InboxDrafts.java +++ b/src/main/java/com/resend/services/inboxes/InboxDrafts.java @@ -69,7 +69,9 @@ public ListInboxDraftsResponseSuccess list(String inboxId, ListParams params) th * Creates an unsent draft on an inbox. Omit {@code thread_id} and {@code reply_to_email_id} for a draft that * starts a new thread. Pass both together for a reply draft in an existing thread. 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. + * existing draft. That request ignores every other field, except {@code attachments}, which replaces the + * attachments of the existing draft when sent. A reply draft created without {@code subject} stores an empty + * subject and gets no {@code Re:} prefix, so set one before you send it. * * @param inboxId The inbox ID. An email address is not accepted. * @param createInboxDraftOptions The draft content. @@ -124,7 +126,8 @@ public RemoveInboxDraftResponseSuccess remove(String inboxId, String draftId) th /** * Sends a draft from the inbox address. A reply draft is sent in its thread. A draft without a thread starts - * a new thread. The draft must have at least one {@code to} recipient, or the request returns a 422. + * a new thread. The draft must have at least one {@code to} recipient, a non-empty {@code subject}, and + * {@code html} or {@code text}, or the request returns a 422. * A draft that was already sent returns a 409. * * @param inboxId The inbox ID. An email address is not accepted. diff --git a/src/main/java/com/resend/services/inboxes/InboxThreadEmails.java b/src/main/java/com/resend/services/inboxes/InboxThreadEmails.java index 5eab7b0..82adc6c 100644 --- a/src/main/java/com/resend/services/inboxes/InboxThreadEmails.java +++ b/src/main/java/com/resend/services/inboxes/InboxThreadEmails.java @@ -53,7 +53,8 @@ public ListInboxThreadEmailsResponseSuccess list(String inboxId, String threadId /** * Retrieves a page of the messages of a thread, oldest first. Pass the {@code id} of the last message as * {@code after} to get the next, newer page. Pass the {@code id} of the first message as {@code before} - * to get the previous, older page. You cannot combine {@code after} and {@code before}. + * to get the previous, older page. You cannot combine {@code after} and {@code before}. A cursor that is not + * a message of the thread returns a 404. * * @param inboxId The inbox ID. An email address is not accepted. * @param threadId The thread ID. @@ -104,7 +105,8 @@ public ReplyInboxThreadEmailResponseSuccess reply(String inboxId, String threadI * Forwards a message from the inbox address right away, with no stored draft. The original message is quoted * under a forwarded-message banner, below the optional {@code html} or {@code text} note. The attachments of * the original message are sent with the forward, together with any attachments you add. The request fails - * when they exceed 10 files or 25 MB of {@code content} in total. + * when they exceed 10 files or 25 MB of {@code content} in total. An original attachment that was sent from + * a URL ({@code path}) cannot be forwarded, and the request returns a 422. * * @param inboxId The inbox ID. An email address is not accepted. * @param threadId The thread ID. diff --git a/src/main/java/com/resend/services/inboxes/InboxThreads.java b/src/main/java/com/resend/services/inboxes/InboxThreads.java index 8f81a21..2c80b37 100644 --- a/src/main/java/com/resend/services/inboxes/InboxThreads.java +++ b/src/main/java/com/resend/services/inboxes/InboxThreads.java @@ -62,6 +62,7 @@ public ListInboxThreadsResponseSuccess list(String inboxId) throws ResendExcepti /** * Retrieves the threads in an inbox, most recently active first, with filtering and pagination. * The {@code after} and {@code before} cursors take a thread ID, and only one of them can be passed. + * A cursor thread that is not in the selected folders returns a 404. * To find threads by text, sender, recipient, attachment, or date, use {@link #search(String, SearchInboxThreadsParams)}. * * @param inboxId The inbox ID. An email address is not accepted. @@ -89,7 +90,8 @@ public SearchInboxThreadsResponseSuccess search(String inboxId) throws ResendExc * Finds threads by the text of their emails, who sent or received them, whether they have attachments, * and their dates. Results are ordered by latest activity, newest first, and paginate like the list threads * endpoint. Search can be up to about a minute behind new emails. When more than 10,000 threads match, - * search covers the 10,000 with the newest matching email. + * search covers the 10,000 with the newest matching email. When any email filter is set, the matching email + * must itself be in one of {@code folders}. * * @param inboxId The inbox ID. An email address is not accepted. * @param params The search filters and pagination. @@ -117,7 +119,9 @@ public GetInboxThreadResponseSuccess get(String inboxId, String threadId) throws * Marks a thread read or unread, moves it to a folder, or adds and removes labels. * A label ID cannot be both added and removed in the same request. Removing a label from a thread keeps * the label itself. - * Moving a thread out of {@code trash} restores it and clears its deletion deadline. + * Moving a thread out of {@code trash} restores it and clears its deletion deadline. The changes apply in + * this order: {@code read}, {@code folder}, added labels, removed labels. They are not atomic, so a 404 for + * a label leaves the earlier changes applied. * * @param inboxId The inbox ID. An email address is not accepted. * @param threadId The thread ID. diff --git a/src/main/java/com/resend/services/inboxes/Inboxes.java b/src/main/java/com/resend/services/inboxes/Inboxes.java index a79de4e..7addef6 100644 --- a/src/main/java/com/resend/services/inboxes/Inboxes.java +++ b/src/main/java/com/resend/services/inboxes/Inboxes.java @@ -82,7 +82,7 @@ public InboxDrafts drafts() { /** * Creates an inbox that sends and receives email at an address on one of your domains. * A domain that does not meet the inbox rules returns a 422. An address that is already in use returns a 409. - * A team that has reached the inbox limit of its plan returns a 403. + * A team that has reached the inbox limit of its plan returns a 403. A blocked account returns a 451. * * @param createInboxOptions The inbox details. * @return The ID of the created inbox. @@ -107,6 +107,8 @@ public ListInboxesResponseSuccess list() throws ResendException { /** * Retrieves a paginated list of the inboxes of the team, newest first. * The list items carry fewer fields than a single inbox. Retrieve an inbox to get all its fields. + * The {@code after} and {@code before} cursors take an inbox ID. A value that is not a UUID returns a 422, + * and an unknown ID returns a 404. * * @param params The params used to customize the list. * @return A ListInboxesResponseSuccess containing the list of inboxes. diff --git a/src/main/java/com/resend/services/inboxes/model/CreateInboxDraftOptions.java b/src/main/java/com/resend/services/inboxes/model/CreateInboxDraftOptions.java index 7f7d811..863e6aa 100644 --- a/src/main/java/com/resend/services/inboxes/model/CreateInboxDraftOptions.java +++ b/src/main/java/com/resend/services/inboxes/model/CreateInboxDraftOptions.java @@ -304,8 +304,8 @@ public Builder text(String text) { /** * Set the files to store on the draft. Each one needs {@code content}, {@code filename} and - * {@code content_type}. A {@code path} or {@code content_id} is not accepted. A draft can carry up to - * 10 files and 25 MB in total. + * {@code content_type}. A {@code path} or {@code content_id} is not accepted. The files on a draft can total + * at most 25 MB. An empty file or a blocked file type returns a 422. * * @param attachments The files to store on the draft. * @return The builder instance. @@ -317,8 +317,8 @@ public Builder attachments(Attachment... attachments) { /** * Set the files to store on the draft. Each one needs {@code content}, {@code filename} and - * {@code content_type}. A {@code path} or {@code content_id} is not accepted. A draft can carry up to - * 10 files and 25 MB in total. + * {@code content_type}. A {@code path} or {@code content_id} is not accepted. The files on a draft can total + * at most 25 MB. An empty file or a blocked file type returns a 422. * * @param attachments The files to store on the draft. * @return The builder instance. diff --git a/src/main/java/com/resend/services/inboxes/model/DraftAttachments.java b/src/main/java/com/resend/services/inboxes/model/DraftAttachments.java index fec791b..760e83a 100644 --- a/src/main/java/com/resend/services/inboxes/model/DraftAttachments.java +++ b/src/main/java/com/resend/services/inboxes/model/DraftAttachments.java @@ -13,9 +13,6 @@ static void validate(List attachments) { if (attachments == null) { return; } - if (attachments.size() > 10) { - throw new IllegalArgumentException("A draft can carry at most 10 attachments"); - } for (Attachment attachment : attachments) { if (attachment == null) { throw new IllegalArgumentException("attachments must not contain null"); diff --git a/src/main/java/com/resend/services/inboxes/model/ListInboxThreadsParams.java b/src/main/java/com/resend/services/inboxes/model/ListInboxThreadsParams.java index 372dae5..324cd59 100644 --- a/src/main/java/com/resend/services/inboxes/model/ListInboxThreadsParams.java +++ b/src/main/java/com/resend/services/inboxes/model/ListInboxThreadsParams.java @@ -12,6 +12,7 @@ /** * Represents parameters for listing the threads of an inbox with filtering and pagination. * The {@code after} and {@code before} cursors take a thread ID, and only one of them can be passed. + * A cursor thread that is not in the selected folders returns a 404. */ public class ListInboxThreadsParams { diff --git a/src/main/java/com/resend/services/inboxes/model/ReplyInboxThreadEmailOptions.java b/src/main/java/com/resend/services/inboxes/model/ReplyInboxThreadEmailOptions.java index 3cb38ce..2f875f5 100644 --- a/src/main/java/com/resend/services/inboxes/model/ReplyInboxThreadEmailOptions.java +++ b/src/main/java/com/resend/services/inboxes/model/ReplyInboxThreadEmailOptions.java @@ -239,7 +239,7 @@ public Builder subject(String subject) { } /** - * Set whether to reply to everyone on the original message. The other {@code to} and {@code cc} recipients of the message are copied in {@code cc}. The inbox address is always excluded. Defaults to {@code false}. + * Set whether to reply to everyone on the original message. For a message the inbox received, the reply goes to its {@code Reply-To} addresses, or to its sender when it has none, and copies the other addresses of its {@code To} header and its {@code cc} recipients in {@code cc}. For a message the inbox sent, the reply goes to that message's {@code Reply-To} addresses when they are not the inbox address, otherwise to its {@code to} recipients, and copies its {@code cc} recipients. The inbox address is always excluded, and duplicates are removed. Defaults to {@code false}. * * @param replyAll Whether to reply to everyone on the original message. * @return The builder instance. diff --git a/src/main/java/com/resend/services/inboxes/model/SearchInboxThreadsParams.java b/src/main/java/com/resend/services/inboxes/model/SearchInboxThreadsParams.java index 0373662..ff4694a 100644 --- a/src/main/java/com/resend/services/inboxes/model/SearchInboxThreadsParams.java +++ b/src/main/java/com/resend/services/inboxes/model/SearchInboxThreadsParams.java @@ -14,6 +14,8 @@ * Different parameters combine, so a thread must match all of them. {@code folders}, {@code labels}, and * {@code read} apply to the thread. {@code query}, {@code from}, {@code to}, {@code cc}, {@code bcc}, * {@code has_attachment}, {@code start_date}, and {@code end_date} must all match the same email in the thread. + * When any of these email filters is set, the matching email must itself be in one of {@code folders}. + * For {@code sent}, that means an email the inbox sent. */ public class SearchInboxThreadsParams { diff --git a/src/main/java/com/resend/services/inboxes/model/UpdateInboxAgentOptions.java b/src/main/java/com/resend/services/inboxes/model/UpdateInboxAgentOptions.java index f70a493..1abac09 100644 --- a/src/main/java/com/resend/services/inboxes/model/UpdateInboxAgentOptions.java +++ b/src/main/java/com/resend/services/inboxes/model/UpdateInboxAgentOptions.java @@ -107,7 +107,7 @@ public Builder tone(String tone) { } /** - * Set the actions the agent can take. Replaces the whole set. Pass no actions to disable all actions. + * Set the actions the agent can take. Replaces the whole set. Duplicates are removed. Pass no actions to disable all actions. * * @param enabledActions The enabled actions. * @return The builder instance. @@ -118,7 +118,7 @@ public Builder enabledActions(InboxAgentAction... enabledActions) { } /** - * Set the actions the agent can take. Replaces the whole set. Pass an empty list to disable all actions. + * Set the actions the agent can take. Replaces the whole set. Duplicates are removed. Pass an empty list to disable all actions. * Pass null to leave the actions unchanged. * * @param enabledActions The enabled actions. diff --git a/src/main/java/com/resend/services/inboxes/model/UpdateInboxDraftOptions.java b/src/main/java/com/resend/services/inboxes/model/UpdateInboxDraftOptions.java index c410557..c58f89c 100644 --- a/src/main/java/com/resend/services/inboxes/model/UpdateInboxDraftOptions.java +++ b/src/main/java/com/resend/services/inboxes/model/UpdateInboxDraftOptions.java @@ -225,7 +225,8 @@ public Builder text(String text) { /** * Set the files to store on the draft. Replaces every attachment on the draft. Pass no files to remove * them all. Each one needs {@code content}, {@code filename} and {@code content_type}. A {@code path} or - * {@code content_id} is not accepted. A draft can carry up to 10 files and 25 MB in total. + * {@code content_id} is not accepted. The files on a draft can total at most 25 MB. An empty file or a + * blocked file type returns a 422. * * @param attachments The files to store on the draft. * @return The builder instance. @@ -239,7 +240,7 @@ public Builder attachments(Attachment... attachments) { * Set the files to store on the draft. Replaces every attachment on the draft. Pass an empty list to * remove them all. Pass null to leave the attachments unchanged. Each one needs {@code content}, * {@code filename} and {@code content_type}. A {@code path} or {@code content_id} is not accepted. - * A draft can carry up to 10 files and 25 MB in total. + * The files on a draft can total at most 25 MB. An empty file or a blocked file type returns a 422. * * @param attachments The files to store on the draft. * @return The builder instance. diff --git a/src/test/java/com/resend/services/inboxes/InboxDraftsTest.java b/src/test/java/com/resend/services/inboxes/InboxDraftsTest.java index 7335840..ec4f275 100644 --- a/src/test/java/com/resend/services/inboxes/InboxDraftsTest.java +++ b/src/test/java/com/resend/services/inboxes/InboxDraftsTest.java @@ -122,6 +122,15 @@ public void testDraftAttachmentBuilder_Validation() { assertThrows(IllegalArgumentException.class, () -> CreateInboxDraftOptions.builder().attachments(blankName).build()); } + @Test + public void testDraftAttachmentBuilder_AllowsMoreThanTenFiles() { + CreateInboxDraftOptions.Builder builder = CreateInboxDraftOptions.builder(); + for (int i = 0; i < 11; i++) { + builder.addAttachment(Attachment.builder().fileName("f" + i + ".txt").content("YQ==").contentType("text/plain").build()); + } + assertEquals(11, builder.build().getAttachments().size()); + } + @Test public void testCreateDraftBuilder_Validation() { assertThrows(IllegalArgumentException.class, () -> CreateInboxDraftOptions.builder().to(java.util.Collections.emptyList()).build()); From 91406c00de14b9282a3a5f8cd927fad454c26ea2 Mon Sep 17 00:00:00 2001 From: Felipe Freitag Date: Tue, 13 Oct 2026 13:39:50 -0300 Subject: [PATCH 14/30] feat(inboxes): accept an idempotency key on draft create, draft send, reply and forward --- .../resend/services/inboxes/InboxDrafts.java | 44 ++++++++++++++++ .../services/inboxes/InboxThreadEmails.java | 51 +++++++++++++++++++ .../services/inboxes/InboxDraftsTest.java | 14 +++++ .../inboxes/InboxThreadEmailsTest.java | 13 +++++ 4 files changed, 122 insertions(+) diff --git a/src/main/java/com/resend/services/inboxes/InboxDrafts.java b/src/main/java/com/resend/services/inboxes/InboxDrafts.java index 2365069..b0ad225 100644 --- a/src/main/java/com/resend/services/inboxes/InboxDrafts.java +++ b/src/main/java/com/resend/services/inboxes/InboxDrafts.java @@ -5,6 +5,7 @@ import com.resend.core.net.HttpMethod; import com.resend.core.net.IHttpClient; import com.resend.core.net.ListParams; +import com.resend.core.net.RequestOptions; import com.resend.core.service.BaseService; import com.resend.services.inboxes.model.CreateInboxDraftOptions; import com.resend.services.inboxes.model.CreateInboxDraftResponseSuccess; @@ -83,6 +84,29 @@ public CreateInboxDraftResponseSuccess create(String inboxId, CreateInboxDraftOp return execute("/inboxes/" + inboxId + "/drafts", HttpMethod.POST, payload, MediaType.get("application/json"), CreateInboxDraftResponseSuccess.class); } + /** + * Creates an unsent draft on an inbox. Omit {@code thread_id} and {@code reply_to_email_id} for a draft that + * starts a new thread. Pass both together for a reply draft in an existing thread. 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 {@code attachments}, which replaces the + * attachments of the existing draft when sent. A reply draft created without {@code subject} stores an empty + * subject and gets no {@code Re:} prefix, so set one before you send it. + * + *

Set an idempotency key in {@code requestOptions} so the draft is created only once. A retry with the same key + * within 24 hours returns the first response, with the {@code idempotent-replayed: true} header. Reusing the + * key with a different body, or while the first request is still running, returns a 409.

+ * + * @param inboxId The inbox ID. An email address is not accepted. + * @param createInboxDraftOptions The draft content. + * @param requestOptions The options with additional headers. + * @return The ID of the draft. + * @throws ResendException If an error occurs while creating the draft. + */ + public CreateInboxDraftResponseSuccess create(String inboxId, CreateInboxDraftOptions createInboxDraftOptions, RequestOptions requestOptions) throws ResendException { + String payload = super.resendMapper.writeValue(createInboxDraftOptions); + return execute("/inboxes/" + inboxId + "/drafts", HttpMethod.POST, payload, MediaType.get("application/json"), requestOptions, CreateInboxDraftResponseSuccess.class); + } + /** * Retrieves a single draft. * @@ -138,4 +162,24 @@ public RemoveInboxDraftResponseSuccess remove(String inboxId, String draftId) th public SendInboxDraftResponseSuccess send(String inboxId, String draftId) throws ResendException { return execute("/inboxes/" + inboxId + "/drafts/" + draftId + "/send", HttpMethod.POST, "", MediaType.get("application/json"), SendInboxDraftResponseSuccess.class); } + + /** + * Sends a draft from the inbox address. A reply draft is sent in its thread. A draft without a thread starts + * a new thread. The draft must have at least one {@code to} recipient, a non-empty {@code subject}, and + * {@code html} or {@code text}, or the request returns a 422. + * A draft that was already sent returns a 409. + * + *

Set an idempotency key in {@code requestOptions} so the draft is sent only once. A retry with the same key + * within 24 hours returns the first response, with the {@code idempotent-replayed: true} header. Reusing the + * key with a different body, or while the first request is still running, returns a 409.

+ * + * @param inboxId The inbox ID. An email address is not accepted. + * @param draftId The draft ID. + * @param requestOptions The options with additional headers. + * @return The ID of the sent email. + * @throws ResendException If an error occurs while sending the draft. + */ + public SendInboxDraftResponseSuccess send(String inboxId, String draftId, RequestOptions requestOptions) throws ResendException { + return execute("/inboxes/" + inboxId + "/drafts/" + draftId + "/send", HttpMethod.POST, "", MediaType.get("application/json"), requestOptions, SendInboxDraftResponseSuccess.class); + } } diff --git a/src/main/java/com/resend/services/inboxes/InboxThreadEmails.java b/src/main/java/com/resend/services/inboxes/InboxThreadEmails.java index 82adc6c..cd42256 100644 --- a/src/main/java/com/resend/services/inboxes/InboxThreadEmails.java +++ b/src/main/java/com/resend/services/inboxes/InboxThreadEmails.java @@ -5,6 +5,7 @@ import com.resend.core.net.HttpMethod; import com.resend.core.net.IHttpClient; import com.resend.core.net.ListParams; +import com.resend.core.net.RequestOptions; import com.resend.core.service.BaseService; import com.resend.services.inboxes.model.ForwardInboxThreadEmailOptions; import com.resend.services.inboxes.model.ForwardInboxThreadEmailResponseSuccess; @@ -101,6 +102,31 @@ public ReplyInboxThreadEmailResponseSuccess reply(String inboxId, String threadI return execute(path, HttpMethod.POST, payload, MediaType.get("application/json"), ReplyInboxThreadEmailResponseSuccess.class); } + /** + * Sends a reply from the inbox address right away, with no stored draft. The {@code to} recipients come from + * the message you reply to. For a message the inbox received, the reply goes to its {@code Reply-To} + * addresses, or to its sender when it has no {@code Reply-To}. For a message the inbox sent, the reply goes + * to that message's {@code to} recipients. The reply threads under the message through the + * {@code In-Reply-To} and {@code References} headers. + * + *

Set an idempotency key in {@code requestOptions} so the reply is sent only once. A retry with the same key + * within 24 hours returns the first response, with the {@code idempotent-replayed: true} header. Reusing the + * key with a different body, or while the first request is still running, returns a 409.

+ * + * @param inboxId The inbox ID. An email address is not accepted. + * @param threadId The thread ID. + * @param emailId The email ID of the message to reply to. + * @param replyInboxThreadEmailOptions The reply content. + * @param requestOptions The options with additional headers. + * @return The ID of the sent email. + * @throws ResendException If an error occurs while sending the reply. + */ + public ReplyInboxThreadEmailResponseSuccess reply(String inboxId, String threadId, String emailId, ReplyInboxThreadEmailOptions replyInboxThreadEmailOptions, RequestOptions requestOptions) throws ResendException { + String path = "/inboxes/" + inboxId + "/threads/" + threadId + "/emails/" + emailId + "/reply"; + String payload = super.resendMapper.writeValue(replyInboxThreadEmailOptions); + return execute(path, HttpMethod.POST, payload, MediaType.get("application/json"), requestOptions, ReplyInboxThreadEmailResponseSuccess.class); + } + /** * Forwards a message from the inbox address right away, with no stored draft. The original message is quoted * under a forwarded-message banner, below the optional {@code html} or {@code text} note. The attachments of @@ -120,4 +146,29 @@ public ForwardInboxThreadEmailResponseSuccess forward(String inboxId, String thr String payload = super.resendMapper.writeValue(forwardInboxThreadEmailOptions); return execute(path, HttpMethod.POST, payload, MediaType.get("application/json"), ForwardInboxThreadEmailResponseSuccess.class); } + + /** + * Forwards a message from the inbox address right away, with no stored draft. The original message is quoted + * under a forwarded-message banner, below the optional {@code html} or {@code text} note. The attachments of + * the original message are sent with the forward, together with any attachments you add. The request fails + * when they exceed 10 files or 25 MB of {@code content} in total. An original attachment that was sent from + * a URL ({@code path}) cannot be forwarded, and the request returns a 422. + * + *

Set an idempotency key in {@code requestOptions} so the forward is sent only once. A retry with the same key + * within 24 hours returns the first response, with the {@code idempotent-replayed: true} header. Reusing the + * key with a different body, or while the first request is still running, returns a 409.

+ * + * @param inboxId The inbox ID. An email address is not accepted. + * @param threadId The thread ID. + * @param emailId The email ID of the message to forward. + * @param forwardInboxThreadEmailOptions The forward recipients and content. + * @param requestOptions The options with additional headers. + * @return The ID of the sent email. + * @throws ResendException If an error occurs while sending the forward. + */ + public ForwardInboxThreadEmailResponseSuccess forward(String inboxId, String threadId, String emailId, ForwardInboxThreadEmailOptions forwardInboxThreadEmailOptions, RequestOptions requestOptions) throws ResendException { + String path = "/inboxes/" + inboxId + "/threads/" + threadId + "/emails/" + emailId + "/forward"; + String payload = super.resendMapper.writeValue(forwardInboxThreadEmailOptions); + return execute(path, HttpMethod.POST, payload, MediaType.get("application/json"), requestOptions, ForwardInboxThreadEmailResponseSuccess.class); + } } diff --git a/src/test/java/com/resend/services/inboxes/InboxDraftsTest.java b/src/test/java/com/resend/services/inboxes/InboxDraftsTest.java index ec4f275..ceef622 100644 --- a/src/test/java/com/resend/services/inboxes/InboxDraftsTest.java +++ b/src/test/java/com/resend/services/inboxes/InboxDraftsTest.java @@ -5,6 +5,7 @@ import com.resend.core.net.HttpMethod; import com.resend.core.net.IHttpClient; import com.resend.core.net.ListParams; +import com.resend.core.net.RequestOptions; import com.resend.services.emails.model.Attachment; import com.resend.services.inboxes.model.*; import com.resend.services.util.InboxesUtil; @@ -209,6 +210,19 @@ public void testSendDraft_Success() throws ResendException { assertEquals(SENT_EMAIL_ID, response.getId()); } + @Test + public void testCreateAndSendDraft_WithIdempotencyKey() throws ResendException { + RequestOptions options = RequestOptions.builder().setIdempotencyKey("draft-key-1").build(); + when(httpClient.perform(eq(DRAFTS_PATH), anyString(), eq(HttpMethod.POST), anyString(), any(MediaType.class), eq(options))) + .thenReturn(new AbstractHttpResponse<>(201, "{\"object\":\"inbox_draft\",\"id\":\"" + DRAFT_ID + "\"}", true)); + when(httpClient.perform(eq(DRAFTS_PATH + "/" + DRAFT_ID + "/send"), anyString(), eq(HttpMethod.POST), eq(""), any(MediaType.class), eq(options))) + .thenReturn(new AbstractHttpResponse<>(200, "{\"id\":\"" + SENT_EMAIL_ID + "\"}", true)); + + assertEquals(DRAFT_ID, drafts.create(INBOX_ID, InboxesUtil.createReplyDraftRequest(), options).getId()); + assertEquals(SENT_EMAIL_ID, drafts.send(INBOX_ID, DRAFT_ID, options).getId()); + assertEquals("draft-key-1", options.getIdempotencyKey()); + } + @Test public void testSendDraft_AlreadySent_ThrowsResendException() throws ResendException { when(httpClient.perform(eq(DRAFTS_PATH + "/" + DRAFT_ID + "/send"), anyString(), eq(HttpMethod.POST), eq(""), any(MediaType.class))) diff --git a/src/test/java/com/resend/services/inboxes/InboxThreadEmailsTest.java b/src/test/java/com/resend/services/inboxes/InboxThreadEmailsTest.java index 8f71f10..6703aae 100644 --- a/src/test/java/com/resend/services/inboxes/InboxThreadEmailsTest.java +++ b/src/test/java/com/resend/services/inboxes/InboxThreadEmailsTest.java @@ -5,6 +5,7 @@ import com.resend.core.net.HttpMethod; import com.resend.core.net.IHttpClient; import com.resend.core.net.ListParams; +import com.resend.core.net.RequestOptions; import com.resend.services.inboxes.model.*; import com.resend.services.util.InboxesUtil; import okhttp3.MediaType; @@ -105,6 +106,18 @@ public void testForwardBuilder_RequiresTo() { assertThrows(IllegalArgumentException.class, () -> ForwardInboxThreadEmailOptions.builder().text("FYI").build()); } + @Test + public void testReplyAndForward_WithIdempotencyKey() throws ResendException { + RequestOptions options = RequestOptions.builder().setIdempotencyKey("reply-key-1").build(); + when(httpClient.perform(eq(EMAILS_PATH + "/" + EMAIL_ID + "/reply"), anyString(), eq(HttpMethod.POST), anyString(), any(MediaType.class), eq(options))) + .thenReturn(new AbstractHttpResponse<>(200, "{\"id\":\"" + SENT_EMAIL_ID + "\"}", true)); + when(httpClient.perform(eq(EMAILS_PATH + "/" + EMAIL_ID + "/forward"), anyString(), eq(HttpMethod.POST), anyString(), any(MediaType.class), eq(options))) + .thenReturn(new AbstractHttpResponse<>(200, "{\"id\":\"" + SENT_EMAIL_ID + "\"}", true)); + + assertEquals(SENT_EMAIL_ID, emails.reply(INBOX_ID, THREAD_ID, EMAIL_ID, InboxesUtil.replyRequest(), options).getId()); + assertEquals(SENT_EMAIL_ID, emails.forward(INBOX_ID, THREAD_ID, EMAIL_ID, InboxesUtil.forwardRequest(), options).getId()); + } + @Test public void testReply_ValidationError_ThrowsResendException() throws ResendException { when(httpClient.perform(eq(EMAILS_PATH + "/" + EMAIL_ID + "/reply"), anyString(), eq(HttpMethod.POST), anyString(), any(MediaType.class))) From a16b6bc1f6ab950575469457651466123bb2463a Mon Sep 17 00:00:00 2001 From: Felipe Freitag Date: Tue, 13 Oct 2026 14:13:26 -0300 Subject: [PATCH 15/30] feat(inboxes): accept custom headers on reply, forward and draft send --- .../resend/services/inboxes/InboxDrafts.java | 40 +++++++++ .../model/ForwardInboxThreadEmailOptions.java | 42 +++++++++ .../model/ReplyInboxThreadEmailOptions.java | 42 +++++++++ .../inboxes/model/SendInboxDraftOptions.java | 85 +++++++++++++++++++ .../services/inboxes/InboxDraftsTest.java | 14 +++ .../inboxes/InboxThreadEmailsTest.java | 16 ++++ 6 files changed, 239 insertions(+) create mode 100644 src/main/java/com/resend/services/inboxes/model/SendInboxDraftOptions.java diff --git a/src/main/java/com/resend/services/inboxes/InboxDrafts.java b/src/main/java/com/resend/services/inboxes/InboxDrafts.java index b0ad225..d539dad 100644 --- a/src/main/java/com/resend/services/inboxes/InboxDrafts.java +++ b/src/main/java/com/resend/services/inboxes/InboxDrafts.java @@ -12,6 +12,7 @@ import com.resend.services.inboxes.model.GetInboxDraftResponseSuccess; import com.resend.services.inboxes.model.ListInboxDraftsResponseSuccess; import com.resend.services.inboxes.model.RemoveInboxDraftResponseSuccess; +import com.resend.services.inboxes.model.SendInboxDraftOptions; import com.resend.services.inboxes.model.SendInboxDraftResponseSuccess; import com.resend.services.inboxes.model.UpdateInboxDraftOptions; import com.resend.services.inboxes.model.UpdateInboxDraftResponseSuccess; @@ -182,4 +183,43 @@ public SendInboxDraftResponseSuccess send(String inboxId, String draftId) throws public SendInboxDraftResponseSuccess send(String inboxId, String draftId, RequestOptions requestOptions) throws ResendException { return execute("/inboxes/" + inboxId + "/drafts/" + draftId + "/send", HttpMethod.POST, "", MediaType.get("application/json"), requestOptions, SendInboxDraftResponseSuccess.class); } + + /** + * Sends a draft from the inbox address with custom headers. A reply draft is sent in its thread. A draft + * without a thread starts a new thread. The draft must have at least one {@code to} recipient, a non-empty + * {@code subject}, and {@code html} or {@code text}, or the request returns a 422. + * A draft that was already sent returns a 409. + * + * @param inboxId The inbox ID. An email address is not accepted. + * @param draftId The draft ID. + * @param sendInboxDraftOptions The custom headers to add to the email. + * @return The ID of the sent email. + * @throws ResendException If an error occurs while sending the draft. + */ + public SendInboxDraftResponseSuccess send(String inboxId, String draftId, SendInboxDraftOptions sendInboxDraftOptions) throws ResendException { + String payload = sendInboxDraftOptions == null ? "" : super.resendMapper.writeValue(sendInboxDraftOptions); + return execute("/inboxes/" + inboxId + "/drafts/" + draftId + "/send", HttpMethod.POST, payload, MediaType.get("application/json"), SendInboxDraftResponseSuccess.class); + } + + /** + * Sends a draft from the inbox address with custom headers. A reply draft is sent in its thread. A draft + * without a thread starts a new thread. The draft must have at least one {@code to} recipient, a non-empty + * {@code subject}, and {@code html} or {@code text}, or the request returns a 422. + * A draft that was already sent returns a 409. + * + *

Set an idempotency key in {@code requestOptions} so the draft is sent only once. A retry with the same key + * within 24 hours returns the first response, with the {@code idempotent-replayed: true} header. Reusing the + * key with a different body, or while the first request is still running, returns a 409.

+ * + * @param inboxId The inbox ID. An email address is not accepted. + * @param draftId The draft ID. + * @param sendInboxDraftOptions The custom headers to add to the email. + * @param requestOptions The options with additional headers. + * @return The ID of the sent email. + * @throws ResendException If an error occurs while sending the draft. + */ + public SendInboxDraftResponseSuccess send(String inboxId, String draftId, SendInboxDraftOptions sendInboxDraftOptions, RequestOptions requestOptions) throws ResendException { + String payload = sendInboxDraftOptions == null ? "" : super.resendMapper.writeValue(sendInboxDraftOptions); + return execute("/inboxes/" + inboxId + "/drafts/" + draftId + "/send", HttpMethod.POST, payload, MediaType.get("application/json"), requestOptions, SendInboxDraftResponseSuccess.class); + } } diff --git a/src/main/java/com/resend/services/inboxes/model/ForwardInboxThreadEmailOptions.java b/src/main/java/com/resend/services/inboxes/model/ForwardInboxThreadEmailOptions.java index e2df09e..5a5ca61 100644 --- a/src/main/java/com/resend/services/inboxes/model/ForwardInboxThreadEmailOptions.java +++ b/src/main/java/com/resend/services/inboxes/model/ForwardInboxThreadEmailOptions.java @@ -5,7 +5,9 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; /** * Represents a request to forward a thread email. {@code to}, {@code cc} and {@code bcc} combined cannot exceed 50 recipients. @@ -33,6 +35,9 @@ public class ForwardInboxThreadEmailOptions { @JsonProperty("attachments") private final List attachments; + @JsonProperty("headers") + private final Map headers; + private ForwardInboxThreadEmailOptions(Builder builder) { this.to = builder.to; this.cc = builder.cc; @@ -41,6 +46,7 @@ private ForwardInboxThreadEmailOptions(Builder builder) { this.text = builder.text; this.subject = builder.subject; this.attachments = builder.attachments; + this.headers = builder.headers; } /** @@ -106,6 +112,15 @@ public List getAttachments() { return attachments; } + /** + * Retrieves the custom headers of the email. + * + * @return The custom headers of the email. + */ + public Map getHeaders() { + return headers; + } + /** * Create a new builder instance for constructing ForwardInboxThreadEmailOptions objects. * @@ -126,6 +141,7 @@ public static class Builder { private String text; private String subject; private List attachments; + private Map headers; /** * Creates a new Builder instance. @@ -310,6 +326,32 @@ public Builder addAttachment(Attachment attachment) { return this; } + /** + * Set the custom headers of the email. The {@code In-Reply-To}, {@code References} and {@code Reply-To} headers are set by Resend and cannot be set here. Sending one of them, in any letter case, returns a 422. + * + * @param headers The map of custom headers for the email. + * @return This builder instance for method chaining. + */ + public Builder headers(Map headers) { + this.headers = headers == null ? null : new LinkedHashMap<>(headers); + return this; + } + + /** + * Add a single custom header to the email headers. The {@code In-Reply-To}, {@code References} and {@code Reply-To} headers are set by Resend and cannot be set here. Sending one of them, in any letter case, returns a 422. + * + * @param key The key of the custom header. + * @param value The value of the custom header. + * @return This builder instance for method chaining. + */ + public Builder addHeader(String key, String value) { + if (this.headers == null) { + this.headers = new LinkedHashMap<>(); + } + this.headers.put(key, value); + return this; + } + /** * Build a new ForwardInboxThreadEmailOptions object. * diff --git a/src/main/java/com/resend/services/inboxes/model/ReplyInboxThreadEmailOptions.java b/src/main/java/com/resend/services/inboxes/model/ReplyInboxThreadEmailOptions.java index 2f875f5..9fe94b0 100644 --- a/src/main/java/com/resend/services/inboxes/model/ReplyInboxThreadEmailOptions.java +++ b/src/main/java/com/resend/services/inboxes/model/ReplyInboxThreadEmailOptions.java @@ -5,7 +5,9 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; /** * Represents a request to reply to a thread email. At least one of {@code html} or {@code text} must contain more than whitespace. @@ -33,6 +35,9 @@ public class ReplyInboxThreadEmailOptions { @JsonProperty("attachments") private final List attachments; + @JsonProperty("headers") + private final Map headers; + private ReplyInboxThreadEmailOptions(Builder builder) { this.cc = builder.cc; this.bcc = builder.bcc; @@ -41,6 +46,7 @@ private ReplyInboxThreadEmailOptions(Builder builder) { this.subject = builder.subject; this.replyAll = builder.replyAll; this.attachments = builder.attachments; + this.headers = builder.headers; } /** @@ -106,6 +112,15 @@ public List getAttachments() { return attachments; } + /** + * Retrieves the custom headers of the email. + * + * @return The custom headers of the email. + */ + public Map getHeaders() { + return headers; + } + /** * Create a new builder instance for constructing ReplyInboxThreadEmailOptions objects. * @@ -126,6 +141,7 @@ public static class Builder { private String subject; private Boolean replyAll; private List attachments; + private Map headers; /** * Creates a new Builder instance. @@ -285,6 +301,32 @@ public Builder addAttachment(Attachment attachment) { return this; } + /** + * Set the custom headers of the email. The {@code In-Reply-To}, {@code References} and {@code Reply-To} headers are set by Resend and cannot be set here. Sending one of them, in any letter case, returns a 422. + * + * @param headers The map of custom headers for the email. + * @return This builder instance for method chaining. + */ + public Builder headers(Map headers) { + this.headers = headers == null ? null : new LinkedHashMap<>(headers); + return this; + } + + /** + * Add a single custom header to the email headers. The {@code In-Reply-To}, {@code References} and {@code Reply-To} headers are set by Resend and cannot be set here. Sending one of them, in any letter case, returns a 422. + * + * @param key The key of the custom header. + * @param value The value of the custom header. + * @return This builder instance for method chaining. + */ + public Builder addHeader(String key, String value) { + if (this.headers == null) { + this.headers = new LinkedHashMap<>(); + } + this.headers.put(key, value); + return this; + } + /** * Build a new ReplyInboxThreadEmailOptions object. * diff --git a/src/main/java/com/resend/services/inboxes/model/SendInboxDraftOptions.java b/src/main/java/com/resend/services/inboxes/model/SendInboxDraftOptions.java new file mode 100644 index 0000000..5f9c0f9 --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/model/SendInboxDraftOptions.java @@ -0,0 +1,85 @@ +package com.resend.services.inboxes.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Represents the optional body of a request to send a draft. Its only field is {@code headers}. + */ +public class SendInboxDraftOptions { + + @JsonProperty("headers") + private final Map headers; + + private SendInboxDraftOptions(Builder builder) { + this.headers = builder.headers; + } + + /** + * Retrieves the custom headers of the email. + * + * @return The custom headers of the email. + */ + public Map getHeaders() { + return headers; + } + + /** + * Create a new builder instance for constructing SendInboxDraftOptions objects. + * + * @return A new builder instance. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Builder class for constructing SendInboxDraftOptions objects. + */ + public static class Builder { + private Map headers; + + /** + * Creates a new Builder instance. + */ + public Builder() { + } + + /** + * Set the custom headers of the email. The {@code In-Reply-To}, {@code References} and {@code Reply-To} headers are set by Resend and cannot be set here. Sending one of them, in any letter case, returns a 422. + * + * @param headers The map of custom headers for the email. + * @return This builder instance for method chaining. + */ + public Builder headers(Map headers) { + this.headers = headers == null ? null : new LinkedHashMap<>(headers); + return this; + } + + /** + * Add a single custom header to the email headers. The {@code In-Reply-To}, {@code References} and {@code Reply-To} headers are set by Resend and cannot be set here. Sending one of them, in any letter case, returns a 422. + * + * @param key The key of the custom header. + * @param value The value of the custom header. + * @return This builder instance for method chaining. + */ + public Builder addHeader(String key, String value) { + if (this.headers == null) { + this.headers = new LinkedHashMap<>(); + } + this.headers.put(key, value); + return this; + } + + /** + * Build a new SendInboxDraftOptions object. + * + * @return A new SendInboxDraftOptions object. + */ + public SendInboxDraftOptions build() { + return new SendInboxDraftOptions(this); + } + } +} diff --git a/src/test/java/com/resend/services/inboxes/InboxDraftsTest.java b/src/test/java/com/resend/services/inboxes/InboxDraftsTest.java index ceef622..ce93473 100644 --- a/src/test/java/com/resend/services/inboxes/InboxDraftsTest.java +++ b/src/test/java/com/resend/services/inboxes/InboxDraftsTest.java @@ -223,6 +223,20 @@ public void testCreateAndSendDraft_WithIdempotencyKey() throws ResendException { assertEquals("draft-key-1", options.getIdempotencyKey()); } + @Test + public void testSendDraft_WithHeaders() throws ResendException { + SendInboxDraftOptions options = SendInboxDraftOptions.builder().addHeader("X-Ticket-Id", "4821").build(); + RequestOptions requestOptions = RequestOptions.builder().setIdempotencyKey("send-key-2").build(); + String payload = "{\"headers\":{\"X-Ticket-Id\":\"4821\"}}"; + when(httpClient.perform(eq(DRAFTS_PATH + "/" + DRAFT_ID + "/send"), anyString(), eq(HttpMethod.POST), eq(payload), any(MediaType.class))) + .thenReturn(new AbstractHttpResponse<>(200, "{\"id\":\"" + SENT_EMAIL_ID + "\"}", true)); + when(httpClient.perform(eq(DRAFTS_PATH + "/" + DRAFT_ID + "/send"), anyString(), eq(HttpMethod.POST), eq(payload), any(MediaType.class), eq(requestOptions))) + .thenReturn(new AbstractHttpResponse<>(200, "{\"id\":\"" + SENT_EMAIL_ID + "\"}", true)); + + assertEquals(SENT_EMAIL_ID, drafts.send(INBOX_ID, DRAFT_ID, options).getId()); + assertEquals(SENT_EMAIL_ID, drafts.send(INBOX_ID, DRAFT_ID, options, requestOptions).getId()); + } + @Test public void testSendDraft_AlreadySent_ThrowsResendException() throws ResendException { when(httpClient.perform(eq(DRAFTS_PATH + "/" + DRAFT_ID + "/send"), anyString(), eq(HttpMethod.POST), eq(""), any(MediaType.class))) diff --git a/src/test/java/com/resend/services/inboxes/InboxThreadEmailsTest.java b/src/test/java/com/resend/services/inboxes/InboxThreadEmailsTest.java index 6703aae..1276528 100644 --- a/src/test/java/com/resend/services/inboxes/InboxThreadEmailsTest.java +++ b/src/test/java/com/resend/services/inboxes/InboxThreadEmailsTest.java @@ -106,6 +106,22 @@ public void testForwardBuilder_RequiresTo() { assertThrows(IllegalArgumentException.class, () -> ForwardInboxThreadEmailOptions.builder().text("FYI").build()); } + @Test + public void testReplyAndForward_WithHeaders() throws ResendException { + ReplyInboxThreadEmailOptions reply = ReplyInboxThreadEmailOptions.builder().text("Thanks").addHeader("X-Ticket-Id", "4821").build(); + ForwardInboxThreadEmailOptions forward = ForwardInboxThreadEmailOptions.builder().to("colleague@example.org") + .headers(java.util.Collections.singletonMap("X-Ticket-Id", "4821")).build(); + when(httpClient.perform(eq(EMAILS_PATH + "/" + EMAIL_ID + "/reply"), anyString(), eq(HttpMethod.POST), + eq("{\"text\":\"Thanks\",\"headers\":{\"X-Ticket-Id\":\"4821\"}}"), any(MediaType.class))) + .thenReturn(new AbstractHttpResponse<>(200, "{\"id\":\"" + SENT_EMAIL_ID + "\"}", true)); + when(httpClient.perform(eq(EMAILS_PATH + "/" + EMAIL_ID + "/forward"), anyString(), eq(HttpMethod.POST), + eq("{\"to\":[\"colleague@example.org\"],\"headers\":{\"X-Ticket-Id\":\"4821\"}}"), any(MediaType.class))) + .thenReturn(new AbstractHttpResponse<>(200, "{\"id\":\"" + SENT_EMAIL_ID + "\"}", true)); + + assertEquals(SENT_EMAIL_ID, emails.reply(INBOX_ID, THREAD_ID, EMAIL_ID, reply).getId()); + assertEquals(SENT_EMAIL_ID, emails.forward(INBOX_ID, THREAD_ID, EMAIL_ID, forward).getId()); + } + @Test public void testReplyAndForward_WithIdempotencyKey() throws ResendException { RequestOptions options = RequestOptions.builder().setIdempotencyKey("reply-key-1").build(); From c983091091746319d6ff603397abb5bd791faa21 Mon Sep 17 00:00:00 2001 From: Felipe Freitag Date: Tue, 13 Oct 2026 15:00:49 -0300 Subject: [PATCH 16/30] docs(inboxes): align idempotency, header and attachment notes with the API --- .../com/resend/services/inboxes/InboxDrafts.java | 15 +++++++++------ .../services/inboxes/InboxThreadEmails.java | 10 ++++++---- .../model/ForwardInboxThreadEmailOptions.java | 8 ++++---- .../model/ReplyInboxThreadEmailOptions.java | 8 ++++---- .../inboxes/model/SendInboxDraftOptions.java | 4 ++-- .../inboxes/model/UpdateInboxDraftOptions.java | 10 +++------- .../resend/services/inboxes/InboxDraftsTest.java | 10 ++++++++++ 7 files changed, 38 insertions(+), 27 deletions(-) diff --git a/src/main/java/com/resend/services/inboxes/InboxDrafts.java b/src/main/java/com/resend/services/inboxes/InboxDrafts.java index d539dad..84f619c 100644 --- a/src/main/java/com/resend/services/inboxes/InboxDrafts.java +++ b/src/main/java/com/resend/services/inboxes/InboxDrafts.java @@ -94,8 +94,9 @@ public CreateInboxDraftResponseSuccess create(String inboxId, CreateInboxDraftOp * subject and gets no {@code Re:} prefix, so set one before you send it. * *

Set an idempotency key in {@code requestOptions} so the draft is created only once. A retry with the same key - * within 24 hours returns the first response, with the {@code idempotent-replayed: true} header. Reusing the - * key with a different body, or while the first request is still running, returns a 409.

+ * within 24 hours returns the first successful response, with the {@code idempotent-replayed: true} header. Reusing the + * key with a different body, or while the first request is still running, returns a 409. A failed request is + * not stored, so you can retry it with the same key.

* * @param inboxId The inbox ID. An email address is not accepted. * @param createInboxDraftOptions The draft content. @@ -171,8 +172,9 @@ public SendInboxDraftResponseSuccess send(String inboxId, String draftId) throws * A draft that was already sent returns a 409. * *

Set an idempotency key in {@code requestOptions} so the draft is sent only once. A retry with the same key - * within 24 hours returns the first response, with the {@code idempotent-replayed: true} header. Reusing the - * key with a different body, or while the first request is still running, returns a 409.

+ * within 24 hours returns the first successful response, with the {@code idempotent-replayed: true} header. Reusing the + * key with a different body, or while the first request is still running, returns a 409. A failed request is + * not stored, so you can retry it with the same key.

* * @param inboxId The inbox ID. An email address is not accepted. * @param draftId The draft ID. @@ -208,8 +210,9 @@ public SendInboxDraftResponseSuccess send(String inboxId, String draftId, SendIn * A draft that was already sent returns a 409. * *

Set an idempotency key in {@code requestOptions} so the draft is sent only once. A retry with the same key - * within 24 hours returns the first response, with the {@code idempotent-replayed: true} header. Reusing the - * key with a different body, or while the first request is still running, returns a 409.

+ * within 24 hours returns the first successful response, with the {@code idempotent-replayed: true} header. Reusing the + * key with a different body, or while the first request is still running, returns a 409. A failed request is + * not stored, so you can retry it with the same key.

* * @param inboxId The inbox ID. An email address is not accepted. * @param draftId The draft ID. diff --git a/src/main/java/com/resend/services/inboxes/InboxThreadEmails.java b/src/main/java/com/resend/services/inboxes/InboxThreadEmails.java index cd42256..e3b8a5e 100644 --- a/src/main/java/com/resend/services/inboxes/InboxThreadEmails.java +++ b/src/main/java/com/resend/services/inboxes/InboxThreadEmails.java @@ -110,8 +110,9 @@ public ReplyInboxThreadEmailResponseSuccess reply(String inboxId, String threadI * {@code In-Reply-To} and {@code References} headers. * *

Set an idempotency key in {@code requestOptions} so the reply is sent only once. A retry with the same key - * within 24 hours returns the first response, with the {@code idempotent-replayed: true} header. Reusing the - * key with a different body, or while the first request is still running, returns a 409.

+ * within 24 hours returns the first successful response, with the {@code idempotent-replayed: true} header. Reusing the + * key with a different body, or while the first request is still running, returns a 409. A failed request is + * not stored, so you can retry it with the same key.

* * @param inboxId The inbox ID. An email address is not accepted. * @param threadId The thread ID. @@ -155,8 +156,9 @@ public ForwardInboxThreadEmailResponseSuccess forward(String inboxId, String thr * a URL ({@code path}) cannot be forwarded, and the request returns a 422. * *

Set an idempotency key in {@code requestOptions} so the forward is sent only once. A retry with the same key - * within 24 hours returns the first response, with the {@code idempotent-replayed: true} header. Reusing the - * key with a different body, or while the first request is still running, returns a 409.

+ * within 24 hours returns the first successful response, with the {@code idempotent-replayed: true} header. Reusing the + * key with a different body, or while the first request is still running, returns a 409. A failed request is + * not stored, so you can retry it with the same key.

* * @param inboxId The inbox ID. An email address is not accepted. * @param threadId The thread ID. diff --git a/src/main/java/com/resend/services/inboxes/model/ForwardInboxThreadEmailOptions.java b/src/main/java/com/resend/services/inboxes/model/ForwardInboxThreadEmailOptions.java index 5a5ca61..21b4a5b 100644 --- a/src/main/java/com/resend/services/inboxes/model/ForwardInboxThreadEmailOptions.java +++ b/src/main/java/com/resend/services/inboxes/model/ForwardInboxThreadEmailOptions.java @@ -291,7 +291,7 @@ public Builder subject(String subject) { } /** - * Set the files to attach, as on {@code POST /emails}. Each one needs {@code content} or {@code path}. They share one limit with the attachments of the original message, which the forward also sends: up to 10 files and at most 25 MB of {@code content}. + * Set the files to attach, as on {@code POST /emails}. Each one needs {@code content} or {@code path}. They share one limit with the attachments of the original message, which the forward also sends: up to 10 files and at most 25 MB of {@code content}. An empty file, a blocked file type, or a {@code path} that is not an {@code http} or {@code https} URL or points to {@code localhost} returns a 422. * * @param attachments The files to attach, as on {@code POST /emails}. * @return The builder instance. @@ -302,7 +302,7 @@ public Builder attachments(Attachment... attachments) { } /** - * Set the files to attach, as on {@code POST /emails}. Each one needs {@code content} or {@code path}. They share one limit with the attachments of the original message, which the forward also sends: up to 10 files and at most 25 MB of {@code content}. + * Set the files to attach, as on {@code POST /emails}. Each one needs {@code content} or {@code path}. They share one limit with the attachments of the original message, which the forward also sends: up to 10 files and at most 25 MB of {@code content}. An empty file, a blocked file type, or a {@code path} that is not an {@code http} or {@code https} URL or points to {@code localhost} returns a 422. * * @param attachments The files to attach, as on {@code POST /emails}. * @return The builder instance. @@ -327,7 +327,7 @@ public Builder addAttachment(Attachment attachment) { } /** - * Set the custom headers of the email. The {@code In-Reply-To}, {@code References} and {@code Reply-To} headers are set by Resend and cannot be set here. Sending one of them, in any letter case, returns a 422. + * Set the custom headers of the email. The {@code In-Reply-To}, {@code References} and {@code Reply-To} headers are set by Resend and cannot be set here. Sending one of them, in any letter case, returns a 422. Header names cannot be empty, and names and values cannot contain line breaks or null characters, or the request returns a 422. * * @param headers The map of custom headers for the email. * @return This builder instance for method chaining. @@ -338,7 +338,7 @@ public Builder headers(Map headers) { } /** - * Add a single custom header to the email headers. The {@code In-Reply-To}, {@code References} and {@code Reply-To} headers are set by Resend and cannot be set here. Sending one of them, in any letter case, returns a 422. + * Add a single custom header to the email headers. The {@code In-Reply-To}, {@code References} and {@code Reply-To} headers are set by Resend and cannot be set here. Sending one of them, in any letter case, returns a 422. Header names cannot be empty, and names and values cannot contain line breaks or null characters, or the request returns a 422. * * @param key The key of the custom header. * @param value The value of the custom header. diff --git a/src/main/java/com/resend/services/inboxes/model/ReplyInboxThreadEmailOptions.java b/src/main/java/com/resend/services/inboxes/model/ReplyInboxThreadEmailOptions.java index 9fe94b0..1b6a28a 100644 --- a/src/main/java/com/resend/services/inboxes/model/ReplyInboxThreadEmailOptions.java +++ b/src/main/java/com/resend/services/inboxes/model/ReplyInboxThreadEmailOptions.java @@ -266,7 +266,7 @@ public Builder replyAll(Boolean replyAll) { } /** - * Set the files to attach, as on {@code POST /emails}. Each one needs {@code content} or {@code path}. Up to 10 files. The {@code content} of all files can total at most 25 MB. + * Set the files to attach, as on {@code POST /emails}. Each one needs {@code content} or {@code path}. Up to 10 files. The {@code content} of all files can total at most 25 MB. An empty file, a blocked file type, or a {@code path} that is not an {@code http} or {@code https} URL or points to {@code localhost} returns a 422. * * @param attachments The files to attach, as on {@code POST /emails}. * @return The builder instance. @@ -277,7 +277,7 @@ public Builder attachments(Attachment... attachments) { } /** - * Set the files to attach, as on {@code POST /emails}. Each one needs {@code content} or {@code path}. Up to 10 files. The {@code content} of all files can total at most 25 MB. + * Set the files to attach, as on {@code POST /emails}. Each one needs {@code content} or {@code path}. Up to 10 files. The {@code content} of all files can total at most 25 MB. An empty file, a blocked file type, or a {@code path} that is not an {@code http} or {@code https} URL or points to {@code localhost} returns a 422. * * @param attachments The files to attach, as on {@code POST /emails}. * @return The builder instance. @@ -302,7 +302,7 @@ public Builder addAttachment(Attachment attachment) { } /** - * Set the custom headers of the email. The {@code In-Reply-To}, {@code References} and {@code Reply-To} headers are set by Resend and cannot be set here. Sending one of them, in any letter case, returns a 422. + * Set the custom headers of the email. The {@code In-Reply-To}, {@code References} and {@code Reply-To} headers are set by Resend and cannot be set here. Sending one of them, in any letter case, returns a 422. Header names cannot be empty, and names and values cannot contain line breaks or null characters, or the request returns a 422. * * @param headers The map of custom headers for the email. * @return This builder instance for method chaining. @@ -313,7 +313,7 @@ public Builder headers(Map headers) { } /** - * Add a single custom header to the email headers. The {@code In-Reply-To}, {@code References} and {@code Reply-To} headers are set by Resend and cannot be set here. Sending one of them, in any letter case, returns a 422. + * Add a single custom header to the email headers. The {@code In-Reply-To}, {@code References} and {@code Reply-To} headers are set by Resend and cannot be set here. Sending one of them, in any letter case, returns a 422. Header names cannot be empty, and names and values cannot contain line breaks or null characters, or the request returns a 422. * * @param key The key of the custom header. * @param value The value of the custom header. diff --git a/src/main/java/com/resend/services/inboxes/model/SendInboxDraftOptions.java b/src/main/java/com/resend/services/inboxes/model/SendInboxDraftOptions.java index 5f9c0f9..418121b 100644 --- a/src/main/java/com/resend/services/inboxes/model/SendInboxDraftOptions.java +++ b/src/main/java/com/resend/services/inboxes/model/SendInboxDraftOptions.java @@ -48,7 +48,7 @@ public Builder() { } /** - * Set the custom headers of the email. The {@code In-Reply-To}, {@code References} and {@code Reply-To} headers are set by Resend and cannot be set here. Sending one of them, in any letter case, returns a 422. + * Set the custom headers of the email. The {@code In-Reply-To}, {@code References} and {@code Reply-To} headers are set by Resend and cannot be set here. Sending one of them, in any letter case, returns a 422. Header names cannot be empty, and names and values cannot contain line breaks or null characters, or the request returns a 422. * * @param headers The map of custom headers for the email. * @return This builder instance for method chaining. @@ -59,7 +59,7 @@ public Builder headers(Map headers) { } /** - * Add a single custom header to the email headers. The {@code In-Reply-To}, {@code References} and {@code Reply-To} headers are set by Resend and cannot be set here. Sending one of them, in any letter case, returns a 422. + * Add a single custom header to the email headers. The {@code In-Reply-To}, {@code References} and {@code Reply-To} headers are set by Resend and cannot be set here. Sending one of them, in any letter case, returns a 422. Header names cannot be empty, and names and values cannot contain line breaks or null characters, or the request returns a 422. * * @param key The key of the custom header. * @param value The value of the custom header. diff --git a/src/main/java/com/resend/services/inboxes/model/UpdateInboxDraftOptions.java b/src/main/java/com/resend/services/inboxes/model/UpdateInboxDraftOptions.java index c58f89c..bcbfdd7 100644 --- a/src/main/java/com/resend/services/inboxes/model/UpdateInboxDraftOptions.java +++ b/src/main/java/com/resend/services/inboxes/model/UpdateInboxDraftOptions.java @@ -95,7 +95,7 @@ public String getText() { /** * Get the files to store on the draft. * - * @return The files, or null when they are not set. + * @return The files, or null when they are not set or are cleared. */ @SuppressWarnings("unchecked") public List getAttachments() { @@ -238,7 +238,7 @@ public Builder attachments(Attachment... attachments) { /** * Set the files to store on the draft. Replaces every attachment on the draft. Pass an empty list to - * remove them all. Pass null to leave the attachments unchanged. Each one needs {@code content}, + * remove them all. Pass null to remove them all as well. Each one needs {@code content}, * {@code filename} and {@code content_type}. A {@code path} or {@code content_id} is not accepted. * The files on a draft can total at most 25 MB. An empty file or a blocked file type returns a 422. * @@ -246,11 +246,7 @@ public Builder attachments(Attachment... attachments) { * @return The builder instance. */ public Builder attachments(List attachments) { - if (attachments == null) { - fields.remove("attachments"); - } else { - fields.put("attachments", new ArrayList<>(attachments)); - } + fields.put("attachments", attachments == null ? null : new ArrayList<>(attachments)); return this; } diff --git a/src/test/java/com/resend/services/inboxes/InboxDraftsTest.java b/src/test/java/com/resend/services/inboxes/InboxDraftsTest.java index ce93473..45bec28 100644 --- a/src/test/java/com/resend/services/inboxes/InboxDraftsTest.java +++ b/src/test/java/com/resend/services/inboxes/InboxDraftsTest.java @@ -112,6 +112,16 @@ public void testUpdateDraft_EmptyAttachmentsRemovesThem() throws ResendException assertTrue(options.getAttachments().isEmpty()); } + @Test + public void testUpdateDraft_NullAttachmentsClearsThem() throws ResendException { + UpdateInboxDraftOptions options = UpdateInboxDraftOptions.builder().attachments((java.util.List) null).build(); + when(httpClient.perform(eq(DRAFTS_PATH + "/" + DRAFT_ID), anyString(), eq(HttpMethod.PATCH), + eq("{\"attachments\":null}"), any(MediaType.class))) + .thenReturn(new AbstractHttpResponse<>(200, DRAFT_JSON, true)); + + assertNotNull(drafts.update(INBOX_ID, DRAFT_ID, options)); + } + @Test public void testDraftAttachmentBuilder_Validation() { Attachment withPath = Attachment.builder().fileName("a.txt").path("https://example.com/a.txt").contentType("text/plain").build(); From 3abb768f2d5f5c6aa26ed615a70ab66b280569d4 Mon Sep 17 00:00:00 2001 From: Felipe Freitag Date: Wed, 14 Oct 2026 09:27:13 -0300 Subject: [PATCH 17/30] fix(inboxes): rename message_count to email_count --- .../resend/services/inboxes/model/InboxThread.java | 14 +++++++------- .../inboxes/model/InboxThreadSearchResult.java | 6 +++--- .../resend/services/inboxes/InboxThreadsTest.java | 5 +++-- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/resend/services/inboxes/model/InboxThread.java b/src/main/java/com/resend/services/inboxes/model/InboxThread.java index cb61cf9..644c1f9 100644 --- a/src/main/java/com/resend/services/inboxes/model/InboxThread.java +++ b/src/main/java/com/resend/services/inboxes/model/InboxThread.java @@ -30,8 +30,8 @@ public class InboxThread { @JsonProperty("labels") private List labels; - @JsonProperty("message_count") - private Integer messageCount; + @JsonProperty("email_count") + private Integer emailCount; @JsonProperty("has_attachment") private Boolean hasAttachment; @@ -65,14 +65,14 @@ public InboxThread() { * @param cc The CC recipients of the most recent email. * @param bcc The BCC recipients of the most recent email. Always empty for a received email. * @param labels The labels attached to the thread. - * @param messageCount The number of emails in the thread. + * @param emailCount The number of emails in the thread. * @param hasAttachment Whether the most recent email in the thread has an attachment. * @param hasDraft Whether the thread has an unsent draft. * @param read True only when every email in the thread is read. * @param receivedAt The timestamp when the thread was last active. * @param folder The folder the thread is in. Every thread is in exactly one folder. */ - public InboxThread(String id, String subject, String from, List to, List cc, List bcc, List labels, Integer messageCount, Boolean hasAttachment, Boolean hasDraft, Boolean read, String receivedAt, String folder) { + public InboxThread(String id, String subject, String from, List to, List cc, List bcc, List labels, Integer emailCount, Boolean hasAttachment, Boolean hasDraft, Boolean read, String receivedAt, String folder) { this.id = id; this.subject = subject; this.from = from; @@ -80,7 +80,7 @@ public InboxThread(String id, String subject, String from, List to, List this.cc = cc; this.bcc = bcc; this.labels = labels; - this.messageCount = messageCount; + this.emailCount = emailCount; this.hasAttachment = hasAttachment; this.hasDraft = hasDraft; this.read = read; @@ -156,8 +156,8 @@ public List getLabels() { * * @return The number of emails in the thread. */ - public Integer getMessageCount() { - return messageCount; + public Integer getEmailCount() { + return emailCount; } /** diff --git a/src/main/java/com/resend/services/inboxes/model/InboxThreadSearchResult.java b/src/main/java/com/resend/services/inboxes/model/InboxThreadSearchResult.java index eaa424e..987a5c8 100644 --- a/src/main/java/com/resend/services/inboxes/model/InboxThreadSearchResult.java +++ b/src/main/java/com/resend/services/inboxes/model/InboxThreadSearchResult.java @@ -34,7 +34,7 @@ public InboxThreadSearchResult() { * @param cc The CC recipients of the most recent email. * @param bcc The BCC recipients of the most recent email. Always empty for a received email. * @param labels The labels attached to the thread. - * @param messageCount The number of emails in the thread. + * @param emailCount The number of emails in the thread. * @param hasAttachment Whether the most recent email in the thread has an attachment. * @param hasDraft Whether the thread has an unsent draft. * @param read True only when every email in the thread is read. @@ -43,8 +43,8 @@ public InboxThreadSearchResult() { * @param matchedEmailId The email in the thread that matched the search. The get, reply and forward thread email endpoints accept it as {@code email_id}, but it is not equal to the {@code id} those endpoints return. Null when the search index returns no matched email for the thread. * @param highlights Where {@code query} matched the email in {@code matched_email_id}, as text fragments with each match wrapped in {@code **}, keyed by {@code subject}, {@code body}, {@code from}, {@code to}, {@code cc}, {@code bcc} or {@code attachments}. Only keys with a match are present. Empty when there is no {@code query}. */ - public InboxThreadSearchResult(String id, String subject, String from, List to, List cc, List bcc, List labels, Integer messageCount, Boolean hasAttachment, Boolean hasDraft, Boolean read, String receivedAt, String folder, String matchedEmailId, Map> highlights) { - super(id, subject, from, to, cc, bcc, labels, messageCount, hasAttachment, hasDraft, read, receivedAt, folder); + public InboxThreadSearchResult(String id, String subject, String from, List to, List cc, List bcc, List labels, Integer emailCount, Boolean hasAttachment, Boolean hasDraft, Boolean read, String receivedAt, String folder, String matchedEmailId, Map> highlights) { + super(id, subject, from, to, cc, bcc, labels, emailCount, hasAttachment, hasDraft, read, receivedAt, folder); this.matchedEmailId = matchedEmailId; this.highlights = highlights; } diff --git a/src/test/java/com/resend/services/inboxes/InboxThreadsTest.java b/src/test/java/com/resend/services/inboxes/InboxThreadsTest.java index df514db..10a614f 100644 --- a/src/test/java/com/resend/services/inboxes/InboxThreadsTest.java +++ b/src/test/java/com/resend/services/inboxes/InboxThreadsTest.java @@ -25,7 +25,7 @@ public class InboxThreadsTest { private static final String THREAD_JSON = "{\"id\":\"" + THREAD_ID + "\",\"subject\":\"Refund for order 1041\"," + "\"from\":\"ada@example.org\",\"to\":[\"support@example.com\"],\"cc\":[],\"bcc\":[]," + "\"labels\":[{\"id\":\"" + LABEL_ID + "\",\"name\":\"Urgent\",\"color\":\"#E93D82\"}]," + - "\"message_count\":2,\"has_attachment\":true,\"has_draft\":false,\"read\":false," + + "\"email_count\":2,\"has_attachment\":true,\"has_draft\":false,\"read\":false," + "\"received_at\":\"2026-08-05T14:03:11.229Z\",\"folder\":\"inbox\""; private static final String SUMMARY_JSON = "{\"object\":\"inbox_thread\",\"id\":\"" + THREAD_ID + "\"," + "\"subject\":\"Refund for order 1041\",\"folder\":\"archive\"," + @@ -63,7 +63,7 @@ public void testListThreads_WithFilters() throws ResendException { assertEquals("ada@example.org", thread.getFrom()); assertEquals(Collections.singletonList("support@example.com"), thread.getTo()); assertEquals("#E93D82", thread.getLabels().get(0).getColor()); - assertEquals(2, (int) thread.getMessageCount()); + assertEquals(2, (int) thread.getEmailCount()); assertTrue(thread.getHasAttachment()); assertFalse(thread.getHasDraft()); assertFalse(thread.getRead()); @@ -101,6 +101,7 @@ public void testSearchThreads_Success() throws ResendException { InboxThreadSearchResult result = response.getData().get(0); assertEquals(THREAD_ID, result.getId()); + assertEquals(2, (int) result.getEmailCount()); assertEquals("e8a3f1c6-2b9d-4e5a-a7c4-9d1b6f3e2a80", result.getMatchedEmailId()); assertEquals(Collections.singletonList("September **invoice**"), result.getHighlights().get("subject")); assertFalse(result.getHighlights().containsKey("body")); From 7bc5d22ca321031c9e1770f1b902cfddc306fe86 Mon Sep 17 00:00:00 2001 From: Felipe Freitag Date: Wed, 14 Oct 2026 10:28:20 -0300 Subject: [PATCH 18/30] fix(inboxes): correct the reply recipient, trash and from_name notes --- .../services/inboxes/model/CreateInboxDraftOptions.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/resend/services/inboxes/model/CreateInboxDraftOptions.java b/src/main/java/com/resend/services/inboxes/model/CreateInboxDraftOptions.java index 863e6aa..3e416c6 100644 --- a/src/main/java/com/resend/services/inboxes/model/CreateInboxDraftOptions.java +++ b/src/main/java/com/resend/services/inboxes/model/CreateInboxDraftOptions.java @@ -162,7 +162,7 @@ public Builder() { } /** - * Set the recipients. Each value can include a friendly name, for example {@code Ada Lovelace }. A reply draft without {@code to} is addressed to the sender of the email it replies to, or to its Reply-To when set. Send an empty list to leave it empty. + * Set the recipients. Each value can include a friendly name, for example {@code Ada Lovelace }. A reply draft without {@code 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 {@code to} recipients. Send an empty list to leave it empty. * * @param to The recipients. * @return The builder instance. @@ -173,7 +173,7 @@ public Builder to(String... to) { } /** - * Set the recipients. Each value can include a friendly name, for example {@code Ada Lovelace }. A reply draft without {@code to} is addressed to the sender of the email it replies to, or to its Reply-To when set. Send an empty list to leave it empty. + * Set the recipients. Each value can include a friendly name, for example {@code Ada Lovelace }. A reply draft without {@code 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 {@code to} recipients. Send an empty list to leave it empty. * * @param to The recipients. * @return The builder instance. From c7e990ce528601630cf07023bb2f1ed6394c397f Mon Sep 17 00:00:00 2001 From: Felipe Freitag Date: Wed, 14 Oct 2026 13:36:09 -0300 Subject: [PATCH 19/30] docs(inboxes): describe the agent tone presets --- .../inboxes/model/GetInboxAgentResponseSuccess.java | 2 +- .../java/com/resend/services/inboxes/model/InboxAgent.java | 6 +++--- .../services/inboxes/model/UpdateInboxAgentOptions.java | 6 ++++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/resend/services/inboxes/model/GetInboxAgentResponseSuccess.java b/src/main/java/com/resend/services/inboxes/model/GetInboxAgentResponseSuccess.java index e3c9c62..9e264ce 100644 --- a/src/main/java/com/resend/services/inboxes/model/GetInboxAgentResponseSuccess.java +++ b/src/main/java/com/resend/services/inboxes/model/GetInboxAgentResponseSuccess.java @@ -20,7 +20,7 @@ public GetInboxAgentResponseSuccess() { * * @param object The object type. Always {@code inbox_agent}. * @param instructions The instructions the agent follows when it handles the threads of the inbox. Null when no instructions are set. - * @param tone The tone the agent writes in, such as {@code friendly and concise}. Null when no tone is set. + * @param tone The tone the agent writes in. One of the presets {@code concise_professional}, {@code concise_casual}, {@code expanded_professional}, or {@code expanded_casual}, or custom text in the owner's own words, such as {@code friendly and concise}. Older dashboard values such as {@code Concise · Casual} are custom text. Null when no tone is set. * @param enabledActions The actions the agent can take. Empty when no actions are enabled. */ public GetInboxAgentResponseSuccess(String object, String instructions, String tone, List enabledActions) { diff --git a/src/main/java/com/resend/services/inboxes/model/InboxAgent.java b/src/main/java/com/resend/services/inboxes/model/InboxAgent.java index 84003f2..342cef2 100644 --- a/src/main/java/com/resend/services/inboxes/model/InboxAgent.java +++ b/src/main/java/com/resend/services/inboxes/model/InboxAgent.java @@ -33,7 +33,7 @@ public InboxAgent() { * * @param object The object type. Always {@code inbox_agent}. * @param instructions The instructions the agent follows when it handles the threads of the inbox. Null when no instructions are set. - * @param tone The tone the agent writes in, such as {@code friendly and concise}. Null when no tone is set. + * @param tone The tone the agent writes in. One of the presets {@code concise_professional}, {@code concise_casual}, {@code expanded_professional}, or {@code expanded_casual}, or custom text in the owner's own words, such as {@code friendly and concise}. Older dashboard values such as {@code Concise · Casual} are custom text. Null when no tone is set. * @param enabledActions The actions the agent can take. Empty when no actions are enabled. */ public InboxAgent(String object, String instructions, String tone, List enabledActions) { @@ -62,9 +62,9 @@ public String getInstructions() { } /** - * Get the tone the agent writes in, such as {@code friendly and concise}. Null when no tone is set. + * Get the tone the agent writes in. One of the presets {@code concise_professional}, {@code concise_casual}, {@code expanded_professional}, or {@code expanded_casual}, or custom text in the owner's own words, such as {@code friendly and concise}. Older dashboard values such as {@code Concise · Casual} are custom text. Null when no tone is set. * - * @return The tone the agent writes in, such as {@code friendly and concise}. Null when no tone is set. + * @return The tone the agent writes in. One of the presets {@code concise_professional}, {@code concise_casual}, {@code expanded_professional}, or {@code expanded_casual}, or custom text in the owner's own words, such as {@code friendly and concise}. Older dashboard values such as {@code Concise · Casual} are custom text. Null when no tone is set. */ public String getTone() { return tone; diff --git a/src/main/java/com/resend/services/inboxes/model/UpdateInboxAgentOptions.java b/src/main/java/com/resend/services/inboxes/model/UpdateInboxAgentOptions.java index 1abac09..2ce408b 100644 --- a/src/main/java/com/resend/services/inboxes/model/UpdateInboxAgentOptions.java +++ b/src/main/java/com/resend/services/inboxes/model/UpdateInboxAgentOptions.java @@ -94,8 +94,10 @@ public Builder instructions(String instructions) { } /** - * Set the tone the agent writes in, such as {@code friendly and concise}. - * Max 64 characters. Leading and trailing whitespace is trimmed, and the trimmed value must not be empty. + * Set the tone the agent writes in. Use one of the presets {@code concise_professional}, {@code concise_casual}, + * {@code expanded_professional}, or {@code expanded_casual}, or describe the tone in your own words, such as + * {@code friendly and concise}. Custom text is max 64 characters. Leading and trailing whitespace is trimmed, + * and the trimmed value must not be empty. * Pass null to clear the tone. * * @param tone The tone, or null to clear it. From b9abbcb070ab7cab4497a291028600f501284a1d Mon Sep 17 00:00:00 2001 From: Kewyn Akshlley Date: Fri, 16 Oct 2026 12:39:11 -0300 Subject: [PATCH 20/30] fix(inboxes): send and read the agent actions as `actions` The API names the field `actions`, not `enabled_actions`. The SDK sent `enabled_actions`, which the API ignored (or rejected with a 422 when it was the only field), and read `enabled_actions`, so getActions() was always null. The API also rejects `archive_thread` and `delete_thread`; the accepted values are `archive` and `move_to_trash`. - InboxAgent/UpdateInboxAgentOptions: enabledActions -> actions - InboxAgentAction: ARCHIVE_THREAD -> ARCHIVE, DELETE_THREAD -> MOVE_TO_TRASH --- .../resend/services/inboxes/InboxAgent.java | 2 +- .../model/GetInboxAgentResponseSuccess.java | 6 ++--- .../services/inboxes/model/InboxAgent.java | 14 +++++----- .../inboxes/model/InboxAgentAction.java | 6 ++--- .../model/UpdateInboxAgentOptions.java | 26 +++++++++---------- .../services/inboxes/InboxAgentTest.java | 26 ++++++++++++++----- 6 files changed, 46 insertions(+), 34 deletions(-) diff --git a/src/main/java/com/resend/services/inboxes/InboxAgent.java b/src/main/java/com/resend/services/inboxes/InboxAgent.java index 00a476e..74c26b4 100644 --- a/src/main/java/com/resend/services/inboxes/InboxAgent.java +++ b/src/main/java/com/resend/services/inboxes/InboxAgent.java @@ -35,7 +35,7 @@ public InboxAgent(final String apiKey, final IHttpClient httpClient) { /** * Retrieves the agent settings of an inbox. An inbox without a configured agent returns empty settings: - * {@code instructions} and {@code tone} are null and {@code enabled_actions} is an empty list. + * {@code instructions} and {@code tone} are null and {@code actions} is an empty list. * * @param inboxId The inbox ID. An email address is not accepted. * @return The agent settings of the inbox. diff --git a/src/main/java/com/resend/services/inboxes/model/GetInboxAgentResponseSuccess.java b/src/main/java/com/resend/services/inboxes/model/GetInboxAgentResponseSuccess.java index 9e264ce..21bec2a 100644 --- a/src/main/java/com/resend/services/inboxes/model/GetInboxAgentResponseSuccess.java +++ b/src/main/java/com/resend/services/inboxes/model/GetInboxAgentResponseSuccess.java @@ -21,9 +21,9 @@ public GetInboxAgentResponseSuccess() { * @param object The object type. Always {@code inbox_agent}. * @param instructions The instructions the agent follows when it handles the threads of the inbox. Null when no instructions are set. * @param tone The tone the agent writes in. One of the presets {@code concise_professional}, {@code concise_casual}, {@code expanded_professional}, or {@code expanded_casual}, or custom text in the owner's own words, such as {@code friendly and concise}. Older dashboard values such as {@code Concise · Casual} are custom text. Null when no tone is set. - * @param enabledActions The actions the agent can take. Empty when no actions are enabled. + * @param actions The actions the agent can take. Empty when no actions are enabled. */ - public GetInboxAgentResponseSuccess(String object, String instructions, String tone, List enabledActions) { - super(object, instructions, tone, enabledActions); + public GetInboxAgentResponseSuccess(String object, String instructions, String tone, List actions) { + super(object, instructions, tone, actions); } } diff --git a/src/main/java/com/resend/services/inboxes/model/InboxAgent.java b/src/main/java/com/resend/services/inboxes/model/InboxAgent.java index 342cef2..f72f65a 100644 --- a/src/main/java/com/resend/services/inboxes/model/InboxAgent.java +++ b/src/main/java/com/resend/services/inboxes/model/InboxAgent.java @@ -18,8 +18,8 @@ public class InboxAgent { @JsonProperty("tone") private String tone; - @JsonProperty("enabled_actions") - private List enabledActions; + @JsonProperty("actions") + private List actions; /** * Default constructor @@ -34,13 +34,13 @@ public InboxAgent() { * @param object The object type. Always {@code inbox_agent}. * @param instructions The instructions the agent follows when it handles the threads of the inbox. Null when no instructions are set. * @param tone The tone the agent writes in. One of the presets {@code concise_professional}, {@code concise_casual}, {@code expanded_professional}, or {@code expanded_casual}, or custom text in the owner's own words, such as {@code friendly and concise}. Older dashboard values such as {@code Concise · Casual} are custom text. Null when no tone is set. - * @param enabledActions The actions the agent can take. Empty when no actions are enabled. + * @param actions The actions the agent can take. Empty when no actions are enabled. */ - public InboxAgent(String object, String instructions, String tone, List enabledActions) { + public InboxAgent(String object, String instructions, String tone, List actions) { this.object = object; this.instructions = instructions; this.tone = tone; - this.enabledActions = enabledActions; + this.actions = actions; } /** @@ -75,7 +75,7 @@ public String getTone() { * * @return The actions the agent can take. Empty when no actions are enabled. */ - public List getEnabledActions() { - return enabledActions; + public List getActions() { + return actions; } } diff --git a/src/main/java/com/resend/services/inboxes/model/InboxAgentAction.java b/src/main/java/com/resend/services/inboxes/model/InboxAgentAction.java index 8b250d6..eadbad6 100644 --- a/src/main/java/com/resend/services/inboxes/model/InboxAgentAction.java +++ b/src/main/java/com/resend/services/inboxes/model/InboxAgentAction.java @@ -18,9 +18,9 @@ public enum InboxAgentAction { /** The agent marks threads as spam. */ MARK_AS_SPAM("mark_as_spam"), /** The agent archives threads. */ - ARCHIVE_THREAD("archive_thread"), - /** The agent deletes threads. */ - DELETE_THREAD("delete_thread"); + ARCHIVE("archive"), + /** The agent moves threads to trash. */ + MOVE_TO_TRASH("move_to_trash"); private final String value; diff --git a/src/main/java/com/resend/services/inboxes/model/UpdateInboxAgentOptions.java b/src/main/java/com/resend/services/inboxes/model/UpdateInboxAgentOptions.java index 2ce408b..efa38f1 100644 --- a/src/main/java/com/resend/services/inboxes/model/UpdateInboxAgentOptions.java +++ b/src/main/java/com/resend/services/inboxes/model/UpdateInboxAgentOptions.java @@ -13,7 +13,7 @@ /** * Represents a request to update the agent settings of an inbox. - * At least one of {@code instructions}, {@code tone}, or {@code enabled_actions} must be provided. + * At least one of {@code instructions}, {@code tone}, or {@code actions} must be provided. * Omitted fields keep their current value. */ @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.NONE, getterVisibility = JsonAutoDetect.Visibility.NONE, isGetterVisibility = JsonAutoDetect.Visibility.NONE) @@ -52,11 +52,11 @@ public String getTone() { /** * Get the actions the agent can take. * - * @return The enabled actions, or null when they are not set. + * @return The actions, or null when they are not set. */ @SuppressWarnings("unchecked") - public List getEnabledActions() { - return (List) fields.get("enabled_actions"); + public List getActions() { + return (List) fields.get("actions"); } /** @@ -111,11 +111,11 @@ public Builder tone(String tone) { /** * Set the actions the agent can take. Replaces the whole set. Duplicates are removed. Pass no actions to disable all actions. * - * @param enabledActions The enabled actions. + * @param actions The actions. * @return The builder instance. */ - public Builder enabledActions(InboxAgentAction... enabledActions) { - fields.put("enabled_actions", new ArrayList<>(Arrays.asList(enabledActions))); + public Builder actions(InboxAgentAction... actions) { + fields.put("actions", new ArrayList<>(Arrays.asList(actions))); return this; } @@ -123,14 +123,14 @@ public Builder enabledActions(InboxAgentAction... enabledActions) { * Set the actions the agent can take. Replaces the whole set. Duplicates are removed. Pass an empty list to disable all actions. * Pass null to leave the actions unchanged. * - * @param enabledActions The enabled actions. + * @param actions The actions. * @return The builder instance. */ - public Builder enabledActions(List enabledActions) { - if (enabledActions == null) { - fields.remove("enabled_actions"); + public Builder actions(List actions) { + if (actions == null) { + fields.remove("actions"); } else { - fields.put("enabled_actions", new ArrayList<>(enabledActions)); + fields.put("actions", new ArrayList<>(actions)); } return this; } @@ -143,7 +143,7 @@ public Builder enabledActions(List enabledActions) { */ public UpdateInboxAgentOptions build() { if (fields.isEmpty()) { - throw new IllegalArgumentException("At least one of instructions, tone or enabled_actions must be provided"); + throw new IllegalArgumentException("At least one of instructions, tone or actions must be provided"); } return new UpdateInboxAgentOptions(this); } diff --git a/src/test/java/com/resend/services/inboxes/InboxAgentTest.java b/src/test/java/com/resend/services/inboxes/InboxAgentTest.java index 6245585..225e194 100644 --- a/src/test/java/com/resend/services/inboxes/InboxAgentTest.java +++ b/src/test/java/com/resend/services/inboxes/InboxAgentTest.java @@ -40,7 +40,7 @@ public void setUp() { @Test public void testGetAgent_Success() throws ResendException { String json = "{\"object\":\"inbox_agent\",\"instructions\":null,\"tone\":\"friendly and concise\"," + - "\"enabled_actions\":[\"draft_reply\",\"add_labels\"]}"; + "\"actions\":[\"draft_reply\",\"add_labels\"]}"; when(httpClient.perform(eq(AGENT_PATH), anyString(), eq(HttpMethod.GET), isNull(), any(MediaType.class))) .thenReturn(new AbstractHttpResponse<>(200, json, true)); @@ -49,17 +49,17 @@ public void testGetAgent_Success() throws ResendException { assertEquals("inbox_agent", response.getObject()); assertNull(response.getInstructions()); assertEquals("friendly and concise", response.getTone()); - assertEquals(Arrays.asList("draft_reply", "add_labels"), response.getEnabledActions()); + assertEquals(Arrays.asList("draft_reply", "add_labels"), response.getActions()); } @Test public void testUpdateAgent_SendsOnlySetFields() throws ResendException { UpdateInboxAgentOptions options = UpdateInboxAgentOptions.builder() .tone("friendly and concise") - .enabledActions(InboxAgentAction.DRAFT_REPLY, InboxAgentAction.ADD_LABELS) + .actions(InboxAgentAction.DRAFT_REPLY, InboxAgentAction.ADD_LABELS) .build(); when(httpClient.perform(eq(AGENT_PATH), anyString(), eq(HttpMethod.PATCH), - eq("{\"tone\":\"friendly and concise\",\"enabled_actions\":[\"draft_reply\",\"add_labels\"]}"), any(MediaType.class))) + eq("{\"tone\":\"friendly and concise\",\"actions\":[\"draft_reply\",\"add_labels\"]}"), any(MediaType.class))) .thenReturn(new AbstractHttpResponse<>(200, UPDATE_RESPONSE_JSON, true)); UpdateInboxAgentResponseSuccess response = agent.update(INBOX_ID, options); @@ -72,15 +72,15 @@ public void testUpdateAgent_SendsOnlySetFields() throws ResendException { public void testUpdateAgent_SendsNullToClear() throws ResendException { UpdateInboxAgentOptions options = UpdateInboxAgentOptions.builder() .instructions(null) - .enabledActions(Collections.emptyList()) + .actions(Collections.emptyList()) .build(); when(httpClient.perform(eq(AGENT_PATH), anyString(), eq(HttpMethod.PATCH), - eq("{\"instructions\":null,\"enabled_actions\":[]}"), any(MediaType.class))) + eq("{\"instructions\":null,\"actions\":[]}"), any(MediaType.class))) .thenReturn(new AbstractHttpResponse<>(200, UPDATE_RESPONSE_JSON, true)); assertNotNull(agent.update(INBOX_ID, options)); assertNull(options.getInstructions()); - assertTrue(options.getEnabledActions().isEmpty()); + assertTrue(options.getActions().isEmpty()); } @Test @@ -88,6 +88,18 @@ public void testUpdateAgentBuilder_RequiresAField() { assertThrows(IllegalArgumentException.class, () -> UpdateInboxAgentOptions.builder().build()); } + @Test + public void testUpdateAgent_SendsArchiveAndMoveToTrash() throws ResendException { + UpdateInboxAgentOptions options = UpdateInboxAgentOptions.builder() + .actions(InboxAgentAction.ARCHIVE, InboxAgentAction.MOVE_TO_TRASH) + .build(); + when(httpClient.perform(eq(AGENT_PATH), anyString(), eq(HttpMethod.PATCH), + eq("{\"actions\":[\"archive\",\"move_to_trash\"]}"), any(MediaType.class))) + .thenReturn(new AbstractHttpResponse<>(200, UPDATE_RESPONSE_JSON, true)); + + assertNotNull(agent.update(INBOX_ID, options)); + } + @Test public void testAgentAction_UnknownValue_Throws() { assertEquals(InboxAgentAction.MARK_AS_SPAM, InboxAgentAction.fromValue("mark_as_spam")); From fdb76f3a679ef357e307ac3f2cb7f985fbc76cc0 Mon Sep 17 00:00:00 2001 From: Kewyn Akshlley Date: Fri, 16 Oct 2026 12:40:28 -0300 Subject: [PATCH 21/30] feat(inboxes): add the add, set and remove thread label endpoints The API changes a thread's labels through dedicated endpoints, which the SDK did not expose: - POST /inboxes/:inbox_id/threads/:thread_id/labels -> InboxThreads#addLabels - PUT /inboxes/:inbox_id/threads/:thread_id/labels -> InboxThreads#setLabels - DELETE /inboxes/:inbox_id/threads/:thread_id/labels/:label_id -> InboxThreads#removeLabel Set Thread Labels uses PUT, so HttpMethod gains PUT. --- .../java/com/resend/core/net/HttpMethod.java | 5 ++ .../resend/services/inboxes/InboxThreads.java | 48 +++++++++++ .../model/AddInboxThreadLabelsOptions.java | 86 +++++++++++++++++++ .../AddInboxThreadLabelsResponseSuccess.java | 27 ++++++ .../inboxes/model/InboxThreadLabelList.java | 53 ++++++++++++ ...RemoveInboxThreadLabelResponseSuccess.java | 27 ++++++ .../model/SetInboxThreadLabelsOptions.java | 86 +++++++++++++++++++ .../SetInboxThreadLabelsResponseSuccess.java | 27 ++++++ .../services/inboxes/InboxThreadsTest.java | 48 +++++++++++ 9 files changed, 407 insertions(+) create mode 100644 src/main/java/com/resend/services/inboxes/model/AddInboxThreadLabelsOptions.java create mode 100644 src/main/java/com/resend/services/inboxes/model/AddInboxThreadLabelsResponseSuccess.java create mode 100644 src/main/java/com/resend/services/inboxes/model/InboxThreadLabelList.java create mode 100644 src/main/java/com/resend/services/inboxes/model/RemoveInboxThreadLabelResponseSuccess.java create mode 100644 src/main/java/com/resend/services/inboxes/model/SetInboxThreadLabelsOptions.java create mode 100644 src/main/java/com/resend/services/inboxes/model/SetInboxThreadLabelsResponseSuccess.java diff --git a/src/main/java/com/resend/core/net/HttpMethod.java b/src/main/java/com/resend/core/net/HttpMethod.java index 0d29e2f..9a6f220 100644 --- a/src/main/java/com/resend/core/net/HttpMethod.java +++ b/src/main/java/com/resend/core/net/HttpMethod.java @@ -23,4 +23,9 @@ public enum HttpMethod { * HTTP PATCH method. */ PATCH, + + /** + * HTTP PUT method. + */ + PUT, } diff --git a/src/main/java/com/resend/services/inboxes/InboxThreads.java b/src/main/java/com/resend/services/inboxes/InboxThreads.java index 2c80b37..39f4f8d 100644 --- a/src/main/java/com/resend/services/inboxes/InboxThreads.java +++ b/src/main/java/com/resend/services/inboxes/InboxThreads.java @@ -4,12 +4,17 @@ import com.resend.core.net.HttpMethod; import com.resend.core.net.IHttpClient; import com.resend.core.service.BaseService; +import com.resend.services.inboxes.model.AddInboxThreadLabelsOptions; +import com.resend.services.inboxes.model.AddInboxThreadLabelsResponseSuccess; import com.resend.services.inboxes.model.GetInboxThreadResponseSuccess; import com.resend.services.inboxes.model.ListInboxThreadsParams; import com.resend.services.inboxes.model.ListInboxThreadsResponseSuccess; +import com.resend.services.inboxes.model.RemoveInboxThreadLabelResponseSuccess; import com.resend.services.inboxes.model.RemoveInboxThreadResponseSuccess; import com.resend.services.inboxes.model.SearchInboxThreadsParams; import com.resend.services.inboxes.model.SearchInboxThreadsResponseSuccess; +import com.resend.services.inboxes.model.SetInboxThreadLabelsOptions; +import com.resend.services.inboxes.model.SetInboxThreadLabelsResponseSuccess; import com.resend.services.inboxes.model.UpdateInboxThreadOptions; import com.resend.services.inboxes.model.UpdateInboxThreadResponseSuccess; import okhttp3.MediaType; @@ -147,4 +152,47 @@ public UpdateInboxThreadResponseSuccess update(String inboxId, String threadId, public RemoveInboxThreadResponseSuccess remove(String inboxId, String threadId) throws ResendException { return execute("/inboxes/" + inboxId + "/threads/" + threadId, HttpMethod.DELETE, null, MediaType.get("application/json"), RemoveInboxThreadResponseSuccess.class); } + + /** + * Adds labels to a thread. Labels already on the thread stay on it. If any label ID is not found, nothing changes. + * + * @param inboxId The inbox ID. + * @param threadId The thread ID. + * @param addInboxThreadLabelsOptions The IDs of the labels to add. + * @return Every label on the thread after the change. + * @throws ResendException If an error occurs while adding the labels. + */ + public AddInboxThreadLabelsResponseSuccess addLabels(String inboxId, String threadId, AddInboxThreadLabelsOptions addInboxThreadLabelsOptions) throws ResendException { + String payload = super.resendMapper.writeValue(addInboxThreadLabelsOptions); + return execute("/inboxes/" + inboxId + "/threads/" + threadId + "/labels", HttpMethod.POST, payload, MediaType.get("application/json"), AddInboxThreadLabelsResponseSuccess.class); + } + + /** + * Sets the labels of a thread in one atomic change. Labels on the thread that are not listed are taken off, + * and the labels themselves are kept. An empty list takes every label off the thread. + * + * @param inboxId The inbox ID. + * @param threadId The thread ID. + * @param setInboxThreadLabelsOptions The IDs of the labels the thread should carry. + * @return Every label on the thread after the change. + * @throws ResendException If an error occurs while setting the labels. + */ + public SetInboxThreadLabelsResponseSuccess setLabels(String inboxId, String threadId, SetInboxThreadLabelsOptions setInboxThreadLabelsOptions) throws ResendException { + String payload = super.resendMapper.writeValue(setInboxThreadLabelsOptions); + return execute("/inboxes/" + inboxId + "/threads/" + threadId + "/labels", HttpMethod.PUT, payload, MediaType.get("application/json"), SetInboxThreadLabelsResponseSuccess.class); + } + + /** + * Removes a label from a thread. The label itself is kept, and stays on every other thread that carries it. + * To delete the label everywhere, use {@link InboxLabels#remove(String, String)}. + * + * @param inboxId The inbox ID. + * @param threadId The thread ID. + * @param labelId The label ID. + * @return Every label on the thread after the change. + * @throws ResendException If an error occurs while removing the label. + */ + public RemoveInboxThreadLabelResponseSuccess removeLabel(String inboxId, String threadId, String labelId) throws ResendException { + return execute("/inboxes/" + inboxId + "/threads/" + threadId + "/labels/" + labelId, HttpMethod.DELETE, null, MediaType.get("application/json"), RemoveInboxThreadLabelResponseSuccess.class); + } } diff --git a/src/main/java/com/resend/services/inboxes/model/AddInboxThreadLabelsOptions.java b/src/main/java/com/resend/services/inboxes/model/AddInboxThreadLabelsOptions.java new file mode 100644 index 0000000..ca2efc4 --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/model/AddInboxThreadLabelsOptions.java @@ -0,0 +1,86 @@ +package com.resend.services.inboxes.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Represents a request to add labels to a thread. Labels already on the thread stay on it. + */ +public class AddInboxThreadLabelsOptions { + + @JsonProperty("labels") + private final List labels; + + private AddInboxThreadLabelsOptions(Builder builder) { + this.labels = new ArrayList<>(builder.labels); + } + + /** + * Get the IDs of the labels. + * + * @return The IDs of the labels. + */ + public List getLabels() { + return labels; + } + + /** + * Create a new builder instance for constructing AddInboxThreadLabelsOptions objects. + * + * @return A new builder instance. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Builder class for constructing AddInboxThreadLabelsOptions objects. + */ + public static class Builder { + private List labels; + + /** + * Creates a new Builder instance. + */ + public Builder() { + } + + /** + * Set the IDs of the labels. Between 1 and 10 IDs, each belonging to the inbox. + * + * @param labels The IDs of the labels. + * @return The builder instance. + */ + public Builder labels(String... labels) { + this.labels = new ArrayList<>(Arrays.asList(labels)); + return this; + } + + /** + * Set the IDs of the labels. Between 1 and 10 IDs, each belonging to the inbox. + * + * @param labels The IDs of the labels. + * @return The builder instance. + */ + public Builder labels(List labels) { + this.labels = labels == null ? null : new ArrayList<>(labels); + return this; + } + + /** + * Build a new AddInboxThreadLabelsOptions object. + * + * @return A new AddInboxThreadLabelsOptions object. + * @throws IllegalArgumentException if {@code labels} is null or empty. + */ + public AddInboxThreadLabelsOptions build() { + if (labels == null || labels.isEmpty()) { + throw new IllegalArgumentException("labels must contain at least one label ID"); + } + return new AddInboxThreadLabelsOptions(this); + } + } +} diff --git a/src/main/java/com/resend/services/inboxes/model/AddInboxThreadLabelsResponseSuccess.java b/src/main/java/com/resend/services/inboxes/model/AddInboxThreadLabelsResponseSuccess.java new file mode 100644 index 0000000..a619d14 --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/model/AddInboxThreadLabelsResponseSuccess.java @@ -0,0 +1,27 @@ +package com.resend.services.inboxes.model; + +import java.util.List; + +/** + * Represents a successful response for adding labels to a thread. + * Extends the InboxThreadLabelList class. + */ +public class AddInboxThreadLabelsResponseSuccess extends InboxThreadLabelList { + + /** + * Default constructor + */ + public AddInboxThreadLabelsResponseSuccess() { + + } + + /** + * Constructs a AddInboxThreadLabelsResponseSuccess. + * + * @param object The object type. Always {@code list}. + * @param data Every label on the thread after the change. + */ + public AddInboxThreadLabelsResponseSuccess(String object, List data) { + super(object, data); + } +} diff --git a/src/main/java/com/resend/services/inboxes/model/InboxThreadLabelList.java b/src/main/java/com/resend/services/inboxes/model/InboxThreadLabelList.java new file mode 100644 index 0000000..ba576a6 --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/model/InboxThreadLabelList.java @@ -0,0 +1,53 @@ +package com.resend.services.inboxes.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +/** + * Represents every label on a thread after a change to its labels. + */ +public class InboxThreadLabelList { + + @JsonProperty("object") + private String object; + + @JsonProperty("data") + private List data; + + /** + * Default constructor + */ + public InboxThreadLabelList() { + + } + + /** + * Constructs an InboxThreadLabelList. + * + * @param object The object type. Always {@code list}. + * @param data Every label on the thread after the change. + */ + public InboxThreadLabelList(String object, List data) { + this.object = object; + this.data = data; + } + + /** + * Get the object type. Always {@code list}. + * + * @return The object type. Always {@code list}. + */ + public String getObject() { + return object; + } + + /** + * Get every label on the thread after the change. + * + * @return Every label on the thread after the change. + */ + public List getData() { + return data; + } +} diff --git a/src/main/java/com/resend/services/inboxes/model/RemoveInboxThreadLabelResponseSuccess.java b/src/main/java/com/resend/services/inboxes/model/RemoveInboxThreadLabelResponseSuccess.java new file mode 100644 index 0000000..641b475 --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/model/RemoveInboxThreadLabelResponseSuccess.java @@ -0,0 +1,27 @@ +package com.resend.services.inboxes.model; + +import java.util.List; + +/** + * Represents a successful response for removing a label from a thread. + * Extends the InboxThreadLabelList class. + */ +public class RemoveInboxThreadLabelResponseSuccess extends InboxThreadLabelList { + + /** + * Default constructor + */ + public RemoveInboxThreadLabelResponseSuccess() { + + } + + /** + * Constructs a RemoveInboxThreadLabelResponseSuccess. + * + * @param object The object type. Always {@code list}. + * @param data Every label on the thread after the change. + */ + public RemoveInboxThreadLabelResponseSuccess(String object, List data) { + super(object, data); + } +} diff --git a/src/main/java/com/resend/services/inboxes/model/SetInboxThreadLabelsOptions.java b/src/main/java/com/resend/services/inboxes/model/SetInboxThreadLabelsOptions.java new file mode 100644 index 0000000..e5cd175 --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/model/SetInboxThreadLabelsOptions.java @@ -0,0 +1,86 @@ +package com.resend.services.inboxes.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Represents a request to set the labels of a thread. The thread ends up with exactly the given labels. + */ +public class SetInboxThreadLabelsOptions { + + @JsonProperty("labels") + private final List labels; + + private SetInboxThreadLabelsOptions(Builder builder) { + this.labels = new ArrayList<>(builder.labels); + } + + /** + * Get the IDs of the labels. + * + * @return The IDs of the labels. + */ + public List getLabels() { + return labels; + } + + /** + * Create a new builder instance for constructing SetInboxThreadLabelsOptions objects. + * + * @return A new builder instance. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Builder class for constructing SetInboxThreadLabelsOptions objects. + */ + public static class Builder { + private List labels; + + /** + * Creates a new Builder instance. + */ + public Builder() { + } + + /** + * Set the IDs of the labels. Up to 100 IDs, each belonging to the inbox. An empty list takes every label off the thread. + * + * @param labels The IDs of the labels. + * @return The builder instance. + */ + public Builder labels(String... labels) { + this.labels = new ArrayList<>(Arrays.asList(labels)); + return this; + } + + /** + * Set the IDs of the labels. Up to 100 IDs, each belonging to the inbox. An empty list takes every label off the thread. + * + * @param labels The IDs of the labels. + * @return The builder instance. + */ + public Builder labels(List labels) { + this.labels = labels == null ? null : new ArrayList<>(labels); + return this; + } + + /** + * Build a new SetInboxThreadLabelsOptions object. + * + * @return A new SetInboxThreadLabelsOptions object. + * @throws IllegalArgumentException if {@code labels} is null. + */ + public SetInboxThreadLabelsOptions build() { + if (labels == null) { + throw new IllegalArgumentException("labels must not be null"); + } + return new SetInboxThreadLabelsOptions(this); + } + } +} diff --git a/src/main/java/com/resend/services/inboxes/model/SetInboxThreadLabelsResponseSuccess.java b/src/main/java/com/resend/services/inboxes/model/SetInboxThreadLabelsResponseSuccess.java new file mode 100644 index 0000000..417aefb --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/model/SetInboxThreadLabelsResponseSuccess.java @@ -0,0 +1,27 @@ +package com.resend.services.inboxes.model; + +import java.util.List; + +/** + * Represents a successful response for setting the labels of a thread. + * Extends the InboxThreadLabelList class. + */ +public class SetInboxThreadLabelsResponseSuccess extends InboxThreadLabelList { + + /** + * Default constructor + */ + public SetInboxThreadLabelsResponseSuccess() { + + } + + /** + * Constructs a SetInboxThreadLabelsResponseSuccess. + * + * @param object The object type. Always {@code list}. + * @param data Every label on the thread after the change. + */ + public SetInboxThreadLabelsResponseSuccess(String object, List data) { + super(object, data); + } +} diff --git a/src/test/java/com/resend/services/inboxes/InboxThreadsTest.java b/src/test/java/com/resend/services/inboxes/InboxThreadsTest.java index 10a614f..810ea95 100644 --- a/src/test/java/com/resend/services/inboxes/InboxThreadsTest.java +++ b/src/test/java/com/resend/services/inboxes/InboxThreadsTest.java @@ -168,4 +168,52 @@ public void testFolder_UnknownValue_Throws() { assertEquals(InboxFolder.TRASH, InboxFolder.fromValue("trash")); assertThrows(IllegalArgumentException.class, () -> MoveInboxThreadFolder.fromValue("sent")); } + + @Test + public void testAddLabels() throws ResendException { + AddInboxThreadLabelsOptions options = AddInboxThreadLabelsOptions.builder().labels(LABEL_ID, OTHER_LABEL_ID).build(); + when(httpClient.perform(eq(THREADS_PATH + "/" + THREAD_ID + "/labels"), anyString(), eq(HttpMethod.POST), + eq("{\"labels\":[\"" + LABEL_ID + "\",\"" + OTHER_LABEL_ID + "\"]}"), any(MediaType.class))) + .thenReturn(new AbstractHttpResponse<>(200, "{\"object\":\"list\",\"data\":[" + + "{\"id\":\"" + LABEL_ID + "\",\"name\":\"Urgent\",\"color\":\"#E93D82\"}," + + "{\"id\":\"" + OTHER_LABEL_ID + "\",\"name\":\"Billing\",\"color\":\"#00A2C7\"}]}", true)); + + AddInboxThreadLabelsResponseSuccess response = threads.addLabels(INBOX_ID, THREAD_ID, options); + + assertEquals("list", response.getObject()); + assertEquals(2, response.getData().size()); + assertEquals("Billing", response.getData().get(1).getName()); + } + + @Test + public void testSetLabels_EmptyListClearsLabels() throws ResendException { + SetInboxThreadLabelsOptions options = SetInboxThreadLabelsOptions.builder().labels(Collections.emptyList()).build(); + when(httpClient.perform(eq(THREADS_PATH + "/" + THREAD_ID + "/labels"), anyString(), eq(HttpMethod.PUT), + eq("{\"labels\":[]}"), any(MediaType.class))) + .thenReturn(new AbstractHttpResponse<>(200, "{\"object\":\"list\",\"data\":[]}", true)); + + SetInboxThreadLabelsResponseSuccess response = threads.setLabels(INBOX_ID, THREAD_ID, options); + + assertTrue(response.getData().isEmpty()); + } + + @Test + public void testRemoveLabel() throws ResendException { + when(httpClient.perform(eq(THREADS_PATH + "/" + THREAD_ID + "/labels/" + LABEL_ID), anyString(), eq(HttpMethod.DELETE), + isNull(), any(MediaType.class))) + .thenReturn(new AbstractHttpResponse<>(200, "{\"object\":\"list\",\"data\":[" + + "{\"id\":\"" + OTHER_LABEL_ID + "\",\"name\":\"Billing\",\"color\":\"#00A2C7\"}]}", true)); + + RemoveInboxThreadLabelResponseSuccess response = threads.removeLabel(INBOX_ID, THREAD_ID, LABEL_ID); + + assertEquals(1, response.getData().size()); + assertEquals(OTHER_LABEL_ID, response.getData().get(0).getId()); + } + + @Test + public void testThreadLabelsBuilders_Validate() { + assertThrows(IllegalArgumentException.class, () -> AddInboxThreadLabelsOptions.builder().build()); + assertThrows(IllegalArgumentException.class, () -> AddInboxThreadLabelsOptions.builder().labels(Collections.emptyList()).build()); + assertThrows(IllegalArgumentException.class, () -> SetInboxThreadLabelsOptions.builder().build()); + } } From 0c3c21ece547a9af6c12665b18819e5d4082b1fb Mon Sep 17 00:00:00 2001 From: Kewyn Akshlley Date: Fri, 16 Oct 2026 12:41:13 -0300 Subject: [PATCH 22/30] fix(inboxes): drop add_label_ids and remove_label_ids from the thread update Update Thread takes only `read` and `folder`. An update that set only the label fields returned a 422 ("At least one of `read` or `folder` is required"), and the API reference routes label changes through dedicated endpoints, now exposed as InboxThreads#addLabels, #setLabels and #removeLabel. --- .../resend/services/inboxes/InboxThreads.java | 9 +- .../model/UpdateInboxThreadOptions.java | 91 ++----------------- .../services/inboxes/InboxThreadsTest.java | 13 +-- 3 files changed, 13 insertions(+), 100 deletions(-) diff --git a/src/main/java/com/resend/services/inboxes/InboxThreads.java b/src/main/java/com/resend/services/inboxes/InboxThreads.java index 39f4f8d..08e50fd 100644 --- a/src/main/java/com/resend/services/inboxes/InboxThreads.java +++ b/src/main/java/com/resend/services/inboxes/InboxThreads.java @@ -121,12 +121,9 @@ public GetInboxThreadResponseSuccess get(String inboxId, String threadId) throws } /** - * Marks a thread read or unread, moves it to a folder, or adds and removes labels. - * A label ID cannot be both added and removed in the same request. Removing a label from a thread keeps - * the label itself. - * Moving a thread out of {@code trash} restores it and clears its deletion deadline. The changes apply in - * this order: {@code read}, {@code folder}, added labels, removed labels. They are not atomic, so a 404 for - * a label leaves the earlier changes applied. + * Marks a thread read or unread, or moves it to a folder. + * Moving a thread out of {@code trash} restores it and clears its deletion deadline. + * To change the labels of a thread, use {@link #addLabels}, {@link #setLabels} or {@link #removeLabel}. * * @param inboxId The inbox ID. An email address is not accepted. * @param threadId The thread ID. diff --git a/src/main/java/com/resend/services/inboxes/model/UpdateInboxThreadOptions.java b/src/main/java/com/resend/services/inboxes/model/UpdateInboxThreadOptions.java index 539a89e..bb3d314 100644 --- a/src/main/java/com/resend/services/inboxes/model/UpdateInboxThreadOptions.java +++ b/src/main/java/com/resend/services/inboxes/model/UpdateInboxThreadOptions.java @@ -2,13 +2,10 @@ import com.fasterxml.jackson.annotation.JsonProperty; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - /** - * Represents a request to update a thread. Provide at least one of {@code read}, {@code folder}, {@code add_label_ids}, or {@code remove_label_ids}. A label ID cannot be both added and removed in the same request. + * Represents a request to update a thread. Provide at least one of {@code read} or {@code folder}. + * To change the labels of a thread, use {@link com.resend.services.inboxes.InboxThreads#addLabels}, + * {@link com.resend.services.inboxes.InboxThreads#setLabels} or {@link com.resend.services.inboxes.InboxThreads#removeLabel}. */ public class UpdateInboxThreadOptions { @@ -18,17 +15,9 @@ public class UpdateInboxThreadOptions { @JsonProperty("folder") private final MoveInboxThreadFolder folder; - @JsonProperty("add_label_ids") - private final List addLabelIds; - - @JsonProperty("remove_label_ids") - private final List removeLabelIds; - private UpdateInboxThreadOptions(Builder builder) { this.read = builder.read; this.folder = builder.folder; - this.addLabelIds = builder.addLabelIds; - this.removeLabelIds = builder.removeLabelIds; } /** @@ -49,23 +38,6 @@ public MoveInboxThreadFolder getFolder() { return folder; } - /** - * Get the label IDs to apply to the thread. - * - * @return The label IDs to apply to the thread. - */ - public List getAddLabelIds() { - return addLabelIds; - } - - /** - * Get the label IDs to remove from the thread. - * - * @return The label IDs to remove from the thread. - */ - public List getRemoveLabelIds() { - return removeLabelIds; - } /** * Create a new builder instance for constructing UpdateInboxThreadOptions objects. @@ -82,8 +54,6 @@ public static Builder builder() { public static class Builder { private Boolean read; private MoveInboxThreadFolder folder; - private List addLabelIds; - private List removeLabelIds; /** * Creates a new Builder instance. @@ -113,65 +83,16 @@ public Builder folder(MoveInboxThreadFolder folder) { return this; } - /** - * Set the label IDs to apply to the thread. Between 1 and 10 IDs. A label ID that does not exist in the inbox returns a 404. - * - * @param addLabelIds The label IDs to apply to the thread. - * @return The builder instance. - */ - public Builder addLabelIds(String... addLabelIds) { - this.addLabelIds = new ArrayList<>(Arrays.asList(addLabelIds)); - return this; - } - - /** - * Set the label IDs to apply to the thread. Between 1 and 10 IDs. A label ID that does not exist in the inbox returns a 404. - * - * @param addLabelIds The label IDs to apply to the thread. - * @return The builder instance. - */ - public Builder addLabelIds(List addLabelIds) { - this.addLabelIds = addLabelIds == null ? null : new ArrayList<>(addLabelIds); - return this; - } - - /** - * Set the label IDs to remove from the thread. Between 1 and 10 IDs. The labels themselves are kept. A label ID that does not exist in the inbox returns a 404. - * - * @param removeLabelIds The label IDs to remove from the thread. - * @return The builder instance. - */ - public Builder removeLabelIds(String... removeLabelIds) { - this.removeLabelIds = new ArrayList<>(Arrays.asList(removeLabelIds)); - return this; - } - - /** - * Set the label IDs to remove from the thread. Between 1 and 10 IDs. The labels themselves are kept. A label ID that does not exist in the inbox returns a 404. - * - * @param removeLabelIds The label IDs to remove from the thread. - * @return The builder instance. - */ - public Builder removeLabelIds(List removeLabelIds) { - this.removeLabelIds = removeLabelIds == null ? null : new ArrayList<>(removeLabelIds); - return this; - } /** * Build a new UpdateInboxThreadOptions object. * * @return A new UpdateInboxThreadOptions object. - * @throws IllegalArgumentException if no field is set, a label ID list is empty, or a label ID is both added and removed. + * @throws IllegalArgumentException if neither {@code read} nor {@code folder} is set. */ public UpdateInboxThreadOptions build() { - if (read == null && folder == null && addLabelIds == null && removeLabelIds == null) { - throw new IllegalArgumentException("At least one of read, folder, add_label_ids or remove_label_ids must be provided"); - } - if ((addLabelIds != null && addLabelIds.isEmpty()) || (removeLabelIds != null && removeLabelIds.isEmpty())) { - throw new IllegalArgumentException("add_label_ids and remove_label_ids must contain at least one label ID"); - } - if (addLabelIds != null && removeLabelIds != null && !Collections.disjoint(addLabelIds, removeLabelIds)) { - throw new IllegalArgumentException("A label ID cannot be both added and removed in the same request"); + if (read == null && folder == null) { + throw new IllegalArgumentException("At least one of read or folder must be provided"); } return new UpdateInboxThreadOptions(this); } diff --git a/src/test/java/com/resend/services/inboxes/InboxThreadsTest.java b/src/test/java/com/resend/services/inboxes/InboxThreadsTest.java index 810ea95..ef706f1 100644 --- a/src/test/java/com/resend/services/inboxes/InboxThreadsTest.java +++ b/src/test/java/com/resend/services/inboxes/InboxThreadsTest.java @@ -121,21 +121,19 @@ public void testGetThread_Success() throws ResendException { } @Test - public void testUpdateThread_AddAndRemoveLabels() throws ResendException { + public void testUpdateThread_ReadAndFolder() throws ResendException { UpdateInboxThreadOptions options = UpdateInboxThreadOptions.builder() .read(true) .folder(MoveInboxThreadFolder.ARCHIVE) - .addLabelIds(LABEL_ID) - .removeLabelIds(OTHER_LABEL_ID) .build(); - String payload = "{\"read\":true,\"folder\":\"archive\",\"add_label_ids\":[\"" + LABEL_ID + "\"]," + - "\"remove_label_ids\":[\"" + OTHER_LABEL_ID + "\"]}"; - when(httpClient.perform(eq(THREADS_PATH + "/" + THREAD_ID), anyString(), eq(HttpMethod.PATCH), eq(payload), any(MediaType.class))) + when(httpClient.perform(eq(THREADS_PATH + "/" + THREAD_ID), anyString(), eq(HttpMethod.PATCH), + eq("{\"read\":true,\"folder\":\"archive\"}"), any(MediaType.class))) .thenReturn(new AbstractHttpResponse<>(200, SUMMARY_JSON, true)); UpdateInboxThreadResponseSuccess response = threads.update(INBOX_ID, THREAD_ID, options); assertEquals(THREAD_ID, response.getId()); + assertEquals("archive", response.getFolder()); } @Test @@ -152,9 +150,6 @@ public void testRemoveThread_Success() throws ResendException { @Test public void testUpdateThreadBuilder_Validation() { assertThrows(IllegalArgumentException.class, () -> UpdateInboxThreadOptions.builder().build()); - assertThrows(IllegalArgumentException.class, () -> UpdateInboxThreadOptions.builder() - .addLabelIds(LABEL_ID).removeLabelIds(LABEL_ID).build()); - assertThrows(IllegalArgumentException.class, () -> UpdateInboxThreadOptions.builder().addLabelIds().build()); } @Test From 6764c45cb23fe30c60b844c134a6517f980eafa2 Mon Sep 17 00:00:00 2001 From: Kewyn Akshlley Date: Fri, 16 Oct 2026 12:41:59 -0300 Subject: [PATCH 23/30] fix(inboxes): read updated_at on inboxes and created_at/updated_at on list items Get Inbox returns `updated_at`, and every List Inboxes item returns `created_at` and `updated_at`. The SDK models dropped them. --- .../model/GetInboxResponseSuccess.java | 5 ++-- .../resend/services/inboxes/model/Inbox.java | 16 +++++++++- .../services/inboxes/model/InboxListItem.java | 30 ++++++++++++++++++- .../resend/services/inboxes/InboxesTest.java | 7 +++-- 4 files changed, 52 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/resend/services/inboxes/model/GetInboxResponseSuccess.java b/src/main/java/com/resend/services/inboxes/model/GetInboxResponseSuccess.java index 970f89b..9b1172c 100644 --- a/src/main/java/com/resend/services/inboxes/model/GetInboxResponseSuccess.java +++ b/src/main/java/com/resend/services/inboxes/model/GetInboxResponseSuccess.java @@ -27,8 +27,9 @@ public GetInboxResponseSuccess() { * @param drafts The number of unsent drafts. * @param lastReceived The ISO 8601 timestamp of the newest received email in the Inbox folder. {@code null} when the folder has no received email. * @param createdAt The ISO 8601 timestamp when the inbox was created. + * @param updatedAt The ISO 8601 timestamp when the inbox was updated. */ - public GetInboxResponseSuccess(String object, String id, String name, String emailAddress, String domainId, String receivingAddress, String fromName, Integer unread, Integer drafts, String lastReceived, String createdAt) { - super(object, id, name, emailAddress, domainId, receivingAddress, fromName, unread, drafts, lastReceived, createdAt); + public GetInboxResponseSuccess(String object, String id, String name, String emailAddress, String domainId, String receivingAddress, String fromName, Integer unread, Integer drafts, String lastReceived, String createdAt, String updatedAt) { + super(object, id, name, emailAddress, domainId, receivingAddress, fromName, unread, drafts, lastReceived, createdAt, updatedAt); } } diff --git a/src/main/java/com/resend/services/inboxes/model/Inbox.java b/src/main/java/com/resend/services/inboxes/model/Inbox.java index 51983a9..789ab51 100644 --- a/src/main/java/com/resend/services/inboxes/model/Inbox.java +++ b/src/main/java/com/resend/services/inboxes/model/Inbox.java @@ -40,6 +40,9 @@ public class Inbox { @JsonProperty("created_at") private String createdAt; + @JsonProperty("updated_at") + private String updatedAt; + /** * Default constructor */ @@ -61,8 +64,9 @@ public Inbox() { * @param drafts The number of unsent drafts. * @param lastReceived The ISO 8601 timestamp of the newest received email in the Inbox folder. {@code null} when the folder has no received email. * @param createdAt The ISO 8601 timestamp when the inbox was created. + * @param updatedAt The ISO 8601 timestamp when the inbox was updated. */ - public Inbox(String object, String id, String name, String emailAddress, String domainId, String receivingAddress, String fromName, Integer unread, Integer drafts, String lastReceived, String createdAt) { + public Inbox(String object, String id, String name, String emailAddress, String domainId, String receivingAddress, String fromName, Integer unread, Integer drafts, String lastReceived, String createdAt, String updatedAt) { this.object = object; this.id = id; this.name = name; @@ -74,6 +78,7 @@ public Inbox(String object, String id, String name, String emailAddress, String this.drafts = drafts; this.lastReceived = lastReceived; this.createdAt = createdAt; + this.updatedAt = updatedAt; } /** @@ -174,4 +179,13 @@ public String getLastReceived() { public String getCreatedAt() { return createdAt; } + + /** + * Get the ISO 8601 timestamp when the inbox was updated. + * + * @return The ISO 8601 timestamp when the inbox was updated. + */ + public String getUpdatedAt() { + return updatedAt; + } } diff --git a/src/main/java/com/resend/services/inboxes/model/InboxListItem.java b/src/main/java/com/resend/services/inboxes/model/InboxListItem.java index f22c591..839c8e9 100644 --- a/src/main/java/com/resend/services/inboxes/model/InboxListItem.java +++ b/src/main/java/com/resend/services/inboxes/model/InboxListItem.java @@ -25,6 +25,12 @@ public class InboxListItem { @JsonProperty("last_received") private String lastReceived; + @JsonProperty("created_at") + private String createdAt; + + @JsonProperty("updated_at") + private String updatedAt; + /** * Default constructor */ @@ -41,14 +47,18 @@ public InboxListItem() { * @param fromName The name recipients see when mail is sent from this inbox. {@code null} when not set. * @param unread The number of unread threads in the Inbox folder. * @param lastReceived The ISO 8601 timestamp of the newest received email in the Inbox folder. {@code null} when the folder has no received email. + * @param createdAt The ISO 8601 timestamp when the inbox was created. + * @param updatedAt The ISO 8601 timestamp when the inbox was updated. */ - public InboxListItem(String id, String name, String emailAddress, String fromName, Integer unread, String lastReceived) { + public InboxListItem(String id, String name, String emailAddress, String fromName, Integer unread, String lastReceived, String createdAt, String updatedAt) { this.id = id; this.name = name; this.emailAddress = emailAddress; this.fromName = fromName; this.unread = unread; this.lastReceived = lastReceived; + this.createdAt = createdAt; + this.updatedAt = updatedAt; } /** @@ -104,4 +114,22 @@ public Integer getUnread() { public String getLastReceived() { return lastReceived; } + + /** + * Get the ISO 8601 timestamp when the inbox was created. + * + * @return The ISO 8601 timestamp when the inbox was created. + */ + public String getCreatedAt() { + return createdAt; + } + + /** + * Get the ISO 8601 timestamp when the inbox was updated. + * + * @return The ISO 8601 timestamp when the inbox was updated. + */ + public String getUpdatedAt() { + return updatedAt; + } } diff --git a/src/test/java/com/resend/services/inboxes/InboxesTest.java b/src/test/java/com/resend/services/inboxes/InboxesTest.java index 43c80cb..deeb7c0 100644 --- a/src/test/java/com/resend/services/inboxes/InboxesTest.java +++ b/src/test/java/com/resend/services/inboxes/InboxesTest.java @@ -25,7 +25,7 @@ public class InboxesTest { "\"name\":\"Customer Support\",\"email_address\":\"support@acme.com\"," + "\"domain_id\":\"d91cd9bd-1176-453e-8fc1-35364d380206\",\"receiving_address\":null," + "\"from_name\":\"Ada from Support\",\"unread\":3,\"drafts\":2," + - "\"last_received\":\"2026-08-05T14:03:11.229Z\",\"created_at\":\"2026-08-01T09:12:44.512Z\"}"; + "\"last_received\":\"2026-08-05T14:03:11.229Z\",\"created_at\":\"2026-08-01T09:12:44.512Z\",\"updated_at\":\"2026-08-03T16:20:05.004Z\"}"; @Mock private IHttpClient httpClient; @@ -65,7 +65,7 @@ public void testCreateInbox_PlanLimit_ThrowsResendException() throws ResendExcep public void testListInboxes_Success() throws ResendException { String json = "{\"object\":\"list\",\"has_more\":false,\"data\":[{\"id\":\"" + INBOX_ID + "\"," + "\"name\":null,\"email_address\":\"support@acme.com\",\"from_name\":null,\"unread\":3," + - "\"last_received\":null}]}"; + "\"last_received\":null,\"created_at\":\"2026-07-20T11:00:00.000Z\",\"updated_at\":\"2026-07-20T11:00:00.318Z\"}]}"; when(httpClient.perform(eq("/inboxes?limit=10&after=" + INBOX_ID), anyString(), eq(HttpMethod.GET), isNull(), any(MediaType.class))) .thenReturn(new AbstractHttpResponse<>(200, json, true)); @@ -80,6 +80,8 @@ public void testListInboxes_Success() throws ResendException { assertEquals("support@acme.com", item.getEmailAddress()); assertEquals(3, (int) item.getUnread()); assertNull(item.getLastReceived()); + assertEquals("2026-07-20T11:00:00.000Z", item.getCreatedAt()); + assertEquals("2026-07-20T11:00:00.318Z", item.getUpdatedAt()); } @Test @@ -107,6 +109,7 @@ public void testGetInboxByEmail_Success() throws ResendException { assertEquals(2, (int) response.getDrafts()); assertEquals("2026-08-05T14:03:11.229Z", response.getLastReceived()); assertEquals("2026-08-01T09:12:44.512Z", response.getCreatedAt()); + assertEquals("2026-08-03T16:20:05.004Z", response.getUpdatedAt()); } @Test From 1fe6014e4c3d852e7f3d0de1964c2902b32d8064 Mon Sep 17 00:00:00 2001 From: Kewyn Akshlley Date: Fri, 16 Oct 2026 12:42:30 -0300 Subject: [PATCH 24/30] fix(inboxes): model the update draft response as {object, id} Update Draft returns only `object` and `id`. UpdateInboxDraftResponseSuccess extended the full InboxDraft, so its content getters were always null. --- .../resend/services/inboxes/InboxDrafts.java | 2 +- .../UpdateInboxDraftResponseSuccess.java | 52 ++++++++++++------- .../services/inboxes/InboxDraftsTest.java | 6 ++- 3 files changed, 37 insertions(+), 23 deletions(-) diff --git a/src/main/java/com/resend/services/inboxes/InboxDrafts.java b/src/main/java/com/resend/services/inboxes/InboxDrafts.java index 84f619c..b5ea4cd 100644 --- a/src/main/java/com/resend/services/inboxes/InboxDrafts.java +++ b/src/main/java/com/resend/services/inboxes/InboxDrafts.java @@ -130,7 +130,7 @@ public GetInboxDraftResponseSuccess get(String inboxId, String draftId) throws R * @param inboxId The inbox ID. An email address is not accepted. * @param draftId The draft ID. * @param updateInboxDraftOptions The fields to update. - * @return The updated draft. + * @return The ID of the updated draft. Use {@link #get(String, String)} to read its content. * @throws ResendException If an error occurs while updating the draft. */ public UpdateInboxDraftResponseSuccess update(String inboxId, String draftId, UpdateInboxDraftOptions updateInboxDraftOptions) throws ResendException { diff --git a/src/main/java/com/resend/services/inboxes/model/UpdateInboxDraftResponseSuccess.java b/src/main/java/com/resend/services/inboxes/model/UpdateInboxDraftResponseSuccess.java index 7d8e221..efc46da 100644 --- a/src/main/java/com/resend/services/inboxes/model/UpdateInboxDraftResponseSuccess.java +++ b/src/main/java/com/resend/services/inboxes/model/UpdateInboxDraftResponseSuccess.java @@ -1,12 +1,18 @@ package com.resend.services.inboxes.model; -import java.util.List; +import com.fasterxml.jackson.annotation.JsonProperty; /** * Represents a successful response for updating a draft. - * Extends the InboxDraft class. + * The response carries only the draft ID. Retrieve the draft to read its updated content. */ -public class UpdateInboxDraftResponseSuccess extends InboxDraft { +public class UpdateInboxDraftResponseSuccess { + + @JsonProperty("object") + private String object; + + @JsonProperty("id") + private String id; /** * Default constructor @@ -18,23 +24,29 @@ public UpdateInboxDraftResponseSuccess() { /** * Constructs a UpdateInboxDraftResponseSuccess. * - * @param object The object type. Always {@code inbox_draft}. - * @param id The ID of the draft. - * @param type The draft type. {@code reply} when the draft replies to an email in an existing thread. {@code standalone} when the draft starts a new thread. - * @param to The recipients. {@code null} or empty when no recipients are set. - * @param cc The CC recipients. - * @param bcc The BCC recipients. - * @param subject The subject. - * @param html The HTML body. - * @param text The plain text body. - * @param threadId The Thread ID when the draft is a reply. {@code null} otherwise. - * @param replyToEmailId The Email ID the draft replies to. {@code null} otherwise. - * @param emailId The ID of the outbound email queued when the draft was sent. {@code null} until the draft is sent. - * @param createdAt The timestamp when the draft was created. - * @param updatedAt The timestamp when the draft was last saved. - * @param attachments The files stored on the draft. + * @param object The object type. Always {@code inbox_draft}. + * @param id The ID of the updated draft. + */ + public UpdateInboxDraftResponseSuccess(String object, String id) { + this.object = object; + this.id = id; + } + + /** + * Get the object type. Always {@code inbox_draft}. + * + * @return The object type. Always {@code inbox_draft}. + */ + public String getObject() { + return object; + } + + /** + * Get the ID of the updated draft. + * + * @return The ID of the updated draft. */ - public UpdateInboxDraftResponseSuccess(String object, String id, String type, List to, List cc, List bcc, String subject, String html, String text, String threadId, String replyToEmailId, String emailId, String createdAt, String updatedAt, List attachments) { - super(object, id, type, to, cc, bcc, subject, html, text, threadId, replyToEmailId, emailId, createdAt, updatedAt, attachments); + public String getId() { + return id; } } diff --git a/src/test/java/com/resend/services/inboxes/InboxDraftsTest.java b/src/test/java/com/resend/services/inboxes/InboxDraftsTest.java index 45bec28..f2cbdb8 100644 --- a/src/test/java/com/resend/services/inboxes/InboxDraftsTest.java +++ b/src/test/java/com/resend/services/inboxes/InboxDraftsTest.java @@ -26,6 +26,7 @@ public class InboxDraftsTest { private static final String DRAFTS_PATH = "/inboxes/" + INBOX_ID + "/drafts"; + private static final String UPDATE_RESPONSE_JSON = "{\"object\":\"inbox_draft\",\"id\":\"" + DRAFT_ID + "\"}"; private static final String DRAFT_JSON = "{\"object\":\"inbox_draft\",\"id\":\"" + DRAFT_ID + "\",\"type\":\"reply\"," + "\"to\":[\"ada@example.org\"],\"cc\":[],\"bcc\":[],\"subject\":\"Re: Refund for order 1041\"," + "\"html\":null,\"text\":\"Refund issued for order 1041.\",\"thread_id\":\"" + THREAD_ID + "\"," + @@ -182,10 +183,11 @@ public void testUpdateDraft_SendsNullToClear() throws ResendException { .build(); when(httpClient.perform(eq(DRAFTS_PATH + "/" + DRAFT_ID), anyString(), eq(HttpMethod.PATCH), eq("{\"subject\":null,\"cc\":null,\"text\":\"Updated body.\"}"), any(MediaType.class))) - .thenReturn(new AbstractHttpResponse<>(200, DRAFT_JSON, true)); + .thenReturn(new AbstractHttpResponse<>(200, UPDATE_RESPONSE_JSON, true)); UpdateInboxDraftResponseSuccess response = drafts.update(INBOX_ID, DRAFT_ID, options); + assertEquals("inbox_draft", response.getObject()); assertEquals(DRAFT_ID, response.getId()); } @@ -194,7 +196,7 @@ public void testUpdateDraft_OmitsUnsetFields() throws ResendException { UpdateInboxDraftOptions options = UpdateInboxDraftOptions.builder().to("ada@example.org").build(); when(httpClient.perform(eq(DRAFTS_PATH + "/" + DRAFT_ID), anyString(), eq(HttpMethod.PATCH), eq("{\"to\":[\"ada@example.org\"]}"), any(MediaType.class))) - .thenReturn(new AbstractHttpResponse<>(200, DRAFT_JSON, true)); + .thenReturn(new AbstractHttpResponse<>(200, UPDATE_RESPONSE_JSON, true)); assertNotNull(drafts.update(INBOX_ID, DRAFT_ID, options)); assertEquals(Collections.singletonList("ada@example.org"), options.getTo()); From 30f35d62e016467ab04070c6986ed765f007c819 Mon Sep 17 00:00:00 2001 From: Kewyn Akshlley Date: Fri, 16 Oct 2026 12:43:32 -0300 Subject: [PATCH 25/30] docs(inboxes): correct the inbox ID and send draft notes - Every inbox endpoint accepts the inbox email address in place of its ID, as the API reference states and as calls against the API confirm. Drop the "An email address is not accepted" notes. - Send Draft requires a non-empty subject only for a draft that starts a new thread; a reply draft with an empty subject is sent. --- .../resend/services/inboxes/InboxAgent.java | 4 +- .../resend/services/inboxes/InboxDrafts.java | 42 ++++++++++--------- .../resend/services/inboxes/InboxLabels.java | 8 ++-- .../services/inboxes/InboxThreadEmails.java | 14 +++---- .../resend/services/inboxes/InboxThreads.java | 20 ++++----- .../com/resend/services/inboxes/Inboxes.java | 4 +- 6 files changed, 48 insertions(+), 44 deletions(-) diff --git a/src/main/java/com/resend/services/inboxes/InboxAgent.java b/src/main/java/com/resend/services/inboxes/InboxAgent.java index 74c26b4..3644491 100644 --- a/src/main/java/com/resend/services/inboxes/InboxAgent.java +++ b/src/main/java/com/resend/services/inboxes/InboxAgent.java @@ -37,7 +37,7 @@ public InboxAgent(final String apiKey, final IHttpClient httpClient) { * Retrieves the agent settings of an inbox. An inbox without a configured agent returns empty settings: * {@code instructions} and {@code tone} are null and {@code actions} is an empty list. * - * @param inboxId The inbox ID. An email address is not accepted. + * @param inboxId The inbox ID or the inbox email address. * @return The agent settings of the inbox. * @throws ResendException If an error occurs while retrieving the agent settings. */ @@ -49,7 +49,7 @@ public GetInboxAgentResponseSuccess get(String inboxId) throws ResendException { * Updates the agent settings of an inbox. The agent is created when the inbox has none. * Omitted fields keep their current value. * - * @param inboxId The inbox ID. An email address is not accepted. + * @param inboxId The inbox ID or the inbox email address. * @param updateInboxAgentOptions The settings to update. * @return The ID of the inbox agent. * @throws ResendException If an error occurs while updating the agent settings. diff --git a/src/main/java/com/resend/services/inboxes/InboxDrafts.java b/src/main/java/com/resend/services/inboxes/InboxDrafts.java index b5ea4cd..fea8a9e 100644 --- a/src/main/java/com/resend/services/inboxes/InboxDrafts.java +++ b/src/main/java/com/resend/services/inboxes/InboxDrafts.java @@ -45,7 +45,7 @@ public InboxDrafts(final String apiKey, final IHttpClient httpClient) { /** * Retrieves the unsent drafts on an inbox, most recently updated first. * - * @param inboxId The inbox ID. An email address is not accepted. + * @param inboxId The inbox ID or the inbox email address. * @return A ListInboxDraftsResponseSuccess containing the drafts. * @throws ResendException If an error occurs while retrieving the drafts. */ @@ -58,7 +58,7 @@ public ListInboxDraftsResponseSuccess list(String inboxId) throws ResendExceptio * The {@code after} and {@code before} cursors take a draft ID from the current page. * A cursor that is not an unsent draft on this inbox returns a 404. * - * @param inboxId The inbox ID. An email address is not accepted. + * @param inboxId The inbox ID or the inbox email address. * @param params The params used to paginate the list. * @return A ListInboxDraftsResponseSuccess containing the drafts. * @throws ResendException If an error occurs while retrieving the drafts. @@ -75,7 +75,7 @@ public ListInboxDraftsResponseSuccess list(String inboxId, ListParams params) th * attachments of the existing draft when sent. A reply draft created without {@code subject} stores an empty * subject and gets no {@code Re:} prefix, so set one before you send it. * - * @param inboxId The inbox ID. An email address is not accepted. + * @param inboxId The inbox ID or the inbox email address. * @param createInboxDraftOptions The draft content. * @return The ID of the draft. * @throws ResendException If an error occurs while creating the draft. @@ -98,7 +98,7 @@ public CreateInboxDraftResponseSuccess create(String inboxId, CreateInboxDraftOp * key with a different body, or while the first request is still running, returns a 409. A failed request is * not stored, so you can retry it with the same key.

* - * @param inboxId The inbox ID. An email address is not accepted. + * @param inboxId The inbox ID or the inbox email address. * @param createInboxDraftOptions The draft content. * @param requestOptions The options with additional headers. * @return The ID of the draft. @@ -112,7 +112,7 @@ public CreateInboxDraftResponseSuccess create(String inboxId, CreateInboxDraftOp /** * Retrieves a single draft. * - * @param inboxId The inbox ID. An email address is not accepted. + * @param inboxId The inbox ID or the inbox email address. * @param draftId The draft ID. * @return The draft. * @throws ResendException If an error occurs while retrieving the draft. @@ -127,7 +127,7 @@ public GetInboxDraftResponseSuccess get(String inboxId, String draftId) throws R * Sending {@code attachments} replaces every attachment on the draft, and an empty list removes them all. The body part you omit from that request becomes * {@code null}. A draft that was already sent, or that changed during the request, returns a 409. * - * @param inboxId The inbox ID. An email address is not accepted. + * @param inboxId The inbox ID or the inbox email address. * @param draftId The draft ID. * @param updateInboxDraftOptions The fields to update. * @return The ID of the updated draft. Use {@link #get(String, String)} to read its content. @@ -141,7 +141,7 @@ public UpdateInboxDraftResponseSuccess update(String inboxId, String draftId, Up /** * Discards an unsent draft. A draft that was already sent, or that changed during the request, returns a 409. * - * @param inboxId The inbox ID. An email address is not accepted. + * @param inboxId The inbox ID or the inbox email address. * @param draftId The draft ID. * @return The RemoveInboxDraftResponseSuccess with the details of the removed draft. * @throws ResendException If an error occurs while removing the draft. @@ -152,11 +152,12 @@ public RemoveInboxDraftResponseSuccess remove(String inboxId, String draftId) th /** * Sends a draft from the inbox address. A reply draft is sent in its thread. A draft without a thread starts - * a new thread. The draft must have at least one {@code to} recipient, a non-empty {@code subject}, and - * {@code html} or {@code text}, or the request returns a 422. + * a new thread. The draft must have at least one {@code to} recipient and {@code html} or {@code text}, or the + * request returns a 422. A draft that starts a new thread also needs a non-empty {@code subject}. A reply draft + * can be sent with an empty subject. * A draft that was already sent returns a 409. * - * @param inboxId The inbox ID. An email address is not accepted. + * @param inboxId The inbox ID or the inbox email address. * @param draftId The draft ID. * @return The ID of the sent email. * @throws ResendException If an error occurs while sending the draft. @@ -167,8 +168,9 @@ public SendInboxDraftResponseSuccess send(String inboxId, String draftId) throws /** * Sends a draft from the inbox address. A reply draft is sent in its thread. A draft without a thread starts - * a new thread. The draft must have at least one {@code to} recipient, a non-empty {@code subject}, and - * {@code html} or {@code text}, or the request returns a 422. + * a new thread. The draft must have at least one {@code to} recipient and {@code html} or {@code text}, or the + * request returns a 422. A draft that starts a new thread also needs a non-empty {@code subject}. A reply draft + * can be sent with an empty subject. * A draft that was already sent returns a 409. * *

Set an idempotency key in {@code requestOptions} so the draft is sent only once. A retry with the same key @@ -176,7 +178,7 @@ public SendInboxDraftResponseSuccess send(String inboxId, String draftId) throws * key with a different body, or while the first request is still running, returns a 409. A failed request is * not stored, so you can retry it with the same key.

* - * @param inboxId The inbox ID. An email address is not accepted. + * @param inboxId The inbox ID or the inbox email address. * @param draftId The draft ID. * @param requestOptions The options with additional headers. * @return The ID of the sent email. @@ -188,11 +190,12 @@ public SendInboxDraftResponseSuccess send(String inboxId, String draftId, Reques /** * Sends a draft from the inbox address with custom headers. A reply draft is sent in its thread. A draft - * without a thread starts a new thread. The draft must have at least one {@code to} recipient, a non-empty - * {@code subject}, and {@code html} or {@code text}, or the request returns a 422. + * without a thread starts a new thread. The draft must have at least one {@code to} recipient and {@code html} + * or {@code text}, or the request returns a 422. A draft that starts a new thread also needs a non-empty + * {@code subject}. A reply draft can be sent with an empty subject. * A draft that was already sent returns a 409. * - * @param inboxId The inbox ID. An email address is not accepted. + * @param inboxId The inbox ID or the inbox email address. * @param draftId The draft ID. * @param sendInboxDraftOptions The custom headers to add to the email. * @return The ID of the sent email. @@ -205,8 +208,9 @@ public SendInboxDraftResponseSuccess send(String inboxId, String draftId, SendIn /** * Sends a draft from the inbox address with custom headers. A reply draft is sent in its thread. A draft - * without a thread starts a new thread. The draft must have at least one {@code to} recipient, a non-empty - * {@code subject}, and {@code html} or {@code text}, or the request returns a 422. + * without a thread starts a new thread. The draft must have at least one {@code to} recipient and {@code html} + * or {@code text}, or the request returns a 422. A draft that starts a new thread also needs a non-empty + * {@code subject}. A reply draft can be sent with an empty subject. * A draft that was already sent returns a 409. * *

Set an idempotency key in {@code requestOptions} so the draft is sent only once. A retry with the same key @@ -214,7 +218,7 @@ public SendInboxDraftResponseSuccess send(String inboxId, String draftId, SendIn * key with a different body, or while the first request is still running, returns a 409. A failed request is * not stored, so you can retry it with the same key.

* - * @param inboxId The inbox ID. An email address is not accepted. + * @param inboxId The inbox ID or the inbox email address. * @param draftId The draft ID. * @param sendInboxDraftOptions The custom headers to add to the email. * @param requestOptions The options with additional headers. diff --git a/src/main/java/com/resend/services/inboxes/InboxLabels.java b/src/main/java/com/resend/services/inboxes/InboxLabels.java index e4ffcdc..f9a765a 100644 --- a/src/main/java/com/resend/services/inboxes/InboxLabels.java +++ b/src/main/java/com/resend/services/inboxes/InboxLabels.java @@ -39,7 +39,7 @@ public InboxLabels(final String apiKey, final IHttpClient httpClient) { /** * Retrieves every label on an inbox, oldest first. The list is not paginated. * - * @param inboxId The inbox ID. An email address is not accepted. + * @param inboxId The inbox ID or the inbox email address. * @return A ListInboxLabelsResponseSuccess containing the labels. * @throws ResendException If an error occurs while retrieving the labels. */ @@ -51,7 +51,7 @@ public ListInboxLabelsResponseSuccess list(String inboxId) throws ResendExceptio * Creates a label on an inbox. An inbox can have at most 100 labels. Label names are unique per inbox, * compared without case. A duplicate name or a full inbox returns a 422. * - * @param inboxId The inbox ID. An email address is not accepted. + * @param inboxId The inbox ID or the inbox email address. * @param createInboxLabelOptions The label details. * @return The ID of the created label. * @throws ResendException If an error occurs while creating the label. @@ -65,7 +65,7 @@ public CreateInboxLabelResponseSuccess create(String inboxId, CreateInboxLabelOp * Renames or recolors a label. Omitted fields keep their current value. A new name must not match another * label on the inbox, compared without case, or the request returns a 422. * - * @param inboxId The inbox ID. An email address is not accepted. + * @param inboxId The inbox ID or the inbox email address. * @param labelId The label ID. * @param updateInboxLabelOptions The fields to update. * @return The ID of the updated label. @@ -79,7 +79,7 @@ public UpdateInboxLabelResponseSuccess update(String inboxId, String labelId, Up /** * Deletes a label and removes it from every thread that carries it. * - * @param inboxId The inbox ID. An email address is not accepted. + * @param inboxId The inbox ID or the inbox email address. * @param labelId The label ID. * @return The RemoveInboxLabelResponseSuccess with the details of the removed label. * @throws ResendException If an error occurs while removing the label. diff --git a/src/main/java/com/resend/services/inboxes/InboxThreadEmails.java b/src/main/java/com/resend/services/inboxes/InboxThreadEmails.java index e3b8a5e..ade6d73 100644 --- a/src/main/java/com/resend/services/inboxes/InboxThreadEmails.java +++ b/src/main/java/com/resend/services/inboxes/InboxThreadEmails.java @@ -42,7 +42,7 @@ public InboxThreadEmails(final String apiKey, final IHttpClient httpClie /** * Retrieves the messages of a thread, oldest first. * - * @param inboxId The inbox ID. An email address is not accepted. + * @param inboxId The inbox ID or the inbox email address. * @param threadId The thread ID. * @return A ListInboxThreadEmailsResponseSuccess containing the messages. * @throws ResendException If an error occurs while retrieving the messages. @@ -57,7 +57,7 @@ public ListInboxThreadEmailsResponseSuccess list(String inboxId, String threadId * to get the previous, older page. You cannot combine {@code after} and {@code before}. A cursor that is not * a message of the thread returns a 404. * - * @param inboxId The inbox ID. An email address is not accepted. + * @param inboxId The inbox ID or the inbox email address. * @param threadId The thread ID. * @param params The params used to paginate the list. * @return A ListInboxThreadEmailsResponseSuccess containing the messages. @@ -71,7 +71,7 @@ public ListInboxThreadEmailsResponseSuccess list(String inboxId, String threadId /** * Retrieves a single message of a thread. * - * @param inboxId The inbox ID. An email address is not accepted. + * @param inboxId The inbox ID or the inbox email address. * @param threadId The thread ID. * @param emailId The email ID, as returned in {@code data[].id} by the list thread emails endpoint. * @return The message. @@ -89,7 +89,7 @@ public GetInboxThreadEmailResponseSuccess get(String inboxId, String threadId, S * to that message's {@code to} recipients. The reply threads under the message through the * {@code In-Reply-To} and {@code References} headers. * - * @param inboxId The inbox ID. An email address is not accepted. + * @param inboxId The inbox ID or the inbox email address. * @param threadId The thread ID. * @param emailId The email ID of the message to reply to. * @param replyInboxThreadEmailOptions The reply content. @@ -114,7 +114,7 @@ public ReplyInboxThreadEmailResponseSuccess reply(String inboxId, String threadI * key with a different body, or while the first request is still running, returns a 409. A failed request is * not stored, so you can retry it with the same key.

* - * @param inboxId The inbox ID. An email address is not accepted. + * @param inboxId The inbox ID or the inbox email address. * @param threadId The thread ID. * @param emailId The email ID of the message to reply to. * @param replyInboxThreadEmailOptions The reply content. @@ -135,7 +135,7 @@ public ReplyInboxThreadEmailResponseSuccess reply(String inboxId, String threadI * when they exceed 10 files or 25 MB of {@code content} in total. An original attachment that was sent from * a URL ({@code path}) cannot be forwarded, and the request returns a 422. * - * @param inboxId The inbox ID. An email address is not accepted. + * @param inboxId The inbox ID or the inbox email address. * @param threadId The thread ID. * @param emailId The email ID of the message to forward. * @param forwardInboxThreadEmailOptions The forward recipients and content. @@ -160,7 +160,7 @@ public ForwardInboxThreadEmailResponseSuccess forward(String inboxId, String thr * key with a different body, or while the first request is still running, returns a 409. A failed request is * not stored, so you can retry it with the same key.

* - * @param inboxId The inbox ID. An email address is not accepted. + * @param inboxId The inbox ID or the inbox email address. * @param threadId The thread ID. * @param emailId The email ID of the message to forward. * @param forwardInboxThreadEmailOptions The forward recipients and content. diff --git a/src/main/java/com/resend/services/inboxes/InboxThreads.java b/src/main/java/com/resend/services/inboxes/InboxThreads.java index 08e50fd..99a33d4 100644 --- a/src/main/java/com/resend/services/inboxes/InboxThreads.java +++ b/src/main/java/com/resend/services/inboxes/InboxThreads.java @@ -56,7 +56,7 @@ public InboxThreadEmails emails() { * Retrieves the threads in an inbox, most recently active first. * To find threads by text, sender, recipient, attachment, or date, use {@link #search(String, SearchInboxThreadsParams)}. * - * @param inboxId The inbox ID. An email address is not accepted. + * @param inboxId The inbox ID or the inbox email address. * @return A ListInboxThreadsResponseSuccess containing the threads. * @throws ResendException If an error occurs while retrieving the threads. */ @@ -70,7 +70,7 @@ public ListInboxThreadsResponseSuccess list(String inboxId) throws ResendExcepti * A cursor thread that is not in the selected folders returns a 404. * To find threads by text, sender, recipient, attachment, or date, use {@link #search(String, SearchInboxThreadsParams)}. * - * @param inboxId The inbox ID. An email address is not accepted. + * @param inboxId The inbox ID or the inbox email address. * @param params The params used to filter and paginate the list. * @return A ListInboxThreadsResponseSuccess containing the threads. * @throws ResendException If an error occurs while retrieving the threads. @@ -83,7 +83,7 @@ public ListInboxThreadsResponseSuccess list(String inboxId, ListInboxThreadsPara /** * Searches the threads of an inbox, newest activity first. * - * @param inboxId The inbox ID. An email address is not accepted. + * @param inboxId The inbox ID or the inbox email address. * @return A SearchInboxThreadsResponseSuccess containing the matching threads. * @throws ResendException If an error occurs while searching the threads. */ @@ -98,7 +98,7 @@ public SearchInboxThreadsResponseSuccess search(String inboxId) throws ResendExc * search covers the 10,000 with the newest matching email. When any email filter is set, the matching email * must itself be in one of {@code folders}. * - * @param inboxId The inbox ID. An email address is not accepted. + * @param inboxId The inbox ID or the inbox email address. * @param params The search filters and pagination. * @return A SearchInboxThreadsResponseSuccess containing the matching threads. * @throws ResendException If an error occurs while searching the threads. @@ -111,7 +111,7 @@ public SearchInboxThreadsResponseSuccess search(String inboxId, SearchInboxThrea /** * Retrieves the summary of a thread. The emails in the thread are listed with {@link InboxThreadEmails#list(String, String)}. * - * @param inboxId The inbox ID. An email address is not accepted. + * @param inboxId The inbox ID or the inbox email address. * @param threadId The thread ID. * @return The thread summary. * @throws ResendException If an error occurs while retrieving the thread. @@ -125,7 +125,7 @@ public GetInboxThreadResponseSuccess get(String inboxId, String threadId) throws * Moving a thread out of {@code trash} restores it and clears its deletion deadline. * To change the labels of a thread, use {@link #addLabels}, {@link #setLabels} or {@link #removeLabel}. * - * @param inboxId The inbox ID. An email address is not accepted. + * @param inboxId The inbox ID or the inbox email address. * @param threadId The thread ID. * @param updateInboxThreadOptions The changes to apply. * @return The updated thread summary. @@ -141,7 +141,7 @@ public UpdateInboxThreadResponseSuccess update(String inboxId, String threadId, * To restore the thread, update its folder to {@code inbox}, {@code archive}, or {@code spam}, * which also clears the deletion deadline. * - * @param inboxId The inbox ID. An email address is not accepted. + * @param inboxId The inbox ID or the inbox email address. * @param threadId The thread ID. * @return The RemoveInboxThreadResponseSuccess with the details of the removed thread. * @throws ResendException If an error occurs while removing the thread. @@ -153,7 +153,7 @@ public RemoveInboxThreadResponseSuccess remove(String inboxId, String threadId) /** * Adds labels to a thread. Labels already on the thread stay on it. If any label ID is not found, nothing changes. * - * @param inboxId The inbox ID. + * @param inboxId The inbox ID or the inbox email address. * @param threadId The thread ID. * @param addInboxThreadLabelsOptions The IDs of the labels to add. * @return Every label on the thread after the change. @@ -168,7 +168,7 @@ public AddInboxThreadLabelsResponseSuccess addLabels(String inboxId, String thre * Sets the labels of a thread in one atomic change. Labels on the thread that are not listed are taken off, * and the labels themselves are kept. An empty list takes every label off the thread. * - * @param inboxId The inbox ID. + * @param inboxId The inbox ID or the inbox email address. * @param threadId The thread ID. * @param setInboxThreadLabelsOptions The IDs of the labels the thread should carry. * @return Every label on the thread after the change. @@ -183,7 +183,7 @@ public SetInboxThreadLabelsResponseSuccess setLabels(String inboxId, String thre * Removes a label from a thread. The label itself is kept, and stays on every other thread that carries it. * To delete the label everywhere, use {@link InboxLabels#remove(String, String)}. * - * @param inboxId The inbox ID. + * @param inboxId The inbox ID or the inbox email address. * @param threadId The thread ID. * @param labelId The label ID. * @return Every label on the thread after the change. diff --git a/src/main/java/com/resend/services/inboxes/Inboxes.java b/src/main/java/com/resend/services/inboxes/Inboxes.java index 7addef6..df0eb3b 100644 --- a/src/main/java/com/resend/services/inboxes/Inboxes.java +++ b/src/main/java/com/resend/services/inboxes/Inboxes.java @@ -134,7 +134,7 @@ public GetInboxResponseSuccess get(String inboxIdOrEmail) throws ResendException * Updates the internal name of an inbox or the name recipients see when mail is sent from it. * At least one of {@code name} or {@code from_name} is required. * - * @param inboxId The inbox ID. An email address is not accepted. + * @param inboxId The inbox ID or the inbox email address. * @param updateInboxOptions The fields to update. * @return The ID of the updated inbox. * @throws ResendException If an error occurs while updating the inbox. @@ -147,7 +147,7 @@ public UpdateInboxResponseSuccess update(String inboxId, UpdateInboxOptions upda /** * Removes an inbox. An inbox that does not exist or was already removed returns a 404. * - * @param inboxId The inbox ID. An email address is not accepted. + * @param inboxId The inbox ID or the inbox email address. * @return The RemoveInboxResponseSuccess with the details of the removed inbox. * @throws ResendException If an error occurs while removing the inbox. */ From 0f4144be946c3cf4e38ceccc6eab7694d03816ac Mon Sep 17 00:00:00 2001 From: Kewyn Akshlley Date: Fri, 16 Oct 2026 14:00:05 -0300 Subject: [PATCH 26/30] fix(inboxes): encode every ID in the inbox paths as a single segment Only Inboxes#get encoded its path segment; every other inbox method concatenated the inbox, thread, email, draft and label IDs into the path as-is. An inbox email address can legally contain `/`, `?` or `#`, so `labels().list("a/b@acme.com")` requested `/inboxes/a/b@acme.com/labels` and `a?b@acme.com` turned the rest of the path into a query string. A value like `x/../../domains` resolved to a different endpoint. Move the encoder to URLHelper#encodePathSegment and use it for every dynamic segment. It also rejects null, empty, `.` and `..`, which OkHttp resolves as dot segments even when percent-encoded. --- .../com/resend/core/helper/URLHelper.java | 18 ++++++++++- .../resend/services/inboxes/InboxAgent.java | 5 +-- .../resend/services/inboxes/InboxDrafts.java | 20 ++++++------ .../resend/services/inboxes/InboxLabels.java | 9 +++--- .../services/inboxes/InboxThreadEmails.java | 12 +++---- .../resend/services/inboxes/InboxThreads.java | 17 +++++----- .../com/resend/services/inboxes/Inboxes.java | 17 ++-------- .../com/resend/core/helper/URLHelperTest.java | 31 +++++++++++++++++++ .../services/inboxes/InboxLabelsTest.java | 13 ++++++++ 9 files changed, 97 insertions(+), 45 deletions(-) create mode 100644 src/test/java/com/resend/core/helper/URLHelperTest.java diff --git a/src/main/java/com/resend/core/helper/URLHelper.java b/src/main/java/com/resend/core/helper/URLHelper.java index 30982ee..a8d8b83 100644 --- a/src/main/java/com/resend/core/helper/URLHelper.java +++ b/src/main/java/com/resend/core/helper/URLHelper.java @@ -11,7 +11,7 @@ import java.util.stream.Collectors; /** - * URL Helper for handling query params. + * URL Helper for handling path segments and query params. */ public class URLHelper { @@ -75,6 +75,22 @@ public static String parse(ListParams params, Map extras) { return "?" + query; } + /** + * Encodes a value for use as a single path segment, such as an ID or an email address. + * Characters that would change the path, like {@code /}, {@code ?}, {@code #} and {@code %}, are percent-encoded. + * + * @param value The path segment value. + * @return The encoded path segment. + * @throws IllegalArgumentException If the value is null, empty, {@code .} or {@code ..}, which cannot be sent as a + * single segment. + */ + public static String encodePathSegment(String value) { + if (value == null || value.isEmpty() || value.equals(".") || value.equals("..")) { + throw new IllegalArgumentException("A path segment must not be null, empty, \".\" or \"..\""); + } + return encode(value).replace("+", "%20"); + } + private static String encode(String value) { try { return URLEncoder.encode(value, StandardCharsets.UTF_8.name()); diff --git a/src/main/java/com/resend/services/inboxes/InboxAgent.java b/src/main/java/com/resend/services/inboxes/InboxAgent.java index 3644491..ff2b499 100644 --- a/src/main/java/com/resend/services/inboxes/InboxAgent.java +++ b/src/main/java/com/resend/services/inboxes/InboxAgent.java @@ -1,6 +1,7 @@ package com.resend.services.inboxes; import com.resend.core.exception.ResendException; +import com.resend.core.helper.URLHelper; import com.resend.core.net.HttpMethod; import com.resend.core.net.IHttpClient; import com.resend.core.service.BaseService; @@ -42,7 +43,7 @@ public InboxAgent(final String apiKey, final IHttpClient httpClient) { * @throws ResendException If an error occurs while retrieving the agent settings. */ public GetInboxAgentResponseSuccess get(String inboxId) throws ResendException { - return execute("/inboxes/" + inboxId + "/agent", HttpMethod.GET, null, MediaType.get("application/json"), GetInboxAgentResponseSuccess.class); + return execute("/inboxes/" + URLHelper.encodePathSegment(inboxId) + "/agent", HttpMethod.GET, null, MediaType.get("application/json"), GetInboxAgentResponseSuccess.class); } /** @@ -56,6 +57,6 @@ public GetInboxAgentResponseSuccess get(String inboxId) throws ResendException { */ public UpdateInboxAgentResponseSuccess update(String inboxId, UpdateInboxAgentOptions updateInboxAgentOptions) throws ResendException { String payload = super.resendMapper.writeValue(updateInboxAgentOptions); - return execute("/inboxes/" + inboxId + "/agent", HttpMethod.PATCH, payload, MediaType.get("application/json"), UpdateInboxAgentResponseSuccess.class); + return execute("/inboxes/" + URLHelper.encodePathSegment(inboxId) + "/agent", HttpMethod.PATCH, payload, MediaType.get("application/json"), UpdateInboxAgentResponseSuccess.class); } } diff --git a/src/main/java/com/resend/services/inboxes/InboxDrafts.java b/src/main/java/com/resend/services/inboxes/InboxDrafts.java index fea8a9e..64950cc 100644 --- a/src/main/java/com/resend/services/inboxes/InboxDrafts.java +++ b/src/main/java/com/resend/services/inboxes/InboxDrafts.java @@ -64,7 +64,7 @@ public ListInboxDraftsResponseSuccess list(String inboxId) throws ResendExceptio * @throws ResendException If an error occurs while retrieving the drafts. */ public ListInboxDraftsResponseSuccess list(String inboxId, ListParams params) throws ResendException { - return execute("/inboxes/" + inboxId + "/drafts" + URLHelper.parse(params), HttpMethod.GET, null, MediaType.get("application/json"), ListInboxDraftsResponseSuccess.class); + return execute("/inboxes/" + URLHelper.encodePathSegment(inboxId) + "/drafts" + URLHelper.parse(params), HttpMethod.GET, null, MediaType.get("application/json"), ListInboxDraftsResponseSuccess.class); } /** @@ -82,7 +82,7 @@ public ListInboxDraftsResponseSuccess list(String inboxId, ListParams params) th */ public CreateInboxDraftResponseSuccess create(String inboxId, CreateInboxDraftOptions createInboxDraftOptions) throws ResendException { String payload = super.resendMapper.writeValue(createInboxDraftOptions); - return execute("/inboxes/" + inboxId + "/drafts", HttpMethod.POST, payload, MediaType.get("application/json"), CreateInboxDraftResponseSuccess.class); + return execute("/inboxes/" + URLHelper.encodePathSegment(inboxId) + "/drafts", HttpMethod.POST, payload, MediaType.get("application/json"), CreateInboxDraftResponseSuccess.class); } /** @@ -106,7 +106,7 @@ public CreateInboxDraftResponseSuccess create(String inboxId, CreateInboxDraftOp */ public CreateInboxDraftResponseSuccess create(String inboxId, CreateInboxDraftOptions createInboxDraftOptions, RequestOptions requestOptions) throws ResendException { String payload = super.resendMapper.writeValue(createInboxDraftOptions); - return execute("/inboxes/" + inboxId + "/drafts", HttpMethod.POST, payload, MediaType.get("application/json"), requestOptions, CreateInboxDraftResponseSuccess.class); + return execute("/inboxes/" + URLHelper.encodePathSegment(inboxId) + "/drafts", HttpMethod.POST, payload, MediaType.get("application/json"), requestOptions, CreateInboxDraftResponseSuccess.class); } /** @@ -118,7 +118,7 @@ public CreateInboxDraftResponseSuccess create(String inboxId, CreateInboxDraftOp * @throws ResendException If an error occurs while retrieving the draft. */ public GetInboxDraftResponseSuccess get(String inboxId, String draftId) throws ResendException { - return execute("/inboxes/" + inboxId + "/drafts/" + draftId, HttpMethod.GET, null, MediaType.get("application/json"), GetInboxDraftResponseSuccess.class); + return execute("/inboxes/" + URLHelper.encodePathSegment(inboxId) + "/drafts/" + URLHelper.encodePathSegment(draftId), HttpMethod.GET, null, MediaType.get("application/json"), GetInboxDraftResponseSuccess.class); } /** @@ -135,7 +135,7 @@ public GetInboxDraftResponseSuccess get(String inboxId, String draftId) throws R */ public UpdateInboxDraftResponseSuccess update(String inboxId, String draftId, UpdateInboxDraftOptions updateInboxDraftOptions) throws ResendException { String payload = super.resendMapper.writeValue(updateInboxDraftOptions); - return execute("/inboxes/" + inboxId + "/drafts/" + draftId, HttpMethod.PATCH, payload, MediaType.get("application/json"), UpdateInboxDraftResponseSuccess.class); + return execute("/inboxes/" + URLHelper.encodePathSegment(inboxId) + "/drafts/" + URLHelper.encodePathSegment(draftId), HttpMethod.PATCH, payload, MediaType.get("application/json"), UpdateInboxDraftResponseSuccess.class); } /** @@ -147,7 +147,7 @@ public UpdateInboxDraftResponseSuccess update(String inboxId, String draftId, Up * @throws ResendException If an error occurs while removing the draft. */ public RemoveInboxDraftResponseSuccess remove(String inboxId, String draftId) throws ResendException { - return execute("/inboxes/" + inboxId + "/drafts/" + draftId, HttpMethod.DELETE, null, MediaType.get("application/json"), RemoveInboxDraftResponseSuccess.class); + return execute("/inboxes/" + URLHelper.encodePathSegment(inboxId) + "/drafts/" + URLHelper.encodePathSegment(draftId), HttpMethod.DELETE, null, MediaType.get("application/json"), RemoveInboxDraftResponseSuccess.class); } /** @@ -163,7 +163,7 @@ public RemoveInboxDraftResponseSuccess remove(String inboxId, String draftId) th * @throws ResendException If an error occurs while sending the draft. */ public SendInboxDraftResponseSuccess send(String inboxId, String draftId) throws ResendException { - return execute("/inboxes/" + inboxId + "/drafts/" + draftId + "/send", HttpMethod.POST, "", MediaType.get("application/json"), SendInboxDraftResponseSuccess.class); + return execute("/inboxes/" + URLHelper.encodePathSegment(inboxId) + "/drafts/" + URLHelper.encodePathSegment(draftId) + "/send", HttpMethod.POST, "", MediaType.get("application/json"), SendInboxDraftResponseSuccess.class); } /** @@ -185,7 +185,7 @@ public SendInboxDraftResponseSuccess send(String inboxId, String draftId) throws * @throws ResendException If an error occurs while sending the draft. */ public SendInboxDraftResponseSuccess send(String inboxId, String draftId, RequestOptions requestOptions) throws ResendException { - return execute("/inboxes/" + inboxId + "/drafts/" + draftId + "/send", HttpMethod.POST, "", MediaType.get("application/json"), requestOptions, SendInboxDraftResponseSuccess.class); + return execute("/inboxes/" + URLHelper.encodePathSegment(inboxId) + "/drafts/" + URLHelper.encodePathSegment(draftId) + "/send", HttpMethod.POST, "", MediaType.get("application/json"), requestOptions, SendInboxDraftResponseSuccess.class); } /** @@ -203,7 +203,7 @@ public SendInboxDraftResponseSuccess send(String inboxId, String draftId, Reques */ public SendInboxDraftResponseSuccess send(String inboxId, String draftId, SendInboxDraftOptions sendInboxDraftOptions) throws ResendException { String payload = sendInboxDraftOptions == null ? "" : super.resendMapper.writeValue(sendInboxDraftOptions); - return execute("/inboxes/" + inboxId + "/drafts/" + draftId + "/send", HttpMethod.POST, payload, MediaType.get("application/json"), SendInboxDraftResponseSuccess.class); + return execute("/inboxes/" + URLHelper.encodePathSegment(inboxId) + "/drafts/" + URLHelper.encodePathSegment(draftId) + "/send", HttpMethod.POST, payload, MediaType.get("application/json"), SendInboxDraftResponseSuccess.class); } /** @@ -227,6 +227,6 @@ public SendInboxDraftResponseSuccess send(String inboxId, String draftId, SendIn */ public SendInboxDraftResponseSuccess send(String inboxId, String draftId, SendInboxDraftOptions sendInboxDraftOptions, RequestOptions requestOptions) throws ResendException { String payload = sendInboxDraftOptions == null ? "" : super.resendMapper.writeValue(sendInboxDraftOptions); - return execute("/inboxes/" + inboxId + "/drafts/" + draftId + "/send", HttpMethod.POST, payload, MediaType.get("application/json"), requestOptions, SendInboxDraftResponseSuccess.class); + return execute("/inboxes/" + URLHelper.encodePathSegment(inboxId) + "/drafts/" + URLHelper.encodePathSegment(draftId) + "/send", HttpMethod.POST, payload, MediaType.get("application/json"), requestOptions, SendInboxDraftResponseSuccess.class); } } diff --git a/src/main/java/com/resend/services/inboxes/InboxLabels.java b/src/main/java/com/resend/services/inboxes/InboxLabels.java index f9a765a..7fee6b9 100644 --- a/src/main/java/com/resend/services/inboxes/InboxLabels.java +++ b/src/main/java/com/resend/services/inboxes/InboxLabels.java @@ -1,6 +1,7 @@ package com.resend.services.inboxes; import com.resend.core.exception.ResendException; +import com.resend.core.helper.URLHelper; import com.resend.core.net.HttpMethod; import com.resend.core.net.IHttpClient; import com.resend.core.service.BaseService; @@ -44,7 +45,7 @@ public InboxLabels(final String apiKey, final IHttpClient httpClient) { * @throws ResendException If an error occurs while retrieving the labels. */ public ListInboxLabelsResponseSuccess list(String inboxId) throws ResendException { - return execute("/inboxes/" + inboxId + "/labels", HttpMethod.GET, null, MediaType.get("application/json"), ListInboxLabelsResponseSuccess.class); + return execute("/inboxes/" + URLHelper.encodePathSegment(inboxId) + "/labels", HttpMethod.GET, null, MediaType.get("application/json"), ListInboxLabelsResponseSuccess.class); } /** @@ -58,7 +59,7 @@ public ListInboxLabelsResponseSuccess list(String inboxId) throws ResendExceptio */ public CreateInboxLabelResponseSuccess create(String inboxId, CreateInboxLabelOptions createInboxLabelOptions) throws ResendException { String payload = super.resendMapper.writeValue(createInboxLabelOptions); - return execute("/inboxes/" + inboxId + "/labels", HttpMethod.POST, payload, MediaType.get("application/json"), CreateInboxLabelResponseSuccess.class); + return execute("/inboxes/" + URLHelper.encodePathSegment(inboxId) + "/labels", HttpMethod.POST, payload, MediaType.get("application/json"), CreateInboxLabelResponseSuccess.class); } /** @@ -73,7 +74,7 @@ public CreateInboxLabelResponseSuccess create(String inboxId, CreateInboxLabelOp */ public UpdateInboxLabelResponseSuccess update(String inboxId, String labelId, UpdateInboxLabelOptions updateInboxLabelOptions) throws ResendException { String payload = super.resendMapper.writeValue(updateInboxLabelOptions); - return execute("/inboxes/" + inboxId + "/labels/" + labelId, HttpMethod.PATCH, payload, MediaType.get("application/json"), UpdateInboxLabelResponseSuccess.class); + return execute("/inboxes/" + URLHelper.encodePathSegment(inboxId) + "/labels/" + URLHelper.encodePathSegment(labelId), HttpMethod.PATCH, payload, MediaType.get("application/json"), UpdateInboxLabelResponseSuccess.class); } /** @@ -85,6 +86,6 @@ public UpdateInboxLabelResponseSuccess update(String inboxId, String labelId, Up * @throws ResendException If an error occurs while removing the label. */ public RemoveInboxLabelResponseSuccess remove(String inboxId, String labelId) throws ResendException { - return execute("/inboxes/" + inboxId + "/labels/" + labelId, HttpMethod.DELETE, null, MediaType.get("application/json"), RemoveInboxLabelResponseSuccess.class); + return execute("/inboxes/" + URLHelper.encodePathSegment(inboxId) + "/labels/" + URLHelper.encodePathSegment(labelId), HttpMethod.DELETE, null, MediaType.get("application/json"), RemoveInboxLabelResponseSuccess.class); } } diff --git a/src/main/java/com/resend/services/inboxes/InboxThreadEmails.java b/src/main/java/com/resend/services/inboxes/InboxThreadEmails.java index ade6d73..5a763ea 100644 --- a/src/main/java/com/resend/services/inboxes/InboxThreadEmails.java +++ b/src/main/java/com/resend/services/inboxes/InboxThreadEmails.java @@ -64,7 +64,7 @@ public ListInboxThreadEmailsResponseSuccess list(String inboxId, String threadId * @throws ResendException If an error occurs while retrieving the messages. */ public ListInboxThreadEmailsResponseSuccess list(String inboxId, String threadId, ListParams params) throws ResendException { - String path = "/inboxes/" + inboxId + "/threads/" + threadId + "/emails" + URLHelper.parse(params); + String path = "/inboxes/" + URLHelper.encodePathSegment(inboxId) + "/threads/" + URLHelper.encodePathSegment(threadId) + "/emails" + URLHelper.parse(params); return execute(path, HttpMethod.GET, null, MediaType.get("application/json"), ListInboxThreadEmailsResponseSuccess.class); } @@ -78,7 +78,7 @@ public ListInboxThreadEmailsResponseSuccess list(String inboxId, String threadId * @throws ResendException If an error occurs while retrieving the message. */ public GetInboxThreadEmailResponseSuccess get(String inboxId, String threadId, String emailId) throws ResendException { - String path = "/inboxes/" + inboxId + "/threads/" + threadId + "/emails/" + emailId; + String path = "/inboxes/" + URLHelper.encodePathSegment(inboxId) + "/threads/" + URLHelper.encodePathSegment(threadId) + "/emails/" + URLHelper.encodePathSegment(emailId); return execute(path, HttpMethod.GET, null, MediaType.get("application/json"), GetInboxThreadEmailResponseSuccess.class); } @@ -97,7 +97,7 @@ public GetInboxThreadEmailResponseSuccess get(String inboxId, String threadId, S * @throws ResendException If an error occurs while sending the reply. */ public ReplyInboxThreadEmailResponseSuccess reply(String inboxId, String threadId, String emailId, ReplyInboxThreadEmailOptions replyInboxThreadEmailOptions) throws ResendException { - String path = "/inboxes/" + inboxId + "/threads/" + threadId + "/emails/" + emailId + "/reply"; + String path = "/inboxes/" + URLHelper.encodePathSegment(inboxId) + "/threads/" + URLHelper.encodePathSegment(threadId) + "/emails/" + URLHelper.encodePathSegment(emailId) + "/reply"; String payload = super.resendMapper.writeValue(replyInboxThreadEmailOptions); return execute(path, HttpMethod.POST, payload, MediaType.get("application/json"), ReplyInboxThreadEmailResponseSuccess.class); } @@ -123,7 +123,7 @@ public ReplyInboxThreadEmailResponseSuccess reply(String inboxId, String threadI * @throws ResendException If an error occurs while sending the reply. */ public ReplyInboxThreadEmailResponseSuccess reply(String inboxId, String threadId, String emailId, ReplyInboxThreadEmailOptions replyInboxThreadEmailOptions, RequestOptions requestOptions) throws ResendException { - String path = "/inboxes/" + inboxId + "/threads/" + threadId + "/emails/" + emailId + "/reply"; + String path = "/inboxes/" + URLHelper.encodePathSegment(inboxId) + "/threads/" + URLHelper.encodePathSegment(threadId) + "/emails/" + URLHelper.encodePathSegment(emailId) + "/reply"; String payload = super.resendMapper.writeValue(replyInboxThreadEmailOptions); return execute(path, HttpMethod.POST, payload, MediaType.get("application/json"), requestOptions, ReplyInboxThreadEmailResponseSuccess.class); } @@ -143,7 +143,7 @@ public ReplyInboxThreadEmailResponseSuccess reply(String inboxId, String threadI * @throws ResendException If an error occurs while sending the forward. */ public ForwardInboxThreadEmailResponseSuccess forward(String inboxId, String threadId, String emailId, ForwardInboxThreadEmailOptions forwardInboxThreadEmailOptions) throws ResendException { - String path = "/inboxes/" + inboxId + "/threads/" + threadId + "/emails/" + emailId + "/forward"; + String path = "/inboxes/" + URLHelper.encodePathSegment(inboxId) + "/threads/" + URLHelper.encodePathSegment(threadId) + "/emails/" + URLHelper.encodePathSegment(emailId) + "/forward"; String payload = super.resendMapper.writeValue(forwardInboxThreadEmailOptions); return execute(path, HttpMethod.POST, payload, MediaType.get("application/json"), ForwardInboxThreadEmailResponseSuccess.class); } @@ -169,7 +169,7 @@ public ForwardInboxThreadEmailResponseSuccess forward(String inboxId, String thr * @throws ResendException If an error occurs while sending the forward. */ public ForwardInboxThreadEmailResponseSuccess forward(String inboxId, String threadId, String emailId, ForwardInboxThreadEmailOptions forwardInboxThreadEmailOptions, RequestOptions requestOptions) throws ResendException { - String path = "/inboxes/" + inboxId + "/threads/" + threadId + "/emails/" + emailId + "/forward"; + String path = "/inboxes/" + URLHelper.encodePathSegment(inboxId) + "/threads/" + URLHelper.encodePathSegment(threadId) + "/emails/" + URLHelper.encodePathSegment(emailId) + "/forward"; String payload = super.resendMapper.writeValue(forwardInboxThreadEmailOptions); return execute(path, HttpMethod.POST, payload, MediaType.get("application/json"), requestOptions, ForwardInboxThreadEmailResponseSuccess.class); } diff --git a/src/main/java/com/resend/services/inboxes/InboxThreads.java b/src/main/java/com/resend/services/inboxes/InboxThreads.java index 99a33d4..ada46ff 100644 --- a/src/main/java/com/resend/services/inboxes/InboxThreads.java +++ b/src/main/java/com/resend/services/inboxes/InboxThreads.java @@ -1,6 +1,7 @@ package com.resend.services.inboxes; import com.resend.core.exception.ResendException; +import com.resend.core.helper.URLHelper; import com.resend.core.net.HttpMethod; import com.resend.core.net.IHttpClient; import com.resend.core.service.BaseService; @@ -77,7 +78,7 @@ public ListInboxThreadsResponseSuccess list(String inboxId) throws ResendExcepti */ public ListInboxThreadsResponseSuccess list(String inboxId, ListInboxThreadsParams params) throws ResendException { String query = params == null ? "" : params.toQueryString(); - return execute("/inboxes/" + inboxId + "/threads" + query, HttpMethod.GET, null, MediaType.get("application/json"), ListInboxThreadsResponseSuccess.class); + return execute("/inboxes/" + URLHelper.encodePathSegment(inboxId) + "/threads" + query, HttpMethod.GET, null, MediaType.get("application/json"), ListInboxThreadsResponseSuccess.class); } /** @@ -105,7 +106,7 @@ public SearchInboxThreadsResponseSuccess search(String inboxId) throws ResendExc */ public SearchInboxThreadsResponseSuccess search(String inboxId, SearchInboxThreadsParams params) throws ResendException { String query = params == null ? "" : params.toQueryString(); - return execute("/inboxes/" + inboxId + "/threads/search" + query, HttpMethod.GET, null, MediaType.get("application/json"), SearchInboxThreadsResponseSuccess.class); + return execute("/inboxes/" + URLHelper.encodePathSegment(inboxId) + "/threads/search" + query, HttpMethod.GET, null, MediaType.get("application/json"), SearchInboxThreadsResponseSuccess.class); } /** @@ -117,7 +118,7 @@ public SearchInboxThreadsResponseSuccess search(String inboxId, SearchInboxThrea * @throws ResendException If an error occurs while retrieving the thread. */ public GetInboxThreadResponseSuccess get(String inboxId, String threadId) throws ResendException { - return execute("/inboxes/" + inboxId + "/threads/" + threadId, HttpMethod.GET, null, MediaType.get("application/json"), GetInboxThreadResponseSuccess.class); + return execute("/inboxes/" + URLHelper.encodePathSegment(inboxId) + "/threads/" + URLHelper.encodePathSegment(threadId), HttpMethod.GET, null, MediaType.get("application/json"), GetInboxThreadResponseSuccess.class); } /** @@ -133,7 +134,7 @@ public GetInboxThreadResponseSuccess get(String inboxId, String threadId) throws */ public UpdateInboxThreadResponseSuccess update(String inboxId, String threadId, UpdateInboxThreadOptions updateInboxThreadOptions) throws ResendException { String payload = super.resendMapper.writeValue(updateInboxThreadOptions); - return execute("/inboxes/" + inboxId + "/threads/" + threadId, HttpMethod.PATCH, payload, MediaType.get("application/json"), UpdateInboxThreadResponseSuccess.class); + return execute("/inboxes/" + URLHelper.encodePathSegment(inboxId) + "/threads/" + URLHelper.encodePathSegment(threadId), HttpMethod.PATCH, payload, MediaType.get("application/json"), UpdateInboxThreadResponseSuccess.class); } /** @@ -147,7 +148,7 @@ public UpdateInboxThreadResponseSuccess update(String inboxId, String threadId, * @throws ResendException If an error occurs while removing the thread. */ public RemoveInboxThreadResponseSuccess remove(String inboxId, String threadId) throws ResendException { - return execute("/inboxes/" + inboxId + "/threads/" + threadId, HttpMethod.DELETE, null, MediaType.get("application/json"), RemoveInboxThreadResponseSuccess.class); + return execute("/inboxes/" + URLHelper.encodePathSegment(inboxId) + "/threads/" + URLHelper.encodePathSegment(threadId), HttpMethod.DELETE, null, MediaType.get("application/json"), RemoveInboxThreadResponseSuccess.class); } /** @@ -161,7 +162,7 @@ public RemoveInboxThreadResponseSuccess remove(String inboxId, String threadId) */ public AddInboxThreadLabelsResponseSuccess addLabels(String inboxId, String threadId, AddInboxThreadLabelsOptions addInboxThreadLabelsOptions) throws ResendException { String payload = super.resendMapper.writeValue(addInboxThreadLabelsOptions); - return execute("/inboxes/" + inboxId + "/threads/" + threadId + "/labels", HttpMethod.POST, payload, MediaType.get("application/json"), AddInboxThreadLabelsResponseSuccess.class); + return execute("/inboxes/" + URLHelper.encodePathSegment(inboxId) + "/threads/" + URLHelper.encodePathSegment(threadId) + "/labels", HttpMethod.POST, payload, MediaType.get("application/json"), AddInboxThreadLabelsResponseSuccess.class); } /** @@ -176,7 +177,7 @@ public AddInboxThreadLabelsResponseSuccess addLabels(String inboxId, String thre */ public SetInboxThreadLabelsResponseSuccess setLabels(String inboxId, String threadId, SetInboxThreadLabelsOptions setInboxThreadLabelsOptions) throws ResendException { String payload = super.resendMapper.writeValue(setInboxThreadLabelsOptions); - return execute("/inboxes/" + inboxId + "/threads/" + threadId + "/labels", HttpMethod.PUT, payload, MediaType.get("application/json"), SetInboxThreadLabelsResponseSuccess.class); + return execute("/inboxes/" + URLHelper.encodePathSegment(inboxId) + "/threads/" + URLHelper.encodePathSegment(threadId) + "/labels", HttpMethod.PUT, payload, MediaType.get("application/json"), SetInboxThreadLabelsResponseSuccess.class); } /** @@ -190,6 +191,6 @@ public SetInboxThreadLabelsResponseSuccess setLabels(String inboxId, String thre * @throws ResendException If an error occurs while removing the label. */ public RemoveInboxThreadLabelResponseSuccess removeLabel(String inboxId, String threadId, String labelId) throws ResendException { - return execute("/inboxes/" + inboxId + "/threads/" + threadId + "/labels/" + labelId, HttpMethod.DELETE, null, MediaType.get("application/json"), RemoveInboxThreadLabelResponseSuccess.class); + return execute("/inboxes/" + URLHelper.encodePathSegment(inboxId) + "/threads/" + URLHelper.encodePathSegment(threadId) + "/labels/" + URLHelper.encodePathSegment(labelId), HttpMethod.DELETE, null, MediaType.get("application/json"), RemoveInboxThreadLabelResponseSuccess.class); } } diff --git a/src/main/java/com/resend/services/inboxes/Inboxes.java b/src/main/java/com/resend/services/inboxes/Inboxes.java index df0eb3b..2f0ff29 100644 --- a/src/main/java/com/resend/services/inboxes/Inboxes.java +++ b/src/main/java/com/resend/services/inboxes/Inboxes.java @@ -15,9 +15,6 @@ import com.resend.services.inboxes.model.UpdateInboxResponseSuccess; import okhttp3.MediaType; -import java.io.UnsupportedEncodingException; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; /** * Represents the Resend Inboxes module. @@ -127,7 +124,7 @@ public ListInboxesResponseSuccess list(ListParams params) throws ResendException * @throws ResendException If an error occurs while retrieving the inbox. */ public GetInboxResponseSuccess get(String inboxIdOrEmail) throws ResendException { - return execute("/inboxes/" + encodePathSegment(inboxIdOrEmail), HttpMethod.GET, null, MediaType.get("application/json"), GetInboxResponseSuccess.class); + return execute("/inboxes/" + URLHelper.encodePathSegment(inboxIdOrEmail), HttpMethod.GET, null, MediaType.get("application/json"), GetInboxResponseSuccess.class); } /** @@ -141,7 +138,7 @@ public GetInboxResponseSuccess get(String inboxIdOrEmail) throws ResendException */ public UpdateInboxResponseSuccess update(String inboxId, UpdateInboxOptions updateInboxOptions) throws ResendException { String payload = super.resendMapper.writeValue(updateInboxOptions); - return execute("/inboxes/" + inboxId, HttpMethod.PATCH, payload, MediaType.get("application/json"), UpdateInboxResponseSuccess.class); + return execute("/inboxes/" + URLHelper.encodePathSegment(inboxId), HttpMethod.PATCH, payload, MediaType.get("application/json"), UpdateInboxResponseSuccess.class); } /** @@ -152,14 +149,6 @@ public UpdateInboxResponseSuccess update(String inboxId, UpdateInboxOptions upda * @throws ResendException If an error occurs while removing the inbox. */ public RemoveInboxResponseSuccess remove(String inboxId) throws ResendException { - return execute("/inboxes/" + inboxId, HttpMethod.DELETE, null, MediaType.get("application/json"), RemoveInboxResponseSuccess.class); - } - - private static String encodePathSegment(String value) { - try { - return URLEncoder.encode(value, StandardCharsets.UTF_8.name()).replace("+", "%20"); - } catch (UnsupportedEncodingException e) { - throw new RuntimeException(e); - } + return execute("/inboxes/" + URLHelper.encodePathSegment(inboxId), HttpMethod.DELETE, null, MediaType.get("application/json"), RemoveInboxResponseSuccess.class); } } diff --git a/src/test/java/com/resend/core/helper/URLHelperTest.java b/src/test/java/com/resend/core/helper/URLHelperTest.java new file mode 100644 index 0000000..908c54e --- /dev/null +++ b/src/test/java/com/resend/core/helper/URLHelperTest.java @@ -0,0 +1,31 @@ +package com.resend.core.helper; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class URLHelperTest { + + @Test + public void testEncodePathSegment_LeavesIdsUnchanged() { + assertEquals("b3e2b2b6-3f0e-4c8e-9ad3-2f43a1e2c7f1", URLHelper.encodePathSegment("b3e2b2b6-3f0e-4c8e-9ad3-2f43a1e2c7f1")); + } + + @Test + public void testEncodePathSegment_EncodesReservedCharacters() { + assertEquals("support%40acme.com", URLHelper.encodePathSegment("support@acme.com")); + assertEquals("a%2Fb%40acme.com", URLHelper.encodePathSegment("a/b@acme.com")); + assertEquals("a%3Fb%23c%40acme.com", URLHelper.encodePathSegment("a?b#c@acme.com")); + assertEquals("a%2Bb%25c%40acme.com", URLHelper.encodePathSegment("a+b%c@acme.com")); + assertEquals("a%20b", URLHelper.encodePathSegment("a b")); + assertEquals("x%2F..%2F..%2Fdomains", URLHelper.encodePathSegment("x/../../domains")); + } + + @Test + public void testEncodePathSegment_RejectsValuesThatAreNotASegment() { + assertThrows(IllegalArgumentException.class, () -> URLHelper.encodePathSegment(null)); + assertThrows(IllegalArgumentException.class, () -> URLHelper.encodePathSegment("")); + assertThrows(IllegalArgumentException.class, () -> URLHelper.encodePathSegment(".")); + assertThrows(IllegalArgumentException.class, () -> URLHelper.encodePathSegment("..")); + } +} diff --git a/src/test/java/com/resend/services/inboxes/InboxLabelsTest.java b/src/test/java/com/resend/services/inboxes/InboxLabelsTest.java index c25402b..b867b5e 100644 --- a/src/test/java/com/resend/services/inboxes/InboxLabelsTest.java +++ b/src/test/java/com/resend/services/inboxes/InboxLabelsTest.java @@ -98,4 +98,17 @@ public void testRemoveLabel_Success() throws ResendException { assertEquals(LABEL_ID, response.getId()); assertTrue(response.getDeleted()); } + + @Test + public void testListLabels_EncodesEmailAddressAsOneSegment() throws ResendException { + when(httpClient.perform(eq("/inboxes/a%2Fb%3Fc%40acme.com/labels"), anyString(), eq(HttpMethod.GET), isNull(), any(MediaType.class))) + .thenReturn(new AbstractHttpResponse<>(200, "{\"object\":\"list\",\"data\":[]}", true)); + + assertTrue(labels.list("a/b?c@acme.com").getData().isEmpty()); + } + + @Test + public void testRemoveLabel_RejectsDotSegment() { + assertThrows(IllegalArgumentException.class, () -> labels.remove(INBOX_ID, "..")); + } } From d0a207ceb4acd8955669527ad3f52ee1213dcea1 Mon Sep 17 00:00:00 2001 From: Kewyn Akshlley Date: Fri, 16 Oct 2026 14:00:05 -0300 Subject: [PATCH 27/30] fix(inboxes): reject null or blank IDs in the thread label builders --- .../inboxes/model/AddInboxThreadLabelsOptions.java | 14 +++++++++++++- .../inboxes/model/SetInboxThreadLabelsOptions.java | 14 +++++++++++++- .../resend/services/inboxes/InboxThreadsTest.java | 6 ++++++ 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/resend/services/inboxes/model/AddInboxThreadLabelsOptions.java b/src/main/java/com/resend/services/inboxes/model/AddInboxThreadLabelsOptions.java index ca2efc4..018b334 100644 --- a/src/main/java/com/resend/services/inboxes/model/AddInboxThreadLabelsOptions.java +++ b/src/main/java/com/resend/services/inboxes/model/AddInboxThreadLabelsOptions.java @@ -74,13 +74,25 @@ public Builder labels(List labels) { * Build a new AddInboxThreadLabelsOptions object. * * @return A new AddInboxThreadLabelsOptions object. - * @throws IllegalArgumentException if {@code labels} is null or empty. + * @throws IllegalArgumentException if {@code labels} is null or empty, or contains a null or blank ID. */ public AddInboxThreadLabelsOptions build() { if (labels == null || labels.isEmpty()) { throw new IllegalArgumentException("labels must contain at least one label ID"); } + if (hasBlankId(labels)) { + throw new IllegalArgumentException("labels must not contain a null or blank label ID"); + } return new AddInboxThreadLabelsOptions(this); } + + private static boolean hasBlankId(List labels) { + for (String label : labels) { + if (label == null || label.trim().isEmpty()) { + return true; + } + } + return false; + } } } diff --git a/src/main/java/com/resend/services/inboxes/model/SetInboxThreadLabelsOptions.java b/src/main/java/com/resend/services/inboxes/model/SetInboxThreadLabelsOptions.java index e5cd175..0d05612 100644 --- a/src/main/java/com/resend/services/inboxes/model/SetInboxThreadLabelsOptions.java +++ b/src/main/java/com/resend/services/inboxes/model/SetInboxThreadLabelsOptions.java @@ -74,13 +74,25 @@ public Builder labels(List labels) { * Build a new SetInboxThreadLabelsOptions object. * * @return A new SetInboxThreadLabelsOptions object. - * @throws IllegalArgumentException if {@code labels} is null. + * @throws IllegalArgumentException if {@code labels} is null or contains a null or blank ID. */ public SetInboxThreadLabelsOptions build() { if (labels == null) { throw new IllegalArgumentException("labels must not be null"); } + if (hasBlankId(labels)) { + throw new IllegalArgumentException("labels must not contain a null or blank label ID"); + } return new SetInboxThreadLabelsOptions(this); } + + private static boolean hasBlankId(List labels) { + for (String label : labels) { + if (label == null || label.trim().isEmpty()) { + return true; + } + } + return false; + } } } diff --git a/src/test/java/com/resend/services/inboxes/InboxThreadsTest.java b/src/test/java/com/resend/services/inboxes/InboxThreadsTest.java index ef706f1..f5d2f7f 100644 --- a/src/test/java/com/resend/services/inboxes/InboxThreadsTest.java +++ b/src/test/java/com/resend/services/inboxes/InboxThreadsTest.java @@ -11,6 +11,7 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.Arrays; import java.util.Collections; import static com.resend.services.util.InboxesUtil.*; @@ -210,5 +211,10 @@ public void testThreadLabelsBuilders_Validate() { assertThrows(IllegalArgumentException.class, () -> AddInboxThreadLabelsOptions.builder().build()); assertThrows(IllegalArgumentException.class, () -> AddInboxThreadLabelsOptions.builder().labels(Collections.emptyList()).build()); assertThrows(IllegalArgumentException.class, () -> SetInboxThreadLabelsOptions.builder().build()); + assertThrows(IllegalArgumentException.class, () -> AddInboxThreadLabelsOptions.builder().labels(LABEL_ID, " ").build()); + assertThrows(IllegalArgumentException.class, () -> AddInboxThreadLabelsOptions.builder().labels(Arrays.asList(LABEL_ID, null)).build()); + assertThrows(IllegalArgumentException.class, () -> SetInboxThreadLabelsOptions.builder().labels("").build()); + assertThrows(IllegalArgumentException.class, () -> SetInboxThreadLabelsOptions.builder().labels(Arrays.asList((String) null)).build()); + assertNotNull(SetInboxThreadLabelsOptions.builder().labels(Collections.emptyList()).build()); } } From 51e2e10ec66f0b47facb8babb32b8abb86bd1a9f Mon Sep 17 00:00:00 2001 From: dielduarte Date: Fri, 16 Oct 2026 16:25:19 -0300 Subject: [PATCH 28/30] fix(inboxes): drop search highlights and match the tone and attachment docs Follow resend-openapi #137 and resend-node 7943103: - Search results lose `highlights`. `matched_email_id` is now the same email `id` that the list thread emails endpoint returns. - `tone` is one of the four presets. Custom text is no longer part of the contract. - Reply and forward no longer cap attachments at 10 files. Co-Authored-By: Claude Opus 5.5 --- .../services/inboxes/InboxThreadEmails.java | 4 ++-- .../model/ForwardInboxThreadEmailOptions.java | 4 ++-- .../model/GetInboxAgentResponseSuccess.java | 2 +- .../services/inboxes/model/InboxAgent.java | 6 ++--- .../model/InboxThreadSearchResult.java | 23 ++++--------------- .../model/ReplyInboxThreadEmailOptions.java | 4 ++-- .../model/UpdateInboxAgentOptions.java | 6 ++--- .../services/inboxes/InboxAgentTest.java | 8 +++---- .../services/inboxes/InboxThreadsTest.java | 5 +--- 9 files changed, 21 insertions(+), 41 deletions(-) diff --git a/src/main/java/com/resend/services/inboxes/InboxThreadEmails.java b/src/main/java/com/resend/services/inboxes/InboxThreadEmails.java index 5a763ea..5e39687 100644 --- a/src/main/java/com/resend/services/inboxes/InboxThreadEmails.java +++ b/src/main/java/com/resend/services/inboxes/InboxThreadEmails.java @@ -132,7 +132,7 @@ public ReplyInboxThreadEmailResponseSuccess reply(String inboxId, String threadI * Forwards a message from the inbox address right away, with no stored draft. The original message is quoted * under a forwarded-message banner, below the optional {@code html} or {@code text} note. The attachments of * the original message are sent with the forward, together with any attachments you add. The request fails - * when they exceed 10 files or 25 MB of {@code content} in total. An original attachment that was sent from + * when they exceed 25 MB of {@code content} in total. An original attachment that was sent from * a URL ({@code path}) cannot be forwarded, and the request returns a 422. * * @param inboxId The inbox ID or the inbox email address. @@ -152,7 +152,7 @@ public ForwardInboxThreadEmailResponseSuccess forward(String inboxId, String thr * Forwards a message from the inbox address right away, with no stored draft. The original message is quoted * under a forwarded-message banner, below the optional {@code html} or {@code text} note. The attachments of * the original message are sent with the forward, together with any attachments you add. The request fails - * when they exceed 10 files or 25 MB of {@code content} in total. An original attachment that was sent from + * when they exceed 25 MB of {@code content} in total. An original attachment that was sent from * a URL ({@code path}) cannot be forwarded, and the request returns a 422. * *

Set an idempotency key in {@code requestOptions} so the forward is sent only once. A retry with the same key diff --git a/src/main/java/com/resend/services/inboxes/model/ForwardInboxThreadEmailOptions.java b/src/main/java/com/resend/services/inboxes/model/ForwardInboxThreadEmailOptions.java index 21b4a5b..90148db 100644 --- a/src/main/java/com/resend/services/inboxes/model/ForwardInboxThreadEmailOptions.java +++ b/src/main/java/com/resend/services/inboxes/model/ForwardInboxThreadEmailOptions.java @@ -291,7 +291,7 @@ public Builder subject(String subject) { } /** - * Set the files to attach, as on {@code POST /emails}. Each one needs {@code content} or {@code path}. They share one limit with the attachments of the original message, which the forward also sends: up to 10 files and at most 25 MB of {@code content}. An empty file, a blocked file type, or a {@code path} that is not an {@code http} or {@code https} URL or points to {@code localhost} returns a 422. + * Set the files to attach, as on {@code POST /emails}. Each one needs {@code content} or {@code path}. They share one limit with the attachments of the original message, which the forward also sends: at most 25 MB of {@code content}. An empty file, a blocked file type, or a {@code path} that is not an {@code http} or {@code https} URL or points to {@code localhost} returns a 422. * * @param attachments The files to attach, as on {@code POST /emails}. * @return The builder instance. @@ -302,7 +302,7 @@ public Builder attachments(Attachment... attachments) { } /** - * Set the files to attach, as on {@code POST /emails}. Each one needs {@code content} or {@code path}. They share one limit with the attachments of the original message, which the forward also sends: up to 10 files and at most 25 MB of {@code content}. An empty file, a blocked file type, or a {@code path} that is not an {@code http} or {@code https} URL or points to {@code localhost} returns a 422. + * Set the files to attach, as on {@code POST /emails}. Each one needs {@code content} or {@code path}. They share one limit with the attachments of the original message, which the forward also sends: at most 25 MB of {@code content}. An empty file, a blocked file type, or a {@code path} that is not an {@code http} or {@code https} URL or points to {@code localhost} returns a 422. * * @param attachments The files to attach, as on {@code POST /emails}. * @return The builder instance. diff --git a/src/main/java/com/resend/services/inboxes/model/GetInboxAgentResponseSuccess.java b/src/main/java/com/resend/services/inboxes/model/GetInboxAgentResponseSuccess.java index 21bec2a..dffd57e 100644 --- a/src/main/java/com/resend/services/inboxes/model/GetInboxAgentResponseSuccess.java +++ b/src/main/java/com/resend/services/inboxes/model/GetInboxAgentResponseSuccess.java @@ -20,7 +20,7 @@ public GetInboxAgentResponseSuccess() { * * @param object The object type. Always {@code inbox_agent}. * @param instructions The instructions the agent follows when it handles the threads of the inbox. Null when no instructions are set. - * @param tone The tone the agent writes in. One of the presets {@code concise_professional}, {@code concise_casual}, {@code expanded_professional}, or {@code expanded_casual}, or custom text in the owner's own words, such as {@code friendly and concise}. Older dashboard values such as {@code Concise · Casual} are custom text. Null when no tone is set. + * @param tone The tone the agent writes in. One of {@code concise_professional}, {@code concise_casual}, {@code expanded_professional}, or {@code expanded_casual}. Null when no tone is set. * @param actions The actions the agent can take. Empty when no actions are enabled. */ public GetInboxAgentResponseSuccess(String object, String instructions, String tone, List actions) { diff --git a/src/main/java/com/resend/services/inboxes/model/InboxAgent.java b/src/main/java/com/resend/services/inboxes/model/InboxAgent.java index f72f65a..0b2eb04 100644 --- a/src/main/java/com/resend/services/inboxes/model/InboxAgent.java +++ b/src/main/java/com/resend/services/inboxes/model/InboxAgent.java @@ -33,7 +33,7 @@ public InboxAgent() { * * @param object The object type. Always {@code inbox_agent}. * @param instructions The instructions the agent follows when it handles the threads of the inbox. Null when no instructions are set. - * @param tone The tone the agent writes in. One of the presets {@code concise_professional}, {@code concise_casual}, {@code expanded_professional}, or {@code expanded_casual}, or custom text in the owner's own words, such as {@code friendly and concise}. Older dashboard values such as {@code Concise · Casual} are custom text. Null when no tone is set. + * @param tone The tone the agent writes in. One of {@code concise_professional}, {@code concise_casual}, {@code expanded_professional}, or {@code expanded_casual}. Null when no tone is set. * @param actions The actions the agent can take. Empty when no actions are enabled. */ public InboxAgent(String object, String instructions, String tone, List actions) { @@ -62,9 +62,9 @@ public String getInstructions() { } /** - * Get the tone the agent writes in. One of the presets {@code concise_professional}, {@code concise_casual}, {@code expanded_professional}, or {@code expanded_casual}, or custom text in the owner's own words, such as {@code friendly and concise}. Older dashboard values such as {@code Concise · Casual} are custom text. Null when no tone is set. + * Get the tone the agent writes in. One of {@code concise_professional}, {@code concise_casual}, {@code expanded_professional}, or {@code expanded_casual}. Null when no tone is set. * - * @return The tone the agent writes in. One of the presets {@code concise_professional}, {@code concise_casual}, {@code expanded_professional}, or {@code expanded_casual}, or custom text in the owner's own words, such as {@code friendly and concise}. Older dashboard values such as {@code Concise · Casual} are custom text. Null when no tone is set. + * @return The tone the agent writes in. One of {@code concise_professional}, {@code concise_casual}, {@code expanded_professional}, or {@code expanded_casual}. Null when no tone is set. */ public String getTone() { return tone; diff --git a/src/main/java/com/resend/services/inboxes/model/InboxThreadSearchResult.java b/src/main/java/com/resend/services/inboxes/model/InboxThreadSearchResult.java index 987a5c8..33e64b5 100644 --- a/src/main/java/com/resend/services/inboxes/model/InboxThreadSearchResult.java +++ b/src/main/java/com/resend/services/inboxes/model/InboxThreadSearchResult.java @@ -3,7 +3,6 @@ import com.fasterxml.jackson.annotation.JsonProperty; import java.util.List; -import java.util.Map; /** * Represents a thread that matched a search. @@ -14,9 +13,6 @@ public class InboxThreadSearchResult extends InboxThread { @JsonProperty("matched_email_id") private String matchedEmailId; - @JsonProperty("highlights") - private Map> highlights; - /** * Default constructor */ @@ -40,30 +36,19 @@ public InboxThreadSearchResult() { * @param read True only when every email in the thread is read. * @param receivedAt The timestamp when the thread was last active. * @param folder The folder the thread is in. Every thread is in exactly one folder. - * @param matchedEmailId The email in the thread that matched the search. The get, reply and forward thread email endpoints accept it as {@code email_id}, but it is not equal to the {@code id} those endpoints return. Null when the search index returns no matched email for the thread. - * @param highlights Where {@code query} matched the email in {@code matched_email_id}, as text fragments with each match wrapped in {@code **}, keyed by {@code subject}, {@code body}, {@code from}, {@code to}, {@code cc}, {@code bcc} or {@code attachments}. Only keys with a match are present. Empty when there is no {@code query}. + * @param matchedEmailId The ID of the email in the thread that matched the search. When {@code query} has words or phrases, it is the most relevant match. Otherwise, it is the newest. It is the same {@code id} that the list thread emails endpoint returns, so use it as {@code email_id} with the get, reply and forward endpoints. Null when the search index returns no matched email for the thread. */ - public InboxThreadSearchResult(String id, String subject, String from, List to, List cc, List bcc, List labels, Integer emailCount, Boolean hasAttachment, Boolean hasDraft, Boolean read, String receivedAt, String folder, String matchedEmailId, Map> highlights) { + public InboxThreadSearchResult(String id, String subject, String from, List to, List cc, List bcc, List labels, Integer emailCount, Boolean hasAttachment, Boolean hasDraft, Boolean read, String receivedAt, String folder, String matchedEmailId) { super(id, subject, from, to, cc, bcc, labels, emailCount, hasAttachment, hasDraft, read, receivedAt, folder); this.matchedEmailId = matchedEmailId; - this.highlights = highlights; } /** - * Get the email in the thread that matched the search. The get, reply and forward thread email endpoints accept it as {@code email_id}, but it is not equal to the {@code id} those endpoints return. Null when the search index returns no matched email for the thread. + * Get the ID of the email in the thread that matched the search. When {@code query} has words or phrases, it is the most relevant match. Otherwise, it is the newest. It is the same {@code id} that the list thread emails endpoint returns, so use it as {@code email_id} with the get, reply and forward endpoints. Null when the search index returns no matched email for the thread. * - * @return The email in the thread that matched the search. The get, reply and forward thread email endpoints accept it as {@code email_id}, but it is not equal to the {@code id} those endpoints return. Null when the search index returns no matched email for the thread. + * @return The ID of the email in the thread that matched the search. When {@code query} has words or phrases, it is the most relevant match. Otherwise, it is the newest. It is the same {@code id} that the list thread emails endpoint returns, so use it as {@code email_id} with the get, reply and forward endpoints. Null when the search index returns no matched email for the thread. */ public String getMatchedEmailId() { return matchedEmailId; } - - /** - * Get the places where {@code query} matched the email in {@code matched_email_id}, as text fragments with each match wrapped in {@code **}, keyed by {@code subject}, {@code body}, {@code from}, {@code to}, {@code cc}, {@code bcc} or {@code attachments}. Only keys with a match are present. Empty when there is no {@code query}. - * - * @return Where {@code query} matched the email in {@code matched_email_id}, as text fragments with each match wrapped in {@code **}, keyed by {@code subject}, {@code body}, {@code from}, {@code to}, {@code cc}, {@code bcc} or {@code attachments}. Only keys with a match are present. Empty when there is no {@code query}. - */ - public Map> getHighlights() { - return highlights; - } } diff --git a/src/main/java/com/resend/services/inboxes/model/ReplyInboxThreadEmailOptions.java b/src/main/java/com/resend/services/inboxes/model/ReplyInboxThreadEmailOptions.java index 1b6a28a..cf521cd 100644 --- a/src/main/java/com/resend/services/inboxes/model/ReplyInboxThreadEmailOptions.java +++ b/src/main/java/com/resend/services/inboxes/model/ReplyInboxThreadEmailOptions.java @@ -266,7 +266,7 @@ public Builder replyAll(Boolean replyAll) { } /** - * Set the files to attach, as on {@code POST /emails}. Each one needs {@code content} or {@code path}. Up to 10 files. The {@code content} of all files can total at most 25 MB. An empty file, a blocked file type, or a {@code path} that is not an {@code http} or {@code https} URL or points to {@code localhost} returns a 422. + * Set the files to attach, as on {@code POST /emails}. Each one needs {@code content} or {@code path}. The {@code content} of all files can total at most 25 MB. An empty file, a blocked file type, or a {@code path} that is not an {@code http} or {@code https} URL or points to {@code localhost} returns a 422. * * @param attachments The files to attach, as on {@code POST /emails}. * @return The builder instance. @@ -277,7 +277,7 @@ public Builder attachments(Attachment... attachments) { } /** - * Set the files to attach, as on {@code POST /emails}. Each one needs {@code content} or {@code path}. Up to 10 files. The {@code content} of all files can total at most 25 MB. An empty file, a blocked file type, or a {@code path} that is not an {@code http} or {@code https} URL or points to {@code localhost} returns a 422. + * Set the files to attach, as on {@code POST /emails}. Each one needs {@code content} or {@code path}. The {@code content} of all files can total at most 25 MB. An empty file, a blocked file type, or a {@code path} that is not an {@code http} or {@code https} URL or points to {@code localhost} returns a 422. * * @param attachments The files to attach, as on {@code POST /emails}. * @return The builder instance. diff --git a/src/main/java/com/resend/services/inboxes/model/UpdateInboxAgentOptions.java b/src/main/java/com/resend/services/inboxes/model/UpdateInboxAgentOptions.java index efa38f1..fff6b96 100644 --- a/src/main/java/com/resend/services/inboxes/model/UpdateInboxAgentOptions.java +++ b/src/main/java/com/resend/services/inboxes/model/UpdateInboxAgentOptions.java @@ -94,10 +94,8 @@ public Builder instructions(String instructions) { } /** - * Set the tone the agent writes in. Use one of the presets {@code concise_professional}, {@code concise_casual}, - * {@code expanded_professional}, or {@code expanded_casual}, or describe the tone in your own words, such as - * {@code friendly and concise}. Custom text is max 64 characters. Leading and trailing whitespace is trimmed, - * and the trimmed value must not be empty. + * Set the tone the agent writes in. One of {@code concise_professional}, {@code concise_casual}, + * {@code expanded_professional}, or {@code expanded_casual}. * Pass null to clear the tone. * * @param tone The tone, or null to clear it. diff --git a/src/test/java/com/resend/services/inboxes/InboxAgentTest.java b/src/test/java/com/resend/services/inboxes/InboxAgentTest.java index 225e194..f129ca1 100644 --- a/src/test/java/com/resend/services/inboxes/InboxAgentTest.java +++ b/src/test/java/com/resend/services/inboxes/InboxAgentTest.java @@ -39,7 +39,7 @@ public void setUp() { @Test public void testGetAgent_Success() throws ResendException { - String json = "{\"object\":\"inbox_agent\",\"instructions\":null,\"tone\":\"friendly and concise\"," + + String json = "{\"object\":\"inbox_agent\",\"instructions\":null,\"tone\":\"concise_casual\"," + "\"actions\":[\"draft_reply\",\"add_labels\"]}"; when(httpClient.perform(eq(AGENT_PATH), anyString(), eq(HttpMethod.GET), isNull(), any(MediaType.class))) .thenReturn(new AbstractHttpResponse<>(200, json, true)); @@ -48,18 +48,18 @@ public void testGetAgent_Success() throws ResendException { assertEquals("inbox_agent", response.getObject()); assertNull(response.getInstructions()); - assertEquals("friendly and concise", response.getTone()); + assertEquals("concise_casual", response.getTone()); assertEquals(Arrays.asList("draft_reply", "add_labels"), response.getActions()); } @Test public void testUpdateAgent_SendsOnlySetFields() throws ResendException { UpdateInboxAgentOptions options = UpdateInboxAgentOptions.builder() - .tone("friendly and concise") + .tone("concise_casual") .actions(InboxAgentAction.DRAFT_REPLY, InboxAgentAction.ADD_LABELS) .build(); when(httpClient.perform(eq(AGENT_PATH), anyString(), eq(HttpMethod.PATCH), - eq("{\"tone\":\"friendly and concise\",\"actions\":[\"draft_reply\",\"add_labels\"]}"), any(MediaType.class))) + eq("{\"tone\":\"concise_casual\",\"actions\":[\"draft_reply\",\"add_labels\"]}"), any(MediaType.class))) .thenReturn(new AbstractHttpResponse<>(200, UPDATE_RESPONSE_JSON, true)); UpdateInboxAgentResponseSuccess response = agent.update(INBOX_ID, options); diff --git a/src/test/java/com/resend/services/inboxes/InboxThreadsTest.java b/src/test/java/com/resend/services/inboxes/InboxThreadsTest.java index f5d2f7f..0bf708b 100644 --- a/src/test/java/com/resend/services/inboxes/InboxThreadsTest.java +++ b/src/test/java/com/resend/services/inboxes/InboxThreadsTest.java @@ -93,8 +93,7 @@ public void testSearchThreads_Success() throws ResendException { String path = THREADS_PATH + "/search?limit=10&query=invoice+-draft&from=isabella%40example.com%2Ccarolina%40example.com" + "&has_attachment=true&start_date=2026-09-01&end_date=2026-09-30&folders=inbox"; String json = "{\"object\":\"list\",\"has_more\":false,\"data\":[" + THREAD_JSON + "," + - "\"matched_email_id\":\"e8a3f1c6-2b9d-4e5a-a7c4-9d1b6f3e2a80\"," + - "\"highlights\":{\"subject\":[\"September **invoice**\"],\"attachments\":[\"**invoice**-2026-09.pdf\"]}}]}"; + "\"matched_email_id\":\"e8a3f1c6-2b9d-4e5a-a7c4-9d1b6f3e2a80\"}]}"; when(httpClient.perform(eq(path), anyString(), eq(HttpMethod.GET), isNull(), any(MediaType.class))) .thenReturn(new AbstractHttpResponse<>(200, json, true)); @@ -104,8 +103,6 @@ public void testSearchThreads_Success() throws ResendException { assertEquals(THREAD_ID, result.getId()); assertEquals(2, (int) result.getEmailCount()); assertEquals("e8a3f1c6-2b9d-4e5a-a7c4-9d1b6f3e2a80", result.getMatchedEmailId()); - assertEquals(Collections.singletonList("September **invoice**"), result.getHighlights().get("subject")); - assertFalse(result.getHighlights().containsKey("body")); } @Test From 13c0e3c51d15fd72bce6d165f69f88a801f68257 Mon Sep 17 00:00:00 2001 From: dielduarte Date: Mon, 19 Oct 2026 19:17:15 -0700 Subject: [PATCH 29/30] fix(inboxes): read the agent actions as InboxAgentAction InboxAgent.getActions() returned List while the update builder takes InboxAgentAction, so the settings read from get() could not be passed back to update() without converting them by hand. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Woj8zJMR1hEA3GFfpo2VPr --- .../model/GetInboxAgentResponseSuccess.java | 2 +- .../services/inboxes/model/InboxAgent.java | 6 +++--- .../services/inboxes/InboxAgentTest.java | 20 ++++++++++++++++++- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/resend/services/inboxes/model/GetInboxAgentResponseSuccess.java b/src/main/java/com/resend/services/inboxes/model/GetInboxAgentResponseSuccess.java index dffd57e..8c9a8b9 100644 --- a/src/main/java/com/resend/services/inboxes/model/GetInboxAgentResponseSuccess.java +++ b/src/main/java/com/resend/services/inboxes/model/GetInboxAgentResponseSuccess.java @@ -23,7 +23,7 @@ public GetInboxAgentResponseSuccess() { * @param tone The tone the agent writes in. One of {@code concise_professional}, {@code concise_casual}, {@code expanded_professional}, or {@code expanded_casual}. Null when no tone is set. * @param actions The actions the agent can take. Empty when no actions are enabled. */ - public GetInboxAgentResponseSuccess(String object, String instructions, String tone, List actions) { + public GetInboxAgentResponseSuccess(String object, String instructions, String tone, List actions) { super(object, instructions, tone, actions); } } diff --git a/src/main/java/com/resend/services/inboxes/model/InboxAgent.java b/src/main/java/com/resend/services/inboxes/model/InboxAgent.java index 0b2eb04..1785105 100644 --- a/src/main/java/com/resend/services/inboxes/model/InboxAgent.java +++ b/src/main/java/com/resend/services/inboxes/model/InboxAgent.java @@ -19,7 +19,7 @@ public class InboxAgent { private String tone; @JsonProperty("actions") - private List actions; + private List actions; /** * Default constructor @@ -36,7 +36,7 @@ public InboxAgent() { * @param tone The tone the agent writes in. One of {@code concise_professional}, {@code concise_casual}, {@code expanded_professional}, or {@code expanded_casual}. Null when no tone is set. * @param actions The actions the agent can take. Empty when no actions are enabled. */ - public InboxAgent(String object, String instructions, String tone, List actions) { + public InboxAgent(String object, String instructions, String tone, List actions) { this.object = object; this.instructions = instructions; this.tone = tone; @@ -75,7 +75,7 @@ public String getTone() { * * @return The actions the agent can take. Empty when no actions are enabled. */ - public List getActions() { + public List getActions() { return actions; } } diff --git a/src/test/java/com/resend/services/inboxes/InboxAgentTest.java b/src/test/java/com/resend/services/inboxes/InboxAgentTest.java index f129ca1..3bbe23e 100644 --- a/src/test/java/com/resend/services/inboxes/InboxAgentTest.java +++ b/src/test/java/com/resend/services/inboxes/InboxAgentTest.java @@ -49,7 +49,25 @@ public void testGetAgent_Success() throws ResendException { assertEquals("inbox_agent", response.getObject()); assertNull(response.getInstructions()); assertEquals("concise_casual", response.getTone()); - assertEquals(Arrays.asList("draft_reply", "add_labels"), response.getActions()); + assertEquals(Arrays.asList(InboxAgentAction.DRAFT_REPLY, InboxAgentAction.ADD_LABELS), response.getActions()); + } + + @Test + public void testUpdateAgent_AcceptsTheActionsReadFromGet() throws ResendException { + String json = "{\"object\":\"inbox_agent\",\"instructions\":null,\"tone\":null," + + "\"actions\":[\"archive\",\"forward_thread\"]}"; + when(httpClient.perform(eq(AGENT_PATH), anyString(), eq(HttpMethod.GET), isNull(), any(MediaType.class))) + .thenReturn(new AbstractHttpResponse<>(200, json, true)); + when(httpClient.perform(eq(AGENT_PATH), anyString(), eq(HttpMethod.PATCH), + eq("{\"actions\":[\"archive\",\"forward_thread\"]}"), any(MediaType.class))) + .thenReturn(new AbstractHttpResponse<>(200, UPDATE_RESPONSE_JSON, true)); + + GetInboxAgentResponseSuccess current = agent.get(INBOX_ID); + UpdateInboxAgentOptions options = UpdateInboxAgentOptions.builder() + .actions(current.getActions()) + .build(); + + assertNotNull(agent.update(INBOX_ID, options)); } @Test From 4f6c35af323ebacf07e3a81d852326d406c96ac0 Mon Sep 17 00:00:00 2001 From: Kewyn Akshlley Date: Tue, 20 Oct 2026 20:38:24 -0300 Subject: [PATCH 30/30] fix(inboxes): enforce label ID limits, dedupe agent actions and type agent tone - Reject more than 10 label IDs in AddInboxThreadLabelsOptions and more than 100 in SetInboxThreadLabelsOptions - Remove duplicate actions in UpdateInboxAgentOptions, as its Javadoc states - Add InboxAgentTone enum and use it for the agent tone, matching resend-node --- .../model/AddInboxThreadLabelsOptions.java | 6 +- .../model/GetInboxAgentResponseSuccess.java | 2 +- .../services/inboxes/model/InboxAgent.java | 6 +- .../inboxes/model/InboxAgentTone.java | 56 +++++++++++++++++++ .../model/SetInboxThreadLabelsOptions.java | 6 +- .../model/UpdateInboxAgentOptions.java | 11 ++-- .../services/inboxes/InboxAgentTest.java | 23 +++++++- .../services/inboxes/InboxThreadsTest.java | 8 +++ 8 files changed, 105 insertions(+), 13 deletions(-) create mode 100644 src/main/java/com/resend/services/inboxes/model/InboxAgentTone.java diff --git a/src/main/java/com/resend/services/inboxes/model/AddInboxThreadLabelsOptions.java b/src/main/java/com/resend/services/inboxes/model/AddInboxThreadLabelsOptions.java index 018b334..c671c00 100644 --- a/src/main/java/com/resend/services/inboxes/model/AddInboxThreadLabelsOptions.java +++ b/src/main/java/com/resend/services/inboxes/model/AddInboxThreadLabelsOptions.java @@ -40,6 +40,7 @@ public static Builder builder() { * Builder class for constructing AddInboxThreadLabelsOptions objects. */ public static class Builder { + private static final int MAX_LABELS = 10; private List labels; /** @@ -74,12 +75,15 @@ public Builder labels(List labels) { * Build a new AddInboxThreadLabelsOptions object. * * @return A new AddInboxThreadLabelsOptions object. - * @throws IllegalArgumentException if {@code labels} is null or empty, or contains a null or blank ID. + * @throws IllegalArgumentException if {@code labels} is null or empty, has more than 10 IDs, or contains a null or blank ID. */ public AddInboxThreadLabelsOptions build() { if (labels == null || labels.isEmpty()) { throw new IllegalArgumentException("labels must contain at least one label ID"); } + if (labels.size() > MAX_LABELS) { + throw new IllegalArgumentException("labels must not contain more than " + MAX_LABELS + " label IDs"); + } if (hasBlankId(labels)) { throw new IllegalArgumentException("labels must not contain a null or blank label ID"); } diff --git a/src/main/java/com/resend/services/inboxes/model/GetInboxAgentResponseSuccess.java b/src/main/java/com/resend/services/inboxes/model/GetInboxAgentResponseSuccess.java index 8c9a8b9..145d0a3 100644 --- a/src/main/java/com/resend/services/inboxes/model/GetInboxAgentResponseSuccess.java +++ b/src/main/java/com/resend/services/inboxes/model/GetInboxAgentResponseSuccess.java @@ -23,7 +23,7 @@ public GetInboxAgentResponseSuccess() { * @param tone The tone the agent writes in. One of {@code concise_professional}, {@code concise_casual}, {@code expanded_professional}, or {@code expanded_casual}. Null when no tone is set. * @param actions The actions the agent can take. Empty when no actions are enabled. */ - public GetInboxAgentResponseSuccess(String object, String instructions, String tone, List actions) { + public GetInboxAgentResponseSuccess(String object, String instructions, InboxAgentTone tone, List actions) { super(object, instructions, tone, actions); } } diff --git a/src/main/java/com/resend/services/inboxes/model/InboxAgent.java b/src/main/java/com/resend/services/inboxes/model/InboxAgent.java index 1785105..094e35e 100644 --- a/src/main/java/com/resend/services/inboxes/model/InboxAgent.java +++ b/src/main/java/com/resend/services/inboxes/model/InboxAgent.java @@ -16,7 +16,7 @@ public class InboxAgent { private String instructions; @JsonProperty("tone") - private String tone; + private InboxAgentTone tone; @JsonProperty("actions") private List actions; @@ -36,7 +36,7 @@ public InboxAgent() { * @param tone The tone the agent writes in. One of {@code concise_professional}, {@code concise_casual}, {@code expanded_professional}, or {@code expanded_casual}. Null when no tone is set. * @param actions The actions the agent can take. Empty when no actions are enabled. */ - public InboxAgent(String object, String instructions, String tone, List actions) { + public InboxAgent(String object, String instructions, InboxAgentTone tone, List actions) { this.object = object; this.instructions = instructions; this.tone = tone; @@ -66,7 +66,7 @@ public String getInstructions() { * * @return The tone the agent writes in. One of {@code concise_professional}, {@code concise_casual}, {@code expanded_professional}, or {@code expanded_casual}. Null when no tone is set. */ - public String getTone() { + public InboxAgentTone getTone() { return tone; } diff --git a/src/main/java/com/resend/services/inboxes/model/InboxAgentTone.java b/src/main/java/com/resend/services/inboxes/model/InboxAgentTone.java new file mode 100644 index 0000000..f2e22ec --- /dev/null +++ b/src/main/java/com/resend/services/inboxes/model/InboxAgentTone.java @@ -0,0 +1,56 @@ +package com.resend.services.inboxes.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * Represents the tone the inbox agent writes in. + */ +public enum InboxAgentTone { + /** Short replies in a professional tone. */ + CONCISE_PROFESSIONAL("concise_professional"), + /** Short replies in a casual tone. */ + CONCISE_CASUAL("concise_casual"), + /** Longer replies in a professional tone. */ + EXPANDED_PROFESSIONAL("expanded_professional"), + /** Longer replies in a casual tone. */ + EXPANDED_CASUAL("expanded_casual"); + + private final String value; + + InboxAgentTone(String value) { + this.value = value; + } + + /** + * Returns the string value of the tone. + * + * @return The tone value. + */ + @JsonValue + public String getValue() { + return value; + } + + /** + * Creates an InboxAgentTone from a string value. + * + * @param value The string value. + * @return The corresponding InboxAgentTone. + * @throws IllegalArgumentException If the value is unknown. + */ + @JsonCreator + public static InboxAgentTone fromValue(String value) { + for (InboxAgentTone item : InboxAgentTone.values()) { + if (item.value.equals(value)) { + return item; + } + } + throw new IllegalArgumentException("Unknown tone: " + value); + } + + @Override + public String toString() { + return value; + } +} diff --git a/src/main/java/com/resend/services/inboxes/model/SetInboxThreadLabelsOptions.java b/src/main/java/com/resend/services/inboxes/model/SetInboxThreadLabelsOptions.java index 0d05612..945391e 100644 --- a/src/main/java/com/resend/services/inboxes/model/SetInboxThreadLabelsOptions.java +++ b/src/main/java/com/resend/services/inboxes/model/SetInboxThreadLabelsOptions.java @@ -40,6 +40,7 @@ public static Builder builder() { * Builder class for constructing SetInboxThreadLabelsOptions objects. */ public static class Builder { + private static final int MAX_LABELS = 100; private List labels; /** @@ -74,12 +75,15 @@ public Builder labels(List labels) { * Build a new SetInboxThreadLabelsOptions object. * * @return A new SetInboxThreadLabelsOptions object. - * @throws IllegalArgumentException if {@code labels} is null or contains a null or blank ID. + * @throws IllegalArgumentException if {@code labels} is null, has more than 100 IDs, or contains a null or blank ID. */ public SetInboxThreadLabelsOptions build() { if (labels == null) { throw new IllegalArgumentException("labels must not be null"); } + if (labels.size() > MAX_LABELS) { + throw new IllegalArgumentException("labels must not contain more than " + MAX_LABELS + " label IDs"); + } if (hasBlankId(labels)) { throw new IllegalArgumentException("labels must not contain a null or blank label ID"); } diff --git a/src/main/java/com/resend/services/inboxes/model/UpdateInboxAgentOptions.java b/src/main/java/com/resend/services/inboxes/model/UpdateInboxAgentOptions.java index fff6b96..85157b6 100644 --- a/src/main/java/com/resend/services/inboxes/model/UpdateInboxAgentOptions.java +++ b/src/main/java/com/resend/services/inboxes/model/UpdateInboxAgentOptions.java @@ -8,6 +8,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -45,8 +46,8 @@ public String getInstructions() { * * @return The tone, or null when it is not set or is cleared. */ - public String getTone() { - return (String) fields.get("tone"); + public InboxAgentTone getTone() { + return (InboxAgentTone) fields.get("tone"); } /** @@ -101,7 +102,7 @@ public Builder instructions(String instructions) { * @param tone The tone, or null to clear it. * @return The builder instance. */ - public Builder tone(String tone) { + public Builder tone(InboxAgentTone tone) { fields.put("tone", tone); return this; } @@ -113,7 +114,7 @@ public Builder tone(String tone) { * @return The builder instance. */ public Builder actions(InboxAgentAction... actions) { - fields.put("actions", new ArrayList<>(Arrays.asList(actions))); + fields.put("actions", new ArrayList<>(new LinkedHashSet<>(Arrays.asList(actions)))); return this; } @@ -128,7 +129,7 @@ public Builder actions(List actions) { if (actions == null) { fields.remove("actions"); } else { - fields.put("actions", new ArrayList<>(actions)); + fields.put("actions", new ArrayList<>(new LinkedHashSet<>(actions))); } return this; } diff --git a/src/test/java/com/resend/services/inboxes/InboxAgentTest.java b/src/test/java/com/resend/services/inboxes/InboxAgentTest.java index 3bbe23e..0bc90c7 100644 --- a/src/test/java/com/resend/services/inboxes/InboxAgentTest.java +++ b/src/test/java/com/resend/services/inboxes/InboxAgentTest.java @@ -48,7 +48,7 @@ public void testGetAgent_Success() throws ResendException { assertEquals("inbox_agent", response.getObject()); assertNull(response.getInstructions()); - assertEquals("concise_casual", response.getTone()); + assertEquals(InboxAgentTone.CONCISE_CASUAL, response.getTone()); assertEquals(Arrays.asList(InboxAgentAction.DRAFT_REPLY, InboxAgentAction.ADD_LABELS), response.getActions()); } @@ -73,7 +73,7 @@ public void testUpdateAgent_AcceptsTheActionsReadFromGet() throws ResendExceptio @Test public void testUpdateAgent_SendsOnlySetFields() throws ResendException { UpdateInboxAgentOptions options = UpdateInboxAgentOptions.builder() - .tone("concise_casual") + .tone(InboxAgentTone.CONCISE_CASUAL) .actions(InboxAgentAction.DRAFT_REPLY, InboxAgentAction.ADD_LABELS) .build(); when(httpClient.perform(eq(AGENT_PATH), anyString(), eq(HttpMethod.PATCH), @@ -118,9 +118,28 @@ public void testUpdateAgent_SendsArchiveAndMoveToTrash() throws ResendException assertNotNull(agent.update(INBOX_ID, options)); } + @Test + public void testUpdateAgentBuilder_RemovesDuplicateActions() { + UpdateInboxAgentOptions fromVarargs = UpdateInboxAgentOptions.builder() + .actions(InboxAgentAction.ARCHIVE, InboxAgentAction.DRAFT_REPLY, InboxAgentAction.ARCHIVE) + .build(); + UpdateInboxAgentOptions fromList = UpdateInboxAgentOptions.builder() + .actions(Arrays.asList(InboxAgentAction.DRAFT_REPLY, InboxAgentAction.DRAFT_REPLY, InboxAgentAction.ARCHIVE)) + .build(); + + assertEquals(Arrays.asList(InboxAgentAction.ARCHIVE, InboxAgentAction.DRAFT_REPLY), fromVarargs.getActions()); + assertEquals(Arrays.asList(InboxAgentAction.DRAFT_REPLY, InboxAgentAction.ARCHIVE), fromList.getActions()); + } + @Test public void testAgentAction_UnknownValue_Throws() { assertEquals(InboxAgentAction.MARK_AS_SPAM, InboxAgentAction.fromValue("mark_as_spam")); assertThrows(IllegalArgumentException.class, () -> InboxAgentAction.fromValue("send_email")); } + + @Test + public void testAgentTone_UnknownValue_Throws() { + assertEquals(InboxAgentTone.EXPANDED_PROFESSIONAL, InboxAgentTone.fromValue("expanded_professional")); + assertThrows(IllegalArgumentException.class, () -> InboxAgentTone.fromValue("friendly")); + } } diff --git a/src/test/java/com/resend/services/inboxes/InboxThreadsTest.java b/src/test/java/com/resend/services/inboxes/InboxThreadsTest.java index 0bf708b..7bc0f62 100644 --- a/src/test/java/com/resend/services/inboxes/InboxThreadsTest.java +++ b/src/test/java/com/resend/services/inboxes/InboxThreadsTest.java @@ -214,4 +214,12 @@ public void testThreadLabelsBuilders_Validate() { assertThrows(IllegalArgumentException.class, () -> SetInboxThreadLabelsOptions.builder().labels(Arrays.asList((String) null)).build()); assertNotNull(SetInboxThreadLabelsOptions.builder().labels(Collections.emptyList()).build()); } + + @Test + public void testThreadLabelsBuilders_EnforceMaxCount() { + assertNotNull(AddInboxThreadLabelsOptions.builder().labels(Collections.nCopies(10, LABEL_ID)).build()); + assertThrows(IllegalArgumentException.class, () -> AddInboxThreadLabelsOptions.builder().labels(Collections.nCopies(11, LABEL_ID)).build()); + assertNotNull(SetInboxThreadLabelsOptions.builder().labels(Collections.nCopies(100, LABEL_ID)).build()); + assertThrows(IllegalArgumentException.class, () -> SetInboxThreadLabelsOptions.builder().labels(Collections.nCopies(101, LABEL_ID)).build()); + } }