Skip to content

Commit ca25ef3

Browse files
committed
Add support for Email Logs API
1 parent 859b51e commit ca25ef3

47 files changed

Lines changed: 1435 additions & 44 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
@@ -343,6 +343,7 @@ You can find the [Mailtrap Java API reference](https://mailtrap.github.io/mailtr
343343
- [Sending Domains](examples/java/io/mailtrap/examples/sendingdomains/SendingDomainsExample.java)
344344
- [Suppressions](examples/java/io/mailtrap/examples/suppressions/SuppressionsExample.java)
345345
- [Stats](examples/java/io/mailtrap/examples/sending/StatsExample.java)
346+
- [Email Logs](examples/java/io/mailtrap/examples/emaillogs/EmailLogsExample.java)
346347

347348
### Email Testing API
348349

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("Newsletter", "Alert")))
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: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
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+
final List<String> values = toValueList(value);
92+
final String valueKey = values.size() > 1
93+
? "filters[" + field + "][value][]"
94+
: "filters[" + field + "][value]";
95+
for (final String v : values) {
96+
params.add(enc(valueKey) + "=" + enc(String.valueOf(v)));
97+
}
98+
}
99+
}
100+
101+
private static List<String> toValueList(final Object value) {
102+
if (value instanceof Collection<?> c) {
103+
return c.stream()
104+
.filter(v -> v != null)
105+
.map(String::valueOf)
106+
.collect(Collectors.toList());
107+
}
108+
return Collections.singletonList(String.valueOf(value));
109+
}
110+
111+
private static String enc(final String s) {
112+
return URLEncoder.encode(s, StandardCharsets.UTF_8);
113+
}
114+
}

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.stats.Stats;
@@ -19,4 +20,5 @@ public class MailtrapEmailSendingApi {
1920
private final SendingDomains domains;
2021
private final Suppressions suppressions;
2122
private final Stats stats;
23+
private final EmailLogs emailLogs;
2224
}

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.stats.StatsImpl;
@@ -35,20 +36,22 @@
3536
public final class MailtrapClientFactory {
3637

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

5356
private MailtrapClientFactory() {
5457
}
@@ -69,7 +72,8 @@ public static MailtrapClient createMailtrapClient(final MailtrapConfig config) {
6972

7073
final var sendingContextHolder = configureSendingContext(config);
7174

72-
return new MailtrapClient(sendingApi, testingApi, bulkSendingApi, generalApi, contactsApi, emailTemplatesApi, sendingContextHolder);
75+
return new MailtrapClient(sendingApi, testingApi, bulkSendingApi, generalApi, contactsApi, emailTemplatesApi,
76+
sendingContextHolder);
7377
}
7478

7579
private static MailtrapContactsApi createContactsApi(final MailtrapConfig config) {
@@ -80,7 +84,8 @@ private static MailtrapContactsApi createContactsApi(final MailtrapConfig config
8084
final var contactExports = new ContactExportsImpl(config);
8185
final var contactEvents = new ContactEventsImpl(config);
8286

83-
return new MailtrapContactsApi(contactLists, contacts, contactImports, contactFields, contactExports, contactEvents);
87+
return new MailtrapContactsApi(contactLists, contacts, contactImports, contactFields, contactExports,
88+
contactEvents);
8489
}
8590

8691
private static MailtrapGeneralApi createGeneralApi(final MailtrapConfig config) {
@@ -97,8 +102,9 @@ private static MailtrapEmailSendingApi createSendingApi(final MailtrapConfig con
97102
final var domains = new SendingDomainsImpl(config);
98103
final var suppressions = new SuppressionsImpl(config);
99104
final var stats = new StatsImpl(config);
105+
final var emailLogs = new EmailLogsImpl(config);
100106

101-
return new MailtrapEmailSendingApi(emails, domains, suppressions, stats);
107+
return new MailtrapEmailSendingApi(emails, domains, suppressions, stats, emailLogs);
102108
}
103109

104110
private static MailtrapEmailTestingApi createTestingApi(final MailtrapConfig config) {
@@ -126,9 +132,9 @@ private static MailtrapEmailTemplatesApi createEmailTemplatesApi(final MailtrapC
126132
private static SendingContextHolder configureSendingContext(final MailtrapConfig config) {
127133

128134
return SendingContextHolder.builder()
129-
.sandbox(config.isSandbox())
130-
.inboxId(config.getInboxId())
131-
.bulk(config.isBulk())
132-
.build();
135+
.sandbox(config.isSandbox())
136+
.inboxId(config.getInboxId())
137+
.bulk(config.isBulk())
138+
.build();
133139
}
134140
}

src/main/java/io/mailtrap/model/SendingStream.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import com.fasterxml.jackson.annotation.JsonValue;
55

66
public enum SendingStream {
7-
ANY("any"),
87
TRANSACTIONAL("transactional"),
98
BULK("bulk");
109

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+
}

0 commit comments

Comments
 (0)