Skip to content

Commit

Permalink
JAMES-4092 Update webadmin to filter content from mail repository (#2600
Browse files Browse the repository at this point in the history
)
  • Loading branch information
hungphan227 authored Jan 20, 2025
1 parent 20b2a95 commit 7f5aad4
Show file tree
Hide file tree
Showing 9 changed files with 582 additions and 5 deletions.
23 changes: 20 additions & 3 deletions docs/modules/servers/partials/operate/webadmin.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -3612,17 +3612,34 @@ The answer will contains all mailKey contained in that repository.
Note that this can be used to read mail details.

You can pass additional URL parameters to this call in order to limit
the output: - A limit: no more elements than the specified limit will be
the output:

- limit: no more elements than the specified limit will be
returned. This needs to be strictly positive. If no value is specified,
no limit will be applied. - An offset: allow to skip elements. This
needs to be positive. Default value is zero.
no limit will be applied.
- offset: allow to skip elements. This needs to be positive. Default value is zero.

Example:

....
curl -XGET 'http://ip:port/mailRepositories/var%2Fmail%2Ferror%2F/mails?limit=100&offset=500'
....

You can also pass the following additional URL parameters to filter results:

- updatedBefore: filter mails by mail last updated. For example, if the value is `2d` and the current time is `21-12-2024 13:00:00`, the condition would be: last updated < 19-12-2024 13:00:00 (currentTime - `2d`). Some other value samples: `2d`, `2 days`, `2h`, `2 hours`.
- updatedAfter: filter mails by mail last updated. For example, if the value is `2d` and the current time is `21-12-2024 13:00:00`, the condition would be: last updated > 19-12-2024 13:00:00 (currentTime - `2d`). Some other value samples: `2d`, `2 days`, `2h`, `2 hours`.
- sender: filter mails by mail sender. If the input value is in the special format `*@domain.com`, mails are filters by domain.
- recipient: filter by recipient. If the input value is in the special format `*@domain.com`, mails are filters by domain.
- remoteAddress: filter mails by remoteAddress.
- remoteHost: filter mails by remoteHost.

Example:

....
curl -XGET /mailRepositories/var%2Fmail%2Ferror%2F/mails?updatedBefore=2d&remoteAddress=128.45.67.89
....

Response codes:

* 200: The list of mail keys contained in that mail repository
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,14 @@

package org.apache.james.mailrepository.api;

import java.time.Instant;
import java.util.Collection;
import java.util.Iterator;
import java.util.function.Predicate;

import jakarta.mail.MessagingException;

import org.apache.james.util.streams.Iterators;
import org.apache.mailet.Mail;
import org.reactivestreams.Publisher;

Expand All @@ -33,6 +36,107 @@
* Interface for a Repository to store Mails.
*/
public interface MailRepository {
interface Condition extends Predicate<Mail> {
Condition ALL = any -> true;

default Condition and(Condition other) {
return mail -> test(mail) && other.test(mail);
}
}

class UpdatedBeforeCondition implements Condition {
private final Instant updatedBefore;

public UpdatedBeforeCondition(Instant updatedBefore) {
this.updatedBefore = updatedBefore;
}

@Override
public boolean test(Mail mail) {
return mail.getLastUpdated().toInstant().isBefore(updatedBefore);
}
}

class UpdatedAfterCondition implements Condition {
private final Instant updatedAfter;

public UpdatedAfterCondition(Instant updatedAfter) {
this.updatedAfter = updatedAfter;
}

@Override
public boolean test(Mail mail) {
return mail.getLastUpdated().toInstant().isAfter(updatedAfter);
}
}

class SenderCondition implements Condition {
private final String sender;

public SenderCondition(String sender) {
this.sender = sender;
}

@Override
public boolean test(Mail mail) {
if (sender.startsWith("*@")) {
String domain = sender.substring(2).toLowerCase();
return mail.getMaybeSender()
.asOptional()
.map(mailAddress -> mailAddress.asString().toLowerCase().endsWith("@" + domain))
.orElse(false);
}
return mail.getMaybeSender()
.asOptional()
.map(mailAddress -> mailAddress.asString().equalsIgnoreCase(sender))
.orElse(false);
}
}

class RecipientCondition implements Condition {
private final String recipient;

public RecipientCondition(String recipient) {
this.recipient = recipient;
}

@Override
public boolean test(Mail mail) {
if (recipient.startsWith("*@")) {
String domain = recipient.substring(2).toLowerCase();
return mail.getRecipients().stream()
.anyMatch(address -> address.asString().toLowerCase().endsWith("@" + domain));
}
return mail.getRecipients().stream()
.anyMatch(address -> address.asString().equalsIgnoreCase(recipient));
}
}

class RemoteAddressCondition implements Condition {
private final String remoteAddress;

public RemoteAddressCondition(String remoteAddress) {
this.remoteAddress = remoteAddress;
}

@Override
public boolean test(Mail mail) {
return mail.getRemoteAddr().equalsIgnoreCase(remoteAddress);
}
}

class RemoteHostCondition implements Condition {
private final String remoteHost;

public RemoteHostCondition(String remoteHost) {
this.remoteHost = remoteHost;
}

@Override
public boolean test(Mail mail) {
return mail.getRemoteHost().equalsIgnoreCase(remoteHost);
}
}

/**
* @return Number of mails stored in that repository
Expand All @@ -59,6 +163,21 @@ default Publisher<Long> sizeReactive() {
*/
Iterator<MailKey> list() throws MessagingException;

default Iterator<MailKey> list(Condition condition) throws MessagingException {
if (Condition.ALL.equals(condition)) {
return list();
}
return Iterators.toStream(list())
.filter(key -> {
try {
Mail mail = retrieve(key);
return condition.test(mail);
} catch (MessagingException e) {
throw new RuntimeException(e);
}
}).iterator();
}

/**
* Retrieves a message given a key. At the moment, keys can be obtained from
* list() in superinterface Store.Repository
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import org.apache.james.server.core.MailImpl;
import org.apache.james.server.core.MimeMessageWrapper;
import org.apache.james.util.AuditTrail;
import org.apache.james.util.streams.Iterators;
import org.apache.mailet.Mail;
import org.reactivestreams.Publisher;

Expand Down Expand Up @@ -97,6 +98,22 @@ public Iterator<MailKey> list() {
.iterator();
}

@Override
public Iterator<MailKey> list(Condition condition) {
return Iterators.toStream(list())
.filter(key -> {
Mail mail = retrieveMetadata(key);
return condition.test(mail);
}).iterator();
}

private Mail retrieveMetadata(MailKey key) {
return mailDAO.read(url, key)
.handle(publishIfPresent())
.map(mailDTO -> mailDTO.getMailBuilder().build())
.block();
}

@Override
public Mail retrieve(MailKey key) {
return mailDAO.read(url, key)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,16 @@
import org.apache.james.blob.cassandra.CassandraBlobModule;
import org.apache.james.blob.cassandra.CassandraBlobStoreFactory;
import org.apache.james.blob.mail.MimeMessageStore;
import org.apache.james.core.builder.MimeMessageBuilder;
import org.apache.james.mailrepository.MailRepositoryContract;
import org.apache.james.mailrepository.api.MailKey;
import org.apache.james.mailrepository.api.MailRepository;
import org.apache.james.mailrepository.api.MailRepositoryPath;
import org.apache.james.mailrepository.api.MailRepositoryUrl;
import org.apache.james.mailrepository.api.Protocol;
import org.apache.james.metrics.tests.RecordingMetricFactory;
import org.apache.james.server.core.MailImpl;
import org.apache.mailet.Mail;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Nested;
Expand Down Expand Up @@ -181,6 +184,32 @@ void removeShouldNotAffectMailsWithTheSameContent() throws Exception {
assertThatCode(() -> testee.retrieve(key2))
.doesNotThrowAnyException();
}

@Test
void listWithConditionsShouldReturnStoredMailsKeys() throws Exception {
MailRepository testee = retrieveRepository();

Mail mail = MailImpl.builder()
.name("mail1")
.sender("[email protected]")
.addRecipient("[email protected]")
.mimeMessage(MimeMessageBuilder.mimeMessageBuilder().build())
.build();

Mail mail2 = MailImpl.builder()
.name("mail2")
.sender("[email protected]")
.addRecipient("[email protected]")
.mimeMessage(MimeMessageBuilder.mimeMessageBuilder().build())
.build();

MailKey key1 = testee.store(mail);
testee.store(mail2);

assertThat(testee.list(new MailRepository.SenderCondition("[email protected]")))
.toIterable()
.containsOnly(key1);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ public class ParametersExtractor {

public static final String LIMIT_PARAMETER_NAME = "limit";
public static final String OFFSET_PARAMETER_NAME = "offset";
public static final String UPDATED_BEFORE_PARAMETER_NAME = "updatedBefore";
public static final String UPDATED_AFTER_PARAMETER_NAME = "updatedAfter";
public static final String SENDER_PARAMETER_NAME = "sender";
public static final String RECIPIENT_PARAMETER_NAME = "recipient";
public static final String REMOTE_ADDRESS_PARAMETER_NAME = "remoteAddress";
public static final String REMOTE_HOST_PARAMETER_NAME = "remoteHost";

public static Limit extractLimit(Request request) {
return Limit.from(extractPositiveInteger(request, LIMIT_PARAMETER_NAME)
Expand All @@ -47,6 +53,34 @@ public static Offset extractOffset(Request request) {
return Offset.from(extractPositiveInteger(request, OFFSET_PARAMETER_NAME));
}

public static Optional<Duration> extractUpdatedBeforeParam(Request request) {
return extractDuration(request, UPDATED_BEFORE_PARAMETER_NAME);
}

public static Optional<Duration> extractUpdatedAfterParam(Request request) {
return extractDuration(request, UPDATED_AFTER_PARAMETER_NAME);
}

public static Optional<String> extractSenderParam(Request request) {
return Optional.ofNullable(request.queryParams(SENDER_PARAMETER_NAME))
.filter(s -> !s.isEmpty());
}

public static Optional<String> extractRecipientParam(Request request) {
return Optional.ofNullable(request.queryParams(RECIPIENT_PARAMETER_NAME))
.filter(s -> !s.isEmpty());
}

public static Optional<String> extractRemoteAddressParam(Request request) {
return Optional.ofNullable(request.queryParams(REMOTE_ADDRESS_PARAMETER_NAME))
.filter(s -> !s.isEmpty());
}

public static Optional<String> extractRemoteHostParam(Request request) {
return Optional.ofNullable(request.queryParams(REMOTE_HOST_PARAMETER_NAME))
.filter(s -> !s.isEmpty());
}

public static Optional<Double> extractPositiveDouble(Request request, String parameterName) {
return extractPositiveNumber(request, parameterName, Double::valueOf);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,12 @@
import java.io.IOException;
import java.io.OutputStream;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.Optional;
import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Stream;

import javax.servlet.http.HttpServletResponse;

Expand All @@ -35,6 +38,7 @@
import org.apache.commons.io.output.CountingOutputStream;
import org.apache.james.core.MailAddress;
import org.apache.james.mailrepository.api.MailKey;
import org.apache.james.mailrepository.api.MailRepository;
import org.apache.james.mailrepository.api.MailRepositoryPath;
import org.apache.james.mailrepository.api.MailRepositoryStore;
import org.apache.james.queue.api.MailQueueFactory;
Expand Down Expand Up @@ -148,9 +152,19 @@ public void defineListMails() {
service.get(MAIL_REPOSITORIES + "/:encodedPath/mails", (request, response) -> {
Offset offset = ParametersExtractor.extractOffset(request);
Limit limit = ParametersExtractor.extractLimit(request);
MailRepository.Condition condition
= Stream.of(ParametersExtractor.extractUpdatedBeforeParam(request).map(this::parseDurationToInstant).map(MailRepository.UpdatedBeforeCondition::new),
ParametersExtractor.extractUpdatedAfterParam(request).map(this::parseDurationToInstant).map(MailRepository.UpdatedAfterCondition::new),
ParametersExtractor.extractSenderParam(request).map(MailRepository.SenderCondition::new),
ParametersExtractor.extractRecipientParam(request).map(MailRepository.RecipientCondition::new),
ParametersExtractor.extractRemoteAddressParam(request).map(MailRepository.RemoteAddressCondition::new),
ParametersExtractor.extractRemoteHostParam(request).map(MailRepository.RemoteHostCondition::new))
.flatMap(Optional::stream)
.map(MailRepository.Condition.class::cast)
.reduce(MailRepository.Condition.ALL, MailRepository.Condition::and);
MailRepositoryPath path = getRepositoryPath(request);
try {
return repositoryStoreService.listMails(path, offset, limit)
return repositoryStoreService.listMails(path, offset, limit, condition)
.orElseThrow(() -> repositoryNotFound(request.params("encodedPath"), path));

} catch (MailRepositoryStore.MailRepositoryStoreException | MessagingException e) {
Expand Down Expand Up @@ -393,4 +407,8 @@ private Limit parseLimit(Request request) {
private Optional<Integer> parseMaxRetries(Request request) {
return ParametersExtractor.extractPositiveInteger(request, "maxRetries");
}

private Instant parseDurationToInstant(Duration duration) {
return Instant.now().minus(duration);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,12 @@ public MailRepository createMailRepository(MailRepositoryPath repositoryPath, St
}

public Optional<List<MailKeyDTO>> listMails(MailRepositoryPath path, Offset offset, Limit limit) throws MailRepositoryStore.MailRepositoryStoreException, MessagingException {
return listMails(path, offset, limit, MailRepository.Condition.ALL);
}

public Optional<List<MailKeyDTO>> listMails(MailRepositoryPath path, Offset offset, Limit limit, MailRepository.Condition condition) throws MailRepositoryStore.MailRepositoryStoreException, MessagingException {
Optional<Stream<MailKeyDTO>> maybeMails = Optional.of(getRepositories(path)
.flatMap(Throwing.function((MailRepository repository) -> Iterators.toStream(repository.list())).sneakyThrow())
.flatMap(Throwing.function((MailRepository repository) -> Iterators.toStream(repository.list(condition))).sneakyThrow())
.map(MailKeyDTO::new)
.skip(offset.getOffset()));

Expand Down
Loading

0 comments on commit 7f5aad4

Please sign in to comment.