Skip to content

Commit a651aac

Browse files
committed
Add support for Email Logs API
1 parent 1136e25 commit a651aac

42 files changed

Lines changed: 1376 additions & 18 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,7 @@ You can find the [Mailtrap Java API reference](https://mailtrap.github.io/mailtr
342342
- [Batch](examples/java/io/mailtrap/examples/sending/BatchExample.java)
343343
- [Sending Domains](examples/java/io/mailtrap/examples/sendingdomains/SendingDomainsExample.java)
344344
- [Suppressions](examples/java/io/mailtrap/examples/suppressions/SuppressionsExample.java)
345+
- [Email Logs](examples/java/io/mailtrap/examples/emaillogs/EmailLogsExample.java)
345346

346347
### Email Testing API
347348

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package io.mailtrap.examples.emaillogs;
2+
3+
import io.mailtrap.config.MailtrapConfig;
4+
import io.mailtrap.factory.MailtrapClientFactory;
5+
import io.mailtrap.model.request.emaillogs.EmailLogsListFilters;
6+
import io.mailtrap.model.request.emaillogs.FilterCiString;
7+
import io.mailtrap.model.request.emaillogs.FilterExactString;
8+
import io.mailtrap.model.request.emaillogs.FilterStatus;
9+
import io.mailtrap.model.request.emaillogs.FilterOptionalString;
10+
import io.mailtrap.model.response.emaillogs.MessageStatus;
11+
12+
import java.time.Instant;
13+
import java.time.temporal.ChronoUnit;
14+
import java.util.List;
15+
16+
public class EmailLogsExample {
17+
18+
private static final String TOKEN = "<YOUR MAILTRAP TOKEN>";
19+
private static final long ACCOUNT_ID = 1L;
20+
21+
public static void main(String[] args) {
22+
final var config = new MailtrapConfig.Builder()
23+
.token(TOKEN)
24+
.build();
25+
26+
final var client = MailtrapClientFactory.createMailtrapClient(config);
27+
28+
// List email logs for the last 2 days
29+
final var now = Instant.now();
30+
final var twoDaysAgo = now.minus(2, ChronoUnit.DAYS);
31+
final var filters = EmailLogsListFilters.builder()
32+
.sentAfter(twoDaysAgo.toString())
33+
.sentBefore(now.toString())
34+
.subject(new FilterOptionalString(FilterOptionalString.Operator.not_empty))
35+
.to(new FilterCiString(FilterCiString.Operator.ci_equal, "recipient@example.com"))
36+
.category(new FilterExactString(FilterExactString.Operator.equal,
37+
List.of("Welcome Email")))
38+
.build();
39+
40+
final var listResponse = client.sendingApi().emailLogs()
41+
.list(ACCOUNT_ID, null, filters);
42+
43+
System.out.println("Total: " + listResponse.getTotalCount());
44+
listResponse.getMessages().forEach(
45+
msg -> System.out.println(" " + msg.getMessageId() + " " + msg.getSubject() + " "
46+
+ msg.getStatus()));
47+
48+
// Get a single message by ID (use message_id from list response)
49+
if (!listResponse.getMessages().isEmpty()) {
50+
final var messageId = listResponse.getMessages().get(0).getMessageId();
51+
final var message = client.sendingApi().emailLogs().get(ACCOUNT_ID, messageId);
52+
System.out.println(
53+
"Message: " + message.getSubject() + " events: " + message.getEvents().size());
54+
}
55+
}
56+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package io.mailtrap.api.emaillogs;
2+
3+
import io.mailtrap.model.request.emaillogs.EmailLogsListFilters;
4+
import io.mailtrap.model.response.emaillogs.EmailLogsListResponse;
5+
import io.mailtrap.model.response.emaillogs.EmailLogMessage;
6+
7+
/**
8+
* API for listing and retrieving email sending logs.
9+
*/
10+
public interface EmailLogs {
11+
12+
/**
13+
* Returns a paginated list of email logs for the account.
14+
*
15+
* @param accountId account ID
16+
* @param searchAfter optional cursor (message_id UUID from previous response
17+
* next_page_cursor) for the next page
18+
* @param filters optional filters; pass null or empty to omit
19+
* @return paginated list with messages, total_count, and next_page_cursor
20+
*/
21+
EmailLogsListResponse list(long accountId, String searchAfter, EmailLogsListFilters filters);
22+
23+
/**
24+
* Returns a single email log message by its UUID.
25+
*
26+
* @param accountId account ID
27+
* @param sendingMessageId message UUID
28+
* @return the message with details and events, or throws if not found
29+
*/
30+
EmailLogMessage get(long accountId, String sendingMessageId);
31+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package io.mailtrap.api.emaillogs;
2+
3+
import io.mailtrap.Constants;
4+
import io.mailtrap.api.apiresource.ApiResource;
5+
import io.mailtrap.config.MailtrapConfig;
6+
import io.mailtrap.http.RequestData;
7+
import io.mailtrap.model.request.emaillogs.EmailLogFilter;
8+
import io.mailtrap.model.request.emaillogs.EmailLogsListFilters;
9+
import io.mailtrap.model.response.emaillogs.EmailLogsListResponse;
10+
import io.mailtrap.model.response.emaillogs.EmailLogMessage;
11+
12+
import java.net.URLEncoder;
13+
import java.nio.charset.StandardCharsets;
14+
import java.util.ArrayList;
15+
import java.util.Collection;
16+
import java.util.Collections;
17+
import java.util.List;
18+
import java.util.stream.Collectors;
19+
20+
public class EmailLogsImpl extends ApiResource implements EmailLogs {
21+
22+
public EmailLogsImpl(final MailtrapConfig config) {
23+
super(config);
24+
this.apiHost = Constants.GENERAL_HOST;
25+
}
26+
27+
@Override
28+
public EmailLogsListResponse list(final long accountId, final String searchAfter,
29+
final EmailLogsListFilters filters) {
30+
final String queryString = buildQueryString(searchAfter, filters);
31+
final String url = String.format("%s/api/accounts/%d/email_logs", apiHost, accountId)
32+
+ (queryString.isEmpty() ? "" : "?" + queryString);
33+
34+
return httpClient.get(url, new RequestData(), EmailLogsListResponse.class);
35+
}
36+
37+
@Override
38+
public EmailLogMessage get(final long accountId, final String sendingMessageId) {
39+
if (sendingMessageId == null || sendingMessageId.isBlank()) {
40+
throw new IllegalArgumentException("sendingMessageId must not be null or blank");
41+
}
42+
final String url = String.format("%s/api/accounts/%d/email_logs/%s", apiHost, accountId, sendingMessageId);
43+
return httpClient.get(url, new RequestData(), EmailLogMessage.class);
44+
}
45+
46+
private static String buildQueryString(final String searchAfter, final EmailLogsListFilters filters) {
47+
final List<String> params = new ArrayList<>();
48+
49+
if (searchAfter != null && !searchAfter.isBlank()) {
50+
params.add(enc("search_after") + "=" + enc(searchAfter));
51+
}
52+
53+
if (filters != null) {
54+
appendFilter(params, "sent_after", filters.getSentAfter());
55+
appendFilter(params, "sent_before", filters.getSentBefore());
56+
appendOperatorValue(params, "to", filters.getTo());
57+
appendOperatorValue(params, "from", filters.getFrom());
58+
appendOperatorValue(params, "subject", filters.getSubject());
59+
appendOperatorValue(params, "status", filters.getStatus());
60+
appendOperatorValue(params, "events", filters.getEvents());
61+
appendOperatorValue(params, "clicks_count", filters.getClicksCount());
62+
appendOperatorValue(params, "opens_count", filters.getOpensCount());
63+
appendOperatorValue(params, "client_ip", filters.getClientIp());
64+
appendOperatorValue(params, "sending_ip", filters.getSendingIp());
65+
appendOperatorValue(params, "email_service_provider_response", filters.getEmailServiceProviderResponse());
66+
appendOperatorValue(params, "email_service_provider", filters.getEmailServiceProvider());
67+
appendOperatorValue(params, "recipient_mx", filters.getRecipientMx());
68+
appendOperatorValue(params, "category", filters.getCategory());
69+
appendOperatorValue(params, "sending_domain_id", filters.getSendingDomainId());
70+
appendOperatorValue(params, "sending_stream", filters.getSendingStream());
71+
}
72+
73+
return String.join("&", params);
74+
}
75+
76+
private static void appendFilter(final List<String> params, final String key, final String value) {
77+
if (value != null && !value.isBlank()) {
78+
params.add(enc("filters[" + key + "]") + "=" + enc(value));
79+
}
80+
}
81+
82+
private static void appendOperatorValue(final List<String> params, final String field, final EmailLogFilter spec) {
83+
if (spec == null)
84+
return;
85+
final String operator = spec.getOperatorString();
86+
if (operator == null || operator.isBlank())
87+
return;
88+
params.add(enc("filters[" + field + "][operator]") + "=" + enc(operator));
89+
final Object value = spec.getValue();
90+
if (value != null) {
91+
for (final String v : toValueList(value)) {
92+
params.add(enc("filters[" + field + "][value]") + "=" + enc(String.valueOf(v)));
93+
}
94+
}
95+
}
96+
97+
private static List<String> toValueList(final Object value) {
98+
if (value instanceof Collection<?> c) {
99+
return c.stream()
100+
.filter(v -> v != null)
101+
.map(String::valueOf)
102+
.collect(Collectors.toList());
103+
}
104+
return Collections.singletonList(String.valueOf(value));
105+
}
106+
107+
private static String enc(final String s) {
108+
return URLEncoder.encode(s, StandardCharsets.UTF_8);
109+
}
110+
}

src/main/java/io/mailtrap/client/api/MailtrapEmailSendingApi.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.mailtrap.client.api;
22

3+
import io.mailtrap.api.emaillogs.EmailLogs;
34
import io.mailtrap.api.sendingdomains.SendingDomains;
45
import io.mailtrap.api.sendingemails.SendingEmails;
56
import io.mailtrap.api.suppressions.Suppressions;
@@ -17,4 +18,5 @@ public class MailtrapEmailSendingApi {
1718
private final SendingEmails emails;
1819
private final SendingDomains domains;
1920
private final Suppressions suppressions;
21+
private final EmailLogs emailLogs;
2022
}

src/main/java/io/mailtrap/factory/MailtrapClientFactory.java

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import io.mailtrap.api.messages.MessagesImpl;
1818
import io.mailtrap.api.permissions.PermissionsImpl;
1919
import io.mailtrap.api.projects.ProjectsImpl;
20+
import io.mailtrap.api.emaillogs.EmailLogsImpl;
2021
import io.mailtrap.api.sendingdomains.SendingDomainsImpl;
2122
import io.mailtrap.api.sendingemails.SendingEmailsImpl;
2223
import io.mailtrap.api.suppressions.SuppressionsImpl;
@@ -34,20 +35,22 @@
3435
public final class MailtrapClientFactory {
3536

3637
/**
37-
* Creates a new instance of {@link MailtrapValidator} using the default validator factory.
38-
* Intentionally not wrapped into try-with-resources to not close, as per Jakarta doc, after
39-
* the {@code ValidatorFactory} instance is closed, calling the following methods is not allowed:
38+
* Creates a new instance of {@link MailtrapValidator} using the default
39+
* validator factory.
40+
* Intentionally not wrapped into try-with-resources to not close, as per
41+
* Jakarta doc, after
42+
* the {@code ValidatorFactory} instance is closed, calling the following
43+
* methods is not allowed:
4044
* <ul>
41-
* <li>methods of this {@code ValidatorFactory} instance</li>
42-
* <li>methods of {@link Validator} instances created by this
43-
* {@code ValidatorFactory}</li>
45+
* <li>methods of this {@code ValidatorFactory} instance</li>
46+
* <li>methods of {@link Validator} instances created by this
47+
* {@code ValidatorFactory}</li>
4448
* </ul>
4549
*/
46-
private static final jakarta.validation.ValidatorFactory VALIDATOR_FACTORY =
47-
Validation.buildDefaultValidatorFactory();
48-
49-
private static final MailtrapValidator VALIDATOR =
50-
new MailtrapValidator(VALIDATOR_FACTORY.getValidator());
50+
private static final jakarta.validation.ValidatorFactory VALIDATOR_FACTORY = Validation
51+
.buildDefaultValidatorFactory();
52+
53+
private static final MailtrapValidator VALIDATOR = new MailtrapValidator(VALIDATOR_FACTORY.getValidator());
5154

5255
private MailtrapClientFactory() {
5356
}
@@ -68,7 +71,8 @@ public static MailtrapClient createMailtrapClient(final MailtrapConfig config) {
6871

6972
final var sendingContextHolder = configureSendingContext(config);
7073

71-
return new MailtrapClient(sendingApi, testingApi, bulkSendingApi, generalApi, contactsApi, emailTemplatesApi, sendingContextHolder);
74+
return new MailtrapClient(sendingApi, testingApi, bulkSendingApi, generalApi, contactsApi, emailTemplatesApi,
75+
sendingContextHolder);
7276
}
7377

7478
private static MailtrapContactsApi createContactsApi(final MailtrapConfig config) {
@@ -79,7 +83,8 @@ private static MailtrapContactsApi createContactsApi(final MailtrapConfig config
7983
final var contactExports = new ContactExportsImpl(config);
8084
final var contactEvents = new ContactEventsImpl(config);
8185

82-
return new MailtrapContactsApi(contactLists, contacts, contactImports, contactFields, contactExports, contactEvents);
86+
return new MailtrapContactsApi(contactLists, contacts, contactImports, contactFields, contactExports,
87+
contactEvents);
8388
}
8489

8590
private static MailtrapGeneralApi createGeneralApi(final MailtrapConfig config) {
@@ -95,8 +100,9 @@ private static MailtrapEmailSendingApi createSendingApi(final MailtrapConfig con
95100
final var emails = new SendingEmailsImpl(config, VALIDATOR);
96101
final var domains = new SendingDomainsImpl(config);
97102
final var suppressions = new SuppressionsImpl(config);
103+
final var emailLogs = new EmailLogsImpl(config);
98104

99-
return new MailtrapEmailSendingApi(emails, domains, suppressions);
105+
return new MailtrapEmailSendingApi(emails, domains, suppressions, emailLogs);
100106
}
101107

102108
private static MailtrapEmailTestingApi createTestingApi(final MailtrapConfig config) {
@@ -124,9 +130,9 @@ private static MailtrapEmailTemplatesApi createEmailTemplatesApi(final MailtrapC
124130
private static SendingContextHolder configureSendingContext(final MailtrapConfig config) {
125131

126132
return SendingContextHolder.builder()
127-
.sandbox(config.isSandbox())
128-
.inboxId(config.getInboxId())
129-
.bulk(config.isBulk())
130-
.build();
133+
.sandbox(config.isSandbox())
134+
.inboxId(config.getInboxId())
135+
.bulk(config.isBulk())
136+
.build();
131137
}
132138
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package io.mailtrap.model.request.emaillogs;
2+
3+
/**
4+
* Common contract for email log filters so the API can serialize operator and value.
5+
*/
6+
public interface EmailLogFilter {
7+
8+
/** API operator string (e.g. "equal", "ci_contain"). */
9+
String getOperatorString();
10+
11+
/** Filter value; may be null for operators like empty/not_empty. */
12+
Object getValue();
13+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package io.mailtrap.model.request.emaillogs;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Builder;
5+
import lombok.Data;
6+
import lombok.NoArgsConstructor;
7+
8+
/**
9+
* Filters for listing email logs. All fields are optional.
10+
* Date range uses sent_after and sent_before (ISO 8601 date-time strings).
11+
* Other filters use concrete types so operators are enforced per field.
12+
*/
13+
@Data
14+
@NoArgsConstructor
15+
@AllArgsConstructor
16+
@Builder
17+
public class EmailLogsListFilters {
18+
19+
private String sentAfter;
20+
private String sentBefore;
21+
private FilterCiString to;
22+
private FilterCiString from;
23+
private FilterOptionalString subject;
24+
private FilterStatus status;
25+
private FilterEvents events;
26+
private FilterNumber clicksCount;
27+
private FilterNumber opensCount;
28+
private FilterString clientIp;
29+
private FilterString sendingIp;
30+
private FilterCiString emailServiceProviderResponse;
31+
private FilterExactString emailServiceProvider;
32+
private FilterExactString recipientMx;
33+
private FilterExactString category;
34+
private FilterSendingDomainId sendingDomainId;
35+
private FilterSendingStream sendingStream;
36+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package io.mailtrap.model.request.emaillogs;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Data;
5+
import lombok.NoArgsConstructor;
6+
7+
/**
8+
* Filter with case-insensitive string operators (e.g. to, from). Value may be single or list for ci_equal, ci_not_equal.
9+
*/
10+
@Data
11+
@NoArgsConstructor
12+
@AllArgsConstructor
13+
public class FilterCiString implements EmailLogFilter {
14+
15+
public enum Operator {
16+
ci_contain, ci_not_contain, ci_equal, ci_not_equal
17+
}
18+
19+
private Operator operator;
20+
private Object value;
21+
22+
@Override
23+
public String getOperatorString() {
24+
return operator == null ? null : operator.name();
25+
}
26+
27+
@Override
28+
public Object getValue() {
29+
return value;
30+
}
31+
}

0 commit comments

Comments
 (0)