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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ public class CreateEmailOptions {
@JsonProperty("scheduled_at")
private final String scheduledAt;

@JsonProperty("template")
private final Template template;

private CreateEmailOptions(Builder builder) {
this.from = builder.from;
this.to = builder.to;
Expand All @@ -57,6 +60,7 @@ private CreateEmailOptions(Builder builder) {
this.html = builder.html;
this.headers = builder.headers;
this.scheduledAt = builder.scheduledAt;
this.template = builder.template;
}

/**
Expand Down Expand Up @@ -167,6 +171,15 @@ public String getScheduledAt() {
return scheduledAt;
}

/**
* Retrieves the template configuration of the email.
*
* @return The template configuration of the email.
*/
public Template getTemplate() {
return template;
}

/**
* Creates a new builder instance to construct CreateEmailOptions.
*
Expand All @@ -192,6 +205,7 @@ public static class Builder {
private List<Tag> tags;
private Map<String, String> headers;
private String scheduledAt;
private Template template;

/**
* Set the 'from' email address.
Expand Down Expand Up @@ -528,6 +542,21 @@ public Builder scheduledAt(String scheduledAt) {
return this;
}

/**
* Set the template configuration for the email.
* <p>
* Note: If a template is provided, you cannot send html, text, or react in the payload.
* When sending a template, the payload for from, subject, and reply_to take precedence
* over the template's defaults for these fields.
*
* @param template The template configuration.
* @return This builder instance for method chaining.
*/
public Builder template(Template template) {
this.template = template;
return this;
}

/**
* Builds and returns a {@code CreateEmailOptions} based on the configured properties.
*
Expand Down
172 changes: 172 additions & 0 deletions src/main/java/com/resend/services/emails/model/Template.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
package com.resend.services.emails.model;

import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.HashMap;
import java.util.Map;

/**
* Represents a template configuration for sending emails.
*/
public class Template {

@JsonProperty("id")
private final String id;

@JsonProperty("variables")
private final Map<String, Object> variables;

/**
* Constructs a Template using the provided builder.
*
* @param builder The builder to construct the Template.
*/
private Template(Builder builder) {
this.id = builder.id;
this.variables = builder.variables;
Copy link
Contributor

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

Choose a reason for hiding this comment

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

The Template constructor stores the builder's mutable variables map by reference, so reusing the builder mutates previously built Template instances. Make a defensive copy when assigning.

Prompt for AI agents
Address the following comment on src/main/java/com/resend/services/emails/model/Template.java at line 25:

<comment>The Template constructor stores the builder&#39;s mutable variables map by reference, so reusing the builder mutates previously built Template instances. Make a defensive copy when assigning.</comment>

<file context>
@@ -0,0 +1,172 @@
+     */
+    private Template(Builder builder) {
+        this.id = builder.id;
+        this.variables = builder.variables;
+    }
+
</file context>
Fix with Cubic

}

/**
* Gets the template ID.
*
* @return The template ID.
*/
public String getId() {
return id;
}

/**
* Gets the template variables.
*
* @return The template variables.
*/
public Map<String, Object> getVariables() {
return variables;
}

/**
* Creates a new builder instance for constructing Template objects.
*
* @return A new builder instance.
*/
public static Builder builder() {
return new Builder();
}

/**
* Helper method to create a Variable for use with varargs methods.
*
* @param key The variable key.
* @param value The variable value.
* @return A new Variable instance.
*/
public static Variable variable(String key, Object value) {
return new Variable(key, value);
}

/**
* Represents a template variable with a key and value.
*/
public static class Variable {
private final String key;
private final Object value;

/**
* Constructs a Variable with the specified key and value.
*
* @param key The variable key.
* @param value The variable value.
*/
public Variable(String key, Object value) {
this.key = key;
this.value = value;
}

/**
* Gets the variable key.
*
* @return The variable key.
*/
public String getKey() {
return key;
}

/**
* Gets the variable value.
*
* @return The variable value.
*/
public Object getValue() {
return value;
}
}

/**
* Builder class for constructing Template objects.
*/
public static class Builder {
private String id;
private Map<String, Object> variables;

/**
* Set the template ID.
*
* @param id The template ID (must be a published template).
* @return The builder instance.
*/
public Builder id(String id) {
this.id = id;
return this;
}

/**
* Set the template variables.
*
* @param variables The template variables as key/value pairs.
* @return The builder instance.
*/
public Builder variables(Map<String, Object> variables) {
this.variables = variables;
return this;
}

/**
* Add multiple template variables using varargs.
*
* @param variables The variables to add.
* @return The builder instance.
*/
public Builder variables(Variable... variables) {
if (this.variables == null) {
this.variables = new HashMap<>();
}
for (Variable variable : variables) {
this.variables.put(variable.getKey(), variable.getValue());
}
return this;
}

/**
* Add a single template variable.
*
* @param key The variable key.
* @param value The variable value.
* @return The builder instance.
*/
public Builder addVariable(String key, Object value) {
if (this.variables == null) {
this.variables = new HashMap<>();
}
this.variables.put(key, value);
return this;
}

/**
* Build a new Template object.
*
* @return A new Template object.
*/
public Template build() {
return new Template(this);
}
}
}
67 changes: 67 additions & 0 deletions src/test/java/com/resend/services/emails/EmailsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -218,4 +218,71 @@ public void testListAttachmentsWithPagination_Success() throws ResendException {
assertEquals(expectedResponse.hasMore(), response.hasMore());
verify(emails, times(1)).listAttachments(emailId, params);
}

@Test
public void testSendEmail_WithTemplate_Success() throws ResendException {
Template template = Template.builder()
.id("template_123")
.addVariable("firstName", "John")
.addVariable("lastName", "Doe")
.addVariable("company", "Acme Corp")
.build();

CreateEmailOptions emailWithTemplate = CreateEmailOptions.builder()
.from("Acme <[email protected]>")
.to("[email protected]")
.subject("Welcome John!")
.template(template)
Copy link
Contributor

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

Choose a reason for hiding this comment

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

Rule violated: API Key Permission Check SDK Methods

This new template-based send path exercises additional Resend SDK capabilities, but nothing here confirms the production API keys cover the template permissions required by the API. Please verify those credentials so template sends don’t start failing after release.

Prompt for AI agents
Address the following comment on src/test/java/com/resend/services/emails/EmailsTest.java at line 235:

<comment>This new template-based send path exercises additional Resend SDK capabilities, but nothing here confirms the production API keys cover the template permissions required by the API. Please verify those credentials so template sends don’t start failing after release.</comment>

<file context>
@@ -218,4 +218,71 @@ public void testListAttachmentsWithPagination_Success() throws ResendException {
+                .from(&quot;Acme &lt;[email protected]&gt;&quot;)
+                .to(&quot;[email protected]&quot;)
+                .subject(&quot;Welcome John!&quot;)
+                .template(template)
+                .build();
+
</file context>
Fix with Cubic

.build();

CreateEmailResponse expectedResponse = EmailsUtil.createSendEmailResponse();

when(emails.send(emailWithTemplate)).thenReturn(expectedResponse);

CreateEmailResponse response = emails.send(emailWithTemplate);

assertNotNull(response);
assertEquals(expectedResponse.getId(), response.getId());
verify(emails, times(1)).send(emailWithTemplate);
}

@Test
public void testSendBatchEmails_WithTemplate_Success() throws ResendException {
Template template1 = Template.builder()
.id("template_123")
.addVariable("firstName", "John")
.addVariable("company", "Tech Corp")
.build();

Template template2 = Template.builder()
.id("template_123")
.addVariable("firstName", "Jane")
.addVariable("company", "Design Studios")
.build();

CreateEmailOptions email1 = CreateEmailOptions.builder()
.from("Acme <[email protected]>")
.to("[email protected]")
.subject("Welcome John!")
.template(template1)
.build();

CreateEmailOptions email2 = CreateEmailOptions.builder()
.from("Acme <[email protected]>")
.to("[email protected]")
.subject("Welcome Jane!")
.template(template2)
.build();

List<CreateEmailOptions> batchEmails = java.util.Arrays.asList(email1, email2);
CreateBatchEmailsResponse expectedResponse = EmailsUtil.createBatchEmailsResponse();

when(batch.send(batchEmails)).thenReturn(expectedResponse);

CreateBatchEmailsResponse response = batch.send(batchEmails);

assertNotNull(response);
assertEquals(expectedResponse.getData().size(), response.getData().size());
verify(batch, times(1)).send(batchEmails);
}
}
38 changes: 38 additions & 0 deletions src/test/java/com/resend/services/util/EmailsUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,35 @@ public static Tag createTag() {
.build();
}

public static Template createTemplate() {
Map<String, Object> variables = new HashMap<>();
variables.put("name", "John");
variables.put("company", "Acme Corp");

return Template.builder()
.id("template_123")
.variables(variables)
.build();
}

public static Template createTemplateWithAddVariable() {
return Template.builder()
.id("template_123")
.addVariable("name", "John")
.addVariable("company", "Acme Corp")
.build();
}

public static Template createTemplateWithVarargs() {
return Template.builder()
.id("template_123")
.variables(
Template.variable("name", "John"),
Template.variable("company", "Acme Corp")
)
.build();
}

public static CreateEmailOptions createEmailOptions() {
return CreateEmailOptions.builder()
.from("Acme <[email protected]>")
Expand All @@ -42,6 +71,15 @@ public static CreateEmailOptions createEmailOptions() {
.build();
}

public static CreateEmailOptions createEmailOptionsWithTemplate() {
return CreateEmailOptions.builder()
.from("Acme <[email protected]>")
.to(Arrays.asList("[email protected]"))
.subject("Welcome to Acme")
.template(createTemplate())
.build();
}

public static RequestOptions createRequestOptions() {
return RequestOptions.builder()
.setIdempotencyKey("welcome-user/123456789").build();
Expand Down