This is an automated email from the ASF dual-hosted git repository. btellier pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/james-project.git
commit 871d5f233225ce56be6e71bc99f973a9c01598f0 Author: Benoit Tellier <[email protected]> AuthorDate: Wed Sep 23 13:50:30 2020 +0700 JAMES-3375 MailboxManager::search should enable finner grained control over the search mailboxes Enable to either: - Search personal mailboxes - Search all accessible mailboxes In addition to the existing inMailboxes/notInMailboxes criteria --- .../mailbox/model/MultimailboxesSearchQuery.java | 74 +++- .../apache/james/mailbox/MailboxManagerTest.java | 68 ++++ .../model/MultimailboxesSearchQueryTest.java | 13 +- .../james/mailbox/store/StoreMailboxManager.java | 24 +- .../contract/EmailQueryMethodContract.scala | 398 ++++++++++++++++++++- .../james/jmap/method/EmailQueryMethod.scala | 14 +- .../james/jmap/utils/search/MailboxFilter.scala | 13 +- 7 files changed, 571 insertions(+), 33 deletions(-) diff --git a/mailbox/api/src/main/java/org/apache/james/mailbox/model/MultimailboxesSearchQuery.java b/mailbox/api/src/main/java/org/apache/james/mailbox/model/MultimailboxesSearchQuery.java index b0b867d..540f05f 100644 --- a/mailbox/api/src/main/java/org/apache/james/mailbox/model/MultimailboxesSearchQuery.java +++ b/mailbox/api/src/main/java/org/apache/james/mailbox/model/MultimailboxesSearchQuery.java @@ -21,6 +21,11 @@ package org.apache.james.mailbox.model; import java.util.Arrays; import java.util.Collection; +import java.util.Objects; +import java.util.Optional; + +import org.apache.james.mailbox.MailboxSession; +import org.apache.james.mailbox.model.search.MailboxQuery; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; @@ -31,18 +36,67 @@ public class MultimailboxesSearchQuery { public static Builder from(SearchQuery searchQuery) { return new Builder(searchQuery); } + + public interface Namespace { + boolean keepAccessible(Mailbox mailbox); + + MailboxQuery associatedMailboxSearchQuery(); + } + + public static class PersonalNamespace implements Namespace { + private final MailboxSession session; + + public PersonalNamespace(MailboxSession session) { + this.session = session; + } + + @Override + public boolean keepAccessible(Mailbox mailbox) { + return mailbox.generateAssociatedPath().belongsTo(session); + } + + @Override + public MailboxQuery associatedMailboxSearchQuery() { + return MailboxQuery.privateMailboxesBuilder(session) + .matchesAllMailboxNames() + .build(); + } + } + + public static class AccessibleNamespace implements Namespace { + @Override + public boolean keepAccessible(Mailbox mailbox) { + return true; + } + + @Override + public MailboxQuery associatedMailboxSearchQuery() { + return MailboxQuery.builder().matchesAllMailboxNames().build(); + } + + @Override + public int hashCode() { + return Objects.hashCode(AccessibleNamespace.class); + } + + @Override + public boolean equals(Object obj) { + return obj instanceof AccessibleNamespace; + } + } public static class Builder { - private final SearchQuery searchQuery; private ImmutableSet.Builder<MailboxId> mailboxIds; private ImmutableSet.Builder<MailboxId> notInMailboxIds; + private Optional<Namespace> namespace; private Builder(SearchQuery searchQuery) { Preconditions.checkNotNull(searchQuery); this.searchQuery = searchQuery; this.mailboxIds = ImmutableSet.builder(); this.notInMailboxIds = ImmutableSet.builder(); + this.namespace = Optional.empty(); } public Builder inMailboxes(Collection<MailboxId> mailboxIds) { @@ -58,13 +112,21 @@ public class MultimailboxesSearchQuery { this.notInMailboxIds.addAll(mailboxIds); return this; } + + public Builder inNamespace(Namespace namespace) { + this.namespace = Optional.of(namespace); + return this; + } public Builder notInMailboxes(MailboxId... mailboxIds) { return notInMailboxes(Arrays.asList(mailboxIds)); } public MultimailboxesSearchQuery build() { - return new MultimailboxesSearchQuery(searchQuery, mailboxIds.build(), notInMailboxIds.build()); + return new MultimailboxesSearchQuery(searchQuery, + mailboxIds.build(), + notInMailboxIds.build(), + namespace.orElse(new AccessibleNamespace())); } } @@ -72,12 +134,14 @@ public class MultimailboxesSearchQuery { private final SearchQuery searchQuery; private final ImmutableSet<MailboxId> inMailboxes; private final ImmutableSet<MailboxId> notInMailboxes; + private final Namespace namespace; @VisibleForTesting - MultimailboxesSearchQuery(SearchQuery searchQuery, ImmutableSet<MailboxId> inMailboxes, ImmutableSet<MailboxId> notInMailboxes) { + MultimailboxesSearchQuery(SearchQuery searchQuery, ImmutableSet<MailboxId> inMailboxes, ImmutableSet<MailboxId> notInMailboxes, Namespace namespace) { this.searchQuery = searchQuery; this.inMailboxes = inMailboxes; this.notInMailboxes = notInMailboxes; + this.namespace = namespace; } public ImmutableSet<MailboxId> getInMailboxes() { @@ -91,4 +155,8 @@ public class MultimailboxesSearchQuery { public SearchQuery getSearchQuery() { return searchQuery; } + + public Namespace getNamespace() { + return namespace; + } } diff --git a/mailbox/api/src/test/java/org/apache/james/mailbox/MailboxManagerTest.java b/mailbox/api/src/test/java/org/apache/james/mailbox/MailboxManagerTest.java index 95a220a..a274329 100644 --- a/mailbox/api/src/test/java/org/apache/james/mailbox/MailboxManagerTest.java +++ b/mailbox/api/src/test/java/org/apache/james/mailbox/MailboxManagerTest.java @@ -76,6 +76,8 @@ import org.apache.james.mailbox.model.MessageId; import org.apache.james.mailbox.model.MessageRange; import org.apache.james.mailbox.model.MessageResult; import org.apache.james.mailbox.model.MultimailboxesSearchQuery; +import org.apache.james.mailbox.model.MultimailboxesSearchQuery.AccessibleNamespace; +import org.apache.james.mailbox.model.MultimailboxesSearchQuery.PersonalNamespace; import org.apache.james.mailbox.model.Quota; import org.apache.james.mailbox.model.QuotaRoot; import org.apache.james.mailbox.model.SearchQuery; @@ -1256,6 +1258,39 @@ public abstract class MailboxManagerTest<T extends MailboxManager> { } @Test + void searchForMessageShouldReturnMessagesFromMyDelegatedMailboxesWhenAccessibleNamespace() throws Exception { + assumeTrue(mailboxManager.hasCapability(MailboxCapabilities.ACL)); + + session = mailboxManager.createSystemSession(USER_1); + MailboxSession sessionFromDelegater = mailboxManager.createSystemSession(USER_2); + MailboxPath delegatedMailboxPath = MailboxPath.forUser(USER_2, "SHARED"); + MailboxId delegatedMailboxId = mailboxManager.createMailbox(delegatedMailboxPath, sessionFromDelegater).get(); + MessageManager delegatedMessageManager = mailboxManager.getMailbox(delegatedMailboxId, sessionFromDelegater); + + MessageId messageId = delegatedMessageManager + .appendMessage(AppendCommand.from(message), sessionFromDelegater) + .getId().getMessageId(); + + mailboxManager.setRights(delegatedMailboxPath, + MailboxACL.EMPTY.apply(MailboxACL.command() + .forUser(USER_1) + .rights(MailboxACL.Right.Read, MailboxACL.Right.Lookup) + .asAddition()), + sessionFromDelegater); + + MultimailboxesSearchQuery multiMailboxesQuery = MultimailboxesSearchQuery + .from(SearchQuery.matchAll()) + .inNamespace(new AccessibleNamespace()) + .build(); + + assertThat( + Flux.from(mailboxManager.search(multiMailboxesQuery, session, DEFAULT_MAXIMUM_LIMIT)) + .collectList() + .block()) + .containsOnly(messageId); + } + + @Test void searchForMessageShouldNotReturnMessagesFromMyDelegatedMailboxesICanNotRead() throws Exception { assumeTrue(mailboxManager.hasCapability(MailboxCapabilities.ACL)); @@ -1370,6 +1405,39 @@ public abstract class MailboxManagerTest<T extends MailboxManager> { } @Test + void searchShouldRestrictResultsToTheSuppliedUserNamespace() throws Exception { + assumeTrue(mailboxManager.hasCapability(MailboxCapabilities.ACL)); + + session = mailboxManager.createSystemSession(USER_1); + MailboxSession sessionFromDelegater = mailboxManager.createSystemSession(USER_2); + MailboxPath delegatedMailboxPath = MailboxPath.forUser(USER_2, "SHARED"); + MailboxId delegatedMailboxId = mailboxManager.createMailbox(delegatedMailboxPath, sessionFromDelegater).get(); + MessageManager delegatedMessageManager = mailboxManager.getMailbox(delegatedMailboxId, sessionFromDelegater); + + MessageId messageId = delegatedMessageManager + .appendMessage(AppendCommand.from(message), sessionFromDelegater) + .getId().getMessageId(); + + mailboxManager.setRights(delegatedMailboxPath, + MailboxACL.EMPTY.apply(MailboxACL.command() + .forUser(USER_1) + .rights(MailboxACL.Right.Read, MailboxACL.Right.Lookup) + .asAddition()), + sessionFromDelegater); + + MultimailboxesSearchQuery multiMailboxesQuery = MultimailboxesSearchQuery + .from(SearchQuery.matchAll()) + .inNamespace(new PersonalNamespace(session)) + .build(); + + assertThat( + Flux.from(mailboxManager.search(multiMailboxesQuery, session, DEFAULT_MAXIMUM_LIMIT)) + .collectList() + .block()) + .isEmpty(); + } + + @Test void searchForMessageShouldOnlySearchInGivenMailbox() throws Exception { assumeTrue(mailboxManager.hasCapability(MailboxCapabilities.ACL)); diff --git a/mailbox/api/src/test/java/org/apache/james/mailbox/model/MultimailboxesSearchQueryTest.java b/mailbox/api/src/test/java/org/apache/james/mailbox/model/MultimailboxesSearchQueryTest.java index 6d50cf0..d932a14 100644 --- a/mailbox/api/src/test/java/org/apache/james/mailbox/model/MultimailboxesSearchQueryTest.java +++ b/mailbox/api/src/test/java/org/apache/james/mailbox/model/MultimailboxesSearchQueryTest.java @@ -21,6 +21,7 @@ package org.apache.james.mailbox.model; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import org.apache.james.mailbox.model.MultimailboxesSearchQuery.AccessibleNamespace; import org.junit.jupiter.api.Test; import com.google.common.collect.ImmutableSet; @@ -42,7 +43,7 @@ class MultimailboxesSearchQueryTest { void buildShouldBuildWheninMailboxes() { ImmutableSet<MailboxId> inMailboxes = ImmutableSet.of(); ImmutableSet<MailboxId> notInMailboxes = ImmutableSet.of(); - MultimailboxesSearchQuery expected = new MultimailboxesSearchQuery(EMPTY_QUERY, inMailboxes, notInMailboxes); + MultimailboxesSearchQuery expected = new MultimailboxesSearchQuery(EMPTY_QUERY, inMailboxes, notInMailboxes, new AccessibleNamespace()); MultimailboxesSearchQuery actual = MultimailboxesSearchQuery.from(EMPTY_QUERY).build(); assertThat(actual).isEqualToComparingFieldByField(expected); @@ -52,7 +53,7 @@ class MultimailboxesSearchQueryTest { void buildShouldBuildWhenEmptyMailboxes() { ImmutableSet<MailboxId> inMailboxes = ImmutableSet.of(); ImmutableSet<MailboxId> notInMailboxes = ImmutableSet.of(); - MultimailboxesSearchQuery expected = new MultimailboxesSearchQuery(EMPTY_QUERY, inMailboxes, notInMailboxes); + MultimailboxesSearchQuery expected = new MultimailboxesSearchQuery(EMPTY_QUERY, inMailboxes, notInMailboxes, new AccessibleNamespace()); MultimailboxesSearchQuery actual = MultimailboxesSearchQuery.from(EMPTY_QUERY).inMailboxes().build(); assertThat(actual).isEqualToComparingFieldByField(expected); @@ -62,7 +63,7 @@ class MultimailboxesSearchQueryTest { void buildShouldBuildWhenEmptyNotInMailboxes() { ImmutableSet<MailboxId> inMailboxes = ImmutableSet.of(); ImmutableSet<MailboxId> notInMailboxes = ImmutableSet.of(); - MultimailboxesSearchQuery expected = new MultimailboxesSearchQuery(EMPTY_QUERY, inMailboxes, notInMailboxes); + MultimailboxesSearchQuery expected = new MultimailboxesSearchQuery(EMPTY_QUERY, inMailboxes, notInMailboxes, new AccessibleNamespace()); MultimailboxesSearchQuery actual = MultimailboxesSearchQuery.from(EMPTY_QUERY).notInMailboxes().build(); assertThat(actual).isEqualToComparingFieldByField(expected); @@ -73,7 +74,7 @@ class MultimailboxesSearchQueryTest { void buildShouldBuildWhenOneMailbox() { ImmutableSet<MailboxId> inMailboxes = ImmutableSet.of(ID_1); ImmutableSet<MailboxId> notInMailboxes = ImmutableSet.of(); - MultimailboxesSearchQuery expected = new MultimailboxesSearchQuery(EMPTY_QUERY, inMailboxes, notInMailboxes); + MultimailboxesSearchQuery expected = new MultimailboxesSearchQuery(EMPTY_QUERY, inMailboxes, notInMailboxes, new AccessibleNamespace()); MultimailboxesSearchQuery actual = MultimailboxesSearchQuery.from(EMPTY_QUERY).inMailboxes(ID_1).build(); assertThat(actual).isEqualToComparingFieldByField(expected); @@ -83,7 +84,7 @@ class MultimailboxesSearchQueryTest { void buildShouldBuildWhenOneNotInMailbox() { ImmutableSet<MailboxId> inMailboxes = ImmutableSet.of(); ImmutableSet<MailboxId> notInMailboxes = ImmutableSet.of(ID_1); - MultimailboxesSearchQuery expected = new MultimailboxesSearchQuery(EMPTY_QUERY, inMailboxes, notInMailboxes); + MultimailboxesSearchQuery expected = new MultimailboxesSearchQuery(EMPTY_QUERY, inMailboxes, notInMailboxes, new AccessibleNamespace()); MultimailboxesSearchQuery actual = MultimailboxesSearchQuery.from(EMPTY_QUERY).notInMailboxes(ID_1).build(); assertThat(actual).isEqualToComparingFieldByField(expected); @@ -94,7 +95,7 @@ class MultimailboxesSearchQueryTest { void buildShouldBuildWhenAllDefined() { ImmutableSet<MailboxId> inMailboxes = ImmutableSet.of(ID_1); ImmutableSet<MailboxId> notInMailboxes = ImmutableSet.of(ID_2); - MultimailboxesSearchQuery expected = new MultimailboxesSearchQuery(EMPTY_QUERY, inMailboxes, notInMailboxes); + MultimailboxesSearchQuery expected = new MultimailboxesSearchQuery(EMPTY_QUERY, inMailboxes, notInMailboxes, new AccessibleNamespace()); MultimailboxesSearchQuery actual = MultimailboxesSearchQuery.from(EMPTY_QUERY).inMailboxes(ID_1).notInMailboxes(ID_2).build(); assertThat(actual).isEqualToComparingFieldByField(expected); diff --git a/mailbox/store/src/main/java/org/apache/james/mailbox/store/StoreMailboxManager.java b/mailbox/store/src/main/java/org/apache/james/mailbox/store/StoreMailboxManager.java index faf4d50..52e66c9 100644 --- a/mailbox/store/src/main/java/org/apache/james/mailbox/store/StoreMailboxManager.java +++ b/mailbox/store/src/main/java/org/apache/james/mailbox/store/StoreMailboxManager.java @@ -696,32 +696,32 @@ public class StoreMailboxManager implements MailboxManager { @Override public Flux<MessageId> search(MultimailboxesSearchQuery expression, MailboxSession session, long limit) throws MailboxException { - return getInMailboxes(expression.getInMailboxes(), session) - .filter(id -> !expression.getNotInMailboxes().contains(id)) + return getInMailboxes(expression, session) + .filter(id -> !expression.getNotInMailboxes().contains(id.getMailboxId())) + .filter(mailbox -> expression.getNamespace().keepAccessible(mailbox)) + .map(Mailbox::getMailboxId) .collect(Guavate.toImmutableSet()) .flatMapMany(Throwing.function(ids -> index.search(session, ids, expression.getSearchQuery(), limit))); } - private Flux<MailboxId> getInMailboxes(ImmutableSet<MailboxId> inMailboxes, MailboxSession session) throws MailboxException { - if (inMailboxes.isEmpty()) { - return getAllReadableMailbox(session); + private Flux<Mailbox> getInMailboxes(MultimailboxesSearchQuery expression, MailboxSession session) throws MailboxException { + if (expression.getInMailboxes().isEmpty()) { + return searchMailboxes(expression.getNamespace().associatedMailboxSearchQuery(), session, Right.Read); } else { - return filterReadable(inMailboxes, session); + return filterReadable(expression.getInMailboxes(), session); } } - private Flux<MailboxId> getAllReadableMailbox(MailboxSession session) throws MailboxException { - return searchMailboxes(MailboxQuery.builder().matchesAllMailboxNames().build(), session, Right.Read) - .map(Mailbox::getMailboxId); + private Flux<Mailbox> getAllReadableMailbox(MailboxQuery mailboxQuery, MailboxSession session) throws MailboxException { + return searchMailboxes(mailboxQuery, session, Right.Read); } - private Flux<MailboxId> filterReadable(ImmutableSet<MailboxId> inMailboxes, MailboxSession session) throws MailboxException { + private Flux<Mailbox> filterReadable(ImmutableSet<MailboxId> inMailboxes, MailboxSession session) throws MailboxException { MailboxMapper mailboxMapper = mailboxSessionMapperFactory.getMailboxMapper(session); return Flux.fromIterable(inMailboxes) .concatMap(mailboxMapper::findMailboxById) - .filter(Throwing.<Mailbox>predicate(mailbox -> storeRightManager.hasRight(mailbox, Right.Read, session)).sneakyThrow()) - .map(Mailbox::getMailboxId); + .filter(Throwing.<Mailbox>predicate(mailbox -> storeRightManager.hasRight(mailbox, Right.Read, session)).sneakyThrow()); } @Override diff --git a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailQueryMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailQueryMethodContract.scala index e521a7f..3b5a1f9 100644 --- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailQueryMethodContract.scala +++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailQueryMethodContract.scala @@ -41,11 +41,12 @@ import org.apache.james.jmap.http.UserCredential import org.apache.james.jmap.model.UTCDate import org.apache.james.jmap.rfc8621.contract.Fixture.{ACCEPT_RFC8621_VERSION_HEADER, ANDRE, ANDRE_PASSWORD, BOB, BOB_PASSWORD, DOMAIN, authScheme, baseRequestSpecBuilder} import org.apache.james.mailbox.MessageManager.AppendCommand +import org.apache.james.mailbox.model.MailboxACL.Right import org.apache.james.mailbox.model.MailboxPath.inbox -import org.apache.james.mailbox.model.{MailboxPath, MessageId} +import org.apache.james.mailbox.model.{MailboxACL, MailboxPath, MessageId} import org.apache.james.mime4j.dom.Message import org.apache.james.mime4j.message.DefaultMessageWriter -import org.apache.james.modules.MailboxProbeImpl +import org.apache.james.modules.{ACLProbeImpl, MailboxProbeImpl} import org.apache.james.utils.DataProbeImpl import org.awaitility.Awaitility import org.awaitility.Duration.ONE_HUNDRED_MILLISECONDS @@ -156,6 +157,395 @@ trait EmailQueryMethodContract { } @Test + def emailInSharedMailboxesShouldNotBeDisplayedWhenNoExtension(server: GuiceJamesServer): Unit = { + val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl]) + val andreInboxId = mailboxProbe.createMailbox(inbox(ANDRE)) + val messageId1: MessageId = mailboxProbe + .appendMessage(ANDRE.asString, inbox(ANDRE), + AppendCommand.from( + Message.Builder + .of + .setSubject("test") + .setBody("testmail", StandardCharsets.UTF_8) + .build)) + .getMessageId + + server.getProbe(classOf[ACLProbeImpl]) + .replaceRights(inbox(ANDRE), BOB.asString, new MailboxACL.Rfc4314Rights(Right.Read)) + + val request = + s"""{ + | "using": [ + | "urn:ietf:params:jmap:core", + | "urn:ietf:params:jmap:mail"], + | "methodCalls": [[ + | "Email/query", + | { + | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6" + | }, + | "c1"]] + |}""".stripMargin + + awaitAtMostTenSeconds.untilAsserted { () => + val response = `given` + .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER) + .body(request) + .when + .post + .`then` + .statusCode(SC_OK) + .contentType(JSON) + .extract + .body + .asString + + assertThatJson(response).isEqualTo( + s"""{ + | "sessionState": "75128aab4b1b", + | "methodResponses": [[ + | "Email/query", + | { + | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", + | "queryState": "${generateQueryState()}", + | "canCalculateChanges": false, + | "position": 0, + | "limit": 256, + | "ids": [] + | }, + | "c1" + | ]] + |}""".stripMargin) + } + } + + @Test + def emailInSharedMailboxesShouldBeDisplayedWhenExtension(server: GuiceJamesServer): Unit = { + val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl]) + val andreInboxId = mailboxProbe.createMailbox(inbox(ANDRE)) + val messageId1: MessageId = mailboxProbe + .appendMessage(ANDRE.asString, inbox(ANDRE), + AppendCommand.from( + Message.Builder + .of + .setSubject("test") + .setBody("testmail", StandardCharsets.UTF_8) + .build)) + .getMessageId + + server.getProbe(classOf[ACLProbeImpl]) + .replaceRights(inbox(ANDRE), BOB.asString, new MailboxACL.Rfc4314Rights(Right.Read)) + + val request = + s"""{ + | "using": [ + | "urn:ietf:params:jmap:core", + | "urn:ietf:params:jmap:mail", + | "urn:apache:james:params:jmap:mail:shares"], + | "methodCalls": [[ + | "Email/query", + | { + | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6" + | }, + | "c1"]] + |}""".stripMargin + + awaitAtMostTenSeconds.untilAsserted { () => + val response = `given` + .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER) + .body(request) + .when + .post + .`then` + .statusCode(SC_OK) + .contentType(JSON) + .extract + .body + .asString + + assertThatJson(response).isEqualTo( + s"""{ + | "sessionState": "75128aab4b1b", + | "methodResponses": [[ + | "Email/query", + | { + | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", + | "queryState": "${generateQueryState(messageId1)}", + | "canCalculateChanges": false, + | "position": 0, + | "limit": 256, + | "ids": ["${messageId1.serialize()}"] + | }, + | "c1" + | ]] + |}""".stripMargin) + } + } + + @Test + def inMailboxFilterShouldReturnEmptyForSharedMailboxesWhenNoExtension(server: GuiceJamesServer): Unit = { + val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl]) + val andreInboxId = mailboxProbe.createMailbox(inbox(ANDRE)) + val messageId1: MessageId = mailboxProbe + .appendMessage(ANDRE.asString, inbox(ANDRE), + AppendCommand.from( + Message.Builder + .of + .setSubject("test") + .setBody("testmail", StandardCharsets.UTF_8) + .build)) + .getMessageId + + server.getProbe(classOf[ACLProbeImpl]) + .replaceRights(inbox(ANDRE), BOB.asString, new MailboxACL.Rfc4314Rights(Right.Read)) + + val request = + s"""{ + | "using": [ + | "urn:ietf:params:jmap:core", + | "urn:ietf:params:jmap:mail"], + | "methodCalls": [[ + | "Email/query", + | { + | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", + | "filter": {"inMailbox": "${andreInboxId.serialize()}"} + | }, + | "c1"]] + |}""".stripMargin + + val response = `given` + .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER) + .body(request) + .when + .post + .`then` + .statusCode(SC_OK) + .contentType(JSON) + .extract + .body + .asString + + assertThatJson(response).isEqualTo( + s"""{ + | "sessionState": "75128aab4b1b", + | "methodResponses": [ + | [ + | "Email/query", + | { + | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", + | "queryState": "${generateQueryState()}", + | "canCalculateChanges": false, + | "ids": [ + | + | ], + | "position": 0, + | "limit": 256 + | }, + | "c1" + | ] + | ] + |}""".stripMargin) + } + + @Test + def inMailboxFilterShouldAcceptSharedMailboxesWhenExtension(server: GuiceJamesServer): Unit = { + val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl]) + val andreInboxId = mailboxProbe.createMailbox(inbox(ANDRE)) + val messageId1: MessageId = mailboxProbe + .appendMessage(ANDRE.asString, inbox(ANDRE), + AppendCommand.from( + Message.Builder + .of + .setSubject("test") + .setBody("testmail", StandardCharsets.UTF_8) + .build)) + .getMessageId + + server.getProbe(classOf[ACLProbeImpl]) + .replaceRights(inbox(ANDRE), BOB.asString, new MailboxACL.Rfc4314Rights(Right.Read)) + + val request = + s"""{ + | "using": [ + | "urn:ietf:params:jmap:core", + | "urn:ietf:params:jmap:mail", + | "urn:apache:james:params:jmap:mail:shares"], + | "methodCalls": [[ + | "Email/query", + | { + | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", + | "filter": {"inMailbox": "${andreInboxId.serialize()}"} + | }, + | "c1"]] + |}""".stripMargin + + awaitAtMostTenSeconds.untilAsserted { () => + val response = `given` + .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER) + .body(request) + .when + .post + .`then` + .statusCode(SC_OK) + .contentType(JSON) + .extract + .body + .asString + + assertThatJson(response).isEqualTo( + s"""{ + | "sessionState": "75128aab4b1b", + | "methodResponses": [[ + | "Email/query", + | { + | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", + | "queryState": "${generateQueryState(messageId1)}", + | "canCalculateChanges": false, + | "position": 0, + | "limit": 256, + | "ids": ["${messageId1.serialize()}"] + | }, + | "c1" + | ]] + |}""".stripMargin) + } + } + + @Test + def inMailboxOtherThanFilterShouldReturnEmptyForSharedMailboxesWhenNoExtension(server: GuiceJamesServer): Unit = { + val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl]) + val andreInboxId = mailboxProbe.createMailbox(inbox(ANDRE)) + val messageId1: MessageId = mailboxProbe + .appendMessage(ANDRE.asString, inbox(ANDRE), + AppendCommand.from( + Message.Builder + .of + .setSubject("test") + .setBody("testmail", StandardCharsets.UTF_8) + .build)) + .getMessageId + + server.getProbe(classOf[ACLProbeImpl]) + .replaceRights(inbox(ANDRE), BOB.asString, new MailboxACL.Rfc4314Rights(Right.Read)) + + val request = + s"""{ + | "using": [ + | "urn:ietf:params:jmap:core", + | "urn:ietf:params:jmap:mail"], + | "methodCalls": [[ + | "Email/query", + | { + | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", + | "filter": {"inMailboxOtherThan": ["${andreInboxId.serialize()}"]} + | }, + | "c1"]] + |}""".stripMargin + + val response = `given` + .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER) + .body(request) + .when + .post + .`then` + .statusCode(SC_OK) + .contentType(JSON) + .extract + .body + .asString + + assertThatJson(response).isEqualTo( + s"""{ + | "sessionState": "75128aab4b1b", + | "methodResponses": [[ + | "Email/query", + | { + | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", + | "queryState": "${generateQueryState()}", + | "canCalculateChanges": false, + | "ids": [], + | "position": 0, + | "limit": 256 + | }, + | "c1" + | ]] + |}""".stripMargin) + } + + @Test + def inMailboxOtherThanFilterShouldAcceptSharedMailboxesWhenExtension(server: GuiceJamesServer): Unit = { + val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl]) + val andreInboxId = mailboxProbe.createMailbox(inbox(ANDRE)) + val bobInboxId = mailboxProbe.createMailbox(inbox(BOB)) + val messageId1: MessageId = mailboxProbe + .appendMessage(ANDRE.asString, inbox(ANDRE), + AppendCommand.from( + Message.Builder + .of + .setSubject("test") + .setBody("testmail", StandardCharsets.UTF_8) + .build)) + .getMessageId + val messageId2: MessageId = mailboxProbe + .appendMessage(BOB.asString, inbox(BOB), + AppendCommand.from( + Message.Builder + .of + .setSubject("test") + .setBody("testmail", StandardCharsets.UTF_8) + .build)) + .getMessageId + + server.getProbe(classOf[ACLProbeImpl]) + .replaceRights(inbox(ANDRE), BOB.asString, new MailboxACL.Rfc4314Rights(Right.Read)) + + val request = + s"""{ + | "using": [ + | "urn:ietf:params:jmap:core", + | "urn:ietf:params:jmap:mail", + | "urn:apache:james:params:jmap:mail:shares"], + | "methodCalls": [[ + | "Email/query", + | { + | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", + | "filter": {"inMailboxOtherThan": ["${andreInboxId.serialize()}"]} + | }, + | "c1"]] + |}""".stripMargin + + awaitAtMostTenSeconds.untilAsserted { () => + val response = `given` + .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER) + .body(request) + .when + .post + .`then` + .statusCode(SC_OK) + .contentType(JSON) + .extract + .body + .asString + + assertThatJson(response).isEqualTo( + s"""{ + | "sessionState": "75128aab4b1b", + | "methodResponses": [[ + | "Email/query", + | { + | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", + | "queryState": "${generateQueryState(messageId2)}", + | "canCalculateChanges": false, + | "position": 0, + | "limit": 256, + | "ids": ["${messageId2.serialize()}"] + | }, + | "c1" + | ]] + |}""".stripMargin) + } + } + + @Test def hasAttachmentShouldKeepMessageWithoutAttachmentWhenFalse(server: GuiceJamesServer): Unit = { val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl]) mailboxProbe.createMailbox(MailboxPath.inbox(BOB)) @@ -1342,7 +1732,7 @@ trait EmailQueryMethodContract { .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER) .body(request) .when - .post.prettyPeek() + .post .`then` .statusCode(SC_OK) .contentType(JSON) @@ -1391,7 +1781,7 @@ trait EmailQueryMethodContract { .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER) .body(request) .when - .post.prettyPeek() + .post .`then` .statusCode(SC_OK) .contentType(JSON) diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailQueryMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailQueryMethod.scala index 6bee1dc..4feb97e 100644 --- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailQueryMethod.scala +++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailQueryMethod.scala @@ -45,13 +45,13 @@ import scala.jdk.CollectionConverters._ class EmailQueryMethod @Inject() (serializer: EmailQuerySerializer, mailboxManager: MailboxManager, metricFactory: MetricFactory) extends Method { - override val methodName = MethodName("Email/query") + override val methodName: MethodName = MethodName("Email/query") override val requiredCapabilities: Capabilities = Capabilities(CORE_CAPABILITY, MAIL_CAPABILITY) override def process(capabilities: Set[CapabilityIdentifier], invocation: Invocation, mailboxSession: MailboxSession, processingContext: ProcessingContext): Publisher[(Invocation, ProcessingContext)] = metricFactory.decoratePublisherWithTimerMetricLogP99(JMAP_RFC8621_PREFIX + methodName.value, asEmailQueryRequest(invocation.arguments) - .flatMap(processRequest(mailboxSession, invocation, _)) + .flatMap(processRequest(mailboxSession, invocation, _, capabilities)) .onErrorResume { case e: UnsupportedRequestParameterException => SMono.just(Invocation.error( ErrorCode.InvalidArguments, @@ -71,10 +71,10 @@ class EmailQueryMethod @Inject() (serializer: EmailQuerySerializer, } .map(invocationResult => (invocationResult, processingContext))) - private def processRequest(mailboxSession: MailboxSession, invocation: Invocation, request: EmailQueryRequest): SMono[Invocation] = { - searchQueryFromRequest(request) match { + private def processRequest(mailboxSession: MailboxSession, invocation: Invocation, request: EmailQueryRequest, capabilities: Set[CapabilityIdentifier]): SMono[Invocation] = { + searchQueryFromRequest(request, capabilities, mailboxSession) match { case Left(error) => SMono.raiseError(error) - case Right(searchQuery) => for { + case Right(searchQuery) => for { positionToUse <- Position.validateRequestPosition(request.position) limitToUse <- Limit.validateRequestLimit(request.limit) response <- executeQuery(mailboxSession, request, searchQuery, positionToUse, limitToUse) @@ -94,7 +94,7 @@ class EmailQueryMethod @Inject() (serializer: EmailQuerySerializer, limit = Some(limitToUse).filterNot(used => request.limit.map(_.value).contains(used.value)))) } - private def searchQueryFromRequest(request: EmailQueryRequest): Either[UnsupportedOperationException, MultimailboxesSearchQuery] = { + private def searchQueryFromRequest(request: EmailQueryRequest, capabilities: Set[CapabilityIdentifier], session: MailboxSession): Either[UnsupportedOperationException, MultimailboxesSearchQuery] = { val comparators: List[Comparator] = request.comparator.getOrElse(Set(Comparator.default)).toList comparators.map(_.toSort) @@ -104,7 +104,7 @@ class EmailQueryMethod @Inject() (serializer: EmailQuerySerializer, } yield queryFilter .sorts(sorts.asJava) .build()) - .map(MailboxFilter.buildQuery(request, _)) + .map(MailboxFilter.buildQuery(request, _, capabilities, session)) } private def asEmailQueryRequest(arguments: Arguments): SMono[EmailQueryRequest] = diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/utils/search/MailboxFilter.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/utils/search/MailboxFilter.scala index 3ef1239..c547cf2 100644 --- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/utils/search/MailboxFilter.scala +++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/utils/search/MailboxFilter.scala @@ -22,6 +22,10 @@ import java.util.Date import cats.implicits._ import org.apache.james.jmap.mail.{EmailQueryRequest, UnsupportedFilterException} +import org.apache.james.jmap.model.CapabilityIdentifier +import org.apache.james.jmap.model.CapabilityIdentifier.CapabilityIdentifier +import org.apache.james.mailbox.MailboxSession +import org.apache.james.mailbox.model.MultimailboxesSearchQuery.{AccessibleNamespace, Namespace, PersonalNamespace} import org.apache.james.mailbox.model.SearchQuery.DateResolution.Second import org.apache.james.mailbox.model.SearchQuery.{DateComparator, DateOperator, DateResolution, InternalDateCriterion} import org.apache.james.mailbox.model.{MultimailboxesSearchQuery, SearchQuery} @@ -47,13 +51,20 @@ case object NotInMailboxFilter extends MailboxFilter { } object MailboxFilter { - def buildQuery(request: EmailQueryRequest, searchQuery: SearchQuery) = { + def buildQuery(request: EmailQueryRequest, searchQuery: SearchQuery, capabilities: Set[CapabilityIdentifier], session: MailboxSession) = { val multiMailboxQueryBuilder = MultimailboxesSearchQuery.from(searchQuery) + .inNamespace(queryNamespace(capabilities, session)) List(InMailboxFilter, NotInMailboxFilter).foldLeft(multiMailboxQueryBuilder)((builder, filter) => filter.toQuery(builder, request)) .build() } + private def queryNamespace(capabilities: Set[CapabilityIdentifier], session: MailboxSession): Namespace = if (capabilities.contains(CapabilityIdentifier.JAMES_SHARES)) { + new AccessibleNamespace() + } else { + new PersonalNamespace(session) + } + sealed trait QueryFilter { def toQuery(builder: SearchQuery.Builder, request: EmailQueryRequest): Either[UnsupportedFilterException, SearchQuery.Builder] } --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
