Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,7 @@ You can find the [Mailtrap Java API reference](https://mailtrap.github.io/mailtr
- [Batch](examples/java/io/mailtrap/examples/sending/BatchExample.java)
- [Sending Domains](examples/java/io/mailtrap/examples/sendingdomains/SendingDomainsExample.java)
- [Suppressions](examples/java/io/mailtrap/examples/suppressions/SuppressionsExample.java)
- [Email Logs](examples/java/io/mailtrap/examples/emaillogs/EmailLogsExample.java)

### Email Testing API

Expand Down
56 changes: 56 additions & 0 deletions examples/java/io/mailtrap/examples/emaillogs/EmailLogsExample.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package io.mailtrap.examples.emaillogs;

import io.mailtrap.config.MailtrapConfig;
import io.mailtrap.factory.MailtrapClientFactory;
import io.mailtrap.model.request.emaillogs.EmailLogsListFilters;
import io.mailtrap.model.request.emaillogs.FilterCiString;
import io.mailtrap.model.request.emaillogs.FilterExactString;
import io.mailtrap.model.request.emaillogs.FilterStatus;
import io.mailtrap.model.request.emaillogs.FilterOptionalString;
import io.mailtrap.model.response.emaillogs.MessageStatus;

import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.List;

public class EmailLogsExample {

private static final String TOKEN = "<YOUR MAILTRAP TOKEN>";
private static final long ACCOUNT_ID = 1L;

public static void main(String[] args) {
final var config = new MailtrapConfig.Builder()
.token(TOKEN)
.build();

final var client = MailtrapClientFactory.createMailtrapClient(config);

// List email logs for the last 2 days
final var now = Instant.now();
final var twoDaysAgo = now.minus(2, ChronoUnit.DAYS);
final var filters = EmailLogsListFilters.builder()
.sentAfter(twoDaysAgo.toString())
.sentBefore(now.toString())
.subject(new FilterOptionalString(FilterOptionalString.Operator.not_empty))
.to(new FilterCiString(FilterCiString.Operator.ci_equal, "recipient@example.com"))
.category(new FilterExactString(FilterExactString.Operator.equal,
List.of("Welcome Email")))
.build();

final var listResponse = client.sendingApi().emailLogs()
.list(ACCOUNT_ID, null, filters);

System.out.println("Total: " + listResponse.getTotalCount());
listResponse.getMessages().forEach(
msg -> System.out.println(" " + msg.getMessageId() + " " + msg.getSubject() + " "
+ msg.getStatus()));

// Get a single message by ID (use message_id from list response)
if (!listResponse.getMessages().isEmpty()) {
final var messageId = listResponse.getMessages().get(0).getMessageId();
final var message = client.sendingApi().emailLogs().get(ACCOUNT_ID, messageId);
System.out.println(
"Message: " + message.getSubject() + " events: " + message.getEvents().size());
}
}
}
31 changes: 31 additions & 0 deletions src/main/java/io/mailtrap/api/emaillogs/EmailLogs.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package io.mailtrap.api.emaillogs;

import io.mailtrap.model.request.emaillogs.EmailLogsListFilters;
import io.mailtrap.model.response.emaillogs.EmailLogsListResponse;
import io.mailtrap.model.response.emaillogs.EmailLogMessage;

/**
* API for listing and retrieving email sending logs.
*/
public interface EmailLogs {

/**
* Returns a paginated list of email logs for the account.
*
* @param accountId account ID
* @param searchAfter optional cursor (message_id UUID from previous response
* next_page_cursor) for the next page
* @param filters optional filters; pass null or empty to omit
* @return paginated list with messages, total_count, and next_page_cursor
*/
EmailLogsListResponse list(long accountId, String searchAfter, EmailLogsListFilters filters);

/**
* Returns a single email log message by its UUID.
*
* @param accountId account ID
* @param sendingMessageId message UUID
* @return the message with details and events, or throws if not found
*/
EmailLogMessage get(long accountId, String sendingMessageId);
}
110 changes: 110 additions & 0 deletions src/main/java/io/mailtrap/api/emaillogs/EmailLogsImpl.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package io.mailtrap.api.emaillogs;

import io.mailtrap.Constants;
import io.mailtrap.api.apiresource.ApiResource;
import io.mailtrap.config.MailtrapConfig;
import io.mailtrap.http.RequestData;
import io.mailtrap.model.request.emaillogs.EmailLogFilter;
import io.mailtrap.model.request.emaillogs.EmailLogsListFilters;
import io.mailtrap.model.response.emaillogs.EmailLogsListResponse;
import io.mailtrap.model.response.emaillogs.EmailLogMessage;

import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

public class EmailLogsImpl extends ApiResource implements EmailLogs {

public EmailLogsImpl(final MailtrapConfig config) {
super(config);
this.apiHost = Constants.GENERAL_HOST;
}

@Override
public EmailLogsListResponse list(final long accountId, final String searchAfter,
final EmailLogsListFilters filters) {
final String queryString = buildQueryString(searchAfter, filters);
final String url = String.format("%s/api/accounts/%d/email_logs", apiHost, accountId)
+ (queryString.isEmpty() ? "" : "?" + queryString);

return httpClient.get(url, new RequestData(), EmailLogsListResponse.class);
}

@Override
public EmailLogMessage get(final long accountId, final String sendingMessageId) {
if (sendingMessageId == null || sendingMessageId.isBlank()) {
throw new IllegalArgumentException("sendingMessageId must not be null or blank");
}
final String url = String.format("%s/api/accounts/%d/email_logs/%s", apiHost, accountId, sendingMessageId);
return httpClient.get(url, new RequestData(), EmailLogMessage.class);
}

private static String buildQueryString(final String searchAfter, final EmailLogsListFilters filters) {
final List<String> params = new ArrayList<>();

if (searchAfter != null && !searchAfter.isBlank()) {
params.add(enc("search_after") + "=" + enc(searchAfter));
}

if (filters != null) {
appendFilter(params, "sent_after", filters.getSentAfter());
appendFilter(params, "sent_before", filters.getSentBefore());
appendOperatorValue(params, "to", filters.getTo());
appendOperatorValue(params, "from", filters.getFrom());
appendOperatorValue(params, "subject", filters.getSubject());
appendOperatorValue(params, "status", filters.getStatus());
appendOperatorValue(params, "events", filters.getEvents());
appendOperatorValue(params, "clicks_count", filters.getClicksCount());
appendOperatorValue(params, "opens_count", filters.getOpensCount());
appendOperatorValue(params, "client_ip", filters.getClientIp());
appendOperatorValue(params, "sending_ip", filters.getSendingIp());
appendOperatorValue(params, "email_service_provider_response", filters.getEmailServiceProviderResponse());
appendOperatorValue(params, "email_service_provider", filters.getEmailServiceProvider());
appendOperatorValue(params, "recipient_mx", filters.getRecipientMx());
appendOperatorValue(params, "category", filters.getCategory());
appendOperatorValue(params, "sending_domain_id", filters.getSendingDomainId());
appendOperatorValue(params, "sending_stream", filters.getSendingStream());
}

return String.join("&", params);
}

private static void appendFilter(final List<String> params, final String key, final String value) {
if (value != null && !value.isBlank()) {
params.add(enc("filters[" + key + "]") + "=" + enc(value));
}
}

private static void appendOperatorValue(final List<String> params, final String field, final EmailLogFilter spec) {
if (spec == null)
return;
final String operator = spec.getOperatorString();
if (operator == null || operator.isBlank())
return;
params.add(enc("filters[" + field + "][operator]") + "=" + enc(operator));
final Object value = spec.getValue();
if (value != null) {
for (final String v : toValueList(value)) {
params.add(enc("filters[" + field + "][value]") + "=" + enc(String.valueOf(v)));
}
}
}

private static List<String> toValueList(final Object value) {
if (value instanceof Collection<?> c) {
return c.stream()
.filter(v -> v != null)
.map(String::valueOf)
.collect(Collectors.toList());
}
return Collections.singletonList(String.valueOf(value));
}

private static String enc(final String s) {
return URLEncoder.encode(s, StandardCharsets.UTF_8);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.mailtrap.client.api;

import io.mailtrap.api.emaillogs.EmailLogs;
import io.mailtrap.api.sendingdomains.SendingDomains;
import io.mailtrap.api.sendingemails.SendingEmails;
import io.mailtrap.api.suppressions.Suppressions;
Expand All @@ -17,4 +18,5 @@ public class MailtrapEmailSendingApi {
private final SendingEmails emails;
private final SendingDomains domains;
private final Suppressions suppressions;
private final EmailLogs emailLogs;
}
42 changes: 24 additions & 18 deletions src/main/java/io/mailtrap/factory/MailtrapClientFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import io.mailtrap.api.messages.MessagesImpl;
import io.mailtrap.api.permissions.PermissionsImpl;
import io.mailtrap.api.projects.ProjectsImpl;
import io.mailtrap.api.emaillogs.EmailLogsImpl;
import io.mailtrap.api.sendingdomains.SendingDomainsImpl;
import io.mailtrap.api.sendingemails.SendingEmailsImpl;
import io.mailtrap.api.suppressions.SuppressionsImpl;
Expand All @@ -34,20 +35,22 @@
public final class MailtrapClientFactory {

/**
* Creates a new instance of {@link MailtrapValidator} using the default validator factory.
* Intentionally not wrapped into try-with-resources to not close, as per Jakarta doc, after
* the {@code ValidatorFactory} instance is closed, calling the following methods is not allowed:
* Creates a new instance of {@link MailtrapValidator} using the default
* validator factory.
* Intentionally not wrapped into try-with-resources to not close, as per
* Jakarta doc, after
* the {@code ValidatorFactory} instance is closed, calling the following
* methods is not allowed:
* <ul>
* <li>methods of this {@code ValidatorFactory} instance</li>
* <li>methods of {@link Validator} instances created by this
* {@code ValidatorFactory}</li>
* <li>methods of this {@code ValidatorFactory} instance</li>
* <li>methods of {@link Validator} instances created by this
* {@code ValidatorFactory}</li>
* </ul>
*/
private static final jakarta.validation.ValidatorFactory VALIDATOR_FACTORY =
Validation.buildDefaultValidatorFactory();

private static final MailtrapValidator VALIDATOR =
new MailtrapValidator(VALIDATOR_FACTORY.getValidator());
private static final jakarta.validation.ValidatorFactory VALIDATOR_FACTORY = Validation
.buildDefaultValidatorFactory();

private static final MailtrapValidator VALIDATOR = new MailtrapValidator(VALIDATOR_FACTORY.getValidator());

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

final var sendingContextHolder = configureSendingContext(config);

return new MailtrapClient(sendingApi, testingApi, bulkSendingApi, generalApi, contactsApi, emailTemplatesApi, sendingContextHolder);
return new MailtrapClient(sendingApi, testingApi, bulkSendingApi, generalApi, contactsApi, emailTemplatesApi,
sendingContextHolder);
}

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

return new MailtrapContactsApi(contactLists, contacts, contactImports, contactFields, contactExports, contactEvents);
return new MailtrapContactsApi(contactLists, contacts, contactImports, contactFields, contactExports,
contactEvents);
}

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

return new MailtrapEmailSendingApi(emails, domains, suppressions);
return new MailtrapEmailSendingApi(emails, domains, suppressions, emailLogs);
}

private static MailtrapEmailTestingApi createTestingApi(final MailtrapConfig config) {
Expand Down Expand Up @@ -124,9 +130,9 @@ private static MailtrapEmailTemplatesApi createEmailTemplatesApi(final MailtrapC
private static SendingContextHolder configureSendingContext(final MailtrapConfig config) {

return SendingContextHolder.builder()
.sandbox(config.isSandbox())
.inboxId(config.getInboxId())
.bulk(config.isBulk())
.build();
.sandbox(config.isSandbox())
.inboxId(config.getInboxId())
.bulk(config.isBulk())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.mailtrap.model.request.emaillogs;

/**
* Common contract for email log filters so the API can serialize operator and value.
*/
public interface EmailLogFilter {

/** API operator string (e.g. "equal", "ci_contain"). */
String getOperatorString();

/** Filter value; may be null for operators like empty/not_empty. */
Object getValue();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package io.mailtrap.model.request.emaillogs;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
* Filters for listing email logs. All fields are optional.
* Date range uses sent_after and sent_before (ISO 8601 date-time strings).
* Other filters use concrete types so operators are enforced per field.
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class EmailLogsListFilters {

private String sentAfter;
private String sentBefore;
private FilterCiString to;
private FilterCiString from;
private FilterOptionalString subject;
private FilterStatus status;
private FilterEvents events;
private FilterNumber clicksCount;
private FilterNumber opensCount;
private FilterString clientIp;
private FilterString sendingIp;
private FilterCiString emailServiceProviderResponse;
private FilterExactString emailServiceProvider;
private FilterExactString recipientMx;
private FilterExactString category;
private FilterSendingDomainId sendingDomainId;
private FilterSendingStream sendingStream;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package io.mailtrap.model.request.emaillogs;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
* Filter with case-insensitive string operators (e.g. to, from). Value may be single or list for ci_equal, ci_not_equal.
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class FilterCiString implements EmailLogFilter {

public enum Operator {
ci_contain, ci_not_contain, ci_equal, ci_not_equal
}

private Operator operator;
private Object value;

@Override
public String getOperatorString() {
return operator == null ? null : operator.name();
}

@Override
public Object getValue() {
return value;
}
}
Loading
Loading