diff --git a/README.md b/README.md index b82fb5a..84d64d2 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,11 @@ MailerSend Java SDK - [Send an email with RCPT TO recipients](#send-an-email-with-rcpt-to-recipients) - [Send bulk emails](#send-bulk-emails) - [Get bulk request status](#get-bulk-request-status) + - [Emails](#emails) + - [Get a list of emails](#get-a-list-of-emails) + - [Emails filters](#emails-filters) + - [Emails pagination](#emails-pagination) + - [Get a single email](#get-a-single-email) - [Inbound routes](#inbound-routes) - [Get a list of inbound routes](#get-a-list-of-inbound-routes) - [Get an inbound route](#get-an-inbound-route) @@ -846,6 +851,258 @@ public void getBulkEmailsStatus() { } ``` +## Emails + +An email is the record of a message delivered to one recipient. Emails retrieval follows the builder pattern and is accessible from the `MailerSend.emails()` object, the same object used for sending. + +The SDK returns an `EmailsList` object for the list of emails and an `EmailInfo` object for a single email, or throws a `MailerSendException` on a failed request. + +### Get a list of emails + +`domainId`, `dateFrom` and `dateTo` are required, the request throws a `MailerSendException` without them. Emails are returned newest first. + +```java +import com.mailersend.sdk.MailerSend; +import com.mailersend.sdk.emails.EmailListItem; +import com.mailersend.sdk.emails.EmailsList; +import com.mailersend.sdk.exceptions.MailerSendException; + +public void getEmails() { + + MailerSend ms = new MailerSend(); + + ms.setToken("Your API token"); + + try { + + EmailsList list = ms.emails() + .domainId("domain id") + .dateFrom(1623073576) + .dateTo(1623074976) + .page(1) + .limit(50) + .getEmails(); + + for (EmailListItem email : list.emails) { + + System.out.println(email.id); + System.out.println(email.status); + System.out.println(email.subject); + System.out.println(email.from); + System.out.println(email.to); + System.out.println(email.messageId); + System.out.println(email.createdAt.toString()); + + // any of opened, clicked, unsubscribed or complained, empty when there was no interaction + for (String interaction : email.interaction) { + + System.out.println(interaction); + } + } + + } catch (MailerSendException e) { + + e.printStackTrace(); + } +} +``` + +Every `EmailListItem` carries `id`, `from`, `to`, `subject`, `text`, `html`, `templateId`, `domainId`, `messageId`, `status`, `tags`, `interaction`, `suppressionReason`, `createdAt`, `updatedAt` and `headers` (an `EmailHeader[]` of `name`/`value` pairs, not a map). `text` and `html` are always `null` in a list item, use [`getEmail()`](#get-a-single-email) to retrieve the message content, its recipient and its activity events. + +A filter that matches nothing, including an unknown recipient email, returns an empty `list.emails` array. An unknown domain id returns a `MailerSendException` with code `404`. + +> **Note:** A token with one of the `activity_read` or `activity_full` scopes is required. Requests to the emails list are limited to 10 requests per minute, shared with [the activities list](#get-a-list-of-activities). Requests to either endpoint count against the same budget. + +### Emails filters + +| Method | Type | Required | Details | +|---------------------------------------|------------|----------|------------------------------------------------------------------------------------------------------------| +| `domainId(String domainId)` | `String` | yes | Must be a domain of your account. | +| `dateFrom(long dateFrom)` | `long` | yes | Unix timestamp in seconds, assumed to be UTC. Also accepts a `java.util.Date`. | +| `dateTo(long dateTo)` | `long` | yes | Unix timestamp in seconds, must be higher than `dateFrom` and not in the future. Also accepts a `Date`. | +| `page(int page)` | `int` | no | Min 1, max 100, default 1, see [Emails pagination](#emails-pagination). | +| `limit(int limit)` | `int` | no | Min 10, max 1000, default 25. | +| `status(String... status)` | `String[]` | no | Any of the constants in `com.mailersend.sdk.emails.EmailStatus`, combined with OR. | +| `interaction(String... interaction)` | `String[]` | no | Any of the constants in `com.mailersend.sdk.emails.EmailInteraction`, combined with OR. | +| `recipientEmail(String recipientEmail)` | `String` | no | Exact, case insensitive match. | +| `messageId(String messageId)` | `String` | no | Exact match against the id of the message that created the email. | +| `templateId(String templateId)` | `String` | no | Exact match. | +| `subject(String subject)` | `String` | no | Partial, case insensitive match, minimum 3 characters. | +| `tag(String tag)` | `String` | no | Exact match against a value of the email's tags. | + +The `status` and `interaction` values are combined with OR within a filter and with AND between the two filters, so the example below returns the emails that are sent or delivered and that were opened. + +```java +import com.mailersend.sdk.MailerSend; +import com.mailersend.sdk.emails.EmailInteraction; +import com.mailersend.sdk.emails.EmailListItem; +import com.mailersend.sdk.emails.EmailStatus; +import com.mailersend.sdk.emails.EmailsList; +import com.mailersend.sdk.exceptions.MailerSendException; + +public void getFilteredEmails() { + + MailerSend ms = new MailerSend(); + + ms.setToken("Your API token"); + + try { + + Date dateFrom = DateUtils.addDays(new Date(), -7); // you'll need apache-commons for this + Date dateTo = new Date(); + + EmailsList list = ms.emails() + .domainId("domain id") + .dateFrom(dateFrom) + .dateTo(dateTo) + .limit(50) + .status(EmailStatus.SENT, EmailStatus.DELIVERED) + .interaction(EmailInteraction.OPENED) + .recipientEmail("tyra.cummerata@example.org") + .subject("order") + .tag("receipt") + .getEmails(); + + for (EmailListItem email : list.emails) { + + System.out.println(email.id); + } + + } catch (MailerSendException e) { + + e.printStackTrace(); + } +} +``` + +> **Note:** The filters are kept on the `Emails` object, so set the ones you need on every call instead of relying on the values of a previous request. + +### Emails pagination + +The emails list paginates by page number, the same way [the activities list](#activities-pagination) does. Set the page with `page(int page)` and the page size with `limit(int limit)`. + +The result set is not counted, so `meta` carries no `total` and no `lastPage`, and `links.last` is always `null`. Use `hasNext()` to find out whether more results exist. + +`next()` and `previous()` repeat the original request for the neighbouring page and return `null` when there is none, so you can walk the whole result set without tracking the page yourself. + +```java +import com.mailersend.sdk.MailerSend; +import com.mailersend.sdk.emails.EmailListItem; +import com.mailersend.sdk.emails.EmailsList; +import com.mailersend.sdk.exceptions.MailerSendException; + +public void getAllEmails() { + + MailerSend ms = new MailerSend(); + + ms.setToken("Your API token"); + + try { + + EmailsList list = ms.emails() + .domainId("domain id") + .dateFrom(1623073576) + .dateTo(1623074976) + .limit(100) + .getEmails(); + + while (list != null) { + + System.out.println(list.getCurrentPage()); // also available as list.meta.currentPage + System.out.println(list.meta.currentPageUrl); + System.out.println(list.meta.limit); // the per_page of the response + System.out.println(list.links.next); // the full url of the next page, null on the last page + + for (EmailListItem email : list.emails) { + + System.out.println(email.id); + } + + // returns null when there is no next page + list = list.next(); + } + + } catch (MailerSendException e) { + + e.printStackTrace(); + } +} +``` + +You can also request a page yourself, together with the same domain id, dates and filters as the original request. + +```java +EmailsList list = ms.emails() + .domainId("domain id") + .dateFrom(1623073576) + .dateTo(1623074976) + .page(2) + .getEmails(); + +if (list.hasPrevious()) { + + EmailsList previousPage = list.previous(); +} +``` + +### Get a single email + +Returns an `EmailInfo` object with the email, its content, its recipient and its activity events, none of which are present in a list item. The events are returned newest first and are capped at the 200 most recent ones. They are returned even when content tracking is disabled for the domain, in which case `html` and `text` are `null`. + +```java +import com.mailersend.sdk.MailerSend; +import com.mailersend.sdk.emails.EmailActivity; +import com.mailersend.sdk.emails.EmailInfo; +import com.mailersend.sdk.exceptions.MailerSendException; + +public void getSingleEmail() { + + MailerSend ms = new MailerSend(); + + ms.setToken("Your API token"); + + try { + + EmailInfo email = ms.emails().getEmail("email id"); + + System.out.println(email.id); + System.out.println(email.status); + System.out.println(email.subject); + System.out.println(email.from); + System.out.println(email.to); // also available as email.recipient.email + System.out.println(email.messageId); + System.out.println(email.domainId); + System.out.println(email.templateId); // null when no template was used + System.out.println(email.suppressionReason); // only set when the status is rejected + System.out.println(email.createdAt.toString()); + + // any of opened, clicked, unsubscribed or complained, empty when there was no interaction + for (String interaction : email.interaction) { + + System.out.println(interaction); + } + + for (EmailActivity activity : email.activity) { + + System.out.println(activity.id); + System.out.println(activity.type); + System.out.println(activity.createdAt.toString()); + + // only set on suppressed events + System.out.println(activity.suppressionReason); + } + + } catch (MailerSendException e) { + + e.printStackTrace(); + } +} +``` + +> **Note:** A token with one of the `email_full`, `activity_read` or `activity_full` scopes is required. + +> **Note:** The `junk` event type is reported as `soft_bounced`. The `deferred` and `suppressed` event types are only included if your plan has those features enabled, they are available on the Starter plan and above. + ## Inbound routes ### Get a list of inbound routes diff --git a/src/main/java/com/mailersend/sdk/emails/EmailActivity.java b/src/main/java/com/mailersend/sdk/emails/EmailActivity.java new file mode 100644 index 0000000..eca95c3 --- /dev/null +++ b/src/main/java/com/mailersend/sdk/emails/EmailActivity.java @@ -0,0 +1,47 @@ +/************************************************* + * MailerSend Java SDK + * https://github.com/mailersend/mailersend-java + * + * @author MailerSend + * https://mailersend.com + **************************************************/ +package com.mailersend.sdk.emails; + +import java.util.Date; + +import com.google.gson.annotations.SerializedName; + +/** + * An activity event of a single email + * + * @author mailersend + * @version $Id: $Id + */ +public class EmailActivity { + + /** The id of the event, can be passed to ms.activities().getSingleActivity() for the full event */ + @SerializedName("id") + public String id; + + /** The event type, check com.mailersend.sdk.util.EventTypes for the possible values */ + @SerializedName("type") + public String type; + + /** Only present on suppressed events, one of on_hold, hard_bounced, unsubscribed, spam_complained or blocklisted */ + @SerializedName("suppression_reason") + public String suppressionReason; + + public Date createdAt; + + @SerializedName("created_at") + private String createdAtString; + + + /** + * Converts the retrieved dates to java.util.Date + */ + protected void parseDates() { + + createdAt = EmailDates.parse(createdAtString); + } +} diff --git a/src/main/java/com/mailersend/sdk/emails/EmailDates.java b/src/main/java/com/mailersend/sdk/emails/EmailDates.java new file mode 100644 index 0000000..13cf75b --- /dev/null +++ b/src/main/java/com/mailersend/sdk/emails/EmailDates.java @@ -0,0 +1,62 @@ +/************************************************* + * MailerSend Java SDK + * https://github.com/mailersend/mailersend-java + * + * @author MailerSend + * https://mailersend.com + **************************************************/ +package com.mailersend.sdk.emails; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAccessor; +import java.util.Date; + +/** + * Parses the date strings returned by the emails endpoints + */ +final class EmailDates { + + private static final DateTimeFormatter SPACE_SEPARATED_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + private EmailDates() { + + // intentionally left empty + } + + + /** + * Converts an API date string to a java.util.Date. Returns null for empty or unparseable values + * + * @param value the date string as returned by the API + * @return the parsed date or null + */ + static Date parse(String value) { + + if (value == null || value.isBlank()) { + + return null; + } + + try { + + TemporalAccessor ta = DateTimeFormatter.ISO_INSTANT.parse(value); + + return Date.from(Instant.from(ta)); + } catch (Exception e) { + + // the API may also return dates as "2020-06-04 12:00:00", assumed to be UTC + try { + + LocalDateTime localDateTime = LocalDateTime.parse(value, SPACE_SEPARATED_FORMATTER); + + return Date.from(localDateTime.toInstant(ZoneOffset.UTC)); + } catch (Exception ex) { + + return null; + } + } + } +} diff --git a/src/main/java/com/mailersend/sdk/emails/EmailInfo.java b/src/main/java/com/mailersend/sdk/emails/EmailInfo.java new file mode 100644 index 0000000..4ef2a86 --- /dev/null +++ b/src/main/java/com/mailersend/sdk/emails/EmailInfo.java @@ -0,0 +1,134 @@ +/************************************************* + * MailerSend Java SDK + * https://github.com/mailersend/mailersend-java + * + * @author MailerSend + * https://mailersend.com + **************************************************/ +package com.mailersend.sdk.emails; + +import java.util.Date; + +import com.google.gson.annotations.SerializedName; +import com.mailersend.sdk.util.ApiRecipient; + +/** + * A single email retrieved from the API, together with its activity events. + * Returned by ms.emails().getEmail() + * + * @author mailersend + * @version $Id: $Id + */ +public class EmailInfo { + + @SerializedName("id") + public String id; + + /** The sender's email address */ + @SerializedName("from") + public String from; + + /** The recipient's email address, also available as recipient.email */ + @SerializedName("to") + public String to; + + @SerializedName("subject") + public String subject; + + /** Null when content tracking is disabled for the domain */ + @SerializedName("text") + public String text; + + /** Null when content tracking is disabled for the domain */ + @SerializedName("html") + public String html; + + /** The id of the template used, null when no template was used */ + @SerializedName("template_id") + public String templateId; + + @SerializedName("domain_id") + public String domainId; + + /** The id of the message that created this email */ + @SerializedName("message_id") + public String messageId; + + /** The status of the email, one of queued, sent, rejected or delivered */ + @SerializedName("status") + public String status; + + @SerializedName("tags") + public String[] tags; + + /** Any of opened, clicked, unsubscribed or complained recorded for the email, empty when there was no interaction */ + @SerializedName("interaction") + public String[] interaction; + + /** Only set when the status is rejected, null otherwise */ + @SerializedName("suppression_reason") + public String suppressionReason; + + /** The recipient the email was addressed to */ + @SerializedName("recipient") + public ApiRecipient recipient; + + /** The custom headers the email was sent with, null when there were none */ + @SerializedName("headers") + public EmailHeader[] headers; + + /** + * The activity events of the email, newest first and capped at the 200 most recent ones. + * Empty when no events were recorded. Present even when content tracking is disabled + */ + @SerializedName("activity") + public EmailActivity[] activity; + + public Date createdAt; + + public Date updatedAt; + + @SerializedName("created_at") + private String createdAtString; + + @SerializedName("updated_at") + private String updatedAtString; + + + /** + * Is called to perform any actions after the deserialization of the response + * to the /email/{email_id} endpoint + * Do not call directly + */ + public void postDeserialize() { + + parseDates(); + + if (recipient != null) { + + recipient.parseDates(); + } + + if (activity == null) { + + activity = new EmailActivity[0]; + + return; + } + + for (EmailActivity activityItem : activity) { + + activityItem.parseDates(); + } + } + + + /** + * Converts the retrieved dates to java.util.Date + */ + private void parseDates() { + + createdAt = EmailDates.parse(createdAtString); + updatedAt = EmailDates.parse(updatedAtString); + } +} diff --git a/src/main/java/com/mailersend/sdk/emails/EmailInteraction.java b/src/main/java/com/mailersend/sdk/emails/EmailInteraction.java new file mode 100644 index 0000000..7f43846 --- /dev/null +++ b/src/main/java/com/mailersend/sdk/emails/EmailInteraction.java @@ -0,0 +1,35 @@ +/************************************************* + * MailerSend Java SDK + * https://github.com/mailersend/mailersend-java + * + * @author MailerSend + * https://mailersend.com + **************************************************/ +package com.mailersend.sdk.emails; + +/** + * The possible recipient interactions, used by the interaction filter of the emails list endpoint + * + * @author mailersend + * @version $Id: $Id + */ +public class EmailInteraction { + + /** Constant OPENED="opened" */ + public static final String OPENED = "opened"; + + /** Constant CLICKED="clicked" */ + public static final String CLICKED = "clicked"; + + /** Constant UNSUBSCRIBED="unsubscribed" */ + public static final String UNSUBSCRIBED = "unsubscribed"; + + /** Constant COMPLAINED="complained" */ + public static final String COMPLAINED = "complained"; + + /** + * Matches emails with none of the other interactions recorded. + * Filter value only, it is never returned in a response + */ + public static final String NO_INTERACTION = "no_interaction"; +} diff --git a/src/main/java/com/mailersend/sdk/emails/EmailListItem.java b/src/main/java/com/mailersend/sdk/emails/EmailListItem.java new file mode 100644 index 0000000..3b41af4 --- /dev/null +++ b/src/main/java/com/mailersend/sdk/emails/EmailListItem.java @@ -0,0 +1,94 @@ +/************************************************* + * MailerSend Java SDK + * https://github.com/mailersend/mailersend-java + * + * @author MailerSend + * https://mailersend.com + **************************************************/ +package com.mailersend.sdk.emails; + +import java.util.Date; + +import com.google.gson.annotations.SerializedName; + +/** + * A single email as returned by the emails list endpoint + * + * @author mailersend + * @version $Id: $Id + */ +public class EmailListItem { + + @SerializedName("id") + public String id; + + /** The sender's email address */ + @SerializedName("from") + public String from; + + /** The recipient's email address */ + @SerializedName("to") + public String to; + + @SerializedName("subject") + public String subject; + + /** Always null in a list item, use ms.emails().getEmail() to get the content of an email */ + @SerializedName("text") + public String text; + + /** Always null in a list item, use ms.emails().getEmail() to get the content of an email */ + @SerializedName("html") + public String html; + + /** The id of the template used, null when no template was used */ + @SerializedName("template_id") + public String templateId; + + @SerializedName("domain_id") + public String domainId; + + /** The id of the message that created this email */ + @SerializedName("message_id") + public String messageId; + + /** The status of the email, one of queued, sent, rejected or delivered */ + @SerializedName("status") + public String status; + + /** The tags the email was sent with, null when it was sent without tags */ + @SerializedName("tags") + public String[] tags; + + /** Any of opened, clicked, unsubscribed or complained recorded for the email, empty when there was no interaction */ + @SerializedName("interaction") + public String[] interaction; + + /** Only set when the status is rejected, null otherwise */ + @SerializedName("suppression_reason") + public String suppressionReason; + + public Date createdAt; + + public Date updatedAt; + + /** The custom headers the email was sent with, null when there were none */ + @SerializedName("headers") + public EmailHeader[] headers; + + @SerializedName("created_at") + private String createdAtString; + + @SerializedName("updated_at") + private String updatedAtString; + + + /** + * Converts the retrieved dates to java.util.Date + */ + protected void parseDates() { + + createdAt = EmailDates.parse(createdAtString); + updatedAt = EmailDates.parse(updatedAtString); + } +} diff --git a/src/main/java/com/mailersend/sdk/emails/EmailStatus.java b/src/main/java/com/mailersend/sdk/emails/EmailStatus.java new file mode 100644 index 0000000..15ddbf0 --- /dev/null +++ b/src/main/java/com/mailersend/sdk/emails/EmailStatus.java @@ -0,0 +1,29 @@ +/************************************************* + * MailerSend Java SDK + * https://github.com/mailersend/mailersend-java + * + * @author MailerSend + * https://mailersend.com + **************************************************/ +package com.mailersend.sdk.emails; + +/** + * The possible email statuses, used by the status filter of the emails list endpoint + * + * @author mailersend + * @version $Id: $Id + */ +public class EmailStatus { + + /** Constant QUEUED="queued" */ + public static final String QUEUED = "queued"; + + /** Constant SENT="sent" */ + public static final String SENT = "sent"; + + /** Constant REJECTED="rejected" */ + public static final String REJECTED = "rejected"; + + /** Constant DELIVERED="delivered" */ + public static final String DELIVERED = "delivered"; +} diff --git a/src/main/java/com/mailersend/sdk/emails/Emails.java b/src/main/java/com/mailersend/sdk/emails/Emails.java index 558cbde..ea90f1c 100644 --- a/src/main/java/com/mailersend/sdk/emails/Emails.java +++ b/src/main/java/com/mailersend/sdk/emails/Emails.java @@ -8,7 +8,10 @@ package com.mailersend.sdk.emails; import java.lang.reflect.Type; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Date; import com.google.gson.Gson; import com.google.gson.GsonBuilder; @@ -36,9 +39,22 @@ public class Emails { private MailerSend apiObjectReference; - + private Recipient defaultFrom = null; - + + private String domainIdFilter = null; + private Long dateFromFilter = null; + private Long dateToFilter = null; + private int limitFilter = -1; + private int pageFilter = -1; + private String[] statusFilter = null; + private String[] interactionFilter = null; + private String recipientEmailFilter = null; + private String messageIdFilter = null; + private String templateIdFilter = null; + private String subjectFilter = null; + private String tagFilter = null; + /** *

Constructor for Emails.

* @@ -212,8 +228,377 @@ public BulkSendStatus deserialize(JsonElement json, Type typeOfT, JsonDeserializ BulkSendStatus status = customGson.fromJson(response.responseString, BulkSendStatus.class); status.parseDates(); - + return status; - + + } + + + /** + * Sets the domain id to retrieve the emails for. Required by getEmails() + * + * @param domainId a {@link java.lang.String} object. + * @return a {@link com.mailersend.sdk.emails.Emails} object. + */ + public Emails domainId(String domainId) { + + domainIdFilter = domainId; + + return this; + } + + + /** + * Sets the from date as a unix timestamp. Required by getEmails() + * + * @param dateFrom a long, the date as a unix timestamp in seconds. + * @return a {@link com.mailersend.sdk.emails.Emails} object. + */ + public Emails dateFrom(long dateFrom) { + + dateFromFilter = dateFrom; + + return this; + } + + + /** + * Sets the from date. Required by getEmails() + * + * @param dateFrom a {@link java.util.Date} object. + * @return a {@link com.mailersend.sdk.emails.Emails} object. + */ + public Emails dateFrom(Date dateFrom) { + + dateFromFilter = dateFrom == null ? null : dateFrom.getTime() / 1000; + + return this; + } + + + /** + * Sets the to date as a unix timestamp. Required by getEmails() + * + * @param dateTo a long, the date as a unix timestamp in seconds. + * @return a {@link com.mailersend.sdk.emails.Emails} object. + */ + public Emails dateTo(long dateTo) { + + dateToFilter = dateTo; + + return this; + } + + + /** + * Sets the to date. Required by getEmails() + * + * @param dateTo a {@link java.util.Date} object. + * @return a {@link com.mailersend.sdk.emails.Emails} object. + */ + public Emails dateTo(Date dateTo) { + + dateToFilter = dateTo == null ? null : dateTo.getTime() / 1000; + + return this; + } + + + /** + * Sets the results limit (10 - 1000, default 25) + * + * @param limit a int. + * @return a {@link com.mailersend.sdk.emails.Emails} object. + */ + public Emails limit(int limit) { + + limitFilter = limit; + + return this; + } + + + /** + * Sets the results page to retrieve (1 - 100, default 1) + * + * @param page a int. + * @return a {@link com.mailersend.sdk.emails.Emails} object. + */ + public Emails page(int page) { + + pageFilter = page; + + return this; + } + + + /** + * Filters the emails by status. Multiple values are combined with OR + * + * @param status one or more of the constants in {@link com.mailersend.sdk.emails.EmailStatus}. + * @return a {@link com.mailersend.sdk.emails.Emails} object. + */ + public Emails status(String... status) { + + statusFilter = status; + + return this; + } + + + /** + * Filters the emails by recipient interaction. Multiple values are combined with OR + * + * @param interaction one or more of the constants in {@link com.mailersend.sdk.emails.EmailInteraction}. + * @return a {@link com.mailersend.sdk.emails.Emails} object. + */ + public Emails interaction(String... interaction) { + + interactionFilter = interaction; + + return this; + } + + + /** + * Filters the emails by the recipient's email address + * + * @param recipientEmail a {@link java.lang.String} object. + * @return a {@link com.mailersend.sdk.emails.Emails} object. + */ + public Emails recipientEmail(String recipientEmail) { + + recipientEmailFilter = recipientEmail; + + return this; + } + + + /** + * Filters the emails by the id of the message that created them + * + * @param messageId a {@link java.lang.String} object. + * @return a {@link com.mailersend.sdk.emails.Emails} object. + */ + public Emails messageId(String messageId) { + + messageIdFilter = messageId; + + return this; + } + + + /** + * Filters the emails by template id + * + * @param templateId a {@link java.lang.String} object. + * @return a {@link com.mailersend.sdk.emails.Emails} object. + */ + public Emails templateId(String templateId) { + + templateIdFilter = templateId; + + return this; + } + + + /** + * Filters the emails by subject. Partial, case insensitive match, minimum 3 characters + * + * @param subject a {@link java.lang.String} object. + * @return a {@link com.mailersend.sdk.emails.Emails} object. + */ + public Emails subject(String subject) { + + subjectFilter = subject; + + return this; + } + + + /** + * Filters the emails by tag. Exact match against a value of the email's tags + * + * @param tag a {@link java.lang.String} object. + * @return a {@link com.mailersend.sdk.emails.Emails} object. + */ + public Emails tag(String tag) { + + tagFilter = tag; + + return this; + } + + + /** + * Gets a list of emails using the set filters. The domain id, from date and to date are required. + * Use EmailsList.next() to get the following results page + * + * @throws com.mailersend.sdk.exceptions.MailerSendException + * @return a {@link com.mailersend.sdk.emails.EmailsList} object. + */ + public EmailsList getEmails() throws MailerSendException { + + if (domainIdFilter == null || domainIdFilter.isBlank()) { + + throw new MailerSendException("A domain id is required."); + } + + if (dateFromFilter == null || dateToFilter == null) { + + throw new MailerSendException("Date from and Date to dates are required."); + } + + if (dateToFilter <= dateFromFilter) { + + throw new MailerSendException("From date cannot be after to date."); + } + + return requestEmails(prepareParamsUrl(), pageFilter); + } + + + /** + * Gets a single email with its activity events + * + * @param emailId a {@link java.lang.String} object. + * @throws com.mailersend.sdk.exceptions.MailerSendException + * @return a {@link com.mailersend.sdk.emails.EmailInfo} object. + */ + public EmailInfo getEmail(String emailId) throws MailerSendException { + + String endpoint = "/email/".concat(emailId); + + MailerSendApi api = new MailerSendApi(); + api.setToken(apiObjectReference.getToken()); + + SingleEmailResponse response = api.getRequest(endpoint, SingleEmailResponse.class); + + if (response.email != null) { + + response.email.postDeserialize(); + } + + return response.email; + } + + + /** + * Does the request to the emails endpoint with the given query parameters and page + * @param query The query part of the request url, without the page + * @param page The results page to retrieve, pass -1 to let the API default to the first page + * @return the found list of emails + * @throws MailerSendException + */ + EmailsList requestEmails(String query, int page) throws MailerSendException { + + String endpoint = "/emails".concat(query); + + if (page > -1) { + + endpoint = endpoint.concat(query.isEmpty() ? "?" : "&").concat("page=").concat(String.valueOf(page)); + } + + MailerSendApi api = new MailerSendApi(); + api.setToken(apiObjectReference.getToken()); + + EmailsList response = api.getRequest(endpoint, EmailsList.class); + + response.postDeserialize(); + + // we pass these to the EmailsList object so that it can retrieve the next and previous pages + response.mailersendObj = apiObjectReference; + response.baseQuery = query; + + return response; + } + + + /** + * Prepares the query part of the emails request url, without the page + * @return + */ + private String prepareParamsUrl() { + + ArrayList params = new ArrayList(); + + params.add("domain_id=".concat(urlEncode(domainIdFilter))); + + params.add("date_from=".concat(String.valueOf(dateFromFilter))); + + params.add("date_to=".concat(String.valueOf(dateToFilter))); + + if (limitFilter > -1) { + + params.add("limit=".concat(String.valueOf(limitFilter))); + } + + if (statusFilter != null) { + + for (String status : statusFilter) { + + params.add("status[]=".concat(urlEncode(status))); + } + } + + if (interactionFilter != null) { + + for (String interaction : interactionFilter) { + + params.add("interaction[]=".concat(urlEncode(interaction))); + } + } + + if (recipientEmailFilter != null) { + + params.add("recipient_email=".concat(urlEncode(recipientEmailFilter))); + } + + if (messageIdFilter != null) { + + params.add("message_id=".concat(urlEncode(messageIdFilter))); + } + + if (templateIdFilter != null) { + + params.add("template_id=".concat(urlEncode(templateIdFilter))); + } + + if (subjectFilter != null) { + + params.add("subject=".concat(urlEncode(subjectFilter))); + } + + if (tagFilter != null) { + + params.add("tag=".concat(urlEncode(tagFilter))); + } + + String requestParams = ""; + + for (int i = 0; i < params.size(); i++) { + + String attrSep = "&"; + + if (i == 0) { + + attrSep = "?"; + } + + requestParams = requestParams.concat(attrSep).concat(params.get(i)); + } + + return requestParams; + } + + + /** + * Url encodes a query parameter value + * @param value + * @return + */ + private String urlEncode(String value) { + + return URLEncoder.encode(value, StandardCharsets.UTF_8); } } diff --git a/src/main/java/com/mailersend/sdk/emails/EmailsList.java b/src/main/java/com/mailersend/sdk/emails/EmailsList.java new file mode 100644 index 0000000..83a712d --- /dev/null +++ b/src/main/java/com/mailersend/sdk/emails/EmailsList.java @@ -0,0 +1,124 @@ +/************************************************* + * MailerSend Java SDK + * https://github.com/mailersend/mailersend-java + * + * @author MailerSend + * https://mailersend.com + **************************************************/ +package com.mailersend.sdk.emails; + +import com.google.gson.annotations.SerializedName; +import com.mailersend.sdk.MailerSend; +import com.mailersend.sdk.exceptions.MailerSendException; +import com.mailersend.sdk.util.PaginatedResponse; + +/** + * The response of the emails list endpoint + * + * @author mailersend + * @version $Id: $Id + */ +public class EmailsList extends PaginatedResponse { + + @SerializedName("data") + public EmailListItem[] emails; + + protected transient MailerSend mailersendObj; + + /** The query parameters of the request that returned this list, without the page */ + protected transient String baseQuery; + + + /** + * Returns the current results page + * + * @return a int. + */ + public int getCurrentPage() { + + if (meta != null) { + + return meta.currentPage; + } + + return 0; + } + + + /** + * Whether there is a next results page. + * The endpoint returns no total and no last page, so the links are the only way to tell + * + * @return a boolean. + */ + public boolean hasNext() { + + return links != null && links.next != null; + } + + + /** + * Whether there is a previous results page + * + * @return a boolean. + */ + public boolean hasPrevious() { + + return links != null && links.prev != null; + } + + + /** + * Gets the next results page using the original filters + * + * @throws com.mailersend.sdk.exceptions.MailerSendException + * @return a {@link com.mailersend.sdk.emails.EmailsList} object or null if there are no more results + */ + public EmailsList next() throws MailerSendException { + + if (mailersendObj == null || !hasNext()) { + + return null; + } + + return mailersendObj.emails().requestEmails(baseQuery, getCurrentPage() + 1); + } + + + /** + * Gets the previous results page using the original filters + * + * @throws com.mailersend.sdk.exceptions.MailerSendException + * @return a {@link com.mailersend.sdk.emails.EmailsList} object or null if this is the first page + */ + public EmailsList previous() throws MailerSendException { + + if (mailersendObj == null || !hasPrevious()) { + + return null; + } + + return mailersendObj.emails().requestEmails(baseQuery, getCurrentPage() - 1); + } + + + /** + * Is called to perform any actions after the deserialization of the response + * to the /emails endpoint + * Do not call directly + */ + public void postDeserialize() { + + if (emails == null) { + + emails = new EmailListItem[0]; + + return; + } + + for (EmailListItem email : emails) { + + email.parseDates(); + } + } +} diff --git a/src/main/java/com/mailersend/sdk/emails/SingleEmailResponse.java b/src/main/java/com/mailersend/sdk/emails/SingleEmailResponse.java new file mode 100644 index 0000000..14dbece --- /dev/null +++ b/src/main/java/com/mailersend/sdk/emails/SingleEmailResponse.java @@ -0,0 +1,17 @@ +/************************************************* + * MailerSend Java SDK + * https://github.com/mailersend/mailersend-java + * + * @author MailerSend + * https://mailersend.com + **************************************************/ +package com.mailersend.sdk.emails; + +import com.google.gson.annotations.SerializedName; +import com.mailersend.sdk.MailerSendResponse; + +class SingleEmailResponse extends MailerSendResponse { + + @SerializedName("data") + public EmailInfo email; +} diff --git a/src/main/java/com/mailersend/sdk/util/ResponseMeta.java b/src/main/java/com/mailersend/sdk/util/ResponseMeta.java index 5d8d0b1..0c823e1 100644 --- a/src/main/java/com/mailersend/sdk/util/ResponseMeta.java +++ b/src/main/java/com/mailersend/sdk/util/ResponseMeta.java @@ -19,7 +19,11 @@ public class ResponseMeta { @SerializedName("current_page") public int currentPage; - + + /** The full url of the current results page. Only returned by some endpoints, null otherwise */ + @SerializedName("current_page_url") + public String currentPageUrl; + @SerializedName("from") public int from; diff --git a/src/test/java/com/mailersend/sdk/tests/EmailsTest.java b/src/test/java/com/mailersend/sdk/tests/EmailsTest.java new file mode 100644 index 0000000..663ffd5 --- /dev/null +++ b/src/test/java/com/mailersend/sdk/tests/EmailsTest.java @@ -0,0 +1,972 @@ +/************************************************* + * MailerSend Java SDK + * https://github.com/mailersend/mailersend-java + * + * @author MailerSend + * https://mailersend.com + **************************************************/ +package com.mailersend.sdk.tests; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandler; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; + +import com.mailersend.sdk.MailerSend; +import com.mailersend.sdk.emails.EmailActivity; +import com.mailersend.sdk.emails.EmailHeader; +import com.mailersend.sdk.emails.EmailInfo; +import com.mailersend.sdk.emails.EmailInteraction; +import com.mailersend.sdk.emails.EmailListItem; +import com.mailersend.sdk.emails.EmailStatus; +import com.mailersend.sdk.emails.Emails; +import com.mailersend.sdk.emails.EmailsList; +import com.mailersend.sdk.exceptions.MailerSendException; +import com.mailersend.sdk.util.MailerSendHttpClientFactory; +import com.mailersend.sdk.vcr.HttpClientVcr; +import com.mailersend.sdk.vcr.HttpClientVcrResponse; +import com.mailersend.sdk.vcr.VcrRecorder; + +/** + * Tests the emails list and single email endpoints, ms.emails().getEmails() and + * ms.emails().getEmail() + */ +public class EmailsTest { + + private static final String EMAILS_URL = "https://api.mailersend.com/v1/emails"; + + private static final String EMAIL_URL = "https://api.mailersend.com/v1/email/"; + + /** Fixed timestamps so that the request urls, and with them the fixture hashes, are stable */ + private static final long DATE_FROM = 1756256400L; + + private static final long DATE_TO = 1756342800L; + + /** The query part of the request url that the required filters below produce */ + private static final String REQUIRED_QUERY = "?domain_id=" + TestHelper.domainId + + "&date_from=" + DATE_FROM + "&date_to=" + DATE_TO; + + private static final String EMAIL_ID = "6a8fa9b1902fab56e0ce50dd"; + + private static final String SUPPRESSED_EMAIL_ID = "6a8fa9b1902fab56e0ce50ee"; + + private static final String BARE_EMAIL_ID = "6a8fa9b1902fab56e0ce50cc"; + + /** 2026-08-27 03:06:25 UTC, the created_at and updated_at of the emails in the fixtures */ + private static final long CREATED_AT = 1787799985000L; + + @BeforeEach + public void setupEach(TestInfo info) throws IOException { + + VcrRecorder.useRecording("EmailsTest_" + info.getDisplayName()); + } + + @AfterEach + public void afterEach() throws IOException { + + VcrRecorder.stopRecording(); + } + + + /* + * ---------------------------------------------------------------------------------- + * The request url and the filters + * ---------------------------------------------------------------------------------- + */ + + + /** + * Tests that getEmails() requests the emails endpoint with the domain id, the dates, + * the page and the limit in the query + */ + @Test + public void testGetEmailsRequestsTheEmailsEndpoint() { + + MailerSend ms = new MailerSend(); + ms.setToken(TestHelper.validToken); + + RequestCapturingClient client = captureRequests(); + + try { + + withRequiredFilters(ms).page(2).limit(50).getEmails(); + + } catch (MailerSendException e) { + + fail(e.getMessage()); + } + + assertEquals(1, client.requestedUris.size()); + + assertEquals(EMAILS_URL + REQUIRED_QUERY + "&limit=50&page=2", + client.lastRequestedUri().toString()); + } + + + /** + * Tests that the status and the interaction filters are serialized as repeated status[] and + * interaction[] parameters. The API validates both as arrays, a scalar status=sent is a 422 + */ + @Test + public void testStatusAndInteractionAreRepeatedArrayParams() { + + MailerSend ms = new MailerSend(); + ms.setToken(TestHelper.validToken); + + RequestCapturingClient client = captureRequests(); + + try { + + withRequiredFilters(ms) + .status(EmailStatus.SENT, EmailStatus.DELIVERED) + .interaction(EmailInteraction.OPENED, EmailInteraction.CLICKED) + .getEmails(); + + } catch (MailerSendException e) { + + fail(e.getMessage()); + } + + String query = client.lastRequestedUri().getQuery(); + + assertEquals("domain_id=" + TestHelper.domainId + + "&date_from=" + DATE_FROM + + "&date_to=" + DATE_TO + + "&status[]=sent&status[]=delivered" + + "&interaction[]=opened&interaction[]=clicked", query); + + assertTrue(query.contains("status[]=sent")); + assertTrue(query.contains("status[]=delivered")); + assertTrue(query.contains("interaction[]=opened")); + assertTrue(query.contains("interaction[]=clicked")); + + // the values must not be comma joined into a single scalar parameter + assertFalse(query.contains("status=sent")); + assertFalse(query.contains("interaction=opened")); + assertFalse(query.contains("sent,delivered")); + assertFalse(query.contains("opened,clicked")); + assertFalse(query.contains("%2C")); + assertFalse(query.matches(".*[?&]status=.*")); + assertFalse(query.matches(".*[?&]interaction=.*")); + } + + + /** + * Tests that a single status and a single interaction are still sent as array parameters + */ + @Test + public void testASingleStatusIsStillAnArrayParam() { + + MailerSend ms = new MailerSend(); + ms.setToken(TestHelper.validToken); + + RequestCapturingClient client = captureRequests(); + + try { + + withRequiredFilters(ms) + .status(EmailStatus.QUEUED) + .interaction(EmailInteraction.NO_INTERACTION) + .getEmails(); + + } catch (MailerSendException e) { + + fail(e.getMessage()); + } + + assertEquals(EMAILS_URL + REQUIRED_QUERY + + "&status[]=queued&interaction[]=no_interaction", + client.lastRequestedUri().toString()); + } + + + /** + * Tests that the optional filters are all sent, and url encoded, when they are set + */ + @Test + public void testOptionalFiltersArePresentWhenSet() { + + MailerSend ms = new MailerSend(); + ms.setToken(TestHelper.validToken); + + RequestCapturingClient client = captureRequests(); + + try { + + withRequiredFilters(ms) + .limit(100) + .recipientEmail("rcpt+tag@example.org") + .messageId("6a8fa9b1902fab56e0ce50aa") + .templateId("7nxe3yjmeq28vp0k") + .subject("Welcome friend") + .tag("news letter") + .getEmails(); + + } catch (MailerSendException e) { + + fail(e.getMessage()); + } + + String uri = client.lastRequestedUri().toString(); + + assertEquals(EMAILS_URL + REQUIRED_QUERY + + "&limit=100" + + "&recipient_email=rcpt%2Btag%40example.org" + + "&message_id=6a8fa9b1902fab56e0ce50aa" + + "&template_id=7nxe3yjmeq28vp0k" + + "&subject=Welcome+friend" + + "&tag=news+letter", uri); + } + + + /** + * Tests that none of the optional filters are sent when they are not set + */ + @Test + public void testOptionalFiltersAreAbsentWhenNotSet() { + + MailerSend ms = new MailerSend(); + ms.setToken(TestHelper.validToken); + + RequestCapturingClient client = captureRequests(); + + try { + + withRequiredFilters(ms).getEmails(); + + } catch (MailerSendException e) { + + fail(e.getMessage()); + } + + String uri = client.lastRequestedUri().toString(); + + assertEquals(EMAILS_URL + REQUIRED_QUERY, uri); + + assertFalse(uri.contains("limit=")); + assertFalse(uri.contains("page=")); + assertFalse(uri.contains("status")); + assertFalse(uri.contains("interaction")); + assertFalse(uri.contains("recipient_email")); + assertFalse(uri.contains("message_id")); + assertFalse(uri.contains("template_id")); + assertFalse(uri.contains("subject")); + assertFalse(uri.contains("tag")); + } + + + /** + * Tests that empty status and interaction filters add no parameters + */ + @Test + public void testEmptyStatusAndInteractionFiltersAddNoParams() { + + MailerSend ms = new MailerSend(); + ms.setToken(TestHelper.validToken); + + RequestCapturingClient client = captureRequests(); + + try { + + withRequiredFilters(ms).status().interaction().getEmails(); + + } catch (MailerSendException e) { + + fail(e.getMessage()); + } + + assertEquals(EMAILS_URL + REQUIRED_QUERY, client.lastRequestedUri().toString()); + } + + + /** + * Tests that the date filters accept java.util.Date objects and convert them to + * unix timestamps in seconds + */ + @Test + public void testDateFiltersAcceptDateObjects() { + + MailerSend ms = new MailerSend(); + ms.setToken(TestHelper.validToken); + + RequestCapturingClient client = captureRequests(); + + try { + + ms.emails() + .domainId(TestHelper.domainId) + // the milliseconds are dropped when converting to seconds + .dateFrom(new Date(DATE_FROM * 1000 + 987)) + .dateTo(new Date(DATE_TO * 1000)) + .getEmails(); + + } catch (MailerSendException e) { + + fail(e.getMessage()); + } + + assertEquals(EMAILS_URL + REQUIRED_QUERY, client.lastRequestedUri().toString()); + } + + + /** + * Tests that the filters stay set on the shared Emails instance between calls + */ + @Test + public void testFiltersPersistBetweenCalls() { + + MailerSend ms = new MailerSend(); + ms.setToken(TestHelper.validToken); + + // ms.emails() hands out the same object every time, which is what makes the filters persist + assertSame(ms.emails(), ms.emails()); + + RequestCapturingClient client = captureRequests(); + + try { + + withRequiredFilters(ms).tag("newsletter").getEmails(); + + // no filters are set again for the second call + ms.emails().getEmails(); + + } catch (MailerSendException e) { + + fail(e.getMessage()); + } + + assertEquals(2, client.requestedUris.size()); + + assertEquals(EMAILS_URL + REQUIRED_QUERY + "&tag=newsletter", + client.requestedUris.get(0).toString()); + + assertEquals(client.requestedUris.get(0).toString(), client.requestedUris.get(1).toString()); + + // a new MailerSend object starts with no filters at all + MailerSend other = new MailerSend(); + other.setToken(TestHelper.validToken); + + assertThrows(MailerSendException.class, () -> other.emails().getEmails()); + } + + + /** + * Tests that getEmails() requires a domain id + */ + @Test + public void testGetEmailsRequiresADomainId() { + + MailerSend ms = new MailerSend(); + ms.setToken(TestHelper.validToken); + + MailerSendException e = assertThrows(MailerSendException.class, () -> + ms.emails().dateFrom(DATE_FROM).dateTo(DATE_TO).getEmails()); + + assertEquals("A domain id is required.", e.getMessage()); + + MailerSendException blank = assertThrows(MailerSendException.class, () -> + ms.emails().domainId(" ").dateFrom(DATE_FROM).dateTo(DATE_TO).getEmails()); + + assertEquals("A domain id is required.", blank.getMessage()); + } + + + /** + * Tests that getEmails() requires both dates + */ + @Test + public void testGetEmailsRequiresBothDates() { + + MailerSend ms = new MailerSend(); + ms.setToken(TestHelper.validToken); + + MailerSendException e = assertThrows(MailerSendException.class, () -> + ms.emails().domainId(TestHelper.domainId).dateFrom(DATE_FROM).getEmails()); + + assertEquals("Date from and Date to dates are required.", e.getMessage()); + + MailerSendException nullDate = assertThrows(MailerSendException.class, () -> + ms.emails().domainId(TestHelper.domainId) + .dateFrom(DATE_FROM) + .dateTo((Date) null) + .getEmails()); + + assertEquals("Date from and Date to dates are required.", nullDate.getMessage()); + } + + + /** + * Tests that the date of the dateFrom filter can't be after or equal to the dateTo filter + */ + @Test + public void testDateFromAfterDateToThrows() { + + MailerSend ms = new MailerSend(); + ms.setToken(TestHelper.validToken); + + MailerSendException after = assertThrows(MailerSendException.class, () -> + ms.emails().domainId(TestHelper.domainId) + .dateFrom(DATE_TO) + .dateTo(DATE_FROM) + .getEmails()); + + assertEquals("From date cannot be after to date.", after.getMessage()); + + MailerSendException same = assertThrows(MailerSendException.class, () -> + ms.emails().domainId(TestHelper.domainId) + .dateFrom(DATE_FROM) + .dateTo(DATE_FROM) + .getEmails()); + + assertEquals("From date cannot be after to date.", same.getMessage()); + } + + + /** + * Tests that getEmail() requests the singular email endpoint + */ + @Test + public void testGetEmailRequestsTheSingularEmailEndpoint() { + + MailerSend ms = new MailerSend(); + ms.setToken(TestHelper.validToken); + + RequestCapturingClient client = captureRequests(); + client.responseBody = "{\"data\":{\"id\":\"" + EMAIL_ID + "\"}}"; + + try { + + ms.emails().getEmail(EMAIL_ID); + + } catch (MailerSendException e) { + + fail(e.getMessage()); + } + + assertEquals("https://api.mailersend.com/v1/email/" + EMAIL_ID, + client.lastRequestedUri().toString()); + + // the endpoint is singular, the list endpoint is the plural one + assertFalse(client.lastRequestedUri().getPath().startsWith("/v1/emails")); + } + + + /* + * ---------------------------------------------------------------------------------- + * The emails list response + * ---------------------------------------------------------------------------------- + */ + + + /** + * Tests that a populated results page deserializes into an EmailsList + */ + @Test + public void testGetEmailsParsesThePopulatedEnvelope() { + + MailerSend ms = new MailerSend(); + ms.setToken(TestHelper.validToken); + + try { + + EmailsList emails = withRequiredFilters(ms).getEmails(); + + assertNotNull(emails.emails); + assertEquals(1, emails.emails.length); + + EmailListItem email = emails.emails[0]; + + assertEquals(EMAIL_ID, email.id); + assertEquals("sender@example.com", email.from); + assertEquals("rcpt@example.org", email.to); + assertEquals("Welcome", email.subject); + + // the list endpoint never returns the content of an email + assertNull(email.text); + assertNull(email.html); + + assertEquals("7nxe3yjmeq28vp0k", email.templateId); + assertEquals("7nxe3yjmeq28vp0k", email.domainId); + assertEquals("6a8fa9b1902fab56e0ce50aa", email.messageId); + assertEquals(EmailStatus.SENT, email.status); + + assertNotNull(email.tags); + assertEquals(1, email.tags.length); + assertEquals("newsletter", email.tags[0]); + + assertNotNull(email.interaction); + assertEquals(1, email.interaction.length); + assertEquals(EmailInteraction.OPENED, email.interaction[0]); + + assertNull(email.suppressionReason); + + // the email was sent without custom headers + assertNull(email.headers); + + assertNotNull(email.createdAt); + assertEquals(CREATED_AT, email.createdAt.getTime()); + assertNotNull(email.updatedAt); + assertEquals(CREATED_AT, email.updatedAt.getTime()); + + // the meta of the response + assertNotNull(emails.meta); + assertEquals(1, emails.meta.currentPage); + assertEquals(EMAILS_URL + "?page=1", emails.meta.currentPageUrl); + assertEquals(1, emails.meta.from); + assertEquals(EMAILS_URL, emails.meta.path); + assertEquals(10, emails.meta.limit); + assertEquals(3, emails.meta.to); + + // the endpoint pages without a count, so it returns no last_page and meta.lastPage + // stays at the default of 0. Do not use it to detect the end of the results + assertEquals(0, emails.meta.lastPage); + + // the links of the response + assertNotNull(emails.links); + assertEquals(EMAILS_URL + "?page=1", emails.links.first); + assertNull(emails.links.last); + assertNull(emails.links.prev); + assertNull(emails.links.next); + + assertEquals(1, emails.getCurrentPage()); + assertFalse(emails.hasNext()); + assertFalse(emails.hasPrevious()); + assertNull(emails.next()); + assertNull(emails.previous()); + + assertEquals(200, emails.responseStatusCode); + + } catch (MailerSendException e) { + + fail(e.getMessage()); + } + } + + + /** + * Tests that EmailDates parses both of the date formats it accepts. + * + * The second email of this fixture holds its dates in the database format, + * "2026-08-27 03:06:25", on purpose. The API itself returns the ISO-8601 format that the first + * email holds, and so does every other fixture of this test class. Do not "fix" this one, + * it is what covers the fallback of EmailDates + */ + @Test + public void testEmailDatesParseBothApiFormats() { + + MailerSend ms = new MailerSend(); + ms.setToken(TestHelper.validToken); + + try { + + EmailsList emails = withRequiredFilters(ms).getEmails(); + + assertEquals(2, emails.emails.length); + + // "2026-08-27T03:06:25.000000Z", the format the API returns + assertEquals(CREATED_AT, emails.emails[0].createdAt.getTime()); + assertEquals(CREATED_AT, emails.emails[0].updatedAt.getTime()); + + // "2026-08-27 03:06:25", the database format, assumed to be UTC + assertEquals(CREATED_AT, emails.emails[1].createdAt.getTime()); + assertEquals(CREATED_AT, emails.emails[1].updatedAt.getTime()); + + assertEquals(emails.emails[0].createdAt, emails.emails[1].createdAt); + + } catch (MailerSendException e) { + + fail(e.getMessage()); + } + } + + + /** + * Tests that the custom headers of an email are parsed. The API returns them as an array of + * name and value objects, the same shape that Email.addHeader() sends them in + * + * The fixture holds the response of the API verbatim + */ + @Test + public void testEmailListItemParsesCustomHeaders() { + + MailerSend ms = new MailerSend(); + ms.setToken(TestHelper.validToken); + + try { + + EmailsList emails = withRequiredFilters(ms).getEmails(); + + EmailListItem email = emails.emails[0]; + + assertNotNull(email.headers); + assertEquals(1, email.headers.length); + + EmailHeader header = email.headers[0]; + + assertEquals("X-Custom", header.name); + assertEquals("foo", header.value); + + } catch (MailerSendException e) { + + fail(e.getMessage()); + } + } + + + /** + * Tests that an empty results page deserializes into an EmailsList + */ + @Test + public void testGetEmailsParsesTheEmptyEnvelope() { + + MailerSend ms = new MailerSend(); + ms.setToken(TestHelper.validToken); + + try { + + EmailsList emails = withRequiredFilters(ms).page(2).getEmails(); + + assertNotNull(emails.emails); + assertEquals(0, emails.emails.length); + + assertEquals(2, emails.getCurrentPage()); + assertEquals(2, emails.meta.currentPage); + + // the API returns from and to as null on an empty page, which lands on the int default + assertEquals(0, emails.meta.from); + assertEquals(0, emails.meta.to); + assertEquals(10, emails.meta.limit); + + assertNull(emails.links.next); + assertEquals(EMAILS_URL + "?page=1", emails.links.prev); + + assertFalse(emails.hasNext()); + assertTrue(emails.hasPrevious()); + assertNull(emails.next()); + + } catch (MailerSendException e) { + + fail(e.getMessage()); + } + } + + + /** + * Tests that next() requests the following page and returns null once there is none + */ + @Test + public void testNextReturnsTheFollowingPage() { + + MailerSend ms = new MailerSend(); + ms.setToken(TestHelper.validToken); + + try { + + EmailsList firstPage = withRequiredFilters(ms).getEmails(); + + assertEquals(1, firstPage.getCurrentPage()); + assertEquals(EMAIL_ID, firstPage.emails[0].id); + + // there is a next page even though the endpoint returns no last_page, + // hasNext() reads links.next + assertEquals(0, firstPage.meta.lastPage); + assertTrue(firstPage.hasNext()); + assertFalse(firstPage.hasPrevious()); + + EmailsList secondPage = firstPage.next(); + + assertNotNull(secondPage); + assertEquals(2, secondPage.getCurrentPage()); + assertEquals(1, secondPage.emails.length); + assertEquals("6a8fa9b1902fab56e0ce50ff", secondPage.emails[0].id); + + assertTrue(secondPage.hasPrevious()); + assertFalse(secondPage.hasNext()); + + // the last page has no next page + assertNull(secondPage.next()); + + } catch (MailerSendException e) { + + fail(e.getMessage()); + } + } + + + /** + * Tests that previous() requests the preceding page and returns null once there is none + */ + @Test + public void testPreviousReturnsThePrecedingPage() { + + MailerSend ms = new MailerSend(); + ms.setToken(TestHelper.validToken); + + try { + + EmailsList secondPage = withRequiredFilters(ms).page(2).getEmails(); + + assertEquals(2, secondPage.getCurrentPage()); + assertEquals("6a8fa9b1902fab56e0ce50ff", secondPage.emails[0].id); + assertTrue(secondPage.hasPrevious()); + + EmailsList firstPage = secondPage.previous(); + + assertNotNull(firstPage); + assertEquals(1, firstPage.getCurrentPage()); + assertEquals(1, firstPage.emails.length); + assertEquals(EMAIL_ID, firstPage.emails[0].id); + + assertFalse(firstPage.hasPrevious()); + assertTrue(firstPage.hasNext()); + + // the first page has no previous page + assertNull(firstPage.previous()); + + } catch (MailerSendException e) { + + fail(e.getMessage()); + } + } + + + /* + * ---------------------------------------------------------------------------------- + * The single email response + * ---------------------------------------------------------------------------------- + */ + + + /** + * Tests that a single email deserializes into an EmailInfo, together with its recipient + * and its activity events + */ + @Test + public void testGetEmailParsesTheSingleEmail() { + + MailerSend ms = new MailerSend(); + ms.setToken(TestHelper.validToken); + + try { + + EmailInfo email = ms.emails().getEmail(EMAIL_ID); + + assertNotNull(email); + assertEquals(EMAIL_ID, email.id); + assertEquals("sender@example.com", email.from); + assertEquals("rcpt@example.org", email.to); + assertEquals("Welcome", email.subject); + + // unlike the list endpoint, the single email endpoint returns the content + assertEquals("Hello there", email.text); + assertEquals("

Hello there

", email.html); + + assertEquals("7nxe3yjmeq28vp0k", email.templateId); + assertEquals("7nxe3yjmeq28vp0k", email.domainId); + assertEquals("6a8fa9b1902fab56e0ce50aa", email.messageId); + assertEquals(EmailStatus.SENT, email.status); + assertEquals("newsletter", email.tags[0]); + assertEquals(EmailInteraction.OPENED, email.interaction[0]); + assertNull(email.suppressionReason); + + // the custom headers the email was sent with, in the order the API returns them + assertNotNull(email.headers); + assertEquals(2, email.headers.length); + assertEquals("X-Custom", email.headers[0].name); + assertEquals("foo", email.headers[0].value); + assertEquals("X-Entity-Ref-ID", email.headers[1].name); + assertEquals("abc-123", email.headers[1].value); + + assertEquals(CREATED_AT, email.createdAt.getTime()); + assertEquals(CREATED_AT, email.updatedAt.getTime()); + + // the recipient of the email + assertNotNull(email.recipient); + assertEquals("6a8fa9b1902fab56e0ce50bb", email.recipient.id); + assertEquals("rcpt@example.org", email.recipient.email); + assertEquals(1787220672000L, email.recipient.createdAt.getTime()); + assertEquals(1787307072000L, email.recipient.updatedAt.getTime()); + assertNull(email.recipient.deletedAt); + + // the activity events of the email, newest first + assertNotNull(email.activity); + assertEquals(3, email.activity.length); + + EmailActivity opened = email.activity[0]; + assertEquals("6a8fa9b1902fab56e0ce5003", opened.id); + assertEquals("opened", opened.type); + assertEquals(1787800020000L, opened.createdAt.getTime()); + assertNull(opened.suppressionReason); + + assertEquals("delivered", email.activity[1].type); + assertEquals(1787799990000L, email.activity[1].createdAt.getTime()); + + assertEquals("sent", email.activity[2].type); + assertEquals(CREATED_AT, email.activity[2].createdAt.getTime()); + + } catch (MailerSendException e) { + + fail(e.getMessage()); + } + } + + + /** + * Tests that a rejected email parses its suppression reason, both on the email and on the + * suppressed event + */ + @Test + public void testGetEmailParsesASuppressedEmail() { + + MailerSend ms = new MailerSend(); + ms.setToken(TestHelper.validToken); + + try { + + EmailInfo email = ms.emails().getEmail(SUPPRESSED_EMAIL_ID); + + assertEquals(SUPPRESSED_EMAIL_ID, email.id); + assertEquals(EmailStatus.REJECTED, email.status); + assertEquals("hard_bounced", email.suppressionReason); + assertEquals("blocked@example.org", email.to); + + // no template was used, and no tags or custom headers were sent + assertNull(email.templateId); + assertNull(email.tags); + assertNull(email.headers); + + assertNotNull(email.interaction); + assertEquals(0, email.interaction.length); + + assertEquals(2, email.activity.length); + assertEquals("suppressed", email.activity[0].type); + assertEquals("hard_bounced", email.activity[0].suppressionReason); + assertEquals(CREATED_AT, email.activity[0].createdAt.getTime()); + + assertEquals("queued", email.activity[1].type); + assertNull(email.activity[1].suppressionReason); + + } catch (MailerSendException e) { + + fail(e.getMessage()); + } + } + + + /** + * Tests that an email without activity events and without a recipient does not blow up + */ + @Test + public void testGetEmailWithoutActivityReturnsAnEmptyArray() { + + MailerSend ms = new MailerSend(); + ms.setToken(TestHelper.validToken); + + try { + + EmailInfo email = ms.emails().getEmail(BARE_EMAIL_ID); + + assertEquals(BARE_EMAIL_ID, email.id); + assertEquals(EmailStatus.QUEUED, email.status); + + assertNotNull(email.activity); + assertEquals(0, email.activity.length); + + assertNull(email.recipient); + assertNull(email.tags); + assertNull(email.interaction); + assertNull(email.text); + assertNull(email.html); + + assertEquals(CREATED_AT, email.createdAt.getTime()); + + } catch (MailerSendException e) { + + fail(e.getMessage()); + } + } + + + /* + * ---------------------------------------------------------------------------------- + * Helpers + * ---------------------------------------------------------------------------------- + */ + + + /** + * Sets the filters that getEmails() requires + */ + private Emails withRequiredFilters(MailerSend ms) { + + return ms.emails() + .domainId(TestHelper.domainId) + .dateFrom(DATE_FROM) + .dateTo(DATE_TO); + } + + + /** + * Replaces the recording client with one that captures the request urls, for the tests that + * assert what the SDK requests instead of how it parses the response + */ + private RequestCapturingClient captureRequests() { + + RequestCapturingClient client = new RequestCapturingClient(); + + MailerSendHttpClientFactory.getInstance().setClient(client); + + return client; + } + + + /** + * Records the urls the SDK requests and answers each of them with a canned response + */ + private static class RequestCapturingClient extends HttpClientVcr { + + private final List requestedUris = new ArrayList(); + + private String responseBody = "{\"data\":[],\"links\":{},\"meta\":{\"current_page\":1}}"; + + private URI lastRequestedUri() { + + assertFalse(requestedUris.isEmpty(), "no request was made"); + + return requestedUris.get(requestedUris.size() - 1); + } + + @SuppressWarnings("unchecked") + @Override + public HttpResponse send(HttpRequest request, BodyHandler responseBodyHandler) { + + requestedUris.add(request.uri()); + + HttpClientVcrResponse response = new HttpClientVcrResponse(); + response.body = responseBody; + response.headers = Map.of("content-type", List.of("application/json")); + response.statusCode = 200; + + return (HttpResponse) response; + } + } +} diff --git a/src/test/resources/fixtures/EmailsTest_testEmailDatesParseBothApiFormats().json b/src/test/resources/fixtures/EmailsTest_testEmailDatesParseBothApiFormats().json new file mode 100644 index 0000000..707ea81 --- /dev/null +++ b/src/test/resources/fixtures/EmailsTest_testEmailDatesParseBothApiFormats().json @@ -0,0 +1 @@ +{"9ad2510f4e937ce1fa04ba034b5b966b60dfef43":{"body":"{\"data\":[{\"id\":\"6a8fa9b1902fab56e0ce50dd\",\"from\":\"sender@example.com\",\"to\":\"rcpt@example.org\",\"subject\":\"Welcome\",\"text\":null,\"html\":null,\"template_id\":\"7nxe3yjmeq28vp0k\",\"domain_id\":\"7nxe3yjmeq28vp0k\",\"message_id\":\"6a8fa9b1902fab56e0ce50aa\",\"status\":\"sent\",\"tags\":[\"newsletter\"],\"interaction\":[\"opened\"],\"suppression_reason\":null,\"created_at\":\"2026-08-27T03:06:25.000000Z\",\"updated_at\":\"2026-08-27T03:06:25.000000Z\",\"headers\":null},{\"id\":\"6a8fa9b1902fab56e0ce50d1\",\"from\":\"sender@example.com\",\"to\":\"rcpt@example.org\",\"subject\":\"Welcome\",\"text\":null,\"html\":null,\"template_id\":\"7nxe3yjmeq28vp0k\",\"domain_id\":\"7nxe3yjmeq28vp0k\",\"message_id\":\"6a8fa9b1902fab56e0ce50aa\",\"status\":\"sent\",\"tags\":[\"newsletter\"],\"interaction\":[\"opened\"],\"suppression_reason\":null,\"created_at\":\"2026-08-27 03:06:25\",\"updated_at\":\"2026-08-27 03:06:25\",\"headers\":null}],\"links\":{\"first\":\"https://api.mailersend.com/v1/emails?page=1\",\"last\":null,\"prev\":null,\"next\":null},\"meta\":{\"current_page\":1,\"current_page_url\":\"https://api.mailersend.com/v1/emails?page=1\",\"from\":1,\"path\":\"https://api.mailersend.com/v1/emails\",\"per_page\":10,\"to\":2}}","headers":{":status":["200"],"cache-control":["no-cache, private"],"content-type":["application/json"],"x-ratelimit-limit":["60"],"x-ratelimit-remaining":["59"]},"statusCode":200}} \ No newline at end of file diff --git a/src/test/resources/fixtures/EmailsTest_testEmailListItemParsesCustomHeaders().json b/src/test/resources/fixtures/EmailsTest_testEmailListItemParsesCustomHeaders().json new file mode 100644 index 0000000..f3001f4 --- /dev/null +++ b/src/test/resources/fixtures/EmailsTest_testEmailListItemParsesCustomHeaders().json @@ -0,0 +1 @@ +{"9ad2510f4e937ce1fa04ba034b5b966b60dfef43":{"body":"{\"data\":[{\"id\":\"6a8fa9b1902fab56e0ce50dd\",\"from\":\"sender@example.com\",\"to\":\"rcpt@example.org\",\"subject\":\"Welcome\",\"text\":null,\"html\":null,\"template_id\":\"7nxe3yjmeq28vp0k\",\"domain_id\":\"7nxe3yjmeq28vp0k\",\"message_id\":\"6a8fa9b1902fab56e0ce50aa\",\"status\":\"sent\",\"tags\":[\"newsletter\"],\"interaction\":[\"opened\"],\"suppression_reason\":null,\"created_at\":\"2026-08-27T03:06:25.000000Z\",\"updated_at\":\"2026-08-27T03:06:25.000000Z\",\"headers\":[{\"name\":\"X-Custom\",\"value\":\"foo\"}]}],\"links\":{\"first\":\"https://api.mailersend.com/v1/emails?page=1\",\"last\":null,\"prev\":null,\"next\":null},\"meta\":{\"current_page\":1,\"current_page_url\":\"https://api.mailersend.com/v1/emails?page=1\",\"from\":1,\"path\":\"https://api.mailersend.com/v1/emails\",\"per_page\":10,\"to\":3}}","headers":{":status":["200"],"cache-control":["no-cache, private"],"content-type":["application/json"],"x-ratelimit-limit":["60"],"x-ratelimit-remaining":["59"]},"statusCode":200}} \ No newline at end of file diff --git a/src/test/resources/fixtures/EmailsTest_testGetEmailParsesASuppressedEmail().json b/src/test/resources/fixtures/EmailsTest_testGetEmailParsesASuppressedEmail().json new file mode 100644 index 0000000..1a411d6 --- /dev/null +++ b/src/test/resources/fixtures/EmailsTest_testGetEmailParsesASuppressedEmail().json @@ -0,0 +1 @@ +{"e49965373f878d03054561dd5bce6309af05535a":{"body":"{\"data\":{\"id\":\"6a8fa9b1902fab56e0ce50ee\",\"from\":\"sender@example.com\",\"to\":\"blocked@example.org\",\"subject\":\"Welcome\",\"text\":null,\"html\":null,\"template_id\":null,\"domain_id\":\"7nxe3yjmeq28vp0k\",\"message_id\":\"6a8fa9b1902fab56e0ce50aa\",\"status\":\"rejected\",\"tags\":null,\"interaction\":[],\"suppression_reason\":\"hard_bounced\",\"created_at\":\"2026-08-27T03:06:25.000000Z\",\"updated_at\":\"2026-08-27T03:06:25.000000Z\",\"recipient\":{\"id\":\"6a8fa9b1902fab56e0ce50bc\",\"email\":\"blocked@example.org\",\"created_at\":\"2026-08-20T10:11:12.000000Z\",\"updated_at\":\"2026-08-21T10:11:12.000000Z\",\"deleted_at\":\"\"},\"headers\":null,\"activity\":[{\"id\":\"6a8fa9b1902fab56e0ce5011\",\"type\":\"suppressed\",\"created_at\":\"2026-08-27T03:06:25.000000Z\",\"suppression_reason\":\"hard_bounced\"},{\"id\":\"6a8fa9b1902fab56e0ce5010\",\"type\":\"queued\",\"created_at\":\"2026-08-27T03:06:25.000000Z\"}]}}","headers":{":status":["200"],"cache-control":["no-cache, private"],"content-type":["application/json"],"x-ratelimit-limit":["60"],"x-ratelimit-remaining":["59"]},"statusCode":200}} \ No newline at end of file diff --git a/src/test/resources/fixtures/EmailsTest_testGetEmailParsesTheSingleEmail().json b/src/test/resources/fixtures/EmailsTest_testGetEmailParsesTheSingleEmail().json new file mode 100644 index 0000000..f638727 --- /dev/null +++ b/src/test/resources/fixtures/EmailsTest_testGetEmailParsesTheSingleEmail().json @@ -0,0 +1 @@ +{"92efe2d08257b7f65816584171727dec820f3f87":{"body":"{\"data\":{\"id\":\"6a8fa9b1902fab56e0ce50dd\",\"from\":\"sender@example.com\",\"to\":\"rcpt@example.org\",\"subject\":\"Welcome\",\"text\":\"Hello there\",\"html\":\"

Hello there

\",\"template_id\":\"7nxe3yjmeq28vp0k\",\"domain_id\":\"7nxe3yjmeq28vp0k\",\"message_id\":\"6a8fa9b1902fab56e0ce50aa\",\"status\":\"sent\",\"tags\":[\"newsletter\"],\"interaction\":[\"opened\"],\"suppression_reason\":null,\"created_at\":\"2026-08-27T03:06:25.000000Z\",\"updated_at\":\"2026-08-27T03:06:25.000000Z\",\"recipient\":{\"id\":\"6a8fa9b1902fab56e0ce50bb\",\"email\":\"rcpt@example.org\",\"created_at\":\"2026-08-20T10:11:12.000000Z\",\"updated_at\":\"2026-08-21T10:11:12.000000Z\",\"deleted_at\":\"\"},\"headers\":[{\"name\":\"X-Custom\",\"value\":\"foo\"},{\"name\":\"X-Entity-Ref-ID\",\"value\":\"abc-123\"}],\"activity\":[{\"id\":\"6a8fa9b1902fab56e0ce5003\",\"type\":\"opened\",\"created_at\":\"2026-08-27T03:07:00.000000Z\"},{\"id\":\"6a8fa9b1902fab56e0ce5002\",\"type\":\"delivered\",\"created_at\":\"2026-08-27T03:06:30.000000Z\"},{\"id\":\"6a8fa9b1902fab56e0ce5001\",\"type\":\"sent\",\"created_at\":\"2026-08-27T03:06:25.000000Z\"}]}}","headers":{":status":["200"],"cache-control":["no-cache, private"],"content-type":["application/json"],"x-ratelimit-limit":["60"],"x-ratelimit-remaining":["59"]},"statusCode":200}} \ No newline at end of file diff --git a/src/test/resources/fixtures/EmailsTest_testGetEmailWithoutActivityReturnsAnEmptyArray().json b/src/test/resources/fixtures/EmailsTest_testGetEmailWithoutActivityReturnsAnEmptyArray().json new file mode 100644 index 0000000..5382793 --- /dev/null +++ b/src/test/resources/fixtures/EmailsTest_testGetEmailWithoutActivityReturnsAnEmptyArray().json @@ -0,0 +1 @@ +{"e133ad2f7c99b6bef12743a5b5cdf97daaac1e1c":{"body":"{\"data\":{\"id\":\"6a8fa9b1902fab56e0ce50cc\",\"from\":\"sender@example.com\",\"to\":\"rcpt@example.org\",\"subject\":\"Welcome\",\"status\":\"queued\",\"domain_id\":\"7nxe3yjmeq28vp0k\",\"created_at\":\"2026-08-27T03:06:25.000000Z\",\"updated_at\":\"2026-08-27T03:06:25.000000Z\"}}","headers":{":status":["200"],"cache-control":["no-cache, private"],"content-type":["application/json"],"x-ratelimit-limit":["60"],"x-ratelimit-remaining":["59"]},"statusCode":200}} \ No newline at end of file diff --git a/src/test/resources/fixtures/EmailsTest_testGetEmailsParsesTheEmptyEnvelope().json b/src/test/resources/fixtures/EmailsTest_testGetEmailsParsesTheEmptyEnvelope().json new file mode 100644 index 0000000..618a6b1 --- /dev/null +++ b/src/test/resources/fixtures/EmailsTest_testGetEmailsParsesTheEmptyEnvelope().json @@ -0,0 +1 @@ +{"c5fe2ff6bc77bcc9890375746a22e42abf8020d8":{"body":"{\"data\":[],\"links\":{\"first\":\"https://api.mailersend.com/v1/emails?page=1\",\"last\":null,\"prev\":\"https://api.mailersend.com/v1/emails?page=1\",\"next\":null},\"meta\":{\"current_page\":2,\"current_page_url\":\"https://api.mailersend.com/v1/emails?page=2\",\"from\":null,\"path\":\"https://api.mailersend.com/v1/emails\",\"per_page\":10,\"to\":null}}","headers":{":status":["200"],"cache-control":["no-cache, private"],"content-type":["application/json"],"x-ratelimit-limit":["60"],"x-ratelimit-remaining":["59"]},"statusCode":200}} \ No newline at end of file diff --git a/src/test/resources/fixtures/EmailsTest_testGetEmailsParsesThePopulatedEnvelope().json b/src/test/resources/fixtures/EmailsTest_testGetEmailsParsesThePopulatedEnvelope().json new file mode 100644 index 0000000..8ef77f3 --- /dev/null +++ b/src/test/resources/fixtures/EmailsTest_testGetEmailsParsesThePopulatedEnvelope().json @@ -0,0 +1 @@ +{"9ad2510f4e937ce1fa04ba034b5b966b60dfef43":{"body":"{\"data\":[{\"id\":\"6a8fa9b1902fab56e0ce50dd\",\"from\":\"sender@example.com\",\"to\":\"rcpt@example.org\",\"subject\":\"Welcome\",\"text\":null,\"html\":null,\"template_id\":\"7nxe3yjmeq28vp0k\",\"domain_id\":\"7nxe3yjmeq28vp0k\",\"message_id\":\"6a8fa9b1902fab56e0ce50aa\",\"status\":\"sent\",\"tags\":[\"newsletter\"],\"interaction\":[\"opened\"],\"suppression_reason\":null,\"created_at\":\"2026-08-27T03:06:25.000000Z\",\"updated_at\":\"2026-08-27T03:06:25.000000Z\",\"headers\":null}],\"links\":{\"first\":\"https://api.mailersend.com/v1/emails?page=1\",\"last\":null,\"prev\":null,\"next\":null},\"meta\":{\"current_page\":1,\"current_page_url\":\"https://api.mailersend.com/v1/emails?page=1\",\"from\":1,\"path\":\"https://api.mailersend.com/v1/emails\",\"per_page\":10,\"to\":3}}","headers":{":status":["200"],"cache-control":["no-cache, private"],"content-type":["application/json"],"x-ratelimit-limit":["60"],"x-ratelimit-remaining":["59"]},"statusCode":200}} \ No newline at end of file diff --git a/src/test/resources/fixtures/EmailsTest_testNextReturnsTheFollowingPage().json b/src/test/resources/fixtures/EmailsTest_testNextReturnsTheFollowingPage().json new file mode 100644 index 0000000..2bf1820 --- /dev/null +++ b/src/test/resources/fixtures/EmailsTest_testNextReturnsTheFollowingPage().json @@ -0,0 +1 @@ +{"9ad2510f4e937ce1fa04ba034b5b966b60dfef43":{"body":"{\"data\":[{\"id\":\"6a8fa9b1902fab56e0ce50dd\",\"from\":\"sender@example.com\",\"to\":\"rcpt@example.org\",\"subject\":\"Welcome\",\"text\":null,\"html\":null,\"template_id\":\"7nxe3yjmeq28vp0k\",\"domain_id\":\"7nxe3yjmeq28vp0k\",\"message_id\":\"6a8fa9b1902fab56e0ce50aa\",\"status\":\"sent\",\"tags\":[\"newsletter\"],\"interaction\":[\"opened\"],\"suppression_reason\":null,\"created_at\":\"2026-08-27T03:06:25.000000Z\",\"updated_at\":\"2026-08-27T03:06:25.000000Z\",\"headers\":null}],\"links\":{\"first\":\"https://api.mailersend.com/v1/emails?page=1\",\"last\":null,\"prev\":null,\"next\":\"https://api.mailersend.com/v1/emails?page=2\"},\"meta\":{\"current_page\":1,\"current_page_url\":\"https://api.mailersend.com/v1/emails?page=1\",\"from\":1,\"path\":\"https://api.mailersend.com/v1/emails\",\"per_page\":1,\"to\":1}}","headers":{":status":["200"],"cache-control":["no-cache, private"],"content-type":["application/json"],"x-ratelimit-limit":["60"],"x-ratelimit-remaining":["59"]},"statusCode":200},"c5fe2ff6bc77bcc9890375746a22e42abf8020d8":{"body":"{\"data\":[{\"id\":\"6a8fa9b1902fab56e0ce50ff\",\"from\":\"sender@example.com\",\"to\":\"rcpt@example.org\",\"subject\":\"Welcome\",\"text\":null,\"html\":null,\"template_id\":\"7nxe3yjmeq28vp0k\",\"domain_id\":\"7nxe3yjmeq28vp0k\",\"message_id\":\"6a8fa9b1902fab56e0ce50aa\",\"status\":\"sent\",\"tags\":[\"newsletter\"],\"interaction\":[\"opened\"],\"suppression_reason\":null,\"created_at\":\"2026-08-27T03:06:25.000000Z\",\"updated_at\":\"2026-08-27T03:06:25.000000Z\",\"headers\":null}],\"links\":{\"first\":\"https://api.mailersend.com/v1/emails?page=1\",\"last\":null,\"prev\":\"https://api.mailersend.com/v1/emails?page=1\",\"next\":null},\"meta\":{\"current_page\":2,\"current_page_url\":\"https://api.mailersend.com/v1/emails?page=2\",\"from\":2,\"path\":\"https://api.mailersend.com/v1/emails\",\"per_page\":1,\"to\":2}}","headers":{":status":["200"],"cache-control":["no-cache, private"],"content-type":["application/json"],"x-ratelimit-limit":["60"],"x-ratelimit-remaining":["59"]},"statusCode":200}} \ No newline at end of file diff --git a/src/test/resources/fixtures/EmailsTest_testPreviousReturnsThePrecedingPage().json b/src/test/resources/fixtures/EmailsTest_testPreviousReturnsThePrecedingPage().json new file mode 100644 index 0000000..6c5281d --- /dev/null +++ b/src/test/resources/fixtures/EmailsTest_testPreviousReturnsThePrecedingPage().json @@ -0,0 +1 @@ +{"c5fe2ff6bc77bcc9890375746a22e42abf8020d8":{"body":"{\"data\":[{\"id\":\"6a8fa9b1902fab56e0ce50ff\",\"from\":\"sender@example.com\",\"to\":\"rcpt@example.org\",\"subject\":\"Welcome\",\"text\":null,\"html\":null,\"template_id\":\"7nxe3yjmeq28vp0k\",\"domain_id\":\"7nxe3yjmeq28vp0k\",\"message_id\":\"6a8fa9b1902fab56e0ce50aa\",\"status\":\"sent\",\"tags\":[\"newsletter\"],\"interaction\":[\"opened\"],\"suppression_reason\":null,\"created_at\":\"2026-08-27T03:06:25.000000Z\",\"updated_at\":\"2026-08-27T03:06:25.000000Z\",\"headers\":null}],\"links\":{\"first\":\"https://api.mailersend.com/v1/emails?page=1\",\"last\":null,\"prev\":\"https://api.mailersend.com/v1/emails?page=1\",\"next\":null},\"meta\":{\"current_page\":2,\"current_page_url\":\"https://api.mailersend.com/v1/emails?page=2\",\"from\":2,\"path\":\"https://api.mailersend.com/v1/emails\",\"per_page\":1,\"to\":2}}","headers":{":status":["200"],"cache-control":["no-cache, private"],"content-type":["application/json"],"x-ratelimit-limit":["60"],"x-ratelimit-remaining":["59"]},"statusCode":200},"eaed65bec2e2175a28bd4c3761c1eb0390486db3":{"body":"{\"data\":[{\"id\":\"6a8fa9b1902fab56e0ce50dd\",\"from\":\"sender@example.com\",\"to\":\"rcpt@example.org\",\"subject\":\"Welcome\",\"text\":null,\"html\":null,\"template_id\":\"7nxe3yjmeq28vp0k\",\"domain_id\":\"7nxe3yjmeq28vp0k\",\"message_id\":\"6a8fa9b1902fab56e0ce50aa\",\"status\":\"sent\",\"tags\":[\"newsletter\"],\"interaction\":[\"opened\"],\"suppression_reason\":null,\"created_at\":\"2026-08-27T03:06:25.000000Z\",\"updated_at\":\"2026-08-27T03:06:25.000000Z\",\"headers\":null}],\"links\":{\"first\":\"https://api.mailersend.com/v1/emails?page=1\",\"last\":null,\"prev\":null,\"next\":\"https://api.mailersend.com/v1/emails?page=2\"},\"meta\":{\"current_page\":1,\"current_page_url\":\"https://api.mailersend.com/v1/emails?page=1\",\"from\":1,\"path\":\"https://api.mailersend.com/v1/emails\",\"per_page\":1,\"to\":1}}","headers":{":status":["200"],"cache-control":["no-cache, private"],"content-type":["application/json"],"x-ratelimit-limit":["60"],"x-ratelimit-remaining":["59"]},"statusCode":200}} \ No newline at end of file