From 929f573632daba40ecdd53733e25012a147ae121 Mon Sep 17 00:00:00 2001 From: Piotr Bednarz Date: Tue, 10 Mar 2026 14:58:16 +0100 Subject: [PATCH] Add Stats API --- .../examples/general/StatsExample.java | 43 ++++ .../java/io/mailtrap/api/stats/Stats.java | 57 ++++++ .../io/mailtrap/api/stats/StatsFilter.java | 21 ++ .../java/io/mailtrap/api/stats/StatsImpl.java | 92 +++++++++ .../client/api/MailtrapGeneralApi.java | 2 + .../factory/MailtrapClientFactory.java | 4 +- .../stats/SendingStatGroupResponse.java | 20 ++ .../response/stats/SendingStatsResponse.java | 38 ++++ .../io/mailtrap/api/stats/StatsImplTest.java | 189 ++++++++++++++++++ src/test/resources/api/stats/byCategory.json | 17 ++ src/test/resources/api/stats/byDate.json | 17 ++ src/test/resources/api/stats/byDomain.json | 32 +++ .../api/stats/byEmailServiceProvider.json | 17 ++ src/test/resources/api/stats/getStats.json | 12 ++ 14 files changed, 560 insertions(+), 1 deletion(-) create mode 100644 examples/java/io/mailtrap/examples/general/StatsExample.java create mode 100644 src/main/java/io/mailtrap/api/stats/Stats.java create mode 100644 src/main/java/io/mailtrap/api/stats/StatsFilter.java create mode 100644 src/main/java/io/mailtrap/api/stats/StatsImpl.java create mode 100644 src/main/java/io/mailtrap/model/response/stats/SendingStatGroupResponse.java create mode 100644 src/main/java/io/mailtrap/model/response/stats/SendingStatsResponse.java create mode 100644 src/test/java/io/mailtrap/api/stats/StatsImplTest.java create mode 100644 src/test/resources/api/stats/byCategory.json create mode 100644 src/test/resources/api/stats/byDate.json create mode 100644 src/test/resources/api/stats/byDomain.json create mode 100644 src/test/resources/api/stats/byEmailServiceProvider.json create mode 100644 src/test/resources/api/stats/getStats.json diff --git a/examples/java/io/mailtrap/examples/general/StatsExample.java b/examples/java/io/mailtrap/examples/general/StatsExample.java new file mode 100644 index 0000000..07e8978 --- /dev/null +++ b/examples/java/io/mailtrap/examples/general/StatsExample.java @@ -0,0 +1,43 @@ +package io.mailtrap.examples.general; + +import io.mailtrap.api.stats.StatsFilter; +import io.mailtrap.config.MailtrapConfig; +import io.mailtrap.factory.MailtrapClientFactory; + +public class StatsExample { + + private static final String TOKEN = ""; + private static final long ACCOUNT_ID = 1L; + + // Set these to the desired date range (format: YYYY-MM-DD) + private static final String START_DATE = ""; + private static final String END_DATE = ""; + + public static void main(String[] args) { + final var config = new MailtrapConfig.Builder() + .token(TOKEN) + .build(); + + final var client = MailtrapClientFactory.createMailtrapClient(config); + + final var filter = StatsFilter.builder() + .startDate(START_DATE) + .endDate(END_DATE) + .build(); + + System.out.println("=== Aggregated Stats ==="); + System.out.println(client.generalApi().stats().getStats(ACCOUNT_ID, filter)); + + System.out.println("\n=== Stats by Domains ==="); + System.out.println(client.generalApi().stats().byDomain(ACCOUNT_ID, filter)); + + System.out.println("\n=== Stats by Categories ==="); + System.out.println(client.generalApi().stats().byCategory(ACCOUNT_ID, filter)); + + System.out.println("\n=== Stats by Email Service Providers ==="); + System.out.println(client.generalApi().stats().byEmailServiceProvider(ACCOUNT_ID, filter)); + + System.out.println("\n=== Stats by Date ==="); + System.out.println(client.generalApi().stats().byDate(ACCOUNT_ID, filter)); + } +} diff --git a/src/main/java/io/mailtrap/api/stats/Stats.java b/src/main/java/io/mailtrap/api/stats/Stats.java new file mode 100644 index 0000000..6f07d25 --- /dev/null +++ b/src/main/java/io/mailtrap/api/stats/Stats.java @@ -0,0 +1,57 @@ +package io.mailtrap.api.stats; + +import io.mailtrap.model.response.stats.SendingStatGroupResponse; +import io.mailtrap.model.response.stats.SendingStatsResponse; + +import java.util.List; + +/** + * Interface representing the Mailtrap API for interaction with sending statistics + */ +public interface Stats { + + /** + * Get aggregated sending stats + * + * @param accountId unique account ID + * @param filter stats filter parameters + * @return aggregated sending statistics + */ + SendingStatsResponse getStats(long accountId, StatsFilter filter); + + /** + * Get sending stats grouped by domains + * + * @param accountId unique account ID + * @param filter stats filter parameters + * @return list of sending statistics grouped by domain + */ + List byDomain(long accountId, StatsFilter filter); + + /** + * Get sending stats grouped by categories + * + * @param accountId unique account ID + * @param filter stats filter parameters + * @return list of sending statistics grouped by category + */ + List byCategory(long accountId, StatsFilter filter); + + /** + * Get sending stats grouped by email service providers + * + * @param accountId unique account ID + * @param filter stats filter parameters + * @return list of sending statistics grouped by email service provider + */ + List byEmailServiceProvider(long accountId, StatsFilter filter); + + /** + * Get sending stats grouped by date + * + * @param accountId unique account ID + * @param filter stats filter parameters + * @return list of sending statistics grouped by date + */ + List byDate(long accountId, StatsFilter filter); +} diff --git a/src/main/java/io/mailtrap/api/stats/StatsFilter.java b/src/main/java/io/mailtrap/api/stats/StatsFilter.java new file mode 100644 index 0000000..bad4c61 --- /dev/null +++ b/src/main/java/io/mailtrap/api/stats/StatsFilter.java @@ -0,0 +1,21 @@ +package io.mailtrap.api.stats; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class StatsFilter { + private String startDate; + private String endDate; + private List sendingDomainIds; + private List sendingStreams; + private List categories; + private List emailServiceProviders; +} diff --git a/src/main/java/io/mailtrap/api/stats/StatsImpl.java b/src/main/java/io/mailtrap/api/stats/StatsImpl.java new file mode 100644 index 0000000..e43de4c --- /dev/null +++ b/src/main/java/io/mailtrap/api/stats/StatsImpl.java @@ -0,0 +1,92 @@ +package io.mailtrap.api.stats; + +import io.mailtrap.Constants; +import io.mailtrap.api.apiresource.ApiResource; +import io.mailtrap.config.MailtrapConfig; +import io.mailtrap.http.RequestData; +import io.mailtrap.model.response.stats.SendingStatGroupResponse; +import io.mailtrap.model.response.stats.SendingStatsResponse; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static io.mailtrap.http.RequestData.entry; + +public class StatsImpl extends ApiResource implements Stats { + + public StatsImpl(final MailtrapConfig config) { + super(config); + this.apiHost = Constants.GENERAL_HOST; + } + + @Override + public SendingStatsResponse getStats(final long accountId, final StatsFilter filter) { + return httpClient.get( + buildUrl(accountId, ""), + new RequestData(buildStatsQueryParams(filter)), + SendingStatsResponse.class + ); + } + + @Override + public List byDomain(final long accountId, final StatsFilter filter) { + return httpClient.getList( + buildUrl(accountId, "/domains"), + new RequestData(buildStatsQueryParams(filter)), + SendingStatGroupResponse.class + ); + } + + @Override + public List byCategory(final long accountId, final StatsFilter filter) { + return httpClient.getList( + buildUrl(accountId, "/categories"), + new RequestData(buildStatsQueryParams(filter)), + SendingStatGroupResponse.class + ); + } + + @Override + public List byEmailServiceProvider(final long accountId, final StatsFilter filter) { + return httpClient.getList( + buildUrl(accountId, "/email_service_providers"), + new RequestData(buildStatsQueryParams(filter)), + SendingStatGroupResponse.class + ); + } + + @Override + public List byDate(final long accountId, final StatsFilter filter) { + return httpClient.getList( + buildUrl(accountId, "/date"), + new RequestData(buildStatsQueryParams(filter)), + SendingStatGroupResponse.class + ); + } + + private String buildUrl(final long accountId, final String suffix) { + return String.format(apiHost + "/api/accounts/%d/stats%s", accountId, suffix); + } + + private Map> buildStatsQueryParams(final StatsFilter filter) { + if (filter == null) { + throw new IllegalArgumentException("filter must not be null"); + } + if (filter.getStartDate() == null || filter.getStartDate().trim().isEmpty()) { + throw new IllegalArgumentException("startDate must not be null or empty"); + } + if (filter.getEndDate() == null || filter.getEndDate().trim().isEmpty()) { + throw new IllegalArgumentException("endDate must not be null or empty"); + } + + return RequestData.buildQueryParams( + entry("start_date", Optional.ofNullable(filter.getStartDate())), + entry("end_date", Optional.ofNullable(filter.getEndDate())), + entry("sending_domain_ids[]", Optional.ofNullable(filter.getSendingDomainIds())), + entry("sending_streams[]", Optional.ofNullable(filter.getSendingStreams())), + entry("categories[]", Optional.ofNullable(filter.getCategories())), + entry("email_service_providers[]", Optional.ofNullable(filter.getEmailServiceProviders())) + ); + } +} diff --git a/src/main/java/io/mailtrap/client/api/MailtrapGeneralApi.java b/src/main/java/io/mailtrap/client/api/MailtrapGeneralApi.java index 67a01e0..ce10150 100644 --- a/src/main/java/io/mailtrap/client/api/MailtrapGeneralApi.java +++ b/src/main/java/io/mailtrap/client/api/MailtrapGeneralApi.java @@ -4,6 +4,7 @@ import io.mailtrap.api.accounts.Accounts; import io.mailtrap.api.billing.Billing; import io.mailtrap.api.permissions.Permissions; +import io.mailtrap.api.stats.Stats; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.experimental.Accessors; @@ -19,4 +20,5 @@ public class MailtrapGeneralApi { private final Accounts accounts; private final Billing billing; private final Permissions permissions; + private final Stats stats; } diff --git a/src/main/java/io/mailtrap/factory/MailtrapClientFactory.java b/src/main/java/io/mailtrap/factory/MailtrapClientFactory.java index 572e523..9a6b1d5 100644 --- a/src/main/java/io/mailtrap/factory/MailtrapClientFactory.java +++ b/src/main/java/io/mailtrap/factory/MailtrapClientFactory.java @@ -19,6 +19,7 @@ import io.mailtrap.api.projects.ProjectsImpl; import io.mailtrap.api.sendingdomains.SendingDomainsImpl; import io.mailtrap.api.sendingemails.SendingEmailsImpl; +import io.mailtrap.api.stats.StatsImpl; import io.mailtrap.api.suppressions.SuppressionsImpl; import io.mailtrap.api.testingemails.TestingEmailsImpl; import io.mailtrap.client.MailtrapClient; @@ -87,8 +88,9 @@ private static MailtrapGeneralApi createGeneralApi(final MailtrapConfig config) final var accounts = new AccountsImpl(config); final var billing = new BillingImpl(config); final var permissions = new PermissionsImpl(config); + final var stats = new StatsImpl(config); - return new MailtrapGeneralApi(accountAccess, accounts, billing, permissions); + return new MailtrapGeneralApi(accountAccess, accounts, billing, permissions, stats); } private static MailtrapEmailSendingApi createSendingApi(final MailtrapConfig config) { diff --git a/src/main/java/io/mailtrap/model/response/stats/SendingStatGroupResponse.java b/src/main/java/io/mailtrap/model/response/stats/SendingStatGroupResponse.java new file mode 100644 index 0000000..11be6e1 --- /dev/null +++ b/src/main/java/io/mailtrap/model/response/stats/SendingStatGroupResponse.java @@ -0,0 +1,20 @@ +package io.mailtrap.model.response.stats; + +import com.fasterxml.jackson.annotation.JsonAnySetter; +import lombok.Data; + +@Data +public class SendingStatGroupResponse { + + private String name; + private Object value; + private SendingStatsResponse stats; + + @JsonAnySetter + public void setDynamicField(String key, Object value) { + if (!"stats".equals(key)) { + this.name = key; + this.value = value; + } + } +} diff --git a/src/main/java/io/mailtrap/model/response/stats/SendingStatsResponse.java b/src/main/java/io/mailtrap/model/response/stats/SendingStatsResponse.java new file mode 100644 index 0000000..ddebc54 --- /dev/null +++ b/src/main/java/io/mailtrap/model/response/stats/SendingStatsResponse.java @@ -0,0 +1,38 @@ +package io.mailtrap.model.response.stats; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class SendingStatsResponse { + + @JsonProperty("delivery_count") + private int deliveryCount; + + @JsonProperty("delivery_rate") + private double deliveryRate; + + @JsonProperty("bounce_count") + private int bounceCount; + + @JsonProperty("bounce_rate") + private double bounceRate; + + @JsonProperty("open_count") + private int openCount; + + @JsonProperty("open_rate") + private double openRate; + + @JsonProperty("click_count") + private int clickCount; + + @JsonProperty("click_rate") + private double clickRate; + + @JsonProperty("spam_count") + private int spamCount; + + @JsonProperty("spam_rate") + private double spamRate; +} diff --git a/src/test/java/io/mailtrap/api/stats/StatsImplTest.java b/src/test/java/io/mailtrap/api/stats/StatsImplTest.java new file mode 100644 index 0000000..823bc5c --- /dev/null +++ b/src/test/java/io/mailtrap/api/stats/StatsImplTest.java @@ -0,0 +1,189 @@ +package io.mailtrap.api.stats; + +import io.mailtrap.Constants; +import io.mailtrap.config.MailtrapConfig; +import io.mailtrap.factory.MailtrapClientFactory; +import io.mailtrap.model.response.stats.SendingStatGroupResponse; +import io.mailtrap.model.response.stats.SendingStatsResponse; +import io.mailtrap.testutils.BaseTest; +import io.mailtrap.testutils.DataMock; +import io.mailtrap.testutils.TestHttpClient; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class StatsImplTest extends BaseTest { + + private Stats api; + + private final String startDate = "2026-01-01"; + private final String endDate = "2026-01-31"; + private final String baseUrl = Constants.GENERAL_HOST + "/api/accounts/" + accountId + "/stats"; + + @BeforeEach + public void init() { + final Map defaultQueryParams = Map.of( + "start_date", startDate, + "end_date", endDate + ); + + final TestHttpClient httpClient = new TestHttpClient(List.of( + DataMock.build(baseUrl, "GET", null, "api/stats/getStats.json", defaultQueryParams), + DataMock.build(baseUrl + "/domains", "GET", null, "api/stats/byDomain.json", defaultQueryParams), + DataMock.build(baseUrl + "/categories", "GET", null, "api/stats/byCategory.json", defaultQueryParams), + DataMock.build(baseUrl + "/email_service_providers", "GET", null, "api/stats/byEmailServiceProvider.json", defaultQueryParams), + DataMock.build(baseUrl + "/date", "GET", null, "api/stats/byDate.json", defaultQueryParams) + )); + + final MailtrapConfig testConfig = new MailtrapConfig.Builder() + .httpClient(httpClient) + .token("dummy_token") + .build(); + + api = MailtrapClientFactory.createMailtrapClient(testConfig).generalApi().stats(); + } + + @Test + void test_getStats() { + final StatsFilter filter = StatsFilter.builder() + .startDate(startDate) + .endDate(endDate) + .build(); + + final SendingStatsResponse response = api.getStats(accountId, filter); + + assertNotNull(response); + assertEquals(150, response.getDeliveryCount()); + assertEquals(0.95, response.getDeliveryRate()); + assertEquals(8, response.getBounceCount()); + assertEquals(0.05, response.getBounceRate()); + assertEquals(120, response.getOpenCount()); + assertEquals(0.8, response.getOpenRate()); + assertEquals(60, response.getClickCount()); + assertEquals(0.5, response.getClickRate()); + assertEquals(2, response.getSpamCount()); + assertEquals(0.013, response.getSpamRate()); + } + + @Test + void test_byDomain() { + final StatsFilter filter = StatsFilter.builder() + .startDate(startDate) + .endDate(endDate) + .build(); + + final List response = api.byDomain(accountId, filter); + + assertNotNull(response); + assertEquals(2, response.size()); + + assertEquals("sending_domain_id", response.get(0).getName()); + assertEquals(1, response.get(0).getValue()); + assertEquals(100, response.get(0).getStats().getDeliveryCount()); + assertEquals(0.96, response.get(0).getStats().getDeliveryRate()); + + assertEquals("sending_domain_id", response.get(1).getName()); + assertEquals(2, response.get(1).getValue()); + assertEquals(50, response.get(1).getStats().getDeliveryCount()); + } + + @Test + void test_byCategory() { + final StatsFilter filter = StatsFilter.builder() + .startDate(startDate) + .endDate(endDate) + .build(); + + final List response = api.byCategory(accountId, filter); + + assertNotNull(response); + assertEquals(1, response.size()); + assertEquals("category", response.get(0).getName()); + assertEquals("Welcome Email", response.get(0).getValue()); + assertEquals(100, response.get(0).getStats().getDeliveryCount()); + assertEquals(0.97, response.get(0).getStats().getDeliveryRate()); + } + + @Test + void test_byEmailServiceProvider() { + final StatsFilter filter = StatsFilter.builder() + .startDate(startDate) + .endDate(endDate) + .build(); + + final List response = api.byEmailServiceProvider(accountId, filter); + + assertNotNull(response); + assertEquals(1, response.size()); + assertEquals("email_service_provider", response.get(0).getName()); + assertEquals("Gmail", response.get(0).getValue()); + assertEquals(80, response.get(0).getStats().getDeliveryCount()); + assertEquals(0.97, response.get(0).getStats().getDeliveryRate()); + } + + @Test + void test_getStats_nullFilter() { + assertThrows(IllegalArgumentException.class, () -> api.getStats(accountId, null)); + } + + @Test + void test_getStats_missingStartDate() { + final StatsFilter filter = StatsFilter.builder() + .endDate(endDate) + .build(); + + assertThrows(IllegalArgumentException.class, () -> api.getStats(accountId, filter)); + } + + @Test + void test_getStats_missingEndDate() { + final StatsFilter filter = StatsFilter.builder() + .startDate(startDate) + .build(); + + assertThrows(IllegalArgumentException.class, () -> api.getStats(accountId, filter)); + } + + @Test + void test_getStats_emptyStartDate() { + final StatsFilter filter = StatsFilter.builder() + .startDate("") + .endDate(endDate) + .build(); + + assertThrows(IllegalArgumentException.class, () -> api.getStats(accountId, filter)); + } + + @Test + void test_getStats_emptyEndDate() { + final StatsFilter filter = StatsFilter.builder() + .startDate(startDate) + .endDate("") + .build(); + + assertThrows(IllegalArgumentException.class, () -> api.getStats(accountId, filter)); + } + + @Test + void test_byDate() { + final StatsFilter filter = StatsFilter.builder() + .startDate(startDate) + .endDate(endDate) + .build(); + + final List response = api.byDate(accountId, filter); + + assertNotNull(response); + assertEquals(1, response.size()); + assertEquals("date", response.get(0).getName()); + assertEquals("2026-01-01", response.get(0).getValue()); + assertEquals(5, response.get(0).getStats().getDeliveryCount()); + assertEquals(1.0, response.get(0).getStats().getDeliveryRate()); + } +} diff --git a/src/test/resources/api/stats/byCategory.json b/src/test/resources/api/stats/byCategory.json new file mode 100644 index 0000000..d337f7e --- /dev/null +++ b/src/test/resources/api/stats/byCategory.json @@ -0,0 +1,17 @@ +[ + { + "category": "Welcome Email", + "stats": { + "delivery_count": 100, + "delivery_rate": 0.97, + "bounce_count": 3, + "bounce_rate": 0.03, + "open_count": 85, + "open_rate": 0.85, + "click_count": 45, + "click_rate": 0.53, + "spam_count": 0, + "spam_rate": 0.0 + } + } +] diff --git a/src/test/resources/api/stats/byDate.json b/src/test/resources/api/stats/byDate.json new file mode 100644 index 0000000..15bc82b --- /dev/null +++ b/src/test/resources/api/stats/byDate.json @@ -0,0 +1,17 @@ +[ + { + "date": "2026-01-01", + "stats": { + "delivery_count": 5, + "delivery_rate": 1.0, + "bounce_count": 0, + "bounce_rate": 0.0, + "open_count": 4, + "open_rate": 0.8, + "click_count": 2, + "click_rate": 0.5, + "spam_count": 0, + "spam_rate": 0.0 + } + } +] diff --git a/src/test/resources/api/stats/byDomain.json b/src/test/resources/api/stats/byDomain.json new file mode 100644 index 0000000..93cca01 --- /dev/null +++ b/src/test/resources/api/stats/byDomain.json @@ -0,0 +1,32 @@ +[ + { + "sending_domain_id": 1, + "stats": { + "delivery_count": 100, + "delivery_rate": 0.96, + "bounce_count": 4, + "bounce_rate": 0.04, + "open_count": 80, + "open_rate": 0.8, + "click_count": 40, + "click_rate": 0.5, + "spam_count": 1, + "spam_rate": 0.01 + } + }, + { + "sending_domain_id": 2, + "stats": { + "delivery_count": 50, + "delivery_rate": 0.93, + "bounce_count": 4, + "bounce_rate": 0.07, + "open_count": 40, + "open_rate": 0.8, + "click_count": 20, + "click_rate": 0.5, + "spam_count": 1, + "spam_rate": 0.02 + } + } +] diff --git a/src/test/resources/api/stats/byEmailServiceProvider.json b/src/test/resources/api/stats/byEmailServiceProvider.json new file mode 100644 index 0000000..8e94c20 --- /dev/null +++ b/src/test/resources/api/stats/byEmailServiceProvider.json @@ -0,0 +1,17 @@ +[ + { + "email_service_provider": "Gmail", + "stats": { + "delivery_count": 80, + "delivery_rate": 0.97, + "bounce_count": 2, + "bounce_rate": 0.03, + "open_count": 70, + "open_rate": 0.88, + "click_count": 35, + "click_rate": 0.5, + "spam_count": 1, + "spam_rate": 0.013 + } + } +] diff --git a/src/test/resources/api/stats/getStats.json b/src/test/resources/api/stats/getStats.json new file mode 100644 index 0000000..9cd9d81 --- /dev/null +++ b/src/test/resources/api/stats/getStats.json @@ -0,0 +1,12 @@ +{ + "delivery_count": 150, + "delivery_rate": 0.95, + "bounce_count": 8, + "bounce_rate": 0.05, + "open_count": 120, + "open_rate": 0.8, + "click_count": 60, + "click_rate": 0.5, + "spam_count": 2, + "spam_rate": 0.013 +}