diff --git a/src/main/java/co/lettermint/api/ApiClient.java b/src/main/java/co/lettermint/api/ApiClient.java index 225756a..0cbbc42 100644 --- a/src/main/java/co/lettermint/api/ApiClient.java +++ b/src/main/java/co/lettermint/api/ApiClient.java @@ -137,6 +137,14 @@ public MessageData retrieve(String messageId) { return client.get("/messages/" + segment(messageId), MessageData.class); } + public RescheduleMessageResponse reschedule(String messageId, RescheduleMessageRequest payload) { + return client.patch("/messages/" + segment(messageId), payload, RescheduleMessageResponse.class); + } + + public RescheduleMessageResponse cancel(String messageId) { + return client.post("/messages/" + segment(messageId) + "/cancel", null, RescheduleMessageResponse.class); + } + public MessageEventsResponse events(String messageId) { return events(messageId, null); } diff --git a/src/main/java/co/lettermint/client/LettermintClient.java b/src/main/java/co/lettermint/client/LettermintClient.java index 1c9dd94..8b18732 100644 --- a/src/main/java/co/lettermint/client/LettermintClient.java +++ b/src/main/java/co/lettermint/client/LettermintClient.java @@ -111,6 +111,10 @@ public T put(String path, Object payload, Class responseClass) { return request("PUT", url(path, null), payload, responseClass, null, null); } + public T patch(String path, Object payload, Class responseClass) { + return request("PATCH", url(path, null), payload, responseClass, null, null); + } + public T delete(String path, Class responseClass) { return request("DELETE", url(path, null), null, responseClass, null, null); } @@ -148,6 +152,8 @@ private T request(String method, String requestUrl, Object payload, Class requestBuilder.post(body != null ? body : RequestBody.create(new byte[0], JSON)); } else if ("PUT".equals(method)) { requestBuilder.put(body != null ? body : RequestBody.create(new byte[0], JSON)); + } else if ("PATCH".equals(method)) { + requestBuilder.patch(body != null ? body : RequestBody.create(new byte[0], JSON)); } else if ("DELETE".equals(method)) { requestBuilder.delete(); } else { diff --git a/src/main/java/co/lettermint/endpoints/EmailEndpoint.java b/src/main/java/co/lettermint/endpoints/EmailEndpoint.java index 62e7145..4ddf102 100644 --- a/src/main/java/co/lettermint/endpoints/EmailEndpoint.java +++ b/src/main/java/co/lettermint/endpoints/EmailEndpoint.java @@ -20,6 +20,7 @@ public class EmailEndpoint extends Endpoint { private List bcc; private List replyTo; private String subject; + private String scheduledAt; private String html; private String text; private Map headers; @@ -46,6 +47,7 @@ private void reset() { this.bcc = new ArrayList<>(); this.replyTo = new ArrayList<>(); this.subject = null; + this.scheduledAt = null; this.html = null; this.text = null; this.headers = new LinkedHashMap<>(); @@ -107,6 +109,12 @@ public EmailEndpoint subject(String subject) { return this; } + /** Set the requested delivery time for the email. */ + public EmailEndpoint scheduledAt(String scheduledAt) { + this.scheduledAt = scheduledAt; + return this; + } + /** * Set the HTML content of the email. */ @@ -296,6 +304,10 @@ private Map buildPayload() { payload.put("subject", subject); } + if (scheduledAt != null) { + payload.put("scheduled_at", scheduledAt); + } + if (html != null) { payload.put("html", html); } diff --git a/src/main/java/co/lettermint/models/api/MessageData.java b/src/main/java/co/lettermint/models/api/MessageData.java index 34dd24a..3d92447 100644 --- a/src/main/java/co/lettermint/models/api/MessageData.java +++ b/src/main/java/co/lettermint/models/api/MessageData.java @@ -19,6 +19,9 @@ public class MessageData { @JsonProperty("status_changed_at") public String statusChangedAt; + @JsonProperty("scheduled_at") + public String scheduledAt; + @JsonProperty("tag") public String tag; diff --git a/src/main/java/co/lettermint/models/api/MessageEventType.java b/src/main/java/co/lettermint/models/api/MessageEventType.java index 4cca4f9..7d71fb3 100644 --- a/src/main/java/co/lettermint/models/api/MessageEventType.java +++ b/src/main/java/co/lettermint/models/api/MessageEventType.java @@ -1,6 +1,10 @@ package co.lettermint.models.api; public final class MessageEventType { + public static final String SCHEDULED = "scheduled"; + public static final String RESCHEDULED = "rescheduled"; + public static final String CANCELED = "canceled"; + public static final String RELEASED = "released"; public static final String QUEUED = "queued"; public static final String PROCESSED = "processed"; public static final String SUPPRESSED = "suppressed"; diff --git a/src/main/java/co/lettermint/models/api/MessageListData.java b/src/main/java/co/lettermint/models/api/MessageListData.java index 19d735d..eddc6e3 100644 --- a/src/main/java/co/lettermint/models/api/MessageListData.java +++ b/src/main/java/co/lettermint/models/api/MessageListData.java @@ -16,6 +16,9 @@ public class MessageListData { @JsonProperty("status") public String status; + @JsonProperty("scheduled_at") + public String scheduledAt; + @JsonProperty("spam_score") public Double spamScore; diff --git a/src/main/java/co/lettermint/models/api/MessageStatus.java b/src/main/java/co/lettermint/models/api/MessageStatus.java index f78d0d0..e6d743a 100644 --- a/src/main/java/co/lettermint/models/api/MessageStatus.java +++ b/src/main/java/co/lettermint/models/api/MessageStatus.java @@ -1,6 +1,7 @@ package co.lettermint.models.api; public final class MessageStatus { + public static final String SCHEDULED = "scheduled"; public static final String PENDING = "pending"; public static final String QUEUED = "queued"; public static final String SUPPRESSED = "suppressed"; @@ -15,6 +16,7 @@ public final class MessageStatus { public static final String BLOCKED = "blocked"; public static final String POLICYREJECTED = "policy_rejected"; public static final String UNSUBSCRIBED = "unsubscribed"; + public static final String CANCELED = "canceled"; private MessageStatus() {} } diff --git a/src/main/java/co/lettermint/models/api/RescheduleMessageRequest.java b/src/main/java/co/lettermint/models/api/RescheduleMessageRequest.java new file mode 100644 index 0000000..45ebb24 --- /dev/null +++ b/src/main/java/co/lettermint/models/api/RescheduleMessageRequest.java @@ -0,0 +1,10 @@ +package co.lettermint.models.api; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class RescheduleMessageRequest { + @JsonProperty("scheduled_at") + public String scheduledAt; +} diff --git a/src/main/java/co/lettermint/models/api/RescheduleMessageResponse.java b/src/main/java/co/lettermint/models/api/RescheduleMessageResponse.java new file mode 100644 index 0000000..f0add62 --- /dev/null +++ b/src/main/java/co/lettermint/models/api/RescheduleMessageResponse.java @@ -0,0 +1,16 @@ +package co.lettermint.models.api; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class RescheduleMessageResponse { + @JsonProperty("message_id") + public String messageId; + + @JsonProperty("status") + public String status; + + @JsonProperty("scheduled_at") + public String scheduledAt; +} diff --git a/src/main/java/co/lettermint/models/api/SendMailRequest.java b/src/main/java/co/lettermint/models/api/SendMailRequest.java index d0acaa1..22aadd2 100644 --- a/src/main/java/co/lettermint/models/api/SendMailRequest.java +++ b/src/main/java/co/lettermint/models/api/SendMailRequest.java @@ -28,6 +28,9 @@ public class SendMailRequest { @JsonProperty("subject") public String subject; + @JsonProperty("scheduled_at") + public String scheduledAt; + @JsonProperty("headers") public Map headers; diff --git a/src/main/java/co/lettermint/models/api/SendMailResponse.java b/src/main/java/co/lettermint/models/api/SendMailResponse.java index f61c6fa..91f906d 100644 --- a/src/main/java/co/lettermint/models/api/SendMailResponse.java +++ b/src/main/java/co/lettermint/models/api/SendMailResponse.java @@ -10,4 +10,7 @@ public class SendMailResponse { @JsonProperty("status") public String status; + + @JsonProperty("scheduled_at") + public String scheduledAt; } diff --git a/src/test/java/co/lettermint/ApiClientTest.java b/src/test/java/co/lettermint/ApiClientTest.java index 4636244..406e980 100644 --- a/src/test/java/co/lettermint/ApiClientTest.java +++ b/src/test/java/co/lettermint/ApiClientTest.java @@ -145,6 +145,29 @@ void rawMessageEndpointsPreserveResponseBody() throws Exception { } } + @Test + void schedulesMessageChangesWithTypedResponses() throws Exception { + try (MockWebServer server = new MockWebServer()) { + server.enqueue(new MockResponse().setHeader("Content-Type", "application/json").setBody("{\"message_id\":\"msg/id\",\"status\":\"scheduled\",\"scheduled_at\":\"2026-08-27T09:00:00Z\"}")); + server.enqueue(new MockResponse().setHeader("Content-Type", "application/json").setBody("{\"message_id\":\"msg/id\",\"status\":\"canceled\",\"scheduled_at\":null}")); + server.start(); + + ApiClient api = Lettermint.api("api-token", server.url("/v1").toString()); + co.lettermint.models.api.RescheduleMessageRequest payload = new co.lettermint.models.api.RescheduleMessageRequest(); + payload.scheduledAt = "2026-08-27T09:00:00Z"; + assertEquals("scheduled", api.messages().reschedule("msg/id", payload).status); + assertEquals("canceled", api.messages().cancel("msg/id").status); + + RecordedRequest reschedule = server.takeRequest(); + assertEquals("PATCH", reschedule.getMethod()); + assertEquals("/v1/messages/msg%2Fid", reschedule.getPath()); + assertTrue(reschedule.getBody().readUtf8().contains("scheduled_at")); + RecordedRequest cancel = server.takeRequest(); + assertEquals("POST", cancel.getMethod()); + assertEquals("/v1/messages/msg%2Fid/cancel", cancel.getPath()); + } + } + @Test void publicClientRejectsAbsoluteRequestPaths() throws Exception { Lettermint lettermint = new Lettermint("sending-token", "https://api.example.test/v1"); @@ -159,7 +182,7 @@ void publicClientRejectsAbsoluteRequestPaths() throws Exception { @Test void generatedApiModelsMatchCurrentTeamSchema() { - assertEquals("auto_replied", co.lettermint.models.api.MessageEventType.AUTOREPLIED); + assertEquals("scheduled", co.lettermint.models.api.MessageEventType.SCHEDULED); assertEquals("message.auto_replied", co.lettermint.models.api.WebhookEvent.MESSAGEAUTOREPLIED); assertEquals("admin", co.lettermint.models.api.BuiltInTeamRole.ADMIN); assertEquals("enforced", co.lettermint.models.api.TlsPolicy.ENFORCED); @@ -210,6 +233,7 @@ void generatedApiModelsMatchCurrentTeamSchema() { co.lettermint.models.api.MessageListData message = new co.lettermint.models.api.MessageListData(); message.spamScore = 2.5; + message.scheduledAt = "2026-08-27T09:00:00Z"; assertFalse(routeUpdate.settings.generatePlaintextFallback); assertEquals("enforced", routeUpdate.settings.tls); @@ -226,6 +250,7 @@ void generatedApiModelsMatchCurrentTeamSchema() { assertEquals("managed_cname", domain.dkimMode); assertEquals("msg_123", recipient.sourceMessage.id); assertEquals(2.5, message.spamScore); + assertEquals("2026-08-27T09:00:00Z", message.scheduledAt); } @Test @@ -243,6 +268,8 @@ void apiExposesDocumentedOperations() throws Exception { methods.put("v1.blockedFileTypes", api.getClass().getMethod("blockedFileTypes")); methods.put("message.index", api.messages().getClass().getMethod("list")); methods.put("message.show", api.messages().getClass().getMethod("retrieve", String.class)); + methods.put("rescheduleMessage", api.messages().getClass().getMethod("reschedule", String.class, co.lettermint.models.api.RescheduleMessageRequest.class)); + methods.put("cancelScheduledMessage", api.messages().getClass().getMethod("cancel", String.class)); methods.put("message.events", api.messages().getClass().getMethod("events", String.class)); methods.put("message.source", api.messages().getClass().getMethod("source", String.class)); methods.put("message.html", api.messages().getClass().getMethod("html", String.class));