Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JAMES-4092 Update webadmin to filter content from mail repository #2600

Merged
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
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("sender@domain.com")
.addRecipient("rec1@domain.com")
.mimeMessage(MimeMessageBuilder.mimeMessageBuilder().build())
.build();

Mail mail2 = MailImpl.builder()
.name("mail2")
.sender("sender2@domain.com")
.addRecipient("rec1@domain.com")
.mimeMessage(MimeMessageBuilder.mimeMessageBuilder().build())
.build();

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

assertThat(testee.list(new MailRepository.SenderCondition("sender@domain.com")))
.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