You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@james.apache.org by bt...@apache.org on 2020/11/20 02:18:06 UTC

[james-project] branch master updated (6e89015 -> 1f2cfe3)

This is an automated email from the ASF dual-hosted git repository.

btellier pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git.


    from 6e89015  JAMES-1902 uses sortpom to fix pom dependencies ordering
     new 3d231c2  JAMES-3440 Configuration option to enable EmailQuery view
     new 8e6d0ef  JAMES-3440 EmailQuery Limit & Position validation should not rely on Mono
     new 9a60a67  JAMES-3440 EmailQuery Limit & Position validation should not rely on Mono
     new b27a5a3  JAMES-3440 Type limit as an Int
     new 673e909  JAMES-3440 JMAP RFC-8621 should use EmailQueryView when enabled
     new f5d90c6  JAMES-3440 JMAP Draft should use EmailQueryView when enabled
     new 9dff3ba  [Refactoring] Fix indent in GetMessageListMethod
     new 96d47a5  JAMES-3440 Cassandra implementation for EmailQueryView
     new bfa8967  JAMES-3440 Generalize JMAP tasks RunningOptions for reuse
     new 98f7c13  JAMES-3440 Utility to populate EmailQueryView
     new f1cf25d  JAMES-3440 Tasks wrapper to populate EmailQueryView
     new eb35513  JAMES-3440 WebAdmin wrappers around task to populate EmailQueryView
     new f54d2ca  JAMES-3440 Guice registrations for tasks and routes to populate EmailQueryView
     new 6d18bde  JAMES-3450 Email/query reject invalid FilterOperator
     new 9beb98a  JAMES-3441 Enable disabling group event consumption
     new a7d2418  JAMES-3441 Document listeners.xml <executeGroupListeners>
     new 76934c9  [Refactoring] Apply some standards scala idioms
     new 1f2cfe3  JAMES-2884 [REFACTORING] Use Either instead of SMono for request validation

The 18 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../destination/conf/jmap.properties               |   4 +
 .../destination/conf/jmap.properties               |   4 +
 .../destination/conf/listeners.xml                 |   1 +
 .../destination/conf/jmap.properties               |   4 +
 .../destination/conf/listeners.xml                 |   1 +
 .../cassandra/destination/conf/jmap.properties     |   4 +
 .../guice/memory/destination/conf/jmap.properties  |   4 +
 .../servers/pages/distributed/configure/jmap.adoc  |   5 +
 .../pages/distributed/configure/listeners.adoc     |   6 +-
 .../james/modules/data/CassandraJmapModule.java    |   2 +
 .../modules/mailbox/ListenersConfiguration.java    |  26 +++-
 .../mailbox/MailboxListenersLoaderImpl.java        |  10 +-
 .../apache/james/DisabledGroupExecutionTest.java   | 114 ++++++++++++++
 .../org/apache/james/jmap/draft/JMAPModule.java    |   1 +
 .../apache/james/modules/TestJMAPServerModule.java |   1 +
 .../server/JmapTaskSerializationModule.java        |  19 +++
 .../james/modules/server/JmapTasksModule.java      |   4 +
 .../projections/CassandraEmailQueryView.java       |   1 -
 .../src/test/resources/listeners.xml               |   4 +
 .../integration/GetMessageListMethodTest.java      |  22 +++
 .../src/test/resources/listeners.xml               |   4 +
 .../src/test/resources/listeners.xml               |   4 +
 .../jmap/draft/methods/GetMessageListMethod.java   | 158 +++++++++++++------
 .../org/apache/james/jmap/draft/model/Filter.java  |   8 +
 .../james/jmap/draft/model/FilterCondition.java    |  39 +++++
 ... => DistributedEmailQueryMethodNoViewTest.java} |  11 +-
 .../src/test/resources/listeners.xml               |   4 +
 .../contract/EmailQueryMethodContract.scala        | 173 +++++++++++++++++++++
 ....java => MemoryEmailQueryMethodNoViewTest.java} |  11 +-
 .../src/test/resources/listeners.xml               |   4 +
 .../org/apache/james/jmap/core/Capability.scala    |   2 +-
 .../scala/org/apache/james/jmap/core/Query.scala   |  23 ++-
 .../scala/org/apache/james/jmap/core/Session.scala |   4 +-
 .../james/jmap/json/EmailQuerySerializer.scala     |   3 +-
 .../scala/org/apache/james/jmap/mail/Email.scala   |  41 ++---
 .../org/apache/james/jmap/mail/EmailBodyPart.scala |  12 +-
 .../apache/james/jmap/mail/EmailBodyValue.scala    |   4 +-
 .../org/apache/james/jmap/mail/EmailQuery.scala    |  46 +++++-
 .../apache/james/jmap/method/EmailGetMethod.scala  |  14 +-
 .../james/jmap/method/EmailQueryMethod.scala       | 129 +++++++++++----
 .../apache/james/jmap/method/EmailSetMethod.scala  |  10 +-
 .../jmap/method/EmailSubmissionSetMethod.scala     |   6 +-
 .../james/jmap/method/MailboxGetMethod.scala       |  47 +++---
 .../james/jmap/method/MailboxQueryMethod.scala     |  18 +--
 .../james/jmap/method/MailboxSetMethod.scala       |  12 +-
 .../org/apache/james/jmap/method/Method.scala      |  51 +++---
 .../jmap/method/VacationResponseGetMethod.scala    |   9 +-
 .../jmap/method/VacationResponseSetMethod.scala    |  21 +--
 .../apache/james/jmap/routes/JMAPApiRoutes.scala   |   6 +-
 .../apache/james/jmap/routes/SessionRoutes.scala   |   2 +-
 .../apache/james/jmap/routes/SessionSupplier.scala |  10 +-
 .../james/jmap/vacation/VacationResponse.scala     |   6 +-
 .../james/jmap/routes/SessionSupplierTest.scala    |   4 +-
 .../org/apache/james/jmap/JMAPConfiguration.java   |  28 +++-
 .../apache/james/jmap/JMAPConfigurationTest.java   |   9 +-
 .../src/test/resources/listeners.xml               |   4 +
 .../src/test/resources/listeners.xml               |   4 +
 .../apache/james/webadmin/data/jmap/Constants.java |   1 +
 ...Corrector.java => EmailQueryViewPopulator.java} | 148 ++++++------------
 .../jmap/MessageFastViewProjectionCorrector.java   |  21 ---
 ...va => PopulateEmailQueryViewRequestToTask.java} |  10 +-
 ...msTask.java => PopulateEmailQueryViewTask.java} |  43 +++--
 ...mailQueryViewTaskAdditionalInformationDTO.java} |  37 +++--
 .../RecomputeAllFastViewProjectionItemsTask.java   |   1 -
 ...uteAllFastViewTaskAdditionalInformationDTO.java |   1 -
 .../RecomputeUserFastViewProjectionItemsTask.java  |   1 -
 ...teUserFastViewTaskAdditionalInformationDTO.java |   1 -
 .../james/webadmin/data/jmap/RunningOptions.java   |  28 ++--
 .../webadmin/data/jmap/RunningOptionsDTO.java      |   2 -
 .../webadmin/data/jmap/RunningOptionsParser.java   |   2 -
 ...tionItemsTaskAdditionalInformationDTOTest.java} |  18 ++-
 .../PopulateEmailQueryViewRequestToTaskTest.java}  | 155 ++++++++++++------
 ...pulateEmailQueryViewTaskSerializationTest.java} |  16 +-
 ...ctionItemsTaskAdditionalInformationDTOTest.java |   1 -
 ...stViewProjectionItemsTaskSerializationTest.java |   1 -
 ...ctionItemsTaskAdditionalInformationDTOTest.java |   4 +-
 ...stViewProjectionItemsTaskSerializationTest.java |   1 -
 ...json => populateAll.additionalInformation.json} |   2 +-
 ...ecomputeAll.task.json => populateAll.task.json} |   2 +-
 src/site/xdoc/server/config-jmap.xml               |   5 +
 src/site/xdoc/server/config-listeners.xml          |   6 +
 81 files changed, 1169 insertions(+), 516 deletions(-)
 create mode 100644 server/container/guice/memory-guice/src/test/java/org/apache/james/DisabledGroupExecutionTest.java
 copy server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/distributed/{DistributedEmailQueryMethodTest.java => DistributedEmailQueryMethodNoViewTest.java} (85%)
 copy server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/{MemoryEmailQueryMethodTest.java => MemoryEmailQueryMethodNoViewTest.java} (84%)
 copy server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/{MessageFastViewProjectionCorrector.java => EmailQueryViewPopulator.java} (55%)
 copy server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/{RecomputeAllFastViewProjectionItemsRequestToTask.java => PopulateEmailQueryViewRequestToTask.java} (76%)
 copy server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/{RecomputeAllFastViewProjectionItemsTask.java => PopulateEmailQueryViewTask.java} (70%)
 copy server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/{RecomputeAllFastViewTaskAdditionalInformationDTO.java => PopulateEmailQueryViewTaskAdditionalInformationDTO.java} (63%)
 copy mailbox/event/event-rabbitmq/src/main/java/org/apache/james/mailbox/events/RegistrationQueueName.java => server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RunningOptions.java (67%)
 copy server/protocols/webadmin/{webadmin-mailbox/src/test/java/org/apache/james/webadmin/service/MailboxesExportTaskAdditionalInformationDTOTest.java => webadmin-jmap/src/test/java/org/apache/james/webadmin/data/jmap/PopulateEmailQueryViewProjectionItemsTaskAdditionalInformationDTOTest.java} (75%)
 copy server/protocols/webadmin/{webadmin-cassandra/src/test/java/org/apache/james/webadmin/routes/SolveMessageInconsistenciesRequestToTaskTest.java => webadmin-jmap/src/test/java/org/apache/james/webadmin/data/jmap/PopulateEmailQueryViewRequestToTaskTest.java} (60%)
 copy server/protocols/webadmin/{webadmin-mailbox/src/test/java/org/apache/james/webadmin/service/MailboxesExportTaskSerializationTest.java => webadmin-jmap/src/test/java/org/apache/james/webadmin/data/jmap/PopulateEmailQueryViewTaskSerializationTest.java} (78%)
 copy server/protocols/webadmin/webadmin-jmap/src/test/resources/json/{recomputeAll.additionalInformation.json => populateAll.additionalInformation.json} (78%)
 copy server/protocols/webadmin/webadmin-jmap/src/test/resources/json/{recomputeAll.task.json => populateAll.task.json} (50%)


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org


[james-project] 15/18: JAMES-3441 Enable disabling group event consumption

Posted by bt...@apache.org.
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 9beb98aeb05b1248b46648dd46d0abde03ad1635
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Wed Nov 18 16:54:56 2020 +0700

    JAMES-3441 Enable disabling group event consumption
---
 .../destination/conf/listeners.xml                 |   1 +
 .../destination/conf/listeners.xml                 |   1 +
 .../modules/mailbox/ListenersConfiguration.java    |  26 ++++-
 .../mailbox/MailboxListenersLoaderImpl.java        |  10 +-
 .../apache/james/DisabledGroupExecutionTest.java   | 114 +++++++++++++++++++++
 5 files changed, 143 insertions(+), 9 deletions(-)

diff --git a/dockerfiles/run/guice/cassandra-rabbitmq-ldap/destination/conf/listeners.xml b/dockerfiles/run/guice/cassandra-rabbitmq-ldap/destination/conf/listeners.xml
index 948cb92..758c823 100644
--- a/dockerfiles/run/guice/cassandra-rabbitmq-ldap/destination/conf/listeners.xml
+++ b/dockerfiles/run/guice/cassandra-rabbitmq-ldap/destination/conf/listeners.xml
@@ -21,6 +21,7 @@
 <!-- Read https://james.apache.org/server/config-listeners.html for further details -->
 
 <listeners>
+  <executeGroupListeners>true</executeGroupListeners>
   <listener>
     <class>org.apache.james.mailbox.spamassassin.SpamAssassinListener</class>
     <async>true</async>
diff --git a/dockerfiles/run/guice/cassandra-rabbitmq/destination/conf/listeners.xml b/dockerfiles/run/guice/cassandra-rabbitmq/destination/conf/listeners.xml
index 948cb92..758c823 100644
--- a/dockerfiles/run/guice/cassandra-rabbitmq/destination/conf/listeners.xml
+++ b/dockerfiles/run/guice/cassandra-rabbitmq/destination/conf/listeners.xml
@@ -21,6 +21,7 @@
 <!-- Read https://james.apache.org/server/config-listeners.html for further details -->
 
 <listeners>
+  <executeGroupListeners>true</executeGroupListeners>
   <listener>
     <class>org.apache.james.mailbox.spamassassin.SpamAssassinListener</class>
     <async>true</async>
diff --git a/server/container/guice/mailbox/src/main/java/org/apache/james/modules/mailbox/ListenersConfiguration.java b/server/container/guice/mailbox/src/main/java/org/apache/james/modules/mailbox/ListenersConfiguration.java
index 14abbd6..5ed36e6 100644
--- a/server/container/guice/mailbox/src/main/java/org/apache/james/modules/mailbox/ListenersConfiguration.java
+++ b/server/container/guice/mailbox/src/main/java/org/apache/james/modules/mailbox/ListenersConfiguration.java
@@ -19,36 +19,52 @@
 package org.apache.james.modules.mailbox;
 
 import java.util.List;
+import java.util.Optional;
 
 import org.apache.commons.configuration2.HierarchicalConfiguration;
 import org.apache.commons.configuration2.tree.ImmutableNode;
 
 import com.github.steveash.guavate.Guavate;
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
 
 public class ListenersConfiguration {
 
     public static ListenersConfiguration of(ListenerConfiguration... listenersConfiguration) {
-        return new ListenersConfiguration(ImmutableList.copyOf(listenersConfiguration));
+        return new ListenersConfiguration(ImmutableList.copyOf(listenersConfiguration), true);
+    }
+
+    public static ListenersConfiguration disabled() {
+        return new ListenersConfiguration(ImmutableList.of(), false);
     }
 
     public static ListenersConfiguration from(HierarchicalConfiguration<ImmutableNode> configuration) {
         List<HierarchicalConfiguration<ImmutableNode>> listeners = configuration.configurationsAt("listener");
+        Optional<Boolean> consumeGroups = Optional.ofNullable(configuration.getBoolean("executeGroupListeners", null));
 
         return new ListenersConfiguration(listeners
-            .stream()
-            .map(ListenerConfiguration::from)
-            .collect(Guavate.toImmutableList()));
+                .stream()
+                .map(ListenerConfiguration::from)
+                .collect(Guavate.toImmutableList()),
+            consumeGroups.orElse(true));
     }
     
     private final List<ListenerConfiguration> listenersConfiguration;
+    private final boolean enableGroupListenerConsumption;
 
-    @VisibleForTesting ListenersConfiguration(List<ListenerConfiguration> listenersConfiguration) {
+    @VisibleForTesting ListenersConfiguration(List<ListenerConfiguration> listenersConfiguration, boolean enableGroupListenerConsumption) {
+        Preconditions.checkArgument(enableGroupListenerConsumption || listenersConfiguration.isEmpty(),
+            "'executeGroupListeners' can not be false while extra listeners are configured");
         this.listenersConfiguration = listenersConfiguration;
+        this.enableGroupListenerConsumption = enableGroupListenerConsumption;
     }
 
     public List<ListenerConfiguration> getListenersConfiguration() {
         return listenersConfiguration;
     }
+
+    public boolean isGroupListenerConsumptionEnabled() {
+        return enableGroupListenerConsumption;
+    }
 }
diff --git a/server/container/guice/mailbox/src/main/java/org/apache/james/modules/mailbox/MailboxListenersLoaderImpl.java b/server/container/guice/mailbox/src/main/java/org/apache/james/modules/mailbox/MailboxListenersLoaderImpl.java
index da6b8e0..2b40980 100644
--- a/server/container/guice/mailbox/src/main/java/org/apache/james/modules/mailbox/MailboxListenersLoaderImpl.java
+++ b/server/container/guice/mailbox/src/main/java/org/apache/james/modules/mailbox/MailboxListenersLoaderImpl.java
@@ -70,11 +70,13 @@ public class MailboxListenersLoaderImpl implements Configurable, MailboxListener
     public void configure(ListenersConfiguration listenersConfiguration) {
         LOGGER.info("Loading user registered mailbox listeners");
 
-        guiceDefinedListeners.forEach(eventBus::register);
+        if (listenersConfiguration.isGroupListenerConsumptionEnabled()) {
+            guiceDefinedListeners.forEach(eventBus::register);
 
-        listenersConfiguration.getListenersConfiguration().stream()
-            .map(this::createListener)
-            .forEach(this::register);
+            listenersConfiguration.getListenersConfiguration().stream()
+                .map(this::createListener)
+                .forEach(this::register);
+        }
     }
 
     @Override
diff --git a/server/container/guice/memory-guice/src/test/java/org/apache/james/DisabledGroupExecutionTest.java b/server/container/guice/memory-guice/src/test/java/org/apache/james/DisabledGroupExecutionTest.java
new file mode 100644
index 0000000..b2579cd
--- /dev/null
+++ b/server/container/guice/memory-guice/src/test/java/org/apache/james/DisabledGroupExecutionTest.java
@@ -0,0 +1,114 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james;
+
+import static org.apache.james.jmap.JMAPTestingConstants.ALICE;
+import static org.apache.james.jmap.JMAPTestingConstants.ALICE_PASSWORD;
+import static org.apache.james.jmap.JMAPTestingConstants.BOB;
+import static org.apache.james.jmap.JMAPTestingConstants.BOB_PASSWORD;
+import static org.apache.james.jmap.JMAPTestingConstants.DOMAIN;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.apache.james.mailbox.events.Event;
+import org.apache.james.mailbox.events.Group;
+import org.apache.james.mailbox.events.MailboxListener;
+import org.apache.james.mailbox.model.MailboxPath;
+import org.apache.james.modules.MailboxProbeImpl;
+import org.apache.james.modules.TestJMAPServerModule;
+import org.apache.james.modules.mailbox.ListenersConfiguration;
+import org.apache.james.utils.DataProbeImpl;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.reactivestreams.Publisher;
+
+import com.google.inject.multibindings.Multibinder;
+
+import reactor.core.publisher.Mono;
+
+class DisabledGroupExecutionTest {
+
+    public static class ReactiveNoopMailboxListener implements MailboxListener.ReactiveGroupMailboxListener {
+        public static class NoopMailboxListenerGroup extends Group {
+
+        }
+
+        static final Group GROUP = new NoopMailboxListenerGroup();
+
+        private final AtomicBoolean executed = new AtomicBoolean(false);
+
+        @Override
+        public Group getDefaultGroup() {
+            return GROUP;
+        }
+
+        @Override
+        public Publisher<Void> reactiveEvent(Event event) {
+            return Mono.fromRunnable(() -> executed.set(true));
+        }
+
+        @Override
+        public boolean isHandling(Event event) {
+            return true;
+        }
+
+        @Override
+        public void event(Event event) {
+            Mono.from(reactiveEvent(event)).block();
+        }
+
+        public boolean isExecuted() {
+            return executed.get();
+        }
+    }
+
+    static ReactiveNoopMailboxListener listener = new ReactiveNoopMailboxListener();
+
+    @RegisterExtension
+    static JamesServerExtension jamesServerExtension = new JamesServerBuilder<>(JamesServerBuilder.defaultConfigurationProvider())
+        .server(configuration -> MemoryJamesServerMain.createServer(configuration)
+            .overrideWith(new TestJMAPServerModule())
+            .overrideWith(binder -> Multibinder.newSetBinder(binder, MailboxListener.ReactiveGroupMailboxListener.class)
+                .addBinding()
+                .toInstance(listener))
+            .overrideWith(binder -> binder.bind(ListenersConfiguration.class)
+                .toInstance(ListenersConfiguration.disabled())))
+        .lifeCycle(JamesServerExtension.Lifecycle.PER_CLASS)
+        .build();
+
+    @BeforeEach
+    void setUp(GuiceJamesServer guiceJamesServer) throws Exception {
+        DataProbeImpl dataProbe = guiceJamesServer.getProbe(DataProbeImpl.class);
+        dataProbe.fluent()
+            .addDomain(DOMAIN)
+            .addUser(ALICE.asString(), ALICE_PASSWORD)
+            .addUser(BOB.asString(), BOB_PASSWORD);
+    }
+
+    @Test
+    void listenerShouldNotBeExecutedWhenDisabled(GuiceJamesServer server) {
+        server.getProbe(MailboxProbeImpl.class)
+            .createMailbox(MailboxPath.inbox(ALICE));
+
+        assertThat(listener.isExecuted()).isFalse();
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org


[james-project] 01/18: JAMES-3440 Configuration option to enable EmailQuery view

Posted by bt...@apache.org.
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 3d231c2c240a0ff44e8bb7385a84f693fe78f4a3
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Mon Nov 16 13:13:58 2020 +0700

    JAMES-3440 Configuration option to enable EmailQuery view
    
    File: jmap.properties
    Default value: false (as we can not assume admins would have provisioned the view correctly, we
    should let them a grace period to migrate their data).
---
 .../destination/conf/jmap.properties               |  4 ++++
 .../destination/conf/jmap.properties               |  4 ++++
 .../destination/conf/jmap.properties               |  4 ++++
 .../cassandra/destination/conf/jmap.properties     |  4 ++++
 .../guice/memory/destination/conf/jmap.properties  |  4 ++++
 .../servers/pages/distributed/configure/jmap.adoc  |  5 ++++
 .../org/apache/james/jmap/draft/JMAPModule.java    |  1 +
 .../org/apache/james/jmap/JMAPConfiguration.java   | 28 ++++++++++++++++++++--
 .../apache/james/jmap/JMAPConfigurationTest.java   |  9 ++++---
 src/site/xdoc/server/config-jmap.xml               |  5 ++++
 10 files changed, 63 insertions(+), 5 deletions(-)

diff --git a/dockerfiles/run/guice/cassandra-ldap/destination/conf/jmap.properties b/dockerfiles/run/guice/cassandra-ldap/destination/conf/jmap.properties
index 53bc403..1980cfa 100644
--- a/dockerfiles/run/guice/cassandra-ldap/destination/conf/jmap.properties
+++ b/dockerfiles/run/guice/cassandra-ldap/destination/conf/jmap.properties
@@ -12,3 +12,7 @@ tls.secret=james72laBalle
 # which should be a PEM format file.
 #
 jwt.publickeypem.url=file://conf/jwt_publickey
+
+# Should simple Email/query be resolved against a Cassandra projection, or should we resolve them against ElasticSearch?
+# This enables a higher resilience, but the projection needs to be correctly populated. False by default.
+# view.email.query.enabled=true
diff --git a/dockerfiles/run/guice/cassandra-rabbitmq-ldap/destination/conf/jmap.properties b/dockerfiles/run/guice/cassandra-rabbitmq-ldap/destination/conf/jmap.properties
index 53bc403..1980cfa 100644
--- a/dockerfiles/run/guice/cassandra-rabbitmq-ldap/destination/conf/jmap.properties
+++ b/dockerfiles/run/guice/cassandra-rabbitmq-ldap/destination/conf/jmap.properties
@@ -12,3 +12,7 @@ tls.secret=james72laBalle
 # which should be a PEM format file.
 #
 jwt.publickeypem.url=file://conf/jwt_publickey
+
+# Should simple Email/query be resolved against a Cassandra projection, or should we resolve them against ElasticSearch?
+# This enables a higher resilience, but the projection needs to be correctly populated. False by default.
+# view.email.query.enabled=true
diff --git a/dockerfiles/run/guice/cassandra-rabbitmq/destination/conf/jmap.properties b/dockerfiles/run/guice/cassandra-rabbitmq/destination/conf/jmap.properties
index 53bc403..1980cfa 100644
--- a/dockerfiles/run/guice/cassandra-rabbitmq/destination/conf/jmap.properties
+++ b/dockerfiles/run/guice/cassandra-rabbitmq/destination/conf/jmap.properties
@@ -12,3 +12,7 @@ tls.secret=james72laBalle
 # which should be a PEM format file.
 #
 jwt.publickeypem.url=file://conf/jwt_publickey
+
+# Should simple Email/query be resolved against a Cassandra projection, or should we resolve them against ElasticSearch?
+# This enables a higher resilience, but the projection needs to be correctly populated. False by default.
+# view.email.query.enabled=true
diff --git a/dockerfiles/run/guice/cassandra/destination/conf/jmap.properties b/dockerfiles/run/guice/cassandra/destination/conf/jmap.properties
index 53bc403..71d28c6 100644
--- a/dockerfiles/run/guice/cassandra/destination/conf/jmap.properties
+++ b/dockerfiles/run/guice/cassandra/destination/conf/jmap.properties
@@ -12,3 +12,7 @@ tls.secret=james72laBalle
 # which should be a PEM format file.
 #
 jwt.publickeypem.url=file://conf/jwt_publickey
+
+# Should simple Email/query be resolved against a Cassandra projection, or should we resolve them against ElasticSearch?
+# This enables a higher resilience, but the projection needs to be correctly populated. False by default.
+# view.email.query.enabled=true
\ No newline at end of file
diff --git a/dockerfiles/run/guice/memory/destination/conf/jmap.properties b/dockerfiles/run/guice/memory/destination/conf/jmap.properties
index 53bc403..1980cfa 100644
--- a/dockerfiles/run/guice/memory/destination/conf/jmap.properties
+++ b/dockerfiles/run/guice/memory/destination/conf/jmap.properties
@@ -12,3 +12,7 @@ tls.secret=james72laBalle
 # which should be a PEM format file.
 #
 jwt.publickeypem.url=file://conf/jwt_publickey
+
+# Should simple Email/query be resolved against a Cassandra projection, or should we resolve them against ElasticSearch?
+# This enables a higher resilience, but the projection needs to be correctly populated. False by default.
+# view.email.query.enabled=true
diff --git a/docs/modules/servers/pages/distributed/configure/jmap.adoc b/docs/modules/servers/pages/distributed/configure/jmap.adoc
index f2b55d0..5ab56a9 100644
--- a/docs/modules/servers/pages/distributed/configure/jmap.adoc
+++ b/docs/modules/servers/pages/distributed/configure/jmap.adoc
@@ -37,6 +37,11 @@ This should not be the same keystore than the ones used by TLS based protocols.
 | Optional. Configuration max size Upload in new JMAP-RFC-8621.
 | Default value: 30M. Supported units are B (bytes) K (KB) M (MB) G (GB).
 
+| view.email.query.enabled
+| Optional boolean. Defaults to false.
+| Should simple Email/query be resolved against a Cassandra projection, or should we resolve them against ElasticSearch?
+This enables a higher resilience, but the projection needs to be correctly populated.
+
 |===
 
 == Wire tapping
diff --git a/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/draft/JMAPModule.java b/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/draft/JMAPModule.java
index 934da8e..91cf4bf 100644
--- a/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/draft/JMAPModule.java
+++ b/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/draft/JMAPModule.java
@@ -139,6 +139,7 @@ public class JMAPModule extends AbstractModule {
             return JMAPConfiguration.builder()
                 .enabled(configuration.getBoolean("enabled", true))
                 .port(Port.of(configuration.getInt("jmap.port", DEFAULT_JMAP_PORT)))
+                .enableEmailQueryView(Optional.ofNullable(configuration.getBoolean("view.email.query.enabled", null)))
                 .build();
         } catch (FileNotFoundException e) {
             LOGGER.warn("Could not find JMAP configuration file. JMAP server will not be enabled.");
diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/JMAPConfiguration.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/JMAPConfiguration.java
index cf2e61d..5b7c729 100644
--- a/server/protocols/jmap/src/main/java/org/apache/james/jmap/JMAPConfiguration.java
+++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/JMAPConfiguration.java
@@ -33,6 +33,7 @@ public class JMAPConfiguration {
 
     public static class Builder {
         private Optional<Boolean> enabled = Optional.empty();
+        private Optional<Boolean> emailQueryViewEnabled = Optional.empty();
         private Optional<Port> port = Optional.empty();
 
         private Builder() {
@@ -52,6 +53,23 @@ public class JMAPConfiguration {
             return enabled(false);
         }
 
+        public Builder enableEmailQueryView(boolean enabled) {
+            return enableEmailQueryView(Optional.of(enabled));
+        }
+
+        public Builder enableEmailQueryView(Optional<Boolean> enabled) {
+            this.emailQueryViewEnabled = enabled;
+            return this;
+        }
+
+        public Builder enableEmailQueryView() {
+            return enableEmailQueryView(true);
+        }
+
+        public Builder disableEmailQueryView() {
+            return enableEmailQueryView(false);
+        }
+
         public Builder port(Port port) {
             this.port = Optional.of(port);
             return this;
@@ -64,18 +82,20 @@ public class JMAPConfiguration {
 
         public JMAPConfiguration build() {
             Preconditions.checkState(enabled.isPresent(), "You should specify if JMAP server should be started");
-            return new JMAPConfiguration(enabled.get(), port);
+            return new JMAPConfiguration(enabled.get(), port, emailQueryViewEnabled.orElse(false));
         }
 
     }
 
     private final boolean enabled;
     private final Optional<Port> port;
+    private final boolean emailQueryViewEnabled;
 
     @VisibleForTesting
-    JMAPConfiguration(boolean enabled, Optional<Port> port) {
+    JMAPConfiguration(boolean enabled, Optional<Port> port, boolean emailQueryViewEnabled) {
         this.enabled = enabled;
         this.port = port;
+        this.emailQueryViewEnabled = emailQueryViewEnabled;
     }
 
     public boolean isEnabled() {
@@ -85,4 +105,8 @@ public class JMAPConfiguration {
     public Optional<Port> getPort() {
         return port;
     }
+
+    public boolean isEmailQueryViewEnabled() {
+        return emailQueryViewEnabled;
+    }
 }
diff --git a/server/protocols/jmap/src/test/java/org/apache/james/jmap/JMAPConfigurationTest.java b/server/protocols/jmap/src/test/java/org/apache/james/jmap/JMAPConfigurationTest.java
index 68dbf75..011f774 100644
--- a/server/protocols/jmap/src/test/java/org/apache/james/jmap/JMAPConfigurationTest.java
+++ b/server/protocols/jmap/src/test/java/org/apache/james/jmap/JMAPConfigurationTest.java
@@ -41,22 +41,24 @@ class JMAPConfigurationTest {
 
     @Test
     void buildShouldWorkWhenRandomPort() {
-        JMAPConfiguration expectedJMAPConfiguration = new JMAPConfiguration(ENABLED, Optional.empty());
+        JMAPConfiguration expectedJMAPConfiguration = new JMAPConfiguration(ENABLED, Optional.empty(), ENABLED);
 
         JMAPConfiguration jmapConfiguration = JMAPConfiguration.builder()
             .enable()
             .randomPort()
+            .enableEmailQueryView()
             .build();
         assertThat(jmapConfiguration).isEqualToComparingFieldByField(expectedJMAPConfiguration);
     }
 
     @Test
     void buildShouldWorkWhenFixedPort() {
-        JMAPConfiguration expectedJMAPConfiguration = new JMAPConfiguration(ENABLED, Optional.of(Port.of(80)));
+        JMAPConfiguration expectedJMAPConfiguration = new JMAPConfiguration(ENABLED, Optional.of(Port.of(80)), ENABLED);
 
         JMAPConfiguration jmapConfiguration = JMAPConfiguration.builder()
             .enable()
             .port(Port.of(80))
+            .enableEmailQueryView()
             .build();
 
         assertThat(jmapConfiguration).isEqualToComparingFieldByField(expectedJMAPConfiguration);
@@ -64,10 +66,11 @@ class JMAPConfigurationTest {
 
     @Test
     void buildShouldWorkWhenDisabled() {
-        JMAPConfiguration expectedJMAPConfiguration = new JMAPConfiguration(DISABLED, Optional.empty());
+        JMAPConfiguration expectedJMAPConfiguration = new JMAPConfiguration(DISABLED, Optional.empty(), DISABLED);
 
         JMAPConfiguration jmapConfiguration = JMAPConfiguration.builder()
             .disable()
+            .disableEmailQueryView()
             .build();
         assertThat(jmapConfiguration).isEqualToComparingFieldByField(expectedJMAPConfiguration);
     }
diff --git a/src/site/xdoc/server/config-jmap.xml b/src/site/xdoc/server/config-jmap.xml
index ce649ac..ef03111 100644
--- a/src/site/xdoc/server/config-jmap.xml
+++ b/src/site/xdoc/server/config-jmap.xml
@@ -64,6 +64,11 @@
                     <dt><strong>upload.max.size</strong></dt>
                     <dd>Optional. Configuration max size Upload in new JMAP-RFC-8621.</dd>
                     <dd>Default value: 30M. Supported units are B (bytes) K (KB) M (MB) G (GB).</dd>
+
+                    <dt><strong>view.email.query.enabled</strong></dt>
+                    <dd>Optional boolean. Defaults to false.</dd>
+                    <dd>Should simple Email/query be resolved against a Cassandra projection, or should we resolve them against ElasticSearch?
+                        This enables a higher resilience, but the projection needs to be correctly populated.</dd>
                 </dl>
 
             </subsection>


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org


[james-project] 03/18: JAMES-3440 EmailQuery Limit & Position validation should not rely on Mono

Posted by bt...@apache.org.
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 9a60a678adb901943f427e0646d9068953e884c3
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Mon Nov 16 14:00:14 2020 +0700

    JAMES-3440 EmailQuery Limit & Position validation should not rely on Mono
    
    Synchronous checks needs to be represented via an Either
---
 .../james/jmap/method/EmailQueryMethod.scala       | 27 +++++++++++-----------
 1 file changed, 14 insertions(+), 13 deletions(-)

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 b2bfb02..6c32948 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
@@ -54,19 +54,20 @@ class EmailQueryMethod @Inject() (serializer: EmailQuerySerializer,
   private def processRequest(mailboxSession: MailboxSession,
                              invocation: Invocation,
                              request: EmailQueryRequest,
-                             capabilities: Set[CapabilityIdentifier]): SMono[Invocation] =
-    searchQueryFromRequest(request, capabilities, mailboxSession)
-      .flatMap(searchQuery => Limit.validateRequestLimit(request.limit).map((searchQuery, _)))
-      .flatMap {
-        case (searchQuery, limit) => Position.validateRequestPosition(request.position)
-          .map((searchQuery, limit, _))
-      }.map {
-      case (searchQuery, limitToUse, positionToUse) => executeQuery(mailboxSession, request, searchQuery, positionToUse, limitToUse)
-        .map(response => Invocation(
-          methodName = methodName,
-          arguments = Arguments(serializer.serialize(response)),
-          methodCallId = invocation.methodCallId))
-    }.fold(SMono.raiseError, res => res)
+                             capabilities: Set[CapabilityIdentifier]): SMono[Invocation] = {
+    def validation: Either[Throwable, SMono[Invocation]] = for {
+        searchQuery <- searchQueryFromRequest(request, capabilities, mailboxSession)
+        limit <- Limit.validateRequestLimit(request.limit)
+        position <- Position.validateRequestPosition(request.position)
+      } yield {
+        executeQuery(mailboxSession, request, searchQuery, position, limit)
+          .map(response => Invocation(
+            methodName = methodName,
+            arguments = Arguments(serializer.serialize(response)),
+            methodCallId = invocation.methodCallId))
+      }
+    validation.fold(SMono.raiseError, res => res)
+  }
 
   override def getRequest(mailboxSession: MailboxSession, invocation: Invocation): SMono[EmailQueryRequest] = asEmailQueryRequest(invocation.arguments)
 


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org


[james-project] 09/18: JAMES-3440 Generalize JMAP tasks RunningOptions for reuse

Posted by bt...@apache.org.
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 bfa896747c02cb17abac6461b867d25243b60ae0
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Tue Nov 17 17:03:05 2020 +0700

    JAMES-3440 Generalize JMAP tasks RunningOptions for reuse
---
 .../jmap/MessageFastViewProjectionCorrector.java   | 21 ----------------
 .../RecomputeAllFastViewProjectionItemsTask.java   |  1 -
 ...uteAllFastViewTaskAdditionalInformationDTO.java |  1 -
 .../RecomputeUserFastViewProjectionItemsTask.java  |  1 -
 ...teUserFastViewTaskAdditionalInformationDTO.java |  1 -
 ...{RunningOptionsDTO.java => RunningOptions.java} | 28 ++++++++--------------
 .../webadmin/data/jmap/RunningOptionsDTO.java      |  2 --
 .../webadmin/data/jmap/RunningOptionsParser.java   |  2 --
 ...ctionItemsTaskAdditionalInformationDTOTest.java |  1 -
 ...stViewProjectionItemsTaskSerializationTest.java |  1 -
 ...ctionItemsTaskAdditionalInformationDTOTest.java |  4 ++--
 ...stViewProjectionItemsTaskSerializationTest.java |  1 -
 12 files changed, 12 insertions(+), 52 deletions(-)

diff --git a/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/MessageFastViewProjectionCorrector.java b/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/MessageFastViewProjectionCorrector.java
index 612c6ca..ae8df05 100644
--- a/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/MessageFastViewProjectionCorrector.java
+++ b/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/MessageFastViewProjectionCorrector.java
@@ -53,7 +53,6 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import com.github.fge.lambdas.Throwing;
-import com.google.common.base.Preconditions;
 
 import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
@@ -64,26 +63,6 @@ public class MessageFastViewProjectionCorrector {
     public static final int USER_CONCURRENCY = 1;
     public static final int MAILBOX_CONCURRENCY = 1;
 
-    public static class RunningOptions {
-        public static RunningOptions withMessageRatePerSecond(int messageRatePerSecond) {
-            return new RunningOptions(messageRatePerSecond);
-        }
-
-        public static RunningOptions DEFAULT = new RunningOptions(10);
-
-        private final int messagesPerSecond;
-
-        public RunningOptions(int messagesPerSecond) {
-            Preconditions.checkArgument(messagesPerSecond > 0, "'messagesPerSecond' must be strictly positive");
-
-            this.messagesPerSecond = messagesPerSecond;
-        }
-
-        public int getMessagesPerSecond() {
-            return messagesPerSecond;
-        }
-    }
-
     private static class ProjectionEntry {
         private final MessageManager messageManager;
         private final MessageUid uid;
diff --git a/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RecomputeAllFastViewProjectionItemsTask.java b/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RecomputeAllFastViewProjectionItemsTask.java
index 5ff9dc5..f3ecd5a 100644
--- a/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RecomputeAllFastViewProjectionItemsTask.java
+++ b/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RecomputeAllFastViewProjectionItemsTask.java
@@ -29,7 +29,6 @@ import org.apache.james.server.task.json.dto.TaskDTOModule;
 import org.apache.james.task.Task;
 import org.apache.james.task.TaskExecutionDetails;
 import org.apache.james.task.TaskType;
-import org.apache.james.webadmin.data.jmap.MessageFastViewProjectionCorrector.RunningOptions;
 
 import com.fasterxml.jackson.annotation.JsonProperty;
 
diff --git a/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RecomputeAllFastViewTaskAdditionalInformationDTO.java b/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RecomputeAllFastViewTaskAdditionalInformationDTO.java
index 5290fd1..8073074 100644
--- a/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RecomputeAllFastViewTaskAdditionalInformationDTO.java
+++ b/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RecomputeAllFastViewTaskAdditionalInformationDTO.java
@@ -25,7 +25,6 @@ import java.util.Optional;
 import org.apache.james.json.DTOModule;
 import org.apache.james.server.task.json.dto.AdditionalInformationDTO;
 import org.apache.james.server.task.json.dto.AdditionalInformationDTOModule;
-import org.apache.james.webadmin.data.jmap.MessageFastViewProjectionCorrector.RunningOptions;
 
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.google.common.annotations.VisibleForTesting;
diff --git a/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RecomputeUserFastViewProjectionItemsTask.java b/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RecomputeUserFastViewProjectionItemsTask.java
index 3c4d90c..014044a 100644
--- a/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RecomputeUserFastViewProjectionItemsTask.java
+++ b/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RecomputeUserFastViewProjectionItemsTask.java
@@ -20,7 +20,6 @@
 package org.apache.james.webadmin.data.jmap;
 
 import static org.apache.james.webadmin.data.jmap.MessageFastViewProjectionCorrector.Progress;
-import static org.apache.james.webadmin.data.jmap.MessageFastViewProjectionCorrector.RunningOptions;
 
 import java.time.Clock;
 import java.time.Instant;
diff --git a/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RecomputeUserFastViewTaskAdditionalInformationDTO.java b/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RecomputeUserFastViewTaskAdditionalInformationDTO.java
index 8ae9692..e0528cd 100644
--- a/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RecomputeUserFastViewTaskAdditionalInformationDTO.java
+++ b/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RecomputeUserFastViewTaskAdditionalInformationDTO.java
@@ -26,7 +26,6 @@ import org.apache.james.core.Username;
 import org.apache.james.json.DTOModule;
 import org.apache.james.server.task.json.dto.AdditionalInformationDTO;
 import org.apache.james.server.task.json.dto.AdditionalInformationDTOModule;
-import org.apache.james.webadmin.data.jmap.MessageFastViewProjectionCorrector.RunningOptions;
 
 import com.fasterxml.jackson.annotation.JsonProperty;
 
diff --git a/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RunningOptionsDTO.java b/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RunningOptions.java
similarity index 61%
copy from server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RunningOptionsDTO.java
copy to server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RunningOptions.java
index c8efe03..0ca7078 100644
--- a/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RunningOptionsDTO.java
+++ b/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RunningOptions.java
@@ -19,32 +19,24 @@
 
 package org.apache.james.webadmin.data.jmap;
 
-import java.util.Optional;
+import com.google.common.base.Preconditions;
 
-import org.apache.james.webadmin.data.jmap.MessageFastViewProjectionCorrector.RunningOptions;
+public class RunningOptions {
+    public static RunningOptions withMessageRatePerSecond(int messageRatePerSecond) {
+        return new RunningOptions(messageRatePerSecond);
+    }
 
-import com.fasterxml.jackson.annotation.JsonCreator;
-import com.fasterxml.jackson.annotation.JsonProperty;
+    public static RunningOptions DEFAULT = new RunningOptions(10);
 
-public class RunningOptionsDTO {
-    public static RunningOptionsDTO asDTO(RunningOptions domainObject) {
-        return new RunningOptionsDTO(Optional.of(domainObject.getMessagesPerSecond()));
-    }
+    private final int messagesPerSecond;
 
-    private final Optional<Integer> messagesPerSecond;
+    public RunningOptions(int messagesPerSecond) {
+        Preconditions.checkArgument(messagesPerSecond > 0, "'messagesPerSecond' must be strictly positive");
 
-    @JsonCreator
-    public RunningOptionsDTO(
-            @JsonProperty("messagesPerSecond") Optional<Integer> messagesPerSecond) {
         this.messagesPerSecond = messagesPerSecond;
     }
 
-    public Optional<Integer> getMessagesPerSecond() {
+    public int getMessagesPerSecond() {
         return messagesPerSecond;
     }
-
-    public RunningOptions asDomainObject() {
-        return messagesPerSecond.map(RunningOptions::withMessageRatePerSecond)
-            .orElse(RunningOptions.DEFAULT);
-    }
 }
diff --git a/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RunningOptionsDTO.java b/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RunningOptionsDTO.java
index c8efe03..0d27ec7 100644
--- a/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RunningOptionsDTO.java
+++ b/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RunningOptionsDTO.java
@@ -21,8 +21,6 @@ package org.apache.james.webadmin.data.jmap;
 
 import java.util.Optional;
 
-import org.apache.james.webadmin.data.jmap.MessageFastViewProjectionCorrector.RunningOptions;
-
 import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonProperty;
 
diff --git a/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RunningOptionsParser.java b/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RunningOptionsParser.java
index 8aebd6e..45e82c5 100644
--- a/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RunningOptionsParser.java
+++ b/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RunningOptionsParser.java
@@ -21,8 +21,6 @@ package org.apache.james.webadmin.data.jmap;
 
 import java.util.Optional;
 
-import org.apache.james.webadmin.data.jmap.MessageFastViewProjectionCorrector.RunningOptions;
-
 import spark.Request;
 
 public class RunningOptionsParser {
diff --git a/server/protocols/webadmin/webadmin-jmap/src/test/java/org/apache/james/webadmin/data/jmap/RecomputeAllFastViewProjectionItemsTaskAdditionalInformationDTOTest.java b/server/protocols/webadmin/webadmin-jmap/src/test/java/org/apache/james/webadmin/data/jmap/RecomputeAllFastViewProjectionItemsTaskAdditionalInformationDTOTest.java
index 5f301af..0a94124 100644
--- a/server/protocols/webadmin/webadmin-jmap/src/test/java/org/apache/james/webadmin/data/jmap/RecomputeAllFastViewProjectionItemsTaskAdditionalInformationDTOTest.java
+++ b/server/protocols/webadmin/webadmin-jmap/src/test/java/org/apache/james/webadmin/data/jmap/RecomputeAllFastViewProjectionItemsTaskAdditionalInformationDTOTest.java
@@ -26,7 +26,6 @@ import java.time.Instant;
 import org.apache.james.JsonSerializationVerifier;
 import org.apache.james.json.JsonGenericSerializer;
 import org.apache.james.util.ClassLoaderUtils;
-import org.apache.james.webadmin.data.jmap.MessageFastViewProjectionCorrector.RunningOptions;
 import org.junit.jupiter.api.Test;
 
 class RecomputeAllFastViewProjectionItemsTaskAdditionalInformationDTOTest {
diff --git a/server/protocols/webadmin/webadmin-jmap/src/test/java/org/apache/james/webadmin/data/jmap/RecomputeAllFastViewProjectionItemsTaskSerializationTest.java b/server/protocols/webadmin/webadmin-jmap/src/test/java/org/apache/james/webadmin/data/jmap/RecomputeAllFastViewProjectionItemsTaskSerializationTest.java
index fc9f89e..a31eea8 100644
--- a/server/protocols/webadmin/webadmin-jmap/src/test/java/org/apache/james/webadmin/data/jmap/RecomputeAllFastViewProjectionItemsTaskSerializationTest.java
+++ b/server/protocols/webadmin/webadmin-jmap/src/test/java/org/apache/james/webadmin/data/jmap/RecomputeAllFastViewProjectionItemsTaskSerializationTest.java
@@ -25,7 +25,6 @@ import static org.mockito.Mockito.mock;
 import org.apache.james.JsonSerializationVerifier;
 import org.apache.james.json.JsonGenericSerializer;
 import org.apache.james.util.ClassLoaderUtils;
-import org.apache.james.webadmin.data.jmap.MessageFastViewProjectionCorrector.RunningOptions;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
diff --git a/server/protocols/webadmin/webadmin-jmap/src/test/java/org/apache/james/webadmin/data/jmap/RecomputeUserFastViewProjectionItemsTaskAdditionalInformationDTOTest.java b/server/protocols/webadmin/webadmin-jmap/src/test/java/org/apache/james/webadmin/data/jmap/RecomputeUserFastViewProjectionItemsTaskAdditionalInformationDTOTest.java
index baeb7c1..ce0c123 100644
--- a/server/protocols/webadmin/webadmin-jmap/src/test/java/org/apache/james/webadmin/data/jmap/RecomputeUserFastViewProjectionItemsTaskAdditionalInformationDTOTest.java
+++ b/server/protocols/webadmin/webadmin-jmap/src/test/java/org/apache/james/webadmin/data/jmap/RecomputeUserFastViewProjectionItemsTaskAdditionalInformationDTOTest.java
@@ -32,7 +32,7 @@ import org.junit.jupiter.api.Test;
 class RecomputeUserFastViewProjectionItemsTaskAdditionalInformationDTOTest {
     private static final Instant INSTANT = Instant.parse("2007-12-03T10:15:30.00Z");
     private static final RecomputeUserFastViewProjectionItemsTask.AdditionalInformation DOMAIN_OBJECT = new RecomputeUserFastViewProjectionItemsTask.AdditionalInformation(
-        MessageFastViewProjectionCorrector.RunningOptions.withMessageRatePerSecond(20),
+        RunningOptions.withMessageRatePerSecond(20),
         Username.of("bob"), 2, 3, INSTANT);
 
     @Test
@@ -51,7 +51,7 @@ class RecomputeUserFastViewProjectionItemsTaskAdditionalInformationDTOTest {
             .deserialize(ClassLoaderUtils.getSystemResourceAsString("json/recomputeUser.additionalInformation.legacy.json"));
 
         RecomputeUserFastViewProjectionItemsTask.AdditionalInformation expected = new RecomputeUserFastViewProjectionItemsTask.AdditionalInformation(
-            MessageFastViewProjectionCorrector.RunningOptions.DEFAULT,
+            RunningOptions.DEFAULT,
             Username.of("bob"),
             2,
             3,
diff --git a/server/protocols/webadmin/webadmin-jmap/src/test/java/org/apache/james/webadmin/data/jmap/RecomputeUserFastViewProjectionItemsTaskSerializationTest.java b/server/protocols/webadmin/webadmin-jmap/src/test/java/org/apache/james/webadmin/data/jmap/RecomputeUserFastViewProjectionItemsTaskSerializationTest.java
index b5796ba..cb99cb1 100644
--- a/server/protocols/webadmin/webadmin-jmap/src/test/java/org/apache/james/webadmin/data/jmap/RecomputeUserFastViewProjectionItemsTaskSerializationTest.java
+++ b/server/protocols/webadmin/webadmin-jmap/src/test/java/org/apache/james/webadmin/data/jmap/RecomputeUserFastViewProjectionItemsTaskSerializationTest.java
@@ -26,7 +26,6 @@ import org.apache.james.JsonSerializationVerifier;
 import org.apache.james.core.Username;
 import org.apache.james.json.JsonGenericSerializer;
 import org.apache.james.util.ClassLoaderUtils;
-import org.apache.james.webadmin.data.jmap.MessageFastViewProjectionCorrector.RunningOptions;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org


[james-project] 16/18: JAMES-3441 Document listeners.xml

Posted by bt...@apache.org.
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 a7d2418e0f4115806917aae839ed7ade904bd68c
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Thu Nov 19 08:57:45 2020 +0700

    JAMES-3441 Document listeners.xml <executeGroupListeners>
---
 docs/modules/servers/pages/distributed/configure/listeners.adoc | 6 +++++-
 src/site/xdoc/server/config-listeners.xml                       | 6 ++++++
 2 files changed, 11 insertions(+), 1 deletion(-)

diff --git a/docs/modules/servers/pages/distributed/configure/listeners.adoc b/docs/modules/servers/pages/distributed/configure/listeners.adoc
index 6f8fc00..04bd9f9 100644
--- a/docs/modules/servers/pages/distributed/configure/listeners.adoc
+++ b/docs/modules/servers/pages/distributed/configure/listeners.adoc
@@ -17,13 +17,17 @@ to get some examples and hints.
 
 == Configuration
 
+The <executeGroupListeners> controls whether to launch group mailbox listener consumption. Defaults to true. Use with caution:
+never disable on standalone james servers, and ensure at least some instances do consume group mailbox listeners within a
+clustered topology.
+
 Mailbox listener configuration is under the XML element <listener>.
 
 Some MailboxListener allows you to specify if you want to run them synchronously or asynchronously. To do so,
 for MailboxListener that supports this, you can use the *async* attribute (optional, per mailet default) to govern the execution mode.
 If *true* the execution will be scheduled in a reactor elastic scheduler. If *false*, the execution is synchronous.
 
-Already provided additional listeners are docmented below.
+Already provided additional listeners are documented below.
 
 === SpamAssassinListener
 
diff --git a/src/site/xdoc/server/config-listeners.xml b/src/site/xdoc/server/config-listeners.xml
index aa35f71..d9c6c18 100644
--- a/src/site/xdoc/server/config-listeners.xml
+++ b/src/site/xdoc/server/config-listeners.xml
@@ -44,6 +44,12 @@
 
         <section name="MailboxListener configuration">
             <p>
+                The &lt;executeGroupListeners&gt; controls whether to launch group mailbox listener consumption.
+                Defaults to true. Use with caution:
+                never disable on standalone james servers, and ensure at least some instances do consume group mailbox listeners within a
+                clustered topology.
+            </p>
+            <p>
                 Mailbox listener configuration is under the XML element &lt;listener&gt;
             </p>
             <p>


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org


[james-project] 06/18: JAMES-3440 JMAP Draft should use EmailQueryView when enabled

Posted by bt...@apache.org.
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 f5d90c6bc735c4f5657beed27b8558d19fd7c136
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Mon Nov 16 17:28:19 2020 +0700

    JAMES-3440 JMAP Draft should use EmailQueryView when enabled
---
 .../src/test/resources/listeners.xml               |  4 ++
 .../integration/GetMessageListMethodTest.java      | 22 +++++++
 .../src/test/resources/listeners.xml               |  4 ++
 .../src/test/resources/listeners.xml               |  4 ++
 .../jmap/draft/methods/GetMessageListMethod.java   | 77 ++++++++++++++++++++--
 .../org/apache/james/jmap/draft/model/Filter.java  |  8 +++
 .../james/jmap/draft/model/FilterCondition.java    | 39 +++++++++++
 .../src/test/resources/listeners.xml               |  4 ++
 .../src/test/resources/listeners.xml               |  4 ++
 9 files changed, 159 insertions(+), 7 deletions(-)

diff --git a/server/protocols/jmap-draft-integration-testing/cassandra-jmap-draft-integration-testing/src/test/resources/listeners.xml b/server/protocols/jmap-draft-integration-testing/cassandra-jmap-draft-integration-testing/src/test/resources/listeners.xml
index ff2e517..1ff4055 100644
--- a/server/protocols/jmap-draft-integration-testing/cassandra-jmap-draft-integration-testing/src/test/resources/listeners.xml
+++ b/server/protocols/jmap-draft-integration-testing/cassandra-jmap-draft-integration-testing/src/test/resources/listeners.xml
@@ -46,4 +46,8 @@
       <name>second</name>
     </configuration>
   </listener>
+  <listener>
+    <class>org.apache.james.jmap.event.PopulateEmailQueryViewListener</class>
+    <async>true</async>
+  </listener>
 </listeners>
\ No newline at end of file
diff --git a/server/protocols/jmap-draft-integration-testing/jmap-draft-integration-testing-common/src/test/java/org/apache/james/jmap/draft/methods/integration/GetMessageListMethodTest.java b/server/protocols/jmap-draft-integration-testing/jmap-draft-integration-testing-common/src/test/java/org/apache/james/jmap/draft/methods/integration/GetMessageListMethodTest.java
index f130a7d..bfff531 100644
--- a/server/protocols/jmap-draft-integration-testing/jmap-draft-integration-testing-common/src/test/java/org/apache/james/jmap/draft/methods/integration/GetMessageListMethodTest.java
+++ b/server/protocols/jmap-draft-integration-testing/jmap-draft-integration-testing-common/src/test/java/org/apache/james/jmap/draft/methods/integration/GetMessageListMethodTest.java
@@ -1771,6 +1771,28 @@ public abstract class GetMessageListMethodTest {
     }
 
     @Test
+    public void getMessageListShouldSortMessagesWhenSortedByDateDescAndInMailbox() throws Exception {
+        MailboxId mailboxId = mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, ALICE.asString(), "mailbox");
+
+        LocalDate date = LocalDate.now();
+        ComposedMessageId message1 = mailboxProbe.appendMessage(ALICE.asString(), ALICE_MAILBOX,
+                new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes()), convertToDate(date.plusDays(1)), false, new Flags());
+        ComposedMessageId message2 = mailboxProbe.appendMessage(ALICE.asString(), ALICE_MAILBOX,
+                new ByteArrayInputStream("Subject: test2\r\n\r\ntestmail".getBytes()), convertToDate(date), false, new Flags());
+        await();
+
+        given()
+            .header("Authorization", aliceAccessToken.asString())
+            .body("[[\"getMessageList\", {\"filter\":{\"inMailboxes\":[\"" + mailboxId.serialize() + "\"]}, \"sort\":[\"date desc\"]}, \"#0\"]]")
+        .when()
+            .post("/jmap")
+        .then()
+            .statusCode(200)
+            .body(NAME, equalTo("messageList"))
+            .body(ARGUMENTS + ".messageIds", contains(message1.getMessageId().serialize(), message2.getMessageId().serialize()));
+    }
+
+    @Test
     public void getMessageListShouldWorkWhenCollapseThreadIsFalse() {
         given()
             .header("Authorization", aliceAccessToken.asString())
diff --git a/server/protocols/jmap-draft-integration-testing/memory-jmap-draft-integration-testing/src/test/resources/listeners.xml b/server/protocols/jmap-draft-integration-testing/memory-jmap-draft-integration-testing/src/test/resources/listeners.xml
index ae2e80a..a686755 100644
--- a/server/protocols/jmap-draft-integration-testing/memory-jmap-draft-integration-testing/src/test/resources/listeners.xml
+++ b/server/protocols/jmap-draft-integration-testing/memory-jmap-draft-integration-testing/src/test/resources/listeners.xml
@@ -46,4 +46,8 @@
   <listener>
     <class>org.apache.james.mailbox.spamassassin.SpamAssassinListener</class>
   </listener>
+  <listener>
+    <class>org.apache.james.jmap.event.PopulateEmailQueryViewListener</class>
+    <async>true</async>
+  </listener>
 </listeners>
\ No newline at end of file
diff --git a/server/protocols/jmap-draft-integration-testing/rabbitmq-jmap-draft-integration-testing/src/test/resources/listeners.xml b/server/protocols/jmap-draft-integration-testing/rabbitmq-jmap-draft-integration-testing/src/test/resources/listeners.xml
index cac2777..43c8b96 100644
--- a/server/protocols/jmap-draft-integration-testing/rabbitmq-jmap-draft-integration-testing/src/test/resources/listeners.xml
+++ b/server/protocols/jmap-draft-integration-testing/rabbitmq-jmap-draft-integration-testing/src/test/resources/listeners.xml
@@ -49,4 +49,8 @@
   <listener>
     <class>org.apache.james.mailbox.spamassassin.SpamAssassinListener</class>
   </listener>
+  <listener>
+    <class>org.apache.james.jmap.event.PopulateEmailQueryViewListener</class>
+    <async>true</async>
+  </listener>
 </listeners>
\ No newline at end of file
diff --git a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/methods/GetMessageListMethod.java b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/methods/GetMessageListMethod.java
index a14069c..5935b79 100644
--- a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/methods/GetMessageListMethod.java
+++ b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/methods/GetMessageListMethod.java
@@ -21,6 +21,7 @@ package org.apache.james.jmap.draft.methods;
 
 import static org.apache.james.util.ReactorUtils.context;
 
+import java.time.ZonedDateTime;
 import java.util.List;
 import java.util.Optional;
 import java.util.Set;
@@ -31,6 +32,8 @@ import javax.inject.Inject;
 import javax.inject.Named;
 
 import org.apache.commons.lang3.NotImplementedException;
+import org.apache.james.jmap.JMAPConfiguration;
+import org.apache.james.jmap.api.projections.EmailQueryView;
 import org.apache.james.jmap.draft.model.Filter;
 import org.apache.james.jmap.draft.model.FilterCondition;
 import org.apache.james.jmap.draft.model.GetMessageListRequest;
@@ -42,17 +45,19 @@ import org.apache.james.jmap.draft.utils.FilterToCriteria;
 import org.apache.james.jmap.draft.utils.SortConverter;
 import org.apache.james.mailbox.MailboxManager;
 import org.apache.james.mailbox.MailboxSession;
+import org.apache.james.mailbox.exception.MailboxNotFoundException;
 import org.apache.james.mailbox.model.MailboxId;
 import org.apache.james.mailbox.model.MailboxId.Factory;
 import org.apache.james.mailbox.model.MultimailboxesSearchQuery;
 import org.apache.james.mailbox.model.SearchQuery;
 import org.apache.james.metrics.api.MetricFactory;
 import org.apache.james.util.MDCBuilder;
+import org.apache.james.util.streams.Limit;
 
 import com.github.fge.lambdas.Throwing;
 import com.github.steveash.guavate.Guavate;
-import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
 
 import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
@@ -71,17 +76,25 @@ public class GetMessageListMethod implements Method {
     private final long maximumLimit;
     private final GetMessagesMethod getMessagesMethod;
     private final Factory mailboxIdFactory;
+    private final EmailQueryView emailQueryView;
+    private final JMAPConfiguration configuration;
     private final MetricFactory metricFactory;
 
     @Inject
-    @VisibleForTesting public GetMessageListMethod(MailboxManager mailboxManager,
-            @Named(MAXIMUM_LIMIT) long maximumLimit, GetMessagesMethod getMessagesMethod, MailboxId.Factory mailboxIdFactory,
-            MetricFactory metricFactory) {
+    private GetMessageListMethod(MailboxManager mailboxManager,
+                                 @Named(MAXIMUM_LIMIT) long maximumLimit,
+                                 GetMessagesMethod getMessagesMethod,
+                                 Factory mailboxIdFactory,
+                                 EmailQueryView emailQueryView,
+                                 JMAPConfiguration configuration,
+                                 MetricFactory metricFactory) {
 
         this.mailboxManager = mailboxManager;
         this.maximumLimit = maximumLimit;
         this.getMessagesMethod = getMessagesMethod;
         this.mailboxIdFactory = mailboxIdFactory;
+        this.emailQueryView = emailQueryView;
+        this.configuration = configuration;
         this.metricFactory = metricFactory;
     }
 
@@ -149,14 +162,64 @@ public class GetMessageListMethod implements Method {
     }
 
     private Mono<GetMessageListResponse> getMessageListResponse(GetMessageListRequest messageListRequest, MailboxSession mailboxSession) {
+        long position = messageListRequest.getPosition().map(Number::asLong).orElse(DEFAULT_POSITION);
+        long limit = position + messageListRequest.getLimit().map(Number::asLong).orElse(maximumLimit);
+
+        if (isListingContentInMailboxQuery(messageListRequest)) {
+            Filter filter = messageListRequest.getFilter().get();
+            FilterCondition condition = (FilterCondition) filter;
+            String mailboxIdAsString = condition.getInMailboxes().get().iterator().next();
+            MailboxId mailboxId = mailboxIdFactory.fromString(mailboxIdAsString);
+            Limit aLimit = Limit.from(Math.toIntExact(limit));
+
+            return Mono.fromCallable(() -> mailboxManager.getMailbox(mailboxId, mailboxSession))
+                .subscribeOn(Schedulers.elastic())
+                .then(emailQueryView.listMailboxContent(mailboxId, aLimit)
+                    .skip(position)
+                    .take(limit)
+                    .reduce(GetMessageListResponse.builder(), GetMessageListResponse.Builder::messageId)
+                    .map(GetMessageListResponse.Builder::build))
+                .onErrorResume(MailboxNotFoundException.class, e -> Mono.just(GetMessageListResponse.builder().build()));
+        }
+        if (isListingContentInMailboxAfterQuery(messageListRequest)) {
+            Filter filter = messageListRequest.getFilter().get();
+            FilterCondition condition = (FilterCondition) filter;
+            String mailboxIdAsString = condition.getInMailboxes().get().iterator().next();
+            MailboxId mailboxId = mailboxIdFactory.fromString(mailboxIdAsString);
+            ZonedDateTime after = condition.getAfter().get();
+            Limit aLimit = Limit.from(Math.toIntExact(limit));
+
+            return Mono.fromCallable(() -> mailboxManager.getMailbox(mailboxId, mailboxSession))
+                .subscribeOn(Schedulers.elastic())
+                .then(emailQueryView.listMailboxContentSinceReceivedAt(mailboxId, after, aLimit)
+                    .skip(position)
+                    .take(limit)
+                    .reduce(GetMessageListResponse.builder(), GetMessageListResponse.Builder::messageId)
+                    .map(GetMessageListResponse.Builder::build))
+                .onErrorResume(MailboxNotFoundException.class, e -> Mono.just(GetMessageListResponse.builder().build()));
+        }
+        return querySearchBackend(messageListRequest, position, limit, mailboxSession);
+    }
+
+    private boolean isListingContentInMailboxQuery(GetMessageListRequest messageListRequest) {
+        return configuration.isEmailQueryViewEnabled()
+            && messageListRequest.getFilter().map(Filter::inMailboxFilterOnly).orElse(false)
+            && messageListRequest.getSort().equals(ImmutableList.of("date desc"));
+    }
+
+    private boolean isListingContentInMailboxAfterQuery(GetMessageListRequest messageListRequest) {
+        return configuration.isEmailQueryViewEnabled()
+            && messageListRequest.getFilter().map(Filter::inMailboxAndAfterFiltersOnly).orElse(false)
+            && messageListRequest.getSort().equals(ImmutableList.of("date desc"));
+    }
+
+    private Mono<GetMessageListResponse> querySearchBackend(GetMessageListRequest messageListRequest, long position, long limit, MailboxSession mailboxSession) {
         Mono<MultimailboxesSearchQuery> searchQuery = Mono.fromCallable(() -> convertToSearchQuery(messageListRequest))
             .subscribeOn(Schedulers.parallel());
-        Long positionValue = messageListRequest.getPosition().map(Number::asLong).orElse(DEFAULT_POSITION);
-        long limit = positionValue + messageListRequest.getLimit().map(Number::asLong).orElse(maximumLimit);
 
         return searchQuery
             .flatMapMany(Throwing.function(query -> mailboxManager.search(query, mailboxSession, limit)))
-            .skip(positionValue)
+            .skip(position)
             .reduce(GetMessageListResponse.builder(), GetMessageListResponse.Builder::messageId)
             .map(GetMessageListResponse.Builder::build);
     }
diff --git a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/model/Filter.java b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/model/Filter.java
index 0f26d7c..d562634 100644
--- a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/model/Filter.java
+++ b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/model/Filter.java
@@ -39,6 +39,14 @@ public interface Filter {
 
     String prettyPrint(String indentation);
 
+    default boolean inMailboxFilterOnly() {
+        return false;
+    }
+
+    default boolean inMailboxAndAfterFiltersOnly() {
+        return false;
+    }
+
     default List<FilterCondition> breadthFirstVisit() {
         return this.breadthFirstVisit(0)
             .collect(Guavate.toImmutableList());
diff --git a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/model/FilterCondition.java b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/model/FilterCondition.java
index e61aba5..14d723e 100644
--- a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/model/FilterCondition.java
+++ b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/model/FilterCondition.java
@@ -279,6 +279,45 @@ public class FilterCondition implements Filter {
         this.attachmentFileName = attachmentFileName;
     }
 
+    @Override
+    public boolean inMailboxFilterOnly() {
+        return inMailboxes.filter(list -> list.size() == 1).isPresent()
+            && after.isEmpty()
+            && noOtherFiltersSet();
+    }
+
+    @Override
+    public boolean inMailboxAndAfterFiltersOnly() {
+        return inMailboxes.filter(list -> list.size() == 1).isPresent()
+            && after.isPresent()
+            && noOtherFiltersSet();
+    }
+
+    private boolean noOtherFiltersSet() {
+        return notInMailboxes.isEmpty()
+            && before.isEmpty()
+            && minSize.isEmpty()
+            && maxSize.isEmpty()
+            && isFlagged.isEmpty()
+            && isUnread.isEmpty()
+            && isAnswered.isEmpty()
+            && isDraft.isEmpty()
+            && isForwarded.isEmpty()
+            && hasAttachment.isEmpty()
+            && text.isEmpty()
+            && from.isEmpty()
+            && to.isEmpty()
+            && cc.isEmpty()
+            && bcc.isEmpty()
+            && subject.isEmpty()
+            && body.isEmpty()
+            && attachments.isEmpty()
+            && header.isEmpty()
+            && hasKeyword.isEmpty()
+            && notKeyword.isEmpty()
+            && attachmentFileName.isEmpty();
+    }
+
     public Optional<List<String>> getInMailboxes() {
         return inMailboxes;
     }
diff --git a/server/protocols/webadmin-integration-test/distributed-webadmin-integration-test/src/test/resources/listeners.xml b/server/protocols/webadmin-integration-test/distributed-webadmin-integration-test/src/test/resources/listeners.xml
index ff2e517..1ff4055 100644
--- a/server/protocols/webadmin-integration-test/distributed-webadmin-integration-test/src/test/resources/listeners.xml
+++ b/server/protocols/webadmin-integration-test/distributed-webadmin-integration-test/src/test/resources/listeners.xml
@@ -46,4 +46,8 @@
       <name>second</name>
     </configuration>
   </listener>
+  <listener>
+    <class>org.apache.james.jmap.event.PopulateEmailQueryViewListener</class>
+    <async>true</async>
+  </listener>
 </listeners>
\ No newline at end of file
diff --git a/server/protocols/webadmin-integration-test/memory-webadmin-integration-test/src/test/resources/listeners.xml b/server/protocols/webadmin-integration-test/memory-webadmin-integration-test/src/test/resources/listeners.xml
index 59e3fec..a1a139d 100644
--- a/server/protocols/webadmin-integration-test/memory-webadmin-integration-test/src/test/resources/listeners.xml
+++ b/server/protocols/webadmin-integration-test/memory-webadmin-integration-test/src/test/resources/listeners.xml
@@ -43,4 +43,8 @@
       <name>second</name>
     </configuration>
   </listener>
+  <listener>
+    <class>org.apache.james.jmap.event.PopulateEmailQueryViewListener</class>
+    <async>true</async>
+  </listener>
 </listeners>
\ No newline at end of file


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org


[james-project] 07/18: [Refactoring] Fix indent in GetMessageListMethod

Posted by bt...@apache.org.
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 9dff3ba90a9aaca95fbf2147870e7b689f66c90a
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Tue Nov 17 11:34:19 2020 +0700

    [Refactoring] Fix indent in GetMessageListMethod
---
 .../jmap/draft/methods/GetMessageListMethod.java   | 81 +++++++++++-----------
 1 file changed, 40 insertions(+), 41 deletions(-)

diff --git a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/methods/GetMessageListMethod.java b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/methods/GetMessageListMethod.java
index 5935b79..69d64d5e 100644
--- a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/methods/GetMessageListMethod.java
+++ b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/methods/GetMessageListMethod.java
@@ -64,7 +64,6 @@ import reactor.core.publisher.Mono;
 import reactor.core.scheduler.Schedulers;
 
 public class GetMessageListMethod implements Method {
-
     private static final long DEFAULT_POSITION = 0;
     public static final String MAXIMUM_LIMIT = "maximumLimit";
     public static final long DEFAULT_MAXIMUM_LIMIT = 256;
@@ -137,28 +136,28 @@ public class GetMessageListMethod implements Method {
 
     private Flux<JmapResponse> process(MethodCallId methodCallId, MailboxSession mailboxSession, GetMessageListRequest messageListRequest) {
         return getMessageListResponse(messageListRequest, mailboxSession)
-                .flatMapMany(messageListResponse -> Flux.concat(
-                    Mono.just(JmapResponse.builder().methodCallId(methodCallId)
-                        .response(messageListResponse)
-                        .responseName(RESPONSE_NAME)
-                        .build()),
-                    processGetMessages(messageListRequest, messageListResponse, methodCallId, mailboxSession)))
-                .onErrorResume(NotImplementedException.class, e -> Mono.just(JmapResponse.builder()
-                    .methodCallId(methodCallId)
-                    .responseName(RESPONSE_NAME)
-                    .error(ErrorResponse.builder()
-                        .type("invalidArguments")
-                        .description(e.getMessage())
-                        .build())
-                    .build()))
-                .onErrorResume(Filter.TooDeepFilterHierarchyException.class, e -> Mono.just(JmapResponse.builder()
-                    .methodCallId(methodCallId)
+            .flatMapMany(messageListResponse -> Flux.concat(
+                Mono.just(JmapResponse.builder().methodCallId(methodCallId)
+                    .response(messageListResponse)
                     .responseName(RESPONSE_NAME)
-                    .error(ErrorResponse.builder()
-                        .type("invalidArguments")
-                        .description(e.getMessage())
-                        .build())
-                    .build()));
+                    .build()),
+                processGetMessages(messageListRequest, messageListResponse, methodCallId, mailboxSession)))
+            .onErrorResume(NotImplementedException.class, e -> Mono.just(JmapResponse.builder()
+                .methodCallId(methodCallId)
+                .responseName(RESPONSE_NAME)
+                .error(ErrorResponse.builder()
+                    .type("invalidArguments")
+                    .description(e.getMessage())
+                    .build())
+                .build()))
+            .onErrorResume(Filter.TooDeepFilterHierarchyException.class, e -> Mono.just(JmapResponse.builder()
+                .methodCallId(methodCallId)
+                .responseName(RESPONSE_NAME)
+                .error(ErrorResponse.builder()
+                    .type("invalidArguments")
+                    .description(e.getMessage())
+                    .build())
+                .build()));
     }
 
     private Mono<GetMessageListResponse> getMessageListResponse(GetMessageListRequest messageListRequest, MailboxSession mailboxSession) {
@@ -232,9 +231,9 @@ public class GetMessageListMethod implements Method {
 
         SearchQuery.Builder searchQueryBuilder = SearchQuery.builder();
 
-            messageListRequest.getFilter()
-                .map(filter -> new FilterToCriteria().convert(filter).collect(Guavate.toImmutableList()))
-                .ifPresent(searchQueryBuilder::andCriteria);
+        messageListRequest.getFilter()
+            .map(filter -> new FilterToCriteria().convert(filter).collect(Guavate.toImmutableList()))
+            .ifPresent(searchQueryBuilder::andCriteria);
         Set<MailboxId> inMailboxes = buildFilterMailboxesSet(messageListRequest.getFilter(), FilterCondition::getInMailboxes);
         Set<MailboxId> notInMailboxes = buildFilterMailboxesSet(messageListRequest.getFilter(), FilterCondition::getNotInMailboxes);
         List<SearchQuery.Sort> sorts = SortConverter.convertToSorts(messageListRequest.getSort());
@@ -242,10 +241,10 @@ public class GetMessageListMethod implements Method {
             searchQueryBuilder.sorts(sorts);
         }
         return MultimailboxesSearchQuery
-                .from(searchQueryBuilder.build())
-                .inMailboxes(inMailboxes)
-                .notInMailboxes(notInMailboxes)
-                .build();
+            .from(searchQueryBuilder.build())
+            .inMailboxes(inMailboxes)
+            .notInMailboxes(notInMailboxes)
+            .build();
     }
 
     private boolean containsNestedMailboxFilters(Filter filter) {
@@ -276,28 +275,28 @@ public class GetMessageListMethod implements Method {
     
     private Stream<FilterCondition> filterToFilterCondition(Optional<Filter> maybeCondition) {
         return Guavate.stream(maybeCondition)
-                .flatMap(c -> {
-                    if (c instanceof FilterCondition) {
-                        return Stream.of((FilterCondition)c);
-                    }
-                    return Stream.of();
-                });
+            .flatMap(c -> {
+                if (c instanceof FilterCondition) {
+                    return Stream.of((FilterCondition)c);
+                }
+                return Stream.of();
+            });
     }
-    
+
     private Flux<JmapResponse> processGetMessages(GetMessageListRequest messageListRequest, GetMessageListResponse messageListResponse, MethodCallId methodCallId, MailboxSession mailboxSession) {
         if (shouldChainToGetMessages(messageListRequest)) {
             GetMessagesRequest getMessagesRequest = GetMessagesRequest.builder()
-                    .ids(messageListResponse.getMessageIds())
-                    .properties(messageListRequest.getFetchMessageProperties())
-                    .build();
+                .ids(messageListResponse.getMessageIds())
+                .properties(messageListRequest.getFetchMessageProperties())
+                .build();
             return getMessagesMethod.process(getMessagesRequest, methodCallId, mailboxSession);
         }
         return Flux.empty();
     }
 
     private boolean shouldChainToGetMessages(GetMessageListRequest messageListRequest) {
-        return messageListRequest.isFetchMessages().orElse(false) 
-                && !messageListRequest.isFetchThreads().orElse(false);
+        return messageListRequest.isFetchMessages().orElse(false)
+            && !messageListRequest.isFetchThreads().orElse(false);
     }
 
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org


[james-project] 12/18: JAMES-3440 WebAdmin wrappers around task to populate EmailQueryView

Posted by bt...@apache.org.
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 eb3551351b8961d407bf78942df55de359f646d8
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Tue Nov 17 17:07:03 2020 +0700

    JAMES-3440 WebAdmin wrappers around task to populate EmailQueryView
---
 .../apache/james/webadmin/data/jmap/Constants.java |   1 +
 ...va => PopulateEmailQueryViewRequestToTask.java} |  14 +-
 .../PopulateEmailQueryViewRequestToTaskTest.java   | 298 +++++++++++++++++++++
 3 files changed, 310 insertions(+), 3 deletions(-)

diff --git a/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/Constants.java b/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/Constants.java
index b076804..dfc0ad0 100644
--- a/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/Constants.java
+++ b/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/Constants.java
@@ -23,4 +23,5 @@ import org.apache.james.webadmin.tasks.TaskRegistrationKey;
 
 public interface Constants {
     TaskRegistrationKey TASK_REGISTRATION_KEY = TaskRegistrationKey.of("recomputeFastViewProjectionItems");
+    TaskRegistrationKey POPULATE_EMAIL_QUERY_VIEW = TaskRegistrationKey.of("populateEmailQueryView");
 }
diff --git a/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/Constants.java b/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/PopulateEmailQueryViewRequestToTask.java
similarity index 70%
copy from server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/Constants.java
copy to server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/PopulateEmailQueryViewRequestToTask.java
index b076804..90056b5 100644
--- a/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/Constants.java
+++ b/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/PopulateEmailQueryViewRequestToTask.java
@@ -19,8 +19,16 @@
 
 package org.apache.james.webadmin.data.jmap;
 
-import org.apache.james.webadmin.tasks.TaskRegistrationKey;
+import static org.apache.james.webadmin.data.jmap.Constants.POPULATE_EMAIL_QUERY_VIEW;
 
-public interface Constants {
-    TaskRegistrationKey TASK_REGISTRATION_KEY = TaskRegistrationKey.of("recomputeFastViewProjectionItems");
+import javax.inject.Inject;
+
+import org.apache.james.webadmin.tasks.TaskFromRequestRegistry;
+
+public class PopulateEmailQueryViewRequestToTask extends TaskFromRequestRegistry.TaskRegistration {
+    @Inject
+    PopulateEmailQueryViewRequestToTask(EmailQueryViewPopulator populator) {
+        super(POPULATE_EMAIL_QUERY_VIEW,
+            request -> new PopulateEmailQueryViewTask(populator, RunningOptionsParser.parse(request)));
+    }
 }
diff --git a/server/protocols/webadmin/webadmin-jmap/src/test/java/org/apache/james/webadmin/data/jmap/PopulateEmailQueryViewRequestToTaskTest.java b/server/protocols/webadmin/webadmin-jmap/src/test/java/org/apache/james/webadmin/data/jmap/PopulateEmailQueryViewRequestToTaskTest.java
new file mode 100644
index 0000000..e7a9a1d
--- /dev/null
+++ b/server/protocols/webadmin/webadmin-jmap/src/test/java/org/apache/james/webadmin/data/jmap/PopulateEmailQueryViewRequestToTaskTest.java
@@ -0,0 +1,298 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.webadmin.data.jmap;
+
+import static io.restassured.RestAssured.given;
+import static io.restassured.RestAssured.when;
+import static io.restassured.RestAssured.with;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
+
+import org.apache.james.core.Username;
+import org.apache.james.domainlist.api.DomainList;
+import org.apache.james.jmap.memory.projections.MemoryEmailQueryView;
+import org.apache.james.json.DTOConverter;
+import org.apache.james.mailbox.MailboxSession;
+import org.apache.james.mailbox.MessageManager;
+import org.apache.james.mailbox.inmemory.InMemoryMailboxManager;
+import org.apache.james.mailbox.inmemory.manager.InMemoryIntegrationResources;
+import org.apache.james.mailbox.model.ComposedMessageId;
+import org.apache.james.mailbox.model.MailboxId;
+import org.apache.james.mailbox.model.MailboxPath;
+import org.apache.james.task.Hostname;
+import org.apache.james.task.MemoryTaskManager;
+import org.apache.james.task.TaskManager;
+import org.apache.james.user.memory.MemoryUsersRepository;
+import org.apache.james.util.streams.Limit;
+import org.apache.james.webadmin.Routes;
+import org.apache.james.webadmin.WebAdminServer;
+import org.apache.james.webadmin.WebAdminUtils;
+import org.apache.james.webadmin.routes.TasksRoutes;
+import org.apache.james.webadmin.tasks.TaskFromRequestRegistry;
+import org.apache.james.webadmin.utils.ErrorResponder;
+import org.apache.james.webadmin.utils.JsonTransformer;
+import org.eclipse.jetty.http.HttpStatus;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import io.restassured.RestAssured;
+import io.restassured.filter.log.LogDetail;
+import spark.Service;
+
+class PopulateEmailQueryViewRequestToTaskTest {
+    private static final class JMAPRoutes implements Routes {
+        private final EmailQueryViewPopulator populator;
+        private final TaskManager taskManager;
+
+        private JMAPRoutes(EmailQueryViewPopulator populator, TaskManager taskManager) {
+            this.populator = populator;
+            this.taskManager = taskManager;
+        }
+
+        @Override
+        public String getBasePath() {
+            return BASE_PATH;
+        }
+
+        @Override
+        public void define(Service service) {
+            service.post(BASE_PATH,
+                TaskFromRequestRegistry.builder()
+                    .registrations(new PopulateEmailQueryViewRequestToTask(populator))
+                    .buildAsRoute(taskManager),
+                new JsonTransformer());
+        }
+    }
+
+    static final String BASE_PATH = "/:username/mailboxes";
+
+    static final DomainList NO_DOMAIN_LIST = null;
+    static final Username BOB = Username.of("bob");
+
+    private WebAdminServer webAdminServer;
+    private MemoryTaskManager taskManager;
+    private InMemoryMailboxManager mailboxManager;
+    private MailboxId bobInboxboxId;
+    private MailboxSession bobSession;
+    private MemoryEmailQueryView view;
+
+    @BeforeEach
+    void setUp() throws Exception {
+        JsonTransformer jsonTransformer = new JsonTransformer();
+        taskManager = new MemoryTaskManager(new Hostname("foo"));
+
+        mailboxManager = InMemoryIntegrationResources.defaultResources().getMailboxManager();
+        MemoryUsersRepository usersRepository = MemoryUsersRepository.withoutVirtualHosting(NO_DOMAIN_LIST);
+        usersRepository.addUser(BOB, "pass");
+        bobSession = mailboxManager.createSystemSession(BOB);
+        bobInboxboxId = mailboxManager.createMailbox(MailboxPath.inbox(BOB), bobSession)
+            .get();
+
+        view = new MemoryEmailQueryView();
+        webAdminServer = WebAdminUtils.createWebAdminServer(
+            new TasksRoutes(taskManager, jsonTransformer,
+                DTOConverter.of(PopulateEmailQueryViewTaskAdditionalInformationDTO.module())),
+            new JMAPRoutes(
+                new EmailQueryViewPopulator(usersRepository, mailboxManager, view),
+                taskManager))
+            .start();
+
+        RestAssured.requestSpecification = WebAdminUtils.buildRequestSpecification(webAdminServer)
+            .setBasePath("/bob/mailboxes")
+            .log(LogDetail.URI)
+            .build();
+    }
+
+    @AfterEach
+    void afterEach() {
+        webAdminServer.destroy();
+        taskManager.stop();
+    }
+
+    @Test
+    void actionRequestParameterShouldBeCompulsory() {
+        when()
+            .post()
+        .then()
+            .statusCode(HttpStatus.BAD_REQUEST_400)
+            .body("statusCode", is(400))
+            .body("type", is(ErrorResponder.ErrorType.INVALID_ARGUMENT.getType()))
+            .body("message", is("Invalid arguments supplied in the user request"))
+            .body("details", is("'action' query parameter is compulsory. Supported values are [populateEmailQueryView]"));
+    }
+
+    @Test
+    void postShouldFailUponEmptyAction() {
+        given()
+            .queryParam("action", "")
+            .post()
+        .then()
+            .statusCode(HttpStatus.BAD_REQUEST_400)
+            .body("statusCode", is(400))
+            .body("type", is(ErrorResponder.ErrorType.INVALID_ARGUMENT.getType()))
+            .body("message", is("Invalid arguments supplied in the user request"))
+            .body("details", is("'action' query parameter cannot be empty or blank. Supported values are [populateEmailQueryView]"));
+    }
+
+    @Test
+    void postShouldFailWhenMessagesPerSecondIsNotAnInt() {
+        given()
+            .queryParam("action", "populateEmailQueryView")
+            .queryParam("messagesPerSecond", "abc")
+            .post()
+        .then()
+            .statusCode(HttpStatus.BAD_REQUEST_400)
+            .body("statusCode", is(400))
+            .body("type", is(ErrorResponder.ErrorType.INVALID_ARGUMENT.getType()))
+            .body("message", is("Invalid arguments supplied in the user request"))
+            .body("details", is("Illegal value supplied for query parameter 'messagesPerSecond', expecting a strictly positive optional integer"));
+    }
+
+    @Test
+    void postShouldFailWhenMessagesPerSecondIsNegative() {
+        given()
+            .queryParam("action", "populateEmailQueryView")
+            .queryParam("messagesPerSecond", "-1")
+            .post()
+        .then()
+            .statusCode(HttpStatus.BAD_REQUEST_400)
+            .body("statusCode", is(400))
+            .body("type", is(ErrorResponder.ErrorType.INVALID_ARGUMENT.getType()))
+            .body("message", is("Invalid arguments supplied in the user request"))
+            .body("details", is("'messagesPerSecond' must be strictly positive"));
+    }
+
+    @Test
+    void postShouldFailWhenMessagesPerSecondIsZero() {
+        given()
+            .queryParam("action", "populateEmailQueryView")
+            .queryParam("messagesPerSecond", "0")
+            .post()
+        .then()
+            .statusCode(HttpStatus.BAD_REQUEST_400)
+            .body("statusCode", is(400))
+            .body("type", is(ErrorResponder.ErrorType.INVALID_ARGUMENT.getType()))
+            .body("message", is("Invalid arguments supplied in the user request"))
+            .body("details", is("'messagesPerSecond' must be strictly positive"));
+    }
+
+    @Test
+    void postShouldFailUponInvalidAction() {
+        given()
+            .queryParam("action", "invalid")
+            .post()
+        .then()
+            .statusCode(HttpStatus.BAD_REQUEST_400)
+            .body("statusCode", is(400))
+            .body("type", is(ErrorResponder.ErrorType.INVALID_ARGUMENT.getType()))
+            .body("message", is("Invalid arguments supplied in the user request"))
+            .body("details", is("Invalid value supplied for query parameter 'action': invalid. Supported values are [populateEmailQueryView]"));
+    }
+
+    @Test
+    void postShouldCreateANewTask() {
+        given()
+            .queryParam("action", "populateEmailQueryView")
+            .post()
+        .then()
+            .statusCode(HttpStatus.CREATED_201)
+            .body("taskId", notNullValue());
+    }
+
+    @Test
+    void postShouldCreateANewTaskWhenConcurrencyParametersSpecified() {
+        given()
+            .queryParam("messagesPerSecond", "1")
+            .queryParam("action", "populateEmailQueryView")
+            .post()
+        .then()
+            .statusCode(HttpStatus.CREATED_201)
+            .body("taskId", notNullValue());
+    }
+
+    @Test
+    void runningOptionsShouldBePartOfTaskDetails() {
+        String taskId = with()
+            .queryParam("action", "populateEmailQueryView")
+            .queryParam("messagesPerSecond", "20")
+            .post()
+            .jsonPath()
+            .get("taskId");
+
+        given()
+            .basePath(TasksRoutes.BASE)
+        .when()
+            .get(taskId + "/await")
+        .then()
+            .body("taskId", is(taskId))
+            .body("type", is("PopulateEmailQueryViewTask"))
+            .body("additionalInformation.runningOptions.messagesPerSecond", is(20));
+    }
+
+    @Test
+    void populateShouldUpdateProjection() throws Exception {
+        ComposedMessageId messageId = mailboxManager.getMailbox(bobInboxboxId, bobSession).appendMessage(
+            MessageManager.AppendCommand.builder().build("header: value\r\n\r\nbody"),
+            bobSession).getId();
+
+        String taskId = with()
+            .queryParam("action", "populateEmailQueryView")
+            .post()
+            .jsonPath()
+            .get("taskId");
+
+        with()
+            .basePath(TasksRoutes.BASE)
+            .get(taskId + "/await");
+
+        assertThat(view.listMailboxContent(messageId.getMailboxId(), Limit.from(12)).collectList().block())
+            .containsOnly(messageId.getMessageId());
+    }
+
+    @Test
+    void populateShouldBeIdempotent() throws Exception {
+        ComposedMessageId messageId = mailboxManager.getMailbox(bobInboxboxId, bobSession).appendMessage(
+            MessageManager.AppendCommand.builder().build("header: value\r\n\r\nbody"),
+            bobSession).getId();
+
+        String taskId1 = with()
+            .queryParam("action", "populateEmailQueryView")
+            .post()
+            .jsonPath()
+            .get("taskId");
+        with()
+            .basePath(TasksRoutes.BASE)
+            .get(taskId1 + "/await");
+
+        String taskId2 = with()
+            .queryParam("action", "populateEmailQueryView")
+            .post()
+            .jsonPath()
+            .get("taskId");
+        with()
+            .basePath(TasksRoutes.BASE)
+            .get(taskId2 + "/await");
+
+        assertThat(view.listMailboxContent(messageId.getMailboxId(), Limit.from(12)).collectList().block())
+            .containsOnly(messageId.getMessageId());
+    }
+}
\ No newline at end of file


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org


[james-project] 08/18: JAMES-3440 Cassandra implementation for EmailQueryView

Posted by bt...@apache.org.
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 96d47a52c65b16272408cd4cfcd8af50243c8c23
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Tue Nov 17 17:40:13 2020 +0700

    JAMES-3440 Cassandra implementation for EmailQueryView
---
 .../main/java/org/apache/james/modules/data/CassandraJmapModule.java    | 2 ++
 .../james/jmap/cassandra/projections/CassandraEmailQueryView.java       | 1 -
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/server/container/guice/cassandra-guice/src/main/java/org/apache/james/modules/data/CassandraJmapModule.java b/server/container/guice/cassandra-guice/src/main/java/org/apache/james/modules/data/CassandraJmapModule.java
index e970220..859e8c2 100644
--- a/server/container/guice/cassandra-guice/src/main/java/org/apache/james/modules/data/CassandraJmapModule.java
+++ b/server/container/guice/cassandra-guice/src/main/java/org/apache/james/modules/data/CassandraJmapModule.java
@@ -36,6 +36,7 @@ import org.apache.james.jmap.cassandra.access.CassandraAccessModule;
 import org.apache.james.jmap.cassandra.access.CassandraAccessTokenRepository;
 import org.apache.james.jmap.cassandra.filtering.FilteringRuleSetDefineDTOModules;
 import org.apache.james.jmap.cassandra.projections.CassandraEmailQueryView;
+import org.apache.james.jmap.cassandra.projections.CassandraEmailQueryViewModule;
 import org.apache.james.jmap.cassandra.projections.CassandraMessageFastViewProjection;
 import org.apache.james.jmap.cassandra.projections.CassandraMessageFastViewProjectionModule;
 import org.apache.james.jmap.cassandra.vacation.CassandraNotificationRegistry;
@@ -79,6 +80,7 @@ public class CassandraJmapModule extends AbstractModule {
         cassandraDataDefinitions.addBinding().toInstance(CassandraVacationModule.MODULE);
         cassandraDataDefinitions.addBinding().toInstance(CassandraNotificationRegistryModule.MODULE);
         cassandraDataDefinitions.addBinding().toInstance(CassandraMessageFastViewProjectionModule.MODULE);
+        cassandraDataDefinitions.addBinding().toInstance(CassandraEmailQueryViewModule.MODULE);
 
         Multibinder<EventDTOModule<? extends Event, ? extends EventDTO>> eventDTOModuleBinder = Multibinder.newSetBinder(binder(), new TypeLiteral<EventDTOModule<? extends Event, ? extends EventDTO>>() {});
         eventDTOModuleBinder.addBinding().toInstance(FilteringRuleSetDefineDTOModules.FILTERING_RULE_SET_DEFINED);
diff --git a/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/projections/CassandraEmailQueryView.java b/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/projections/CassandraEmailQueryView.java
index 610fba5..4eb2c5a 100644
--- a/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/projections/CassandraEmailQueryView.java
+++ b/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/projections/CassandraEmailQueryView.java
@@ -60,7 +60,6 @@ import reactor.core.publisher.Mono;
 
 public class CassandraEmailQueryView implements EmailQueryView {
     private static final String LIMIT_MARKER = "LIMIT_BIND_MARKER";
-    private static final int CONCURRENCY = 10;
 
     private final CassandraMessageId.Factory messageIdFactory;
     private final CassandraAsyncExecutor executor;


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org


[james-project] 11/18: JAMES-3440 Tasks wrapper to populate EmailQueryView

Posted by bt...@apache.org.
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 f1cf25d3d58bff429002890d1f4d980309665193
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Tue Nov 17 17:06:27 2020 +0700

    JAMES-3440 Tasks wrapper to populate EmailQueryView
---
 .../data/jmap/PopulateEmailQueryViewTask.java      | 160 +++++++++++++++++++++
 ...EmailQueryViewTaskAdditionalInformationDTO.java | 119 +++++++++++++++
 ...ctionItemsTaskAdditionalInformationDTOTest.java |  45 ++++++
 ...opulateEmailQueryViewTaskSerializationTest.java |  45 ++++++
 .../json/populateAll.additionalInformation.json    |  11 ++
 .../src/test/resources/json/populateAll.task.json  |   6 +
 6 files changed, 386 insertions(+)

diff --git a/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/PopulateEmailQueryViewTask.java b/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/PopulateEmailQueryViewTask.java
new file mode 100644
index 0000000..cd25741
--- /dev/null
+++ b/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/PopulateEmailQueryViewTask.java
@@ -0,0 +1,160 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.webadmin.data.jmap;
+
+import java.time.Clock;
+import java.time.Instant;
+import java.util.Optional;
+
+import org.apache.james.json.DTOModule;
+import org.apache.james.server.task.json.dto.TaskDTO;
+import org.apache.james.server.task.json.dto.TaskDTOModule;
+import org.apache.james.task.Task;
+import org.apache.james.task.TaskExecutionDetails;
+import org.apache.james.task.TaskType;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import reactor.core.scheduler.Schedulers;
+
+public class PopulateEmailQueryViewTask implements Task {
+    static final TaskType TASK_TYPE = TaskType.of("PopulateEmailQueryViewTask");
+
+    public static class AdditionalInformation implements TaskExecutionDetails.AdditionalInformation {
+        private static AdditionalInformation from(EmailQueryViewPopulator.Progress progress,
+                                                  RunningOptions runningOptions) {
+            return new AdditionalInformation(runningOptions,
+                progress.getProcessedUserCount(),
+                progress.getProcessedMessageCount(),
+                progress.getFailedUserCount(),
+                progress.getFailedMessageCount(),
+                Clock.systemUTC().instant());
+        }
+
+        private final RunningOptions runningOptions;
+        private final long processedUserCount;
+        private final long processedMessageCount;
+        private final long failedUserCount;
+        private final long failedMessageCount;
+        private final Instant timestamp;
+
+        public AdditionalInformation(RunningOptions runningOptions, long processedUserCount, long processedMessageCount, long failedUserCount, long failedMessageCount, Instant timestamp) {
+            this.runningOptions = runningOptions;
+            this.processedUserCount = processedUserCount;
+            this.processedMessageCount = processedMessageCount;
+            this.failedUserCount = failedUserCount;
+            this.failedMessageCount = failedMessageCount;
+            this.timestamp = timestamp;
+        }
+
+        public long getProcessedUserCount() {
+            return processedUserCount;
+        }
+
+        public long getProcessedMessageCount() {
+            return processedMessageCount;
+        }
+
+        public long getFailedUserCount() {
+            return failedUserCount;
+        }
+
+        public long getFailedMessageCount() {
+            return failedMessageCount;
+        }
+
+        public RunningOptions getRunningOptions() {
+            return runningOptions;
+        }
+
+        @Override
+        public Instant timestamp() {
+            return timestamp;
+        }
+    }
+
+    public static class PopulateEmailQueryViewTaskDTO implements TaskDTO {
+        private final String type;
+        private final Optional<RunningOptionsDTO> runningOptions;
+
+        public PopulateEmailQueryViewTaskDTO(@JsonProperty("type") String type,
+                                             @JsonProperty("runningOptions") Optional<RunningOptionsDTO> runningOptions) {
+            this.type = type;
+            this.runningOptions = runningOptions;
+        }
+
+        @Override
+        public String getType() {
+            return type;
+        }
+
+        public Optional<RunningOptionsDTO> getRunningOptions() {
+            return runningOptions;
+        }
+    }
+
+    public static TaskDTOModule<PopulateEmailQueryViewTask, PopulateEmailQueryViewTaskDTO> module(EmailQueryViewPopulator populator) {
+        return DTOModule
+            .forDomainObject(PopulateEmailQueryViewTask.class)
+            .convertToDTO(PopulateEmailQueryViewTaskDTO.class)
+            .toDomainObjectConverter(dto -> asTask(populator, dto))
+            .toDTOConverter(PopulateEmailQueryViewTask::asDTO)
+            .typeName(TASK_TYPE.asString())
+            .withFactory(TaskDTOModule::new);
+    }
+
+    private static PopulateEmailQueryViewTaskDTO asDTO(PopulateEmailQueryViewTask task, String type) {
+        return new PopulateEmailQueryViewTaskDTO(type, Optional.of(RunningOptionsDTO.asDTO(task.runningOptions)));
+    }
+
+    private static PopulateEmailQueryViewTask asTask(EmailQueryViewPopulator populator, PopulateEmailQueryViewTaskDTO dto) {
+        return new PopulateEmailQueryViewTask(populator,
+            dto.getRunningOptions()
+                .map(RunningOptionsDTO::asDomainObject)
+                .orElse(RunningOptions.DEFAULT));
+    }
+
+    private final EmailQueryViewPopulator populator;
+    private final EmailQueryViewPopulator.Progress progress;
+    private final RunningOptions runningOptions;
+
+    PopulateEmailQueryViewTask(EmailQueryViewPopulator populator, RunningOptions runningOptions) {
+        this.populator = populator;
+        this.runningOptions = runningOptions;
+        this.progress = new EmailQueryViewPopulator.Progress();
+    }
+
+    @Override
+    public Result run() {
+        return populator.populateView(progress, runningOptions)
+            .subscribeOn(Schedulers.elastic())
+            .block();
+    }
+
+    @Override
+    public TaskType type() {
+        return TASK_TYPE;
+    }
+
+    @Override
+    public Optional<TaskExecutionDetails.AdditionalInformation> details() {
+        return Optional.of(AdditionalInformation.from(progress, runningOptions));
+    }
+}
\ No newline at end of file
diff --git a/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/PopulateEmailQueryViewTaskAdditionalInformationDTO.java b/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/PopulateEmailQueryViewTaskAdditionalInformationDTO.java
new file mode 100644
index 0000000..6e2c484
--- /dev/null
+++ b/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/PopulateEmailQueryViewTaskAdditionalInformationDTO.java
@@ -0,0 +1,119 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.webadmin.data.jmap;
+
+import java.time.Instant;
+import java.util.Optional;
+
+import org.apache.james.json.DTOModule;
+import org.apache.james.server.task.json.dto.AdditionalInformationDTO;
+import org.apache.james.server.task.json.dto.AdditionalInformationDTOModule;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.annotations.VisibleForTesting;
+
+public class PopulateEmailQueryViewTaskAdditionalInformationDTO implements AdditionalInformationDTO {
+    public static AdditionalInformationDTOModule<PopulateEmailQueryViewTask.AdditionalInformation, PopulateEmailQueryViewTaskAdditionalInformationDTO> module() {
+        return DTOModule.forDomainObject(PopulateEmailQueryViewTask.AdditionalInformation.class)
+            .convertToDTO(PopulateEmailQueryViewTaskAdditionalInformationDTO.class)
+            .toDomainObjectConverter(PopulateEmailQueryViewTaskAdditionalInformationDTO::toDomainObject)
+            .toDTOConverter(PopulateEmailQueryViewTaskAdditionalInformationDTO::toDTO)
+            .typeName(PopulateEmailQueryViewTask.TASK_TYPE.asString())
+            .withFactory(AdditionalInformationDTOModule::new);
+    }
+
+    private static PopulateEmailQueryViewTask.AdditionalInformation toDomainObject(PopulateEmailQueryViewTaskAdditionalInformationDTO dto) {
+        return new PopulateEmailQueryViewTask.AdditionalInformation(
+            dto.getRunningOptions()
+                .map(RunningOptionsDTO::asDomainObject)
+                .orElse(RunningOptions.DEFAULT),
+            dto.getProcessedUserCount(),
+            dto.getProcessedMessageCount(),
+            dto.getFailedUserCount(),
+            dto.getFailedMessageCount(),
+            dto.timestamp);
+    }
+
+    private static PopulateEmailQueryViewTaskAdditionalInformationDTO toDTO(PopulateEmailQueryViewTask.AdditionalInformation details, String type) {
+        return new PopulateEmailQueryViewTaskAdditionalInformationDTO(
+            type,
+            details.timestamp(),
+            details.getProcessedUserCount(),
+            details.getProcessedMessageCount(),
+            details.getFailedUserCount(),
+            details.getFailedMessageCount(),
+            Optional.of(RunningOptionsDTO.asDTO(details.getRunningOptions())));
+    }
+
+    private final String type;
+    private final Instant timestamp;
+    private final long processedUserCount;
+    private final long processedMessageCount;
+    private final long failedUserCount;
+    private final long failedMessageCount;
+    private final Optional<RunningOptionsDTO> runningOptions;
+
+    @VisibleForTesting
+    PopulateEmailQueryViewTaskAdditionalInformationDTO(@JsonProperty("type") String type,
+                                                       @JsonProperty("timestamp") Instant timestamp,
+                                                       @JsonProperty("processedUserCount") long processedUserCount,
+                                                       @JsonProperty("processedMessageCount") long processedMessageCount,
+                                                       @JsonProperty("failedUserCount") long failedUserCount,
+                                                       @JsonProperty("failedMessageCount") long failedMessageCount,
+                                                       @JsonProperty("runningOptions") Optional<RunningOptionsDTO> runningOptions) {
+        this.type = type;
+        this.timestamp = timestamp;
+        this.processedUserCount = processedUserCount;
+        this.processedMessageCount = processedMessageCount;
+        this.failedUserCount = failedUserCount;
+        this.failedMessageCount = failedMessageCount;
+        this.runningOptions = runningOptions;
+    }
+
+    @Override
+    public String getType() {
+        return type;
+    }
+
+    @Override
+    public Instant getTimestamp() {
+        return timestamp;
+    }
+
+    public long getProcessedUserCount() {
+        return processedUserCount;
+    }
+
+    public long getProcessedMessageCount() {
+        return processedMessageCount;
+    }
+
+    public long getFailedUserCount() {
+        return failedUserCount;
+    }
+
+    public long getFailedMessageCount() {
+        return failedMessageCount;
+    }
+
+    public Optional<RunningOptionsDTO> getRunningOptions() {
+        return runningOptions;
+    }
+}
diff --git a/server/protocols/webadmin/webadmin-jmap/src/test/java/org/apache/james/webadmin/data/jmap/PopulateEmailQueryViewProjectionItemsTaskAdditionalInformationDTOTest.java b/server/protocols/webadmin/webadmin-jmap/src/test/java/org/apache/james/webadmin/data/jmap/PopulateEmailQueryViewProjectionItemsTaskAdditionalInformationDTOTest.java
new file mode 100644
index 0000000..09de56b
--- /dev/null
+++ b/server/protocols/webadmin/webadmin-jmap/src/test/java/org/apache/james/webadmin/data/jmap/PopulateEmailQueryViewProjectionItemsTaskAdditionalInformationDTOTest.java
@@ -0,0 +1,45 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.webadmin.data.jmap;
+
+import java.time.Instant;
+
+import org.apache.james.JsonSerializationVerifier;
+import org.apache.james.util.ClassLoaderUtils;
+import org.junit.jupiter.api.Test;
+
+class PopulateEmailQueryViewProjectionItemsTaskAdditionalInformationDTOTest {
+    private static final Instant INSTANT = Instant.parse("2007-12-03T10:15:30.00Z");
+    private static final PopulateEmailQueryViewTask.AdditionalInformation DOMAIN_OBJECT = new PopulateEmailQueryViewTask.AdditionalInformation(
+        RunningOptions.withMessageRatePerSecond(20),
+        1,
+        2,
+        3,
+        4,
+        INSTANT);
+
+    @Test
+    void shouldMatchJsonSerializationContract() throws Exception {
+        JsonSerializationVerifier.dtoModule(PopulateEmailQueryViewTaskAdditionalInformationDTO.module())
+            .bean(DOMAIN_OBJECT)
+            .json(ClassLoaderUtils.getSystemResourceAsString("json/populateAll.additionalInformation.json"))
+            .verify();
+    }
+}
\ No newline at end of file
diff --git a/server/protocols/webadmin/webadmin-jmap/src/test/java/org/apache/james/webadmin/data/jmap/PopulateEmailQueryViewTaskSerializationTest.java b/server/protocols/webadmin/webadmin-jmap/src/test/java/org/apache/james/webadmin/data/jmap/PopulateEmailQueryViewTaskSerializationTest.java
new file mode 100644
index 0000000..97b5ddc
--- /dev/null
+++ b/server/protocols/webadmin/webadmin-jmap/src/test/java/org/apache/james/webadmin/data/jmap/PopulateEmailQueryViewTaskSerializationTest.java
@@ -0,0 +1,45 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.webadmin.data.jmap;
+
+import static org.mockito.Mockito.mock;
+
+import org.apache.james.JsonSerializationVerifier;
+import org.apache.james.util.ClassLoaderUtils;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class PopulateEmailQueryViewTaskSerializationTest {
+    EmailQueryViewPopulator populator;
+
+    @BeforeEach
+    void setUp() {
+        populator = mock(EmailQueryViewPopulator.class);
+    }
+
+    @Test
+    void shouldMatchJsonSerializationContract() throws Exception {
+        JsonSerializationVerifier.dtoModule(PopulateEmailQueryViewTask.module(populator))
+            .bean(new PopulateEmailQueryViewTask(populator,
+                RunningOptions.withMessageRatePerSecond(2)))
+            .json(ClassLoaderUtils.getSystemResourceAsString("json/populateAll.task.json"))
+            .verify();
+    }
+}
\ No newline at end of file
diff --git a/server/protocols/webadmin/webadmin-jmap/src/test/resources/json/populateAll.additionalInformation.json b/server/protocols/webadmin/webadmin-jmap/src/test/resources/json/populateAll.additionalInformation.json
new file mode 100644
index 0000000..eb99605
--- /dev/null
+++ b/server/protocols/webadmin/webadmin-jmap/src/test/resources/json/populateAll.additionalInformation.json
@@ -0,0 +1,11 @@
+{
+  "type":"PopulateEmailQueryViewTask",
+  "timestamp":"2007-12-03T10:15:30Z",
+  "processedUserCount":1,
+  "processedMessageCount":2,
+  "failedUserCount":3,
+  "failedMessageCount":4,
+  "runningOptions": {
+    "messagesPerSecond":20
+  }
+}
\ No newline at end of file
diff --git a/server/protocols/webadmin/webadmin-jmap/src/test/resources/json/populateAll.task.json b/server/protocols/webadmin/webadmin-jmap/src/test/resources/json/populateAll.task.json
new file mode 100644
index 0000000..ca9a5ee
--- /dev/null
+++ b/server/protocols/webadmin/webadmin-jmap/src/test/resources/json/populateAll.task.json
@@ -0,0 +1,6 @@
+{
+  "type":"PopulateEmailQueryViewTask",
+  "runningOptions": {
+    "messagesPerSecond":2
+  }
+}
\ No newline at end of file


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org


[james-project] 02/18: JAMES-3440 EmailQuery Limit & Position validation should not rely on Mono

Posted by bt...@apache.org.
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 8e6d0efbb67804b8ea662aa1a5c21bdd1fb3c236
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Mon Nov 16 13:57:36 2020 +0700

    JAMES-3440 EmailQuery Limit & Position validation should not rely on Mono
    
    Synchronous checks needs to be represented via an Either
---
 .../scala/org/apache/james/jmap/core/Query.scala   | 21 ++++++++++----------
 .../james/jmap/method/EmailQueryMethod.scala       | 23 ++++++++++++----------
 2 files changed, 23 insertions(+), 21 deletions(-)

diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/Query.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/Query.scala
index 3e746d8..b5d0473 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/Query.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/Query.scala
@@ -25,20 +25,19 @@ import eu.timepit.refined.auto._
 import eu.timepit.refined.numeric.{NonNegative, Positive}
 import eu.timepit.refined.refineV
 import org.apache.james.mailbox.model.{MailboxId, MessageId}
-import reactor.core.scala.publisher.SMono
 
 case class PositionUnparsed(value: Int) extends AnyVal
 object Position {
   type Position = Int Refined NonNegative
   val zero: Position = 0
 
-  def validateRequestPosition(requestPosition: Option[PositionUnparsed]): SMono[Position] = {
+  def validateRequestPosition(requestPosition: Option[PositionUnparsed]): Either[IllegalArgumentException, Position] = {
     val refinedPosition : Option[Either[String, Position]] =  requestPosition.map(position => refineV[NonNegative](position.value))
 
     refinedPosition match {
-      case Some(Left(_))  =>  SMono.raiseError(new IllegalArgumentException(s"Negative position are not supported yet. ${requestPosition.map(_.value).getOrElse("")} was provided."))
-      case Some(Right(position)) => SMono.just(position)
-      case None => SMono.just(Position.zero)
+      case Some(Left(_))  =>  Left(new IllegalArgumentException(s"Negative position are not supported yet. ${requestPosition.map(_.value).getOrElse("")} was provided."))
+      case Some(Right(position)) => Right(position)
+      case None => Right(Position.zero)
     }
   }
 }
@@ -46,16 +45,16 @@ object Position {
 case class LimitUnparsed(value: Long) extends AnyVal
 
 object Limit {
-  type Limit = Long Refined Positive
-  val default: Limit = 256L
+  type Limit = Int Refined Positive
+  val default: Limit = 256
 
-  def validateRequestLimit(requestLimit: Option[LimitUnparsed]): SMono[Limit] = {
+  def validateRequestLimit(requestLimit: Option[LimitUnparsed]): Either[IllegalArgumentException, Limit] = {
     val refinedLimit : Option[Either[String, Limit]] =  requestLimit.map(limit => refineV[Positive](limit.value))
 
     refinedLimit match {
-      case Some(Left(_))  =>  SMono.raiseError(new IllegalArgumentException(s"The limit can not be negative. ${requestLimit.map(_.value).getOrElse("")} was provided."))
-      case Some(Right(limit)) if limit.value < default.value => SMono.just(limit)
-      case _ => SMono.just(default)
+      case Some(Left(_))  =>  Left(new IllegalArgumentException(s"The limit can not be negative. ${requestLimit.map(_.value).getOrElse("")} was provided."))
+      case Some(Right(limit)) if limit.value < default.value => Right(limit)
+      case _ => Right(default)
     }
   }
 }
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 895d890..b2bfb02 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
@@ -54,16 +54,19 @@ class EmailQueryMethod @Inject() (serializer: EmailQuerySerializer,
   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 {
-        positionToUse <- Position.validateRequestPosition(request.position)
-        limitToUse <- Limit.validateRequestLimit(request.limit)
-        response <- executeQuery(mailboxSession, request, searchQuery, positionToUse, limitToUse)
-      } yield Invocation(methodName = methodName, arguments = Arguments(serializer.serialize(response)), methodCallId = invocation.methodCallId)
-    }
-  }
+                             capabilities: Set[CapabilityIdentifier]): SMono[Invocation] =
+    searchQueryFromRequest(request, capabilities, mailboxSession)
+      .flatMap(searchQuery => Limit.validateRequestLimit(request.limit).map((searchQuery, _)))
+      .flatMap {
+        case (searchQuery, limit) => Position.validateRequestPosition(request.position)
+          .map((searchQuery, limit, _))
+      }.map {
+      case (searchQuery, limitToUse, positionToUse) => executeQuery(mailboxSession, request, searchQuery, positionToUse, limitToUse)
+        .map(response => Invocation(
+          methodName = methodName,
+          arguments = Arguments(serializer.serialize(response)),
+          methodCallId = invocation.methodCallId))
+    }.fold(SMono.raiseError, res => res)
 
   override def getRequest(mailboxSession: MailboxSession, invocation: Invocation): SMono[EmailQueryRequest] = asEmailQueryRequest(invocation.arguments)
 


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org


[james-project] 13/18: JAMES-3440 Guice registrations for tasks and routes to populate EmailQueryView

Posted by bt...@apache.org.
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 f54d2cac5633d0634df5cdaed37999516a90d715
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Tue Nov 17 17:11:22 2020 +0700

    JAMES-3440 Guice registrations for tasks and routes to populate EmailQueryView
---
 .../modules/server/JmapTaskSerializationModule.java   | 19 +++++++++++++++++++
 .../apache/james/modules/server/JmapTasksModule.java  |  4 ++++
 2 files changed, 23 insertions(+)

diff --git a/server/container/guice/protocols/webadmin-jmap/src/main/java/org/apache/james/modules/server/JmapTaskSerializationModule.java b/server/container/guice/protocols/webadmin-jmap/src/main/java/org/apache/james/modules/server/JmapTaskSerializationModule.java
index 4c09c55..4684b32 100644
--- a/server/container/guice/protocols/webadmin-jmap/src/main/java/org/apache/james/modules/server/JmapTaskSerializationModule.java
+++ b/server/container/guice/protocols/webadmin-jmap/src/main/java/org/apache/james/modules/server/JmapTaskSerializationModule.java
@@ -24,7 +24,10 @@ import org.apache.james.server.task.json.dto.TaskDTO;
 import org.apache.james.server.task.json.dto.TaskDTOModule;
 import org.apache.james.task.Task;
 import org.apache.james.task.TaskExecutionDetails;
+import org.apache.james.webadmin.data.jmap.EmailQueryViewPopulator;
 import org.apache.james.webadmin.data.jmap.MessageFastViewProjectionCorrector;
+import org.apache.james.webadmin.data.jmap.PopulateEmailQueryViewTask;
+import org.apache.james.webadmin.data.jmap.PopulateEmailQueryViewTaskAdditionalInformationDTO;
 import org.apache.james.webadmin.data.jmap.RecomputeAllFastViewProjectionItemsTask;
 import org.apache.james.webadmin.data.jmap.RecomputeAllFastViewTaskAdditionalInformationDTO;
 import org.apache.james.webadmin.data.jmap.RecomputeUserFastViewProjectionItemsTask;
@@ -42,6 +45,11 @@ public class JmapTaskSerializationModule extends AbstractModule {
     }
 
     @ProvidesIntoSet
+    public TaskDTOModule<? extends Task, ? extends TaskDTO> populateEmailQueryViewTask(EmailQueryViewPopulator populator) {
+        return PopulateEmailQueryViewTask.module(populator);
+    }
+
+    @ProvidesIntoSet
     public TaskDTOModule<? extends Task, ? extends TaskDTO> recomputeUserJmapPreviewsTask(MessageFastViewProjectionCorrector corrector) {
         return RecomputeUserFastViewProjectionItemsTask.module(corrector);
     }
@@ -58,6 +66,17 @@ public class JmapTaskSerializationModule extends AbstractModule {
     }
 
     @ProvidesIntoSet
+    public AdditionalInformationDTOModule<? extends TaskExecutionDetails.AdditionalInformation, ? extends  AdditionalInformationDTO> populateEmailQueryViewAdditionalInformation() {
+        return PopulateEmailQueryViewTaskAdditionalInformationDTO.module();
+    }
+
+    @Named(DTOModuleInjections.WEBADMIN_DTO)
+    @ProvidesIntoSet
+    public AdditionalInformationDTOModule<? extends TaskExecutionDetails.AdditionalInformation, ? extends  AdditionalInformationDTO> webAdminPopulateEmailQueryViewAdditionalInformation() {
+        return PopulateEmailQueryViewTaskAdditionalInformationDTO.module();
+    }
+
+    @ProvidesIntoSet
     public AdditionalInformationDTOModule<? extends TaskExecutionDetails.AdditionalInformation, ? extends  AdditionalInformationDTO> recomputeUserJmapPreviewsAdditionalInformation() {
         return RecomputeUserFastViewTaskAdditionalInformationDTO.module();
     }
diff --git a/server/container/guice/protocols/webadmin-jmap/src/main/java/org/apache/james/modules/server/JmapTasksModule.java b/server/container/guice/protocols/webadmin-jmap/src/main/java/org/apache/james/modules/server/JmapTasksModule.java
index 73b1030..aba434b 100644
--- a/server/container/guice/protocols/webadmin-jmap/src/main/java/org/apache/james/modules/server/JmapTasksModule.java
+++ b/server/container/guice/protocols/webadmin-jmap/src/main/java/org/apache/james/modules/server/JmapTasksModule.java
@@ -19,6 +19,7 @@
 
 package org.apache.james.modules.server;
 
+import org.apache.james.webadmin.data.jmap.PopulateEmailQueryViewRequestToTask;
 import org.apache.james.webadmin.data.jmap.RecomputeAllFastViewProjectionItemsRequestToTask;
 import org.apache.james.webadmin.data.jmap.RecomputeUserFastViewProjectionItemsRequestToTask;
 import org.apache.james.webadmin.routes.MailboxesRoutes;
@@ -37,6 +38,9 @@ public class JmapTasksModule extends AbstractModule {
         Multibinder.newSetBinder(binder(), TaskFromRequestRegistry.TaskRegistration.class, Names.named(MailboxesRoutes.ALL_MAILBOXES_TASKS))
             .addBinding().to(RecomputeAllFastViewProjectionItemsRequestToTask.class);
 
+        Multibinder.newSetBinder(binder(), TaskFromRequestRegistry.TaskRegistration.class, Names.named(MailboxesRoutes.ALL_MAILBOXES_TASKS))
+            .addBinding().to(PopulateEmailQueryViewRequestToTask.class);
+
         Multibinder.newSetBinder(binder(), TaskFromRequestRegistry.TaskRegistration.class, Names.named(UserMailboxesRoutes.USER_MAILBOXES_OPERATIONS_INJECTION_KEY))
             .addBinding().to(RecomputeUserFastViewProjectionItemsRequestToTask.class);
 


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org


[james-project] 10/18: JAMES-3440 Utility to populate EmailQueryView

Posted by bt...@apache.org.
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 98f7c13d5342ab3050f9a4ed88aba90a81b6521f
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Tue Nov 17 17:03:37 2020 +0700

    JAMES-3440 Utility to populate EmailQueryView
---
 .../data/jmap/EmailQueryViewPopulator.java         | 209 +++++++++++++++++++++
 1 file changed, 209 insertions(+)

diff --git a/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/EmailQueryViewPopulator.java b/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/EmailQueryViewPopulator.java
new file mode 100644
index 0000000..2141fbd
--- /dev/null
+++ b/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/EmailQueryViewPopulator.java
@@ -0,0 +1,209 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.webadmin.data.jmap;
+
+import static org.apache.james.mailbox.MailboxManager.MailboxSearchFetchType.Minimal;
+
+import java.io.IOException;
+import java.time.Duration;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+import java.util.Date;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicLong;
+
+import javax.inject.Inject;
+
+import org.apache.james.jmap.api.projections.EmailQueryView;
+import org.apache.james.mailbox.MailboxManager;
+import org.apache.james.mailbox.MailboxSession;
+import org.apache.james.mailbox.MessageManager;
+import org.apache.james.mailbox.exception.MailboxException;
+import org.apache.james.mailbox.model.FetchGroup;
+import org.apache.james.mailbox.model.MailboxId;
+import org.apache.james.mailbox.model.MailboxMetaData;
+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.search.MailboxQuery;
+import org.apache.james.mime4j.dom.Message;
+import org.apache.james.mime4j.stream.MimeConfig;
+import org.apache.james.task.Task;
+import org.apache.james.task.Task.Result;
+import org.apache.james.user.api.UsersRepository;
+import org.apache.james.user.api.UsersRepositoryException;
+import org.apache.james.util.ReactorUtils;
+import org.apache.james.util.streams.Iterators;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.github.fge.lambdas.Throwing;
+
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+public class EmailQueryViewPopulator {
+    private static final Logger LOGGER = LoggerFactory.getLogger(EmailQueryViewPopulator.class);
+    private static final Duration PERIOD = Duration.ofSeconds(1);
+    public static final int USER_CONCURRENCY = 1;
+    public static final int MAILBOX_CONCURRENCY = 1;
+
+    static class Progress {
+        private final AtomicLong processedUserCount;
+        private final AtomicLong processedMessageCount;
+        private final AtomicLong failedUserCount;
+        private final AtomicLong failedMessageCount;
+
+        Progress() {
+            failedUserCount = new AtomicLong();
+            processedMessageCount = new AtomicLong();
+            processedUserCount = new AtomicLong();
+            failedMessageCount = new AtomicLong();
+        }
+
+        private void incrementProcessedUserCount() {
+            processedUserCount.incrementAndGet();
+        }
+
+        private void incrementProcessedMessageCount() {
+            processedMessageCount.incrementAndGet();
+        }
+
+        private void incrementFailedUserCount() {
+            failedUserCount.incrementAndGet();
+        }
+
+        private void incrementFailedMessageCount() {
+            failedMessageCount.incrementAndGet();
+        }
+
+        long getProcessedUserCount() {
+            return processedUserCount.get();
+        }
+
+        long getProcessedMessageCount() {
+            return processedMessageCount.get();
+        }
+
+        long getFailedUserCount() {
+            return failedUserCount.get();
+        }
+
+        long getFailedMessageCount() {
+            return failedMessageCount.get();
+        }
+    }
+
+    private final UsersRepository usersRepository;
+    private final MailboxManager mailboxManager;
+    private final EmailQueryView emailQueryView;
+
+    @Inject
+    EmailQueryViewPopulator(UsersRepository usersRepository,
+                            MailboxManager mailboxManager,
+                            EmailQueryView emailQueryView) {
+        this.usersRepository = usersRepository;
+        this.mailboxManager = mailboxManager;
+        this.emailQueryView = emailQueryView;
+    }
+
+    Mono<Result> populateView(Progress progress, RunningOptions runningOptions) {
+        return correctProjection(listAllMailboxMessages(progress), runningOptions, progress);
+    }
+
+    private Flux<MessageResult> listAllMailboxMessages(Progress progress) {
+        try {
+            return Iterators.toFlux(usersRepository.list())
+                .map(mailboxManager::createSystemSession)
+                .doOnNext(any -> progress.incrementProcessedUserCount())
+                .flatMap(session -> listUserMailboxMessages(progress, session), USER_CONCURRENCY);
+        } catch (UsersRepositoryException e) {
+            return Flux.error(e);
+        }
+    }
+
+    private Flux<MessageResult> listUserMailboxMessages(Progress progress, MailboxSession session) {
+        return listUsersMailboxes(session)
+            .flatMap(mailboxMetadata -> retrieveMailbox(session, mailboxMetadata), MAILBOX_CONCURRENCY)
+            .flatMap(Throwing.function(messageManager -> listAllMessages(messageManager, session)), MAILBOX_CONCURRENCY)
+            .onErrorResume(MailboxException.class, e -> {
+                LOGGER.error("JMAP emailQuery view re-computation aborted for {} as we failed listing user mailboxes", session.getUser(), e);
+                progress.incrementFailedUserCount();
+                return Flux.empty();
+            });
+    }
+
+    private Mono<Result> correctProjection(MessageResult messageResult, Progress progress) {
+        return Mono.fromCallable(() -> {
+            MailboxId mailboxId = messageResult.getMailboxId();
+            MessageId messageId = messageResult.getMessageId();
+            ZonedDateTime receivedAt = ZonedDateTime.ofInstant(messageResult.getInternalDate().toInstant(), ZoneOffset.UTC);
+            Message mime4JMessage = parseMessage(messageResult);
+            Date sentAtDate = Optional.ofNullable(mime4JMessage.getDate()).orElse(messageResult.getInternalDate());
+            ZonedDateTime sentAt = ZonedDateTime.ofInstant(sentAtDate.toInstant(), ZoneOffset.UTC);
+
+            return new EmailQueryView.Entry(mailboxId, messageId, sentAt, receivedAt);
+        })
+            .flatMap(entry -> emailQueryView.save(entry.getMailboxId(), entry.getSentAt(), entry.getReceivedAt(), entry.getMessageId()))
+            .thenReturn(Result.COMPLETED)
+            .doOnSuccess(any -> progress.incrementProcessedMessageCount())
+            .onErrorResume(e -> {
+                LOGGER.error("JMAP emailQuery view re-computation aborted for {} - {} - {}",
+                    messageResult.getMailboxId(),
+                    messageResult.getMessageId(),
+                    messageResult.getUid(), e);
+                progress.incrementFailedMessageCount();
+                return Mono.just(Result.PARTIAL);
+            });
+    }
+
+    private Mono<Result> correctProjection(Flux<MessageResult> entries, RunningOptions runningOptions, Progress progress) {
+        return entries.transform(ReactorUtils.<MessageResult, Result>throttle()
+                .elements(runningOptions.getMessagesPerSecond())
+                .per(PERIOD)
+                .forOperation(entry -> correctProjection(entry, progress)))
+            .reduce(Task::combine)
+            .switchIfEmpty(Mono.just(Result.COMPLETED));
+    }
+
+    private Flux<MailboxMetaData> listUsersMailboxes(MailboxSession session) {
+        return mailboxManager.search(MailboxQuery.privateMailboxesBuilder(session).build(), Minimal, session);
+    }
+
+    private Mono<MessageManager> retrieveMailbox(MailboxSession session, MailboxMetaData mailboxMetadata) {
+        return Mono.fromCallable(() -> mailboxManager.getMailbox(mailboxMetadata.getId(), session));
+    }
+
+    private Flux<MessageResult> listAllMessages(MessageManager messageManager, MailboxSession session) {
+        try {
+            return Iterators.toFlux(messageManager.getMessages(MessageRange.all(), FetchGroup.HEADERS, session));
+        } catch (MailboxException e) {
+            return Flux.error(e);
+        }
+    }
+
+    private Message parseMessage(MessageResult messageResult) throws IOException, MailboxException {
+        return Message.Builder
+            .of()
+            .use(MimeConfig.PERMISSIVE)
+            .parse(messageResult.getFullContent().getInputStream())
+            .build();
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org


[james-project] 17/18: [Refactoring] Apply some standards scala idioms

Posted by bt...@apache.org.
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 76934c9dd29955efeb2c08a3c11973be7399738c
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Wed Nov 18 18:36:11 2020 +0700

    [Refactoring] Apply some standards scala idioms
    
     - Use AnyVal to limit allocations
     - Avoid inner nested class definitions
     - Use case objects
---
 .../org/apache/james/jmap/core/Capability.scala    |  2 +-
 .../scala/org/apache/james/jmap/core/Session.scala |  4 +--
 .../scala/org/apache/james/jmap/mail/Email.scala   | 12 ++++----
 .../org/apache/james/jmap/mail/EmailBodyPart.scala | 12 ++++----
 .../apache/james/jmap/mail/EmailBodyValue.scala    |  4 +--
 .../james/jmap/method/MailboxGetMethod.scala       | 36 +++++++++++-----------
 .../jmap/method/VacationResponseSetMethod.scala    |  4 +--
 .../james/jmap/vacation/VacationResponse.scala     |  6 ++--
 8 files changed, 40 insertions(+), 40 deletions(-)

diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/Capability.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/Capability.scala
index 1cebf30..8ec9dfd 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/Capability.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/Capability.scala
@@ -94,7 +94,7 @@ case class MaxMailboxesPerEmail(value: Option[UnsignedInt])
 case class MaxMailboxDepth(value: Option[UnsignedInt])
 case class MaxSizeMailboxName(value: UnsignedInt)
 case class MaxSizeAttachmentsPerEmail(value: UnsignedInt)
-case class MayCreateTopLevelMailbox(value: Boolean)
+case class MayCreateTopLevelMailbox(value: Boolean) extends AnyVal
 
 final case class MailCapabilityProperties(maxMailboxesPerEmail: MaxMailboxesPerEmail,
                                           maxMailboxDepth: MaxMailboxDepth,
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/Session.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/Session.scala
index 00d6708..1e86baf 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/Session.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/Session.scala
@@ -32,8 +32,8 @@ import org.apache.james.jmap.core.CapabilityIdentifier.CapabilityIdentifier
 import org.apache.james.jmap.core.Id.Id
 import org.apache.james.jmap.core.State.{INSTANCE, State}
 
-case class IsPersonal(value: Boolean)
-case class IsReadOnly(value: Boolean)
+case class IsPersonal(value: Boolean) extends AnyVal
+case class IsReadOnly(value: Boolean) extends AnyVal
 
 object AccountId {
   def from(username: Username): Either[IllegalArgumentException, AccountId] = {
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Email.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Email.scala
index 23fd436..aa75e4e 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Email.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Email.scala
@@ -562,18 +562,18 @@ object EmailFastViewReader {
   val logger: Logger = LoggerFactory.getLogger(classOf[EmailFastViewReader])
 }
 
+private sealed trait FastViewResult
+
+private case class FastViewAvailable(id: MessageId, fastView: MessageFastViewPrecomputedProperties) extends FastViewResult
+
+private case class FastViewUnavailable(id: MessageId) extends FastViewResult
+
 private class EmailFastViewReader @Inject()(messageIdManager: MessageIdManager,
                                             messageFastViewProjection: MessageFastViewProjection,
                                             zoneIdProvider: ZoneIdProvider,
                                             fullViewFactory: EmailFullViewFactory) extends EmailViewReader[EmailView] {
   private val fullReader: GenericEmailViewReader[EmailFullView] = new GenericEmailViewReader[EmailFullView](messageIdManager, FULL_CONTENT, fullViewFactory)
 
-  private sealed trait FastViewResult
-
-  private case class FastViewAvailable(id: MessageId, fastView: MessageFastViewPrecomputedProperties) extends FastViewResult
-
-  private case class FastViewUnavailable(id: MessageId) extends FastViewResult
-
   override def read[T >: EmailView](ids: Seq[MessageId], request: EmailGetRequest, mailboxSession: MailboxSession): SFlux[T] = {
     SMono.fromPublisher(messageFastViewProjection.retrieve(ids.asJava))
       .map(_.asScala.toMap)
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailBodyPart.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailBodyPart.scala
index 631fcb5..c5eb21f 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailBodyPart.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailBodyPart.scala
@@ -166,16 +166,16 @@ object Name {
     }.map(Name(_))
 }
 
-case class Name(value: String)
-case class Type(value: String)
-case class Charset(value: String)
+case class Name(value: String) extends AnyVal
+case class Type(value: String) extends AnyVal
+case class Charset(value: String) extends AnyVal
 
 object Disposition {
   val ATTACHMENT = Disposition("attachment")
   val INLINE = Disposition("inline")
 }
 
-case class Disposition(value: String)
+case class Disposition(value: String) extends AnyVal
 
 object Languages {
   def of(entity: Entity): Option[Languages] =
@@ -190,9 +190,9 @@ case class Languages(value: List[Language]) {
   def asField: Field = new RawField("Content-Language", value.map(_.value).mkString(", "))
 }
 
-case class Language(value: String)
+case class Language(value: String) extends AnyVal
 
-case class Location(value: String) {
+case class Location(value: String) extends AnyVal {
   def asField: Field = new RawField("Content-Location", value)
 }
 
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailBodyValue.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailBodyValue.scala
index efff66a..f026308 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailBodyValue.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailBodyValue.scala
@@ -24,8 +24,8 @@ import java.nio.{ByteBuffer, CharBuffer}
 
 import org.apache.james.jmap.mail.EmailGetRequest.{MaxBodyValueBytes, ZERO}
 
-case class IsEncodingProblem(value: Boolean)
-case class IsTruncated(value: Boolean)
+case class IsEncodingProblem(value: Boolean) extends AnyVal
+case class IsTruncated(value: Boolean) extends AnyVal
 
 case class EmailBodyValue(value: String,
                           isEncodingProblem: IsEncodingProblem,
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxGetMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxGetMethod.scala
index a7adf77..cd036cb 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxGetMethod.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxGetMethod.scala
@@ -42,6 +42,24 @@ import reactor.core.scheduler.Schedulers
 import scala.jdk.CollectionConverters._
 import scala.util.Try
 
+object MailboxGetResults {
+  def merge(result1: MailboxGetResults, result2: MailboxGetResults): MailboxGetResults = result1.merge(result2)
+  def empty(): MailboxGetResults = MailboxGetResults(Set.empty, NotFound(Set.empty))
+  def found(mailbox: Mailbox): MailboxGetResults = MailboxGetResults(Set(mailbox), NotFound(Set.empty))
+  def notFound(mailboxId: UnparsedMailboxId): MailboxGetResults = MailboxGetResults(Set.empty, NotFound(Set(mailboxId)))
+  def notFound(mailboxId: MailboxId): MailboxGetResults = MailboxGetResults(Set.empty, NotFound(Set(MailboxGet.asUnparsed(mailboxId))))
+}
+
+case class MailboxGetResults(mailboxes: Set[Mailbox], notFound: NotFound) {
+  def merge(other: MailboxGetResults): MailboxGetResults = MailboxGetResults(this.mailboxes ++ other.mailboxes, this.notFound.merge(other.notFound))
+
+  def asResponse(accountId: AccountId): MailboxGetResponse = MailboxGetResponse(
+    accountId = accountId,
+    state = INSTANCE,
+    list = mailboxes.toList.sortBy(_.sortOrder),
+    notFound = notFound)
+}
+
 class MailboxGetMethod @Inject() (serializer: MailboxSerializer,
                                   mailboxManager: MailboxManager,
                                   subscriptionManager: SubscriptionManager,
@@ -53,24 +71,6 @@ class MailboxGetMethod @Inject() (serializer: MailboxSerializer,
   override val methodName: MethodName = MethodName("Mailbox/get")
   override val requiredCapabilities: Set[CapabilityIdentifier] = Set(JMAP_CORE, JMAP_MAIL)
 
-  object MailboxGetResults {
-    def merge(result1: MailboxGetResults, result2: MailboxGetResults): MailboxGetResults = result1.merge(result2)
-    def empty(): MailboxGetResults = MailboxGetResults(Set.empty, NotFound(Set.empty))
-    def found(mailbox: Mailbox): MailboxGetResults = MailboxGetResults(Set(mailbox), NotFound(Set.empty))
-    def notFound(mailboxId: UnparsedMailboxId): MailboxGetResults = MailboxGetResults(Set.empty, NotFound(Set(mailboxId)))
-    def notFound(mailboxId: MailboxId): MailboxGetResults = MailboxGetResults(Set.empty, NotFound(Set(MailboxGet.asUnparsed(mailboxId))))
-  }
-
-  case class MailboxGetResults(mailboxes: Set[Mailbox], notFound: NotFound) {
-    def merge(other: MailboxGetResults): MailboxGetResults = MailboxGetResults(this.mailboxes ++ other.mailboxes, this.notFound.merge(other.notFound))
-
-    def asResponse(accountId: AccountId): MailboxGetResponse = MailboxGetResponse(
-      accountId = accountId,
-      state = INSTANCE,
-      list = mailboxes.toList.sortBy(_.sortOrder),
-      notFound = notFound)
-  }
-
   override def doProcess(capabilities: Set[CapabilityIdentifier], invocation: InvocationWithContext, mailboxSession: MailboxSession, request: MailboxGetRequest): SMono[InvocationWithContext] = {
     val requestedProperties: Properties = request.properties.getOrElse(Mailbox.allProperties)
     (requestedProperties -- Mailbox.allProperties match {
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/VacationResponseSetMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/VacationResponseSetMethod.scala
index 46b9bf5..ce0dd34 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/VacationResponseSetMethod.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/VacationResponseSetMethod.scala
@@ -49,7 +49,7 @@ sealed trait VacationResponseUpdateResult {
 
   def asVacationResponseUpdateResults = VacationResponseUpdateResults(updated, notUpdated)
 }
-case class VacationResponseUpdateSuccess() extends VacationResponseUpdateResult {
+case object VacationResponseUpdateSuccess extends VacationResponseUpdateResult {
   override def updated: Map[String, VacationResponseUpdateResponse] = Map(VACATION_RESPONSE_PATCH_OBJECT_KEY -> VacationResponseUpdateResponse(JsObject(Seq())))
 
   override def notUpdated: Map[String, VacationResponseSetError] = Map()
@@ -101,7 +101,7 @@ class VacationResponseSetMethod @Inject()(vacationRepository: VacationRepository
   private def update(validatedPatch: VacationPatch, mailboxSession: MailboxSession): SMono[VacationResponseUpdateResult] =
     SMono.fromPublisher(
       vacationRepository.modifyVacation(toVacationAccountId(mailboxSession), validatedPatch))
-      .`then`(SMono.just(VacationResponseUpdateSuccess()))
+      .`then`(SMono.just(VacationResponseUpdateSuccess))
 
   private def toVacationAccountId(mailboxSession: MailboxSession): AccountId = {
     AccountId.fromUsername(mailboxSession.getUser)
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/vacation/VacationResponse.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/vacation/VacationResponse.scala
index c1c44d5..26cb067 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/vacation/VacationResponse.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/vacation/VacationResponse.scala
@@ -31,11 +31,11 @@ import scala.compat.java8.OptionConverters._
 
 case class VacationResponseId()
 
-case class IsEnabled(value: Boolean)
+case class IsEnabled(value: Boolean) extends AnyVal
 case class FromDate(value: UTCDate)
 case class ToDate(value: UTCDate)
-case class TextBody(value: String)
-case class HtmlBody(value: String)
+case class TextBody(value: String) extends AnyVal
+case class HtmlBody(value: String) extends AnyVal
 
 object VacationResponse {
   val VACATION_RESPONSE_ID: Id = "singleton"


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org


[james-project] 04/18: JAMES-3440 Type limit as an Int

Posted by bt...@apache.org.
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 b27a5a351093c2ff41bf5bff4c5504dc780dd2f6
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Mon Nov 16 14:09:28 2020 +0700

    JAMES-3440 Type limit as an Int
---
 .../jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/Query.scala | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/Query.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/Query.scala
index b5d0473..a290561 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/Query.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/Query.scala
@@ -42,7 +42,7 @@ object Position {
   }
 }
 
-case class LimitUnparsed(value: Long) extends AnyVal
+case class LimitUnparsed(value: Int) extends AnyVal
 
 object Limit {
   type Limit = Int Refined Positive


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org


[james-project] 05/18: JAMES-3440 JMAP RFC-8621 should use EmailQueryView when enabled

Posted by bt...@apache.org.
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 673e909b50c01c8b70c9faaa738588140ff807ad
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Mon Nov 16 14:19:43 2020 +0700

    JAMES-3440 JMAP RFC-8621 should use EmailQueryView when enabled
---
 .../apache/james/modules/TestJMAPServerModule.java |  1 +
 .../DistributedEmailQueryMethodNoViewTest.java     | 62 ++++++++++++++++
 .../src/test/resources/listeners.xml               |  4 +
 .../contract/EmailQueryMethodContract.scala        | 73 ++++++++++++++++++
 .../memory/MemoryEmailQueryMethodNoViewTest.java   | 58 +++++++++++++++
 .../src/test/resources/listeners.xml               |  4 +
 .../org/apache/james/jmap/mail/EmailQuery.scala    | 46 +++++++++++-
 .../james/jmap/method/EmailQueryMethod.scala       | 86 +++++++++++++++++++---
 8 files changed, 320 insertions(+), 14 deletions(-)

diff --git a/server/container/guice/protocols/jmap/src/test/java/org/apache/james/modules/TestJMAPServerModule.java b/server/container/guice/protocols/jmap/src/test/java/org/apache/james/modules/TestJMAPServerModule.java
index d004632..83496ec 100644
--- a/server/container/guice/protocols/jmap/src/test/java/org/apache/james/modules/TestJMAPServerModule.java
+++ b/server/container/guice/protocols/jmap/src/test/java/org/apache/james/modules/TestJMAPServerModule.java
@@ -116,6 +116,7 @@ public class TestJMAPServerModule extends AbstractModule {
         return JMAPConfiguration.builder()
             .enable()
             .randomPort()
+            .enableEmailQueryView()
             .build();
     }
 
diff --git a/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/distributed/DistributedEmailQueryMethodNoViewTest.java b/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/distributed/DistributedEmailQueryMethodNoViewTest.java
new file mode 100644
index 0000000..235118b
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/distributed/DistributedEmailQueryMethodNoViewTest.java
@@ -0,0 +1,62 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.jmap.rfc8621.distributed;
+
+import org.apache.james.CassandraExtension;
+import org.apache.james.CassandraRabbitMQJamesConfiguration;
+import org.apache.james.CassandraRabbitMQJamesServerMain;
+import org.apache.james.DockerElasticSearchExtension;
+import org.apache.james.JamesServerBuilder;
+import org.apache.james.JamesServerExtension;
+import org.apache.james.jmap.JMAPConfiguration;
+import org.apache.james.jmap.rfc8621.contract.EmailQueryMethodContract;
+import org.apache.james.modules.AwsS3BlobStoreExtension;
+import org.apache.james.modules.RabbitMQExtension;
+import org.apache.james.modules.TestJMAPServerModule;
+import org.apache.james.modules.blobstore.BlobStoreConfiguration;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+public class DistributedEmailQueryMethodNoViewTest implements EmailQueryMethodContract {
+    public static final DockerElasticSearchExtension ELASTIC_SEARCH_EXTENSION = new DockerElasticSearchExtension();
+
+    @RegisterExtension
+    static JamesServerExtension testExtension = new JamesServerBuilder<CassandraRabbitMQJamesConfiguration>(tmpDir ->
+        CassandraRabbitMQJamesConfiguration.builder()
+            .workingDirectory(tmpDir)
+            .configurationFromClasspath()
+            .blobStore(BlobStoreConfiguration.builder()
+                    .s3()
+                    .disableCache()
+                    .deduplication())
+            .build())
+        .extension(ELASTIC_SEARCH_EXTENSION)
+        .extension(new CassandraExtension())
+        .extension(new RabbitMQExtension())
+        .extension(new AwsS3BlobStoreExtension())
+        .server(configuration -> CassandraRabbitMQJamesServerMain.createServer(configuration)
+            .overrideWith(new TestJMAPServerModule())
+            .overrideWith(binder -> binder.bind(JMAPConfiguration.class)
+                .toInstance(JMAPConfiguration.builder()
+                    .enable()
+                    .randomPort()
+                    .disableEmailQueryView()
+                    .build())))
+        .build();
+}
diff --git a/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/resources/listeners.xml b/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/resources/listeners.xml
index ff2e517..1ff4055 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/resources/listeners.xml
+++ b/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/resources/listeners.xml
@@ -46,4 +46,8 @@
       <name>second</name>
     </configuration>
   </listener>
+  <listener>
+    <class>org.apache.james.jmap.event.PopulateEmailQueryViewListener</class>
+    <async>true</async>
+  </listener>
 </listeners>
\ No newline at end of file
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 9f9078a..ff8ec97 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
@@ -1629,6 +1629,78 @@ trait EmailQueryMethodContract {
   }
 
   @Test
+  def listMailsShouldBeSortedByDescendingOrderOfSentAtAndInMailbox(server: GuiceJamesServer): Unit = {
+    val mailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.inbox(BOB))
+    val message: Message = Message.Builder
+      .of
+      .setSubject("test")
+      .setBody("testmail", StandardCharsets.UTF_8)
+      .build
+
+    val requestDateMessage1 = Date.from(ZonedDateTime.now().minusDays(1).toInstant)
+    val messageId1: MessageId = server.getProbe(classOf[MailboxProbeImpl])
+      .appendMessage(BOB.asString, MailboxPath.inbox(BOB),
+        AppendCommand.builder()
+          .withInternalDate(requestDateMessage1)
+          .build(message))
+      .getMessageId
+
+    val requestDateMessage2 = Date.from(ZonedDateTime.now().minusDays(2).toInstant)
+    val messageId2 = server.getProbe(classOf[MailboxProbeImpl])
+      .appendMessage(BOB.asString, MailboxPath.inbox(BOB),
+        AppendCommand.builder()
+          .withInternalDate(requestDateMessage2)
+          .build(message))
+      .getMessageId
+
+    val requestDateMessage3 = Date.from(ZonedDateTime.now().minusDays(3).toInstant)
+    val messageId3 = server.getProbe(classOf[MailboxProbeImpl])
+      .appendMessage(BOB.asString, MailboxPath.inbox(BOB),
+        AppendCommand.builder()
+          .withInternalDate(requestDateMessage3)
+          .build(message))
+      .getMessageId
+
+    val request =
+      s"""{
+         |  "using": [
+         |    "urn:ietf:params:jmap:core",
+         |    "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [[
+         |    "Email/query",
+         |    {
+         |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |      "filter": {
+         |        "inMailbox": "${mailboxId.serialize}"
+         |      },
+         |      "comparator": [{
+         |        "property":"sentAt",
+         |        "isAscending": false
+         |      }]
+         |    },
+         |    "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)
+      .inPath("$.methodResponses[0][1].ids")
+      .isEqualTo(s"""["${messageId1.serialize}", "${messageId2.serialize}", "${messageId3.serialize}"]""")
+    }
+  }
+
+  @Test
   def listMailsShouldBeSortedByAscendingOrderOfSentAt(server: GuiceJamesServer): Unit = {
     server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.inbox(BOB))
     val otherMailboxPath = MailboxPath.forUser(BOB, "other")
@@ -2967,6 +3039,7 @@ trait EmailQueryMethodContract {
         .isEqualTo(s"""["${messageId1.serialize}"]""")
     }
   }
+
   @Test
   def shouldListMailsReceivedBeforeADateInclusively(server: GuiceJamesServer): Unit = {
     val message: Message = buildTestMessage
diff --git a/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryEmailQueryMethodNoViewTest.java b/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryEmailQueryMethodNoViewTest.java
new file mode 100644
index 0000000..8cbe04a
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryEmailQueryMethodNoViewTest.java
@@ -0,0 +1,58 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.jmap.rfc8621.memory;
+
+import static org.apache.james.MemoryJamesServerMain.IN_MEMORY_SERVER_AGGREGATE_MODULE;
+
+import org.apache.james.GuiceJamesServer;
+import org.apache.james.JamesServerBuilder;
+import org.apache.james.JamesServerExtension;
+import org.apache.james.jmap.JMAPConfiguration;
+import org.apache.james.jmap.rfc8621.contract.EmailQueryMethodContract;
+import org.apache.james.modules.TestJMAPServerModule;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+public class MemoryEmailQueryMethodNoViewTest implements EmailQueryMethodContract {
+    @RegisterExtension
+    static JamesServerExtension testExtension = new JamesServerBuilder<>(JamesServerBuilder.defaultConfigurationProvider())
+        .server(configuration -> GuiceJamesServer.forConfiguration(configuration)
+            .combineWith(IN_MEMORY_SERVER_AGGREGATE_MODULE)
+            .overrideWith(new TestJMAPServerModule())
+            .overrideWith(binder -> binder.bind(JMAPConfiguration.class)
+                .toInstance(JMAPConfiguration.builder()
+                    .enable()
+                    .randomPort()
+                    .disableEmailQueryView()
+                    .build())))
+        .build();
+
+    @Test
+    @Override
+    @Disabled("JAMES-3377 Not supported for in-memory test")
+    public void emailQueryFilterByTextShouldIgnoreMarkupsInHtmlBody(GuiceJamesServer server) {}
+
+    @Test
+    @Override
+    @Disabled("JAMES-3377 Not supported for in-memory test" +
+        "In memory do not attempt message parsing a performs a full match on the raw message content")
+    public void emailQueryFilterByTextShouldIgnoreAttachmentContent(GuiceJamesServer server) {}
+}
diff --git a/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/resources/listeners.xml b/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/resources/listeners.xml
index 59e3fec..a1a139d 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/resources/listeners.xml
+++ b/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/resources/listeners.xml
@@ -43,4 +43,8 @@
       <name>second</name>
     </configuration>
   </listener>
+  <listener>
+    <class>org.apache.james.jmap.event.PopulateEmailQueryViewListener</class>
+    <async>true</async>
+  </listener>
 </listeners>
\ No newline at end of file
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailQuery.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailQuery.scala
index 7f154e1..462fff0 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailQuery.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailQuery.scala
@@ -35,7 +35,11 @@ case class UnsupportedFilterException(unsupportedFilter: String) extends Unsuppo
 case class UnsupportedNestingException(message: String) extends UnsupportedOperationException
 case class UnsupportedRequestParameterException(unsupportedParam: String) extends UnsupportedOperationException
 
-sealed trait FilterQuery
+sealed trait FilterQuery {
+  def inMailboxFilterOnly: Boolean
+
+  def inMailboxAndAfterFilterOnly: Boolean
+}
 
 sealed trait Operator
 case object And extends Operator
@@ -43,7 +47,11 @@ case object Or extends Operator
 case object Not extends Operator
 
 case class FilterOperator(operator: Operator,
-                          conditions: Seq[FilterQuery]) extends FilterQuery
+                          conditions: Seq[FilterQuery]) extends FilterQuery {
+  override val inMailboxFilterOnly: Boolean = false
+
+  override val inMailboxAndAfterFilterOnly: Boolean = false
+}
 
 case class Text(value: String) extends AnyVal
 case class From(value: String) extends AnyVal
@@ -81,7 +89,35 @@ case class FilterCondition(inMailbox: Option[MailboxId],
                            bcc: Option[Bcc],
                            subject: Option[Subject],
                            header: Option[Header],
-                           body: Option[Body]) extends FilterQuery
+                           body: Option[Body]) extends FilterQuery {
+  private val noOtherFiltersThanInMailboxAndAfter: Boolean = inMailboxOtherThan.isEmpty &&
+    before.isEmpty &&
+    hasKeyword.isEmpty &&
+    notKeyword.isEmpty &&
+    minSize.isEmpty &&
+    maxSize.isEmpty &&
+    hasAttachment.isEmpty &&
+    allInThreadHaveKeyword.isEmpty &&
+    someInThreadHaveKeyword.isEmpty &&
+    noneInThreadHaveKeyword.isEmpty &&
+    text.isEmpty &&
+    from.isEmpty &&
+    to.isEmpty &&
+    cc.isEmpty &&
+    bcc.isEmpty &&
+    subject.isEmpty &&
+    header.isEmpty &&
+    body.isEmpty
+
+  override val inMailboxFilterOnly: Boolean = inMailbox.nonEmpty &&
+    after.isEmpty &&
+    noOtherFiltersThanInMailboxAndAfter
+
+
+  override val inMailboxAndAfterFilterOnly: Boolean = inMailbox.nonEmpty &&
+    after.nonEmpty &&
+    noOtherFiltersThanInMailboxAndAfter
+}
 
 case class EmailQueryRequest(accountId: AccountId,
                              position: Option[PositionUnparsed],
@@ -163,6 +199,10 @@ case class IsAscending(sortByASC: Boolean) extends AnyVal {
 
 case class Collation(value: String) extends AnyVal
 
+object Comparator {
+  val SENT_AT_DESC: Comparator = Comparator(SentAtSortProperty, Some(IsAscending.DESCENDING), None)
+}
+
 case class Comparator(property: SortProperty,
                       isAscending: Option[IsAscending],
                       collation: Option[Collation]) {
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 6c32948..ae7d822 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
@@ -18,31 +18,40 @@
  ****************************************************************/
 package org.apache.james.jmap.method
 
+import java.time.ZonedDateTime
+
 import cats.implicits._
 import eu.timepit.refined.auto._
 import javax.inject.Inject
+import org.apache.james.jmap.JMAPConfiguration
+import org.apache.james.jmap.api.projections.EmailQueryView
 import org.apache.james.jmap.core.CapabilityIdentifier.{CapabilityIdentifier, JMAP_CORE, JMAP_MAIL}
 import org.apache.james.jmap.core.Invocation.{Arguments, MethodName}
 import org.apache.james.jmap.core.Limit.Limit
 import org.apache.james.jmap.core.Position.Position
 import org.apache.james.jmap.core.{CanCalculateChanges, Invocation, Limit, Position, QueryState}
 import org.apache.james.jmap.json.{EmailQuerySerializer, ResponseSerializer}
-import org.apache.james.jmap.mail.{Comparator, EmailQueryRequest, EmailQueryResponse, UnsupportedRequestParameterException}
+import org.apache.james.jmap.mail.{Comparator, EmailQueryRequest, EmailQueryResponse, FilterCondition, UnsupportedRequestParameterException}
 import org.apache.james.jmap.routes.SessionSupplier
 import org.apache.james.jmap.utils.search.MailboxFilter
 import org.apache.james.jmap.utils.search.MailboxFilter.QueryFilter
-import org.apache.james.mailbox.model.MultimailboxesSearchQuery
+import org.apache.james.mailbox.exception.MailboxNotFoundException
+import org.apache.james.mailbox.model.{MailboxId, MessageId, MultimailboxesSearchQuery}
 import org.apache.james.mailbox.{MailboxManager, MailboxSession}
 import org.apache.james.metrics.api.MetricFactory
+import org.apache.james.util.streams.{Limit => JavaLimit}
 import play.api.libs.json.{JsError, JsSuccess}
 import reactor.core.scala.publisher.{SFlux, SMono}
+import reactor.core.scheduler.Schedulers
 
 import scala.jdk.CollectionConverters._
 
 class EmailQueryMethod @Inject() (serializer: EmailQuerySerializer,
                                   mailboxManager: MailboxManager,
                                   val metricFactory: MetricFactory,
-                                  val sessionSupplier: SessionSupplier) extends MethodRequiringAccountId[EmailQueryRequest] {
+                                  val sessionSupplier: SessionSupplier,
+                                  val configuration: JMAPConfiguration,
+                                  val emailQueryView: EmailQueryView) extends MethodRequiringAccountId[EmailQueryRequest] {
   override val methodName: MethodName = MethodName("Email/query")
   override val requiredCapabilities: Set[CapabilityIdentifier] = Set(JMAP_CORE, JMAP_MAIL)
 
@@ -71,17 +80,72 @@ class EmailQueryMethod @Inject() (serializer: EmailQuerySerializer,
 
   override def getRequest(mailboxSession: MailboxSession, invocation: Invocation): SMono[EmailQueryRequest] = asEmailQueryRequest(invocation.arguments)
 
-  private def executeQuery(mailboxSession: MailboxSession, request: EmailQueryRequest, searchQuery: MultimailboxesSearchQuery, position: Position, limitToUse: Limit): SMono[EmailQueryResponse] = {
+  private def executeQuery(session: MailboxSession, request: EmailQueryRequest, searchQuery: MultimailboxesSearchQuery, position: Position, limit: Limit): SMono[EmailQueryResponse] = {
+    val ids: SMono[Seq[MessageId]] = request match {
+      case request: EmailQueryRequest if matchesInMailboxSortedBySentAt(request) =>
+        queryViewForListingSortedBySentAt(session, position, limit, request)
+      case request: EmailQueryRequest if matchesInMailboxAfterSortedBySentAt(request) =>
+        queryViewForContentAfterSortedBySentAt(session, position, limit, request)
+      case _ => executeQueryAgainstSearchIndex(session, searchQuery, position, limit)
+    }
+
+    ids.map(ids => toResponse(request, position, limit, ids))
+  }
+
+  private def queryViewForContentAfterSortedBySentAt(mailboxSession: MailboxSession, position: Position, limitToUse: Limit, request: EmailQueryRequest): SMono[Seq[MessageId]] = {
+    val condition: FilterCondition = request.filter.get.asInstanceOf[FilterCondition]
+    val mailboxId: MailboxId = condition.inMailbox.get
+    val after: ZonedDateTime = condition.after.get.asUTC
+    SMono.fromCallable(() => mailboxManager.getMailbox(mailboxId, mailboxSession))
+      .subscribeOn(Schedulers.elastic())
+      .`then`(SFlux.fromPublisher(
+        emailQueryView.listMailboxContentSinceReceivedAt(mailboxId, after, JavaLimit.from(limitToUse.value)))
+        .drop(position.value)
+        .take(limitToUse.value)
+        .collectSeq())
+      .onErrorResume({
+        case _: MailboxNotFoundException => SMono.just[Seq[MessageId]](Seq())
+        case e => SMono.raiseError[Seq[MessageId]](e)
+      })
+  }
+
+  private def queryViewForListingSortedBySentAt(mailboxSession: MailboxSession, position: Position, limitToUse: Limit, request: EmailQueryRequest): SMono[Seq[MessageId]] = {
+    val mailboxId: MailboxId = request.filter.get.asInstanceOf[FilterCondition].inMailbox.get
+    SMono.fromCallable(() => mailboxManager.getMailbox(mailboxId, mailboxSession))
+      .subscribeOn(Schedulers.elastic())
+      .`then`(SFlux.fromPublisher(
+        emailQueryView.listMailboxContent(mailboxId, JavaLimit.from(limitToUse.value)))
+        .drop(position.value)
+        .take(limitToUse.value)
+        .collectSeq())
+      .onErrorResume({
+        case _: MailboxNotFoundException => SMono.just[Seq[MessageId]](Seq())
+        case e => SMono.raiseError[Seq[MessageId]](e)
+      })
+  }
+
+  private def matchesInMailboxSortedBySentAt(request: EmailQueryRequest): Boolean =
+    configuration.isEmailQueryViewEnabled &&
+      request.filter.exists(_.inMailboxFilterOnly) &&
+      request.comparator.contains(Set(Comparator.SENT_AT_DESC))
+
+  private def matchesInMailboxAfterSortedBySentAt(request: EmailQueryRequest): Boolean =
+    configuration.isEmailQueryViewEnabled &&
+      request.filter.exists(_.inMailboxAndAfterFilterOnly) &&
+      request.comparator.contains(Set(Comparator.SENT_AT_DESC))
+
+  private def toResponse(request: EmailQueryRequest, position: Position, limitToUse: Limit, ids: Seq[MessageId]): EmailQueryResponse =
+    EmailQueryResponse(accountId = request.accountId,
+      queryState = QueryState.forIds(ids),
+      canCalculateChanges = CanCalculateChanges.CANNOT,
+      ids = ids,
+      position = position,
+      limit = Some(limitToUse).filterNot(used => request.limit.map(_.value).contains(used.value)))
+
+  private def executeQueryAgainstSearchIndex(mailboxSession: MailboxSession, searchQuery: MultimailboxesSearchQuery, position: Position, limitToUse: Limit): SMono[Seq[MessageId]] =
     SFlux.fromPublisher(mailboxManager.search(searchQuery, mailboxSession, position.value + limitToUse))
       .drop(position.value)
       .collectSeq()
-      .map(ids => EmailQueryResponse(accountId = request.accountId,
-        queryState = QueryState.forIds(ids),
-        canCalculateChanges = CanCalculateChanges.CANNOT,
-        ids = ids,
-        position = position,
-        limit = Some(limitToUse).filterNot(used => request.limit.map(_.value).contains(used.value))))
-  }
 
   private def searchQueryFromRequest(request: EmailQueryRequest, capabilities: Set[CapabilityIdentifier], session: MailboxSession): Either[UnsupportedOperationException, MultimailboxesSearchQuery] = {
     val comparators: List[Comparator] = request.comparator.getOrElse(Set()).toList


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org


[james-project] 18/18: JAMES-2884 [REFACTORING] Use Either instead of SMono for request validation

Posted by bt...@apache.org.
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 1f2cfe37f4c2b1dec5d149f3d96cb277759c94b2
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Thu Nov 19 13:35:37 2020 +0700

    JAMES-2884 [REFACTORING] Use Either instead of SMono for request validation
    
    This gets rids of 2 nested flatmaps
---
 .../scala/org/apache/james/jmap/mail/Email.scala   | 29 ++++++------
 .../apache/james/jmap/method/EmailGetMethod.scala  | 14 +++---
 .../james/jmap/method/EmailQueryMethod.scala       | 25 +++++------
 .../apache/james/jmap/method/EmailSetMethod.scala  | 10 ++---
 .../jmap/method/EmailSubmissionSetMethod.scala     |  6 +--
 .../james/jmap/method/MailboxGetMethod.scala       | 11 ++---
 .../james/jmap/method/MailboxQueryMethod.scala     | 18 +++-----
 .../james/jmap/method/MailboxSetMethod.scala       | 12 +++--
 .../org/apache/james/jmap/method/Method.scala      | 51 ++++++++++++----------
 .../jmap/method/VacationResponseGetMethod.scala    |  9 ++--
 .../jmap/method/VacationResponseSetMethod.scala    | 17 +++-----
 .../apache/james/jmap/routes/JMAPApiRoutes.scala   |  6 +--
 .../apache/james/jmap/routes/SessionRoutes.scala   |  2 +-
 .../apache/james/jmap/routes/SessionSupplier.scala | 10 ++---
 .../james/jmap/routes/SessionSupplierTest.scala    |  4 +-
 15 files changed, 104 insertions(+), 120 deletions(-)

diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Email.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Email.scala
index aa75e4e..f923fc7 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Email.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Email.scala
@@ -405,10 +405,13 @@ private class GenericEmailViewReader[+EmailView](messageIdManager: MessageIdMana
         ids.toList.asJava,
         fetchGroup,
         mailboxSession))
-      .groupBy(_.getMessageId)
-      .flatMap(groupedFlux => groupedFlux.collectSeq().map(results => (groupedFlux.key(), results)))
+      .collectSeq()
+      .flatMapIterable(messages => messages.groupBy(_.getMessageId).toSet)
       .map(metadataViewFactory.toEmail(request))
-      .flatMap(SMono.fromTry(_))
+      .handle[T]((aTry, sink) => aTry match {
+        case Success(value) => sink.next(value)
+        case Failure(e) => sink.error(e)
+      })
 }
 
 private class EmailMetadataViewFactory @Inject()(zoneIdProvider: ZoneIdProvider) extends EmailViewFactory[EmailMetadataView] {
@@ -574,16 +577,13 @@ private class EmailFastViewReader @Inject()(messageIdManager: MessageIdManager,
                                             fullViewFactory: EmailFullViewFactory) extends EmailViewReader[EmailView] {
   private val fullReader: GenericEmailViewReader[EmailFullView] = new GenericEmailViewReader[EmailFullView](messageIdManager, FULL_CONTENT, fullViewFactory)
 
-  override def read[T >: EmailView](ids: Seq[MessageId], request: EmailGetRequest, mailboxSession: MailboxSession): SFlux[T] = {
+  override def read[T >: EmailView](ids: Seq[MessageId], request: EmailGetRequest, mailboxSession: MailboxSession): SFlux[T] =
     SMono.fromPublisher(messageFastViewProjection.retrieve(ids.asJava))
       .map(_.asScala.toMap)
-      .flatMapMany(fastViews => SFlux.fromIterable(ids)
-        .map(id => fastViews.get(id)
-          .map(FastViewAvailable(id, _))
-          .getOrElse(FastViewUnavailable(id))))
-      .collectSeq()
+      .map(fastViews => ids.map(id => fastViews.get(id)
+        .map(FastViewAvailable(id, _))
+        .getOrElse(FastViewUnavailable(id))))
       .flatMapMany(results => toEmailViews(results, request, mailboxSession))
-  }
 
   private def toEmailViews[T >: EmailView](results: Seq[FastViewResult], request: EmailGetRequest, mailboxSession: MailboxSession): SFlux[T] = {
     val availables: Seq[FastViewAvailable] = results.flatMap {
@@ -618,10 +618,13 @@ private class EmailFastViewReader @Inject()(messageIdManager: MessageIdManager,
     val ids: Seq[MessageId] = fastViews.map(_.id)
 
     SFlux.fromPublisher(messageIdManager.getMessagesReactive(ids.asJava, HEADERS, mailboxSession))
-      .groupBy(_.getMessageId)
-      .flatMap(groupedFlux => groupedFlux.collectSeq().map(results => (groupedFlux.key(), results)))
+      .collectSeq()
+      .flatMapIterable(messages => messages.groupBy(_.getMessageId).toSet)
       .map(x => toEmail(request)(x, fastViewsAsMap(x._1)))
-      .flatMap(SMono.fromTry(_))
+      .handle[EmailView]((aTry, sink) => aTry match {
+        case Success(value) => sink.next(value)
+        case Failure(e) => sink.error(e)
+      })
   }
 
   private def toEmail(request: EmailGetRequest)(message: (MessageId, Seq[MessageResult]), fastView: MessageFastViewPrecomputedProperties): Try[EmailView] = {
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailGetMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailGetMethod.scala
index f228e54..e95d711 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailGetMethod.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailGetMethod.scala
@@ -89,7 +89,11 @@ class EmailGetMethod @Inject() (readerFactory: EmailViewReaderFactory,
     }).map(invocationResult => InvocationWithContext(invocationResult, invocation.processingContext))
   }
 
-  override def getRequest(mailboxSession: MailboxSession, invocation: Invocation): SMono[EmailGetRequest] = asEmailGetRequest(invocation.arguments)
+  override def getRequest(mailboxSession: MailboxSession, invocation: Invocation): Either[IllegalArgumentException, EmailGetRequest] =
+    EmailGetSerializer.deserializeEmailGetRequest(invocation.arguments.value) match {
+      case JsSuccess(emailGetRequest, _) => Right(emailGetRequest)
+      case errors: JsError => Left(new IllegalArgumentException(ResponseSerializer.serialize(errors).toString))
+    }
 
   private def computeResponseInvocation(request: EmailGetRequest, invocation: Invocation, mailboxSession: MailboxSession): SMono[Invocation] =
     validateProperties(request)
@@ -133,12 +137,6 @@ class EmailGetMethod @Inject() (readerFactory: EmailViewReaderFactory,
         }
     }
 
-  private def asEmailGetRequest(arguments: Arguments): SMono[EmailGetRequest] =
-    EmailGetSerializer.deserializeEmailGetRequest(arguments.value) match {
-      case JsSuccess(emailGetRequest, _) => SMono.just(emailGetRequest)
-      case errors: JsError => SMono.raiseError(new IllegalArgumentException(ResponseSerializer.serialize(errors).toString))
-    }
-
   private def getEmails(request: EmailGetRequest, mailboxSession: MailboxSession): SMono[EmailGetResponse] =
     request.ids match {
       case None => SMono.raiseError(new IllegalArgumentException("ids can not be ommited for email/get"))
@@ -181,7 +179,7 @@ class EmailGetMethod @Inject() (readerFactory: EmailViewReaderFactory,
         .read(ids, request, mailboxSession)
         .collectMap(_.metadata.id)
 
-    foundResultsMono.flatMapMany(foundResults => SFlux.fromIterable(ids)
+    foundResultsMono.flatMapIterable(foundResults => ids
       .map(id => foundResults.get(id)
         .map(EmailGetResults.found)
         .getOrElse(EmailGetResults.notFound(id))))
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 ae7d822..e218397 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
@@ -78,7 +78,18 @@ class EmailQueryMethod @Inject() (serializer: EmailQuerySerializer,
     validation.fold(SMono.raiseError, res => res)
   }
 
-  override def getRequest(mailboxSession: MailboxSession, invocation: Invocation): SMono[EmailQueryRequest] = asEmailQueryRequest(invocation.arguments)
+  override def getRequest(mailboxSession: MailboxSession, invocation: Invocation): Either[Exception, EmailQueryRequest] =
+    serializer.deserializeEmailQueryRequest(invocation.arguments.value) match {
+      case JsSuccess(emailQueryRequest, _) => validateRequestParameters(emailQueryRequest)
+      case errors: JsError => Left(new IllegalArgumentException(ResponseSerializer.serialize(errors).toString))
+    }
+
+  private def validateRequestParameters(request: EmailQueryRequest): Either[Exception, EmailQueryRequest] =
+    (request.anchor, request.anchorOffset) match {
+      case (Some(anchor), _) => Left(UnsupportedRequestParameterException("anchor"))
+      case (_, Some(anchorOffset)) => Left(UnsupportedRequestParameterException("anchorOffset"))
+      case _ => Right(request)
+    }
 
   private def executeQuery(session: MailboxSession, request: EmailQueryRequest, searchQuery: MultimailboxesSearchQuery, position: Position, limit: Limit): SMono[EmailQueryResponse] = {
     val ids: SMono[Seq[MessageId]] = request match {
@@ -167,16 +178,4 @@ class EmailQueryMethod @Inject() (serializer: EmailQuerySerializer,
       .map(MailboxFilter.buildQuery(request, _, capabilities, session))
   }
 
-  private def asEmailQueryRequest(arguments: Arguments): SMono[EmailQueryRequest] =
-    serializer.deserializeEmailQueryRequest(arguments.value) match {
-      case JsSuccess(emailQueryRequest, _) => validateRequestParameters(emailQueryRequest)
-      case errors: JsError => SMono.raiseError(new IllegalArgumentException(ResponseSerializer.serialize(errors).toString))
-    }
-
-  private def validateRequestParameters(request: EmailQueryRequest): SMono[EmailQueryRequest] =
-    (request.anchor, request.anchorOffset) match {
-      case (Some(anchor), _) => SMono.raiseError(UnsupportedRequestParameterException("anchor"))
-      case (_, Some(anchorOffset)) => SMono.raiseError(UnsupportedRequestParameterException("anchorOffset"))
-      case _ => SMono.just(request)
-    }
 }
\ No newline at end of file
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetMethod.scala
index ffe0679..4a69b2c 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetMethod.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetMethod.scala
@@ -70,11 +70,9 @@ class EmailSetMethod @Inject()(serializer: EmailSetSerializer,
         }))
   }
 
-  override def getRequest(mailboxSession: MailboxSession, invocation: Invocation): SMono[EmailSetRequest] = asEmailSetRequest(invocation.arguments)
-
-  private def asEmailSetRequest(arguments: Arguments): SMono[EmailSetRequest] =
-    serializer.deserialize(arguments.value) match {
-      case JsSuccess(emailSetRequest, _) => SMono.just(emailSetRequest)
-      case errors: JsError => SMono.raiseError(new IllegalArgumentException(ResponseSerializer.serialize(errors).toString))
+  override def getRequest(mailboxSession: MailboxSession, invocation: Invocation): Either[IllegalArgumentException, EmailSetRequest] =
+    serializer.deserialize(invocation.arguments.value) match {
+      case JsSuccess(emailSetRequest, _) => Right(emailSetRequest)
+      case errors: JsError => Left(new IllegalArgumentException(ResponseSerializer.serialize(errors).toString))
     }
 }
\ No newline at end of file
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSubmissionSetMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSubmissionSetMethod.scala
index 34ce804..905d1c9 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSubmissionSetMethod.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSubmissionSetMethod.scala
@@ -147,10 +147,10 @@ class EmailSubmissionSetMethod @Inject()(serializer: EmailSubmissionSetSerialize
         request = request.implicitEmailSetRequest))
   }
 
-  override def getRequest(mailboxSession: MailboxSession, invocation: Invocation): SMono[EmailSubmissionSetRequest] =
+  override def getRequest(mailboxSession: MailboxSession, invocation: Invocation): Either[IllegalArgumentException, EmailSubmissionSetRequest] =
     serializer.deserializeEmailSubmissionSetRequest(invocation.arguments.value) match {
-      case JsSuccess(emailSubmissionSetRequest, _) => SMono.just(emailSubmissionSetRequest)
-      case errors: JsError => SMono.raiseError(new IllegalArgumentException(ResponseSerializer.serialize(errors).toString))
+      case JsSuccess(emailSubmissionSetRequest, _) => Right(emailSubmissionSetRequest)
+      case errors: JsError => Left(new IllegalArgumentException(ResponseSerializer.serialize(errors).toString))
     }
 
   private def create(request: EmailSubmissionSetRequest,
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxGetMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxGetMethod.scala
index cd036cb..58ff174 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxGetMethod.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxGetMethod.scala
@@ -89,13 +89,10 @@ class MailboxGetMethod @Inject() (serializer: MailboxSerializer,
 
   }
 
-  override def getRequest(mailboxSession: MailboxSession, invocation: Invocation): SMono[MailboxGetRequest] = asMailboxGetRequest(invocation.arguments)
-
-  private def asMailboxGetRequest(arguments: Arguments): SMono[MailboxGetRequest] = {
-    serializer.deserializeMailboxGetRequest(arguments.value) match {
-      case JsSuccess(mailboxGetRequest, _) => SMono.just(mailboxGetRequest)
-      case errors: JsError => SMono.raiseError(new IllegalArgumentException(ResponseSerializer.serialize(errors).toString))
-    }
+  override def getRequest(mailboxSession: MailboxSession, invocation: Invocation): Either[IllegalArgumentException, MailboxGetRequest] =
+    serializer.deserializeMailboxGetRequest(invocation.arguments.value) match {
+    case JsSuccess(mailboxGetRequest, _) => Right(mailboxGetRequest)
+    case errors: JsError => Left(new IllegalArgumentException(ResponseSerializer.serialize(errors).toString))
   }
 
   private def getMailboxes(capabilities: Set[CapabilityIdentifier],
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxQueryMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxQueryMethod.scala
index 123e7aa..b19a8af 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxQueryMethod.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxQueryMethod.scala
@@ -35,7 +35,7 @@ import reactor.core.scheduler.Schedulers
 class MailboxQueryMethod @Inject()(systemMailboxesProvider: SystemMailboxesProvider,
                                    val metricFactory: MetricFactory,
                                    val sessionSupplier: SessionSupplier) extends MethodRequiringAccountId[MailboxQueryRequest] {
-  override val methodName = MethodName("Mailbox/query")
+  override val methodName: MethodName = MethodName("Mailbox/query")
   override val requiredCapabilities: Set[CapabilityIdentifier] = Set(JMAP_CORE, JMAP_MAIL)
 
   override def doProcess(capabilities: Set[CapabilityIdentifier], invocation: InvocationWithContext, mailboxSession: MailboxSession, request: MailboxQueryRequest): SMono[InvocationWithContext] = {
@@ -51,9 +51,13 @@ class MailboxQueryMethod @Inject()(systemMailboxesProvider: SystemMailboxesProvi
       .map(invocationResult => InvocationWithContext(invocationResult, invocation.processingContext))
   }
 
-  override def getRequest(mailboxSession: MailboxSession, invocation: Invocation): SMono[MailboxQueryRequest] = asMailboxQueryRequest(invocation.arguments)
+  override def getRequest(mailboxSession: MailboxSession, invocation: Invocation): Either[IllegalArgumentException, MailboxQueryRequest] =
+    MailboxQuerySerializer.deserialize(invocation.arguments.value) match {
+      case JsSuccess(emailQueryRequest, _) => Right(emailQueryRequest)
+      case errors: JsError => Left(new IllegalArgumentException(ResponseSerializer.serialize(errors).toString))
+    }
 
-  private def processRequest(mailboxSession: MailboxSession, invocation: Invocation, request: MailboxQueryRequest): SMono[Invocation] = {
+  private def processRequest(mailboxSession: MailboxSession, invocation: Invocation, request: MailboxQueryRequest): SMono[Invocation] =
     SFlux.fromPublisher(systemMailboxesProvider.getMailboxByRole(request.filter.role, mailboxSession.getUser))
       .map(_.getId)
       .collectSeq()
@@ -65,12 +69,4 @@ class MailboxQueryMethod @Inject()(systemMailboxesProvider: SystemMailboxesProvi
         limit = Some(Limit.default)))
       .map(response => Invocation(methodName = methodName, arguments = Arguments(MailboxQuerySerializer.serialize(response)), methodCallId = invocation.methodCallId))
       .subscribeOn(Schedulers.elastic())
-  }
-
-  private def asMailboxQueryRequest(arguments: Arguments): SMono[MailboxQueryRequest] =
-    MailboxQuerySerializer.deserialize(arguments.value) match {
-      case JsSuccess(emailQueryRequest, _) => SMono.just(emailQueryRequest)
-      case errors: JsError => SMono.raiseError(new IllegalArgumentException(ResponseSerializer.serialize(errors).toString))
-    }
-
 }
\ No newline at end of file
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetMethod.scala
index 723c208..6b6952c 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetMethod.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetMethod.scala
@@ -57,7 +57,11 @@ class MailboxSetMethod @Inject()(serializer: MailboxSerializer,
     updateResults <- updatePerformer.updateMailboxes(mailboxSession, request, capabilities)
   } yield InvocationWithContext(createResponse(capabilities, invocation.invocation, request, creationResultsWithUpdatedProcessingContext._1, deletionResults, updateResults), creationResultsWithUpdatedProcessingContext._2)
 
-  override def getRequest(mailboxSession: MailboxSession, invocation: Invocation): SMono[MailboxSetRequest] = asMailboxSetRequest(invocation.arguments)
+  override def getRequest(mailboxSession: MailboxSession, invocation: Invocation): Either[IllegalArgumentException, MailboxSetRequest] =
+    serializer.deserializeMailboxSetRequest(invocation.arguments.value) match {
+      case JsSuccess(mailboxSetRequest, _) => Right(mailboxSetRequest)
+      case errors: JsError => Left(new IllegalArgumentException(ResponseSerializer.serialize(errors).toString))
+    }
 
   private def createResponse(capabilities: Set[CapabilityIdentifier],
                              invocation: Invocation,
@@ -81,10 +85,4 @@ class MailboxSetMethod @Inject()(serializer: MailboxSerializer,
       invocation.methodCallId)
   }
 
-  private def asMailboxSetRequest(arguments: Arguments): SMono[MailboxSetRequest] = {
-    serializer.deserializeMailboxSetRequest(arguments.value) match {
-      case JsSuccess(mailboxSetRequest, _) => SMono.just(mailboxSetRequest)
-      case errors: JsError => SMono.raiseError(new IllegalArgumentException(ResponseSerializer.serialize(errors).toString))
-    }
-  }
 }
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/Method.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/Method.scala
index 7a4cdff..c2663a4 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/Method.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/Method.scala
@@ -27,7 +27,9 @@ import org.apache.james.mailbox.MailboxSession
 import org.apache.james.mailbox.exception.MailboxNotFoundException
 import org.apache.james.metrics.api.MetricFactory
 import org.reactivestreams.Publisher
-import reactor.core.scala.publisher.SMono
+import reactor.core.scala.publisher.{SFlux, SMono}
+
+case class AccountNotFoundException(invocation: Invocation) extends IllegalArgumentException
 
 case class InvocationWithContext(invocation: Invocation, processingContext: ProcessingContext) {
   def recordInvocation: InvocationWithContext = InvocationWithContext(invocation, processingContext.recordInvocation(invocation))
@@ -51,47 +53,50 @@ trait MethodRequiringAccountId[REQUEST <: WithAccountId] extends Method {
   def sessionSupplier: SessionSupplier
 
   override def process(capabilities: Set[CapabilityIdentifier], invocation: InvocationWithContext, mailboxSession: MailboxSession): Publisher[InvocationWithContext] = {
-    val result = getRequest(mailboxSession, invocation.invocation)
-      .flatMapMany(request => {
-        validateAccountId(request.accountId, mailboxSession, sessionSupplier, invocation.invocation)
-          .flatMapMany {
-            case Right(_) => doProcess(capabilities, invocation, mailboxSession, request)
-            case Left(errorInvocation) => SMono.just(InvocationWithContext(errorInvocation, invocation.processingContext))
-          }
-      })
-      .onErrorResume {
-        case e: UnsupportedRequestParameterException => SMono.just(InvocationWithContext(Invocation.error(
+    val either: Either[Exception, Publisher[InvocationWithContext]] = for {
+      request <- getRequest(mailboxSession, invocation.invocation)
+      _ <- validateAccountId(request.accountId, mailboxSession, sessionSupplier, invocation.invocation)
+    } yield {
+      doProcess(capabilities, invocation, mailboxSession, request)
+    }
+
+    val result: SFlux[InvocationWithContext] = SFlux.fromPublisher(either.fold(e => SFlux.raiseError[InvocationWithContext](e), r => r))
+      .onErrorResume[InvocationWithContext] {
+        case e: AccountNotFoundException => SFlux.just[InvocationWithContext] (InvocationWithContext(e.invocation, invocation.processingContext))
+        case e: UnsupportedRequestParameterException => SFlux.just[InvocationWithContext] (InvocationWithContext(Invocation.error(
           ErrorCode.InvalidArguments,
           s"The following parameter ${e.unsupportedParam} is syntactically valid, but is not supported by the server.",
           invocation.invocation.methodCallId), invocation.processingContext))
-        case e: UnsupportedSortException => SMono.just(InvocationWithContext(Invocation.error(
+        case e: UnsupportedSortException => SFlux.just[InvocationWithContext] (InvocationWithContext(Invocation.error(
           ErrorCode.UnsupportedSort,
           s"The sort ${e.unsupportedSort} is syntactically valid, but it includes a property the server does not support sorting on or a collation method it does not recognise.",
           invocation.invocation.methodCallId), invocation.processingContext))
-        case e: UnsupportedFilterException => SMono.just(InvocationWithContext(Invocation.error(
+        case e: UnsupportedFilterException => SFlux.just[InvocationWithContext] (InvocationWithContext(Invocation.error(
           ErrorCode.UnsupportedFilter,
           s"The filter ${e.unsupportedFilter} is syntactically valid, but the server cannot process it. If the filter was the result of a user’s search input, the client SHOULD suggest that the user simplify their search.",
           invocation.invocation.methodCallId), invocation.processingContext))
-        case e: UnsupportedNestingException => SMono.just(InvocationWithContext(Invocation.error(
+        case e: UnsupportedNestingException => SFlux.just[InvocationWithContext] (InvocationWithContext(Invocation.error(
           ErrorCode.UnsupportedFilter,
           description = e.message,
           invocation.invocation.methodCallId), invocation.processingContext))
-        case e: IllegalArgumentException => SMono.just(InvocationWithContext(Invocation.error(ErrorCode.InvalidArguments, e.getMessage, invocation.invocation.methodCallId), invocation.processingContext))
-        case e: MailboxNotFoundException => SMono.just(InvocationWithContext(Invocation.error(ErrorCode.InvalidArguments, e.getMessage, invocation.invocation.methodCallId), invocation.processingContext))
-        case e: Throwable => SMono.raiseError(e)
+        case e: IllegalArgumentException => SFlux.just[InvocationWithContext] (InvocationWithContext(Invocation.error(ErrorCode.InvalidArguments, e.getMessage, invocation.invocation.methodCallId), invocation.processingContext))
+        case e: MailboxNotFoundException => SFlux.just[InvocationWithContext] (InvocationWithContext(Invocation.error(ErrorCode.InvalidArguments, e.getMessage, invocation.invocation.methodCallId), invocation.processingContext))
+        case e: Throwable => SFlux.raiseError[InvocationWithContext] (e)
       }
 
     metricFactory.decoratePublisherWithTimerMetricLogP99(JMAP_RFC8621_PREFIX + methodName.value, result)
   }
 
-  private def validateAccountId(accountId: AccountId, mailboxSession: MailboxSession, sessionSupplier: SessionSupplier, invocation: Invocation): SMono[Either[Invocation, Session]] = {
+  private def validateAccountId(accountId: AccountId, mailboxSession: MailboxSession, sessionSupplier: SessionSupplier, invocation: Invocation): Either[IllegalArgumentException, Session] =
     sessionSupplier.generate(mailboxSession.getUser)
-      .filter(session => session.accounts.map(_.accountId).contains(accountId))
-      .map(session => Right[Invocation, Session](session).asInstanceOf[Either[Invocation, Session]])
-      .switchIfEmpty(SMono.just(Left[Invocation, Session](Invocation.error(ErrorCode.AccountNotFound, invocation.methodCallId))))
-  }
+      .flatMap(session =>
+        if (session.accounts.map(_.accountId).contains(accountId)) {
+          Right(session)
+        } else {
+          Left(AccountNotFoundException(Invocation.error(ErrorCode.AccountNotFound, invocation.methodCallId)))
+        })
 
   def doProcess(capabilities: Set[CapabilityIdentifier], invocation: InvocationWithContext, mailboxSession: MailboxSession, request: REQUEST): Publisher[InvocationWithContext]
 
-  def getRequest(mailboxSession: MailboxSession, invocation: Invocation): SMono[REQUEST]
+  def getRequest(mailboxSession: MailboxSession, invocation: Invocation): Either[Exception, REQUEST]
 }
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/VacationResponseGetMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/VacationResponseGetMethod.scala
index 1c8d780..926f5e0 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/VacationResponseGetMethod.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/VacationResponseGetMethod.scala
@@ -82,11 +82,10 @@ class VacationResponseGetMethod @Inject()(vacationRepository: VacationRepository
     }
   }
 
-  override def getRequest(mailboxSession: MailboxSession, invocation: Invocation): SMono[VacationResponseGetRequest] = asVacationResponseGetRequest(invocation.arguments)
-
-  private def asVacationResponseGetRequest(arguments: Arguments): SMono[VacationResponseGetRequest] = VacationSerializer.deserializeVacationResponseGetRequest(arguments.value) match {
-      case JsSuccess(vacationResponseGetRequest, _) => SMono.just(vacationResponseGetRequest)
-      case errors: JsError => SMono.raiseError(new IllegalArgumentException(ResponseSerializer.serialize(errors).toString))
+  override def getRequest(mailboxSession: MailboxSession, invocation: Invocation): Either[IllegalArgumentException, VacationResponseGetRequest] =
+    VacationSerializer.deserializeVacationResponseGetRequest(invocation.arguments.value) match {
+      case JsSuccess(vacationResponseGetRequest, _) => Right(vacationResponseGetRequest)
+      case errors: JsError => Left(new IllegalArgumentException(ResponseSerializer.serialize(errors).toString))
     }
 
   private def handleRequestValidationErrors(exception: Exception, methodCallId: MethodCallId): SMono[Invocation] = exception match {
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/VacationResponseSetMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/VacationResponseSetMethod.scala
index ce0dd34..56bd506 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/VacationResponseSetMethod.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/VacationResponseSetMethod.scala
@@ -81,7 +81,11 @@ class VacationResponseSetMethod @Inject()(vacationRepository: VacationRepository
       .map(InvocationWithContext(_, invocation.processingContext))
   }
 
-  override def getRequest(mailboxSession: MailboxSession, invocation: Invocation): SMono[VacationResponseSetRequest] = asVacationResponseSetRequest(invocation.arguments)
+  override def getRequest(mailboxSession: MailboxSession, invocation: Invocation): Either[IllegalArgumentException, VacationResponseSetRequest] =
+    VacationSerializer.deserializeVacationResponseSetRequest(invocation.arguments.value) match {
+      case JsSuccess(vacationResponseSetRequest, _) => Right(vacationResponseSetRequest)
+      case errors: JsError => Left(new IllegalArgumentException(ResponseSerializer.serialize(errors).toString))
+    }
 
   private def update(mailboxSession: MailboxSession, vacationResponseSetRequest: VacationResponseSetRequest): SMono[VacationResponseUpdateResults] = {
     SFlux.fromIterable(vacationResponseSetRequest.parsePatch()
@@ -103,16 +107,7 @@ class VacationResponseSetMethod @Inject()(vacationRepository: VacationRepository
       vacationRepository.modifyVacation(toVacationAccountId(mailboxSession), validatedPatch))
       .`then`(SMono.just(VacationResponseUpdateSuccess))
 
-  private def toVacationAccountId(mailboxSession: MailboxSession): AccountId = {
-    AccountId.fromUsername(mailboxSession.getUser)
-  }
-
-  private def asVacationResponseSetRequest(arguments: Arguments): SMono[VacationResponseSetRequest] = {
-    VacationSerializer.deserializeVacationResponseSetRequest(arguments.value) match {
-      case JsSuccess(vacationResponseSetRequest, _) => SMono.just(vacationResponseSetRequest)
-      case errors: JsError => SMono.raiseError(new IllegalArgumentException(ResponseSerializer.serialize(errors).toString))
-    }
-  }
+  private def toVacationAccountId(mailboxSession: MailboxSession): AccountId = AccountId.fromUsername(mailboxSession.getUser)
 
   private def createResponse(invocation: Invocation,
                              vacationResponseSetRequest: VacationResponseSetRequest,
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/JMAPApiRoutes.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/JMAPApiRoutes.scala
index af001c3..da67b15 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/JMAPApiRoutes.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/JMAPApiRoutes.scala
@@ -159,12 +159,12 @@ class JMAPApiRoutes (val authenticator: Authenticator,
   }
 
   private def processMethodWithMatchName(capabilities: Set[CapabilityIdentifier], invocation: InvocationWithContext, mailboxSession: MailboxSession): SFlux[InvocationWithContext] =
-    SMono.justOrEmpty(methodsByName.get(invocation.invocation.methodName))
-      .flatMapMany(method => validateCapabilities(capabilities, method.requiredCapabilities)
+    methodsByName.get(invocation.invocation.methodName)
+      .map(method => validateCapabilities(capabilities, method.requiredCapabilities)
         .fold(e => SFlux.just(InvocationWithContext(Invocation.error(ErrorCode.UnknownMethod, e.description, invocation.invocation.methodCallId), invocation.processingContext)),
           _ => SFlux.fromPublisher(method.process(capabilities, invocation, mailboxSession))))
+      .getOrElse(SFlux.just(InvocationWithContext(Invocation.error(ErrorCode.UnknownMethod, invocation.invocation.methodCallId), invocation.processingContext)))
       .onErrorResume(throwable => SMono.just(InvocationWithContext(Invocation.error(ErrorCode.ServerFail, throwable.getMessage, invocation.invocation.methodCallId), invocation.processingContext)))
-      .switchIfEmpty(SFlux.just(InvocationWithContext(Invocation.error(ErrorCode.UnknownMethod, invocation.invocation.methodCallId), invocation.processingContext)))
 
   private def validateCapabilities(capabilities: Set[CapabilityIdentifier], requiredCapabilities: Set[CapabilityIdentifier]): Either[MissingCapabilityException, Unit] = {
     val missingCapabilities = requiredCapabilities -- capabilities
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/SessionRoutes.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/SessionRoutes.scala
index 4c20cea..81f5cc2 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/SessionRoutes.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/SessionRoutes.scala
@@ -54,7 +54,7 @@ class SessionRoutes @Inject() (@Named(InjectionKeys.RFC_8621) val authenticator:
   private val generateSession: JMAPRoute.Action =
     (request, response) => SMono.fromPublisher(authenticator.authenticate(request))
       .map(_.getUser)
-      .flatMap(sessionSupplier.generate)
+      .flatMap(username => sessionSupplier.generate(username).fold(SMono.raiseError[Session], SMono.just[Session]))
       .flatMap(session => sendRespond(session, response))
       .onErrorResume(throwable => SMono.fromPublisher(errorHandling(throwable, response)))
       .subscribeOn(Schedulers.elastic())
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/SessionSupplier.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/SessionSupplier.scala
index 760d95c..c2af050 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/SessionSupplier.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/SessionSupplier.scala
@@ -28,7 +28,7 @@ import reactor.core.scala.publisher.SMono
 class SessionSupplier @Inject() (val configuration: JmapRfc8621Configuration){
   private val maxSizeUpload = configuration.maxUploadSize
 
-  def generate(username: Username): SMono[Session] = {
+  def generate(username: Username): Either[IllegalArgumentException, Session] =
     accounts(username)
       .map(account => Session(
         DefaultCapabilities.supported(maxSizeUpload),
@@ -39,13 +39,9 @@ class SessionSupplier @Inject() (val configuration: JmapRfc8621Configuration){
         downloadUrl = configuration.downloadUrl,
         uploadUrl = configuration.uploadUrl,
         eventSourceUrl = configuration.eventSourceUrl))
-  }
 
-  private def accounts(username: Username): SMono[Account] = SMono.defer(() =>
-    Account.from(username, IsPersonal(true), IsReadOnly(false), DefaultCapabilities.supported(maxSizeUpload).toSet) match {
-      case Left(ex: IllegalArgumentException) => SMono.raiseError(ex)
-      case Right(account: Account) => SMono.just(account)
-    })
+  private def accounts(username: Username): Either[IllegalArgumentException, Account] =
+    Account.from(username, IsPersonal(true), IsReadOnly(false), DefaultCapabilities.supported(maxSizeUpload).toSet)
 
   private def primaryAccounts(accountId: AccountId): Map[CapabilityIdentifier, AccountId] =
     DefaultCapabilities.supported(maxSizeUpload).toSet
diff --git a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/routes/SessionSupplierTest.scala b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/routes/SessionSupplierTest.scala
index 01e53ce..83b7790 100644
--- a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/routes/SessionSupplierTest.scala
+++ b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/routes/SessionSupplierTest.scala
@@ -33,11 +33,11 @@ class SessionSupplierTest extends AnyWordSpec with Matchers {
 
   "generate" should {
     "return correct username" in {
-      new SessionSupplier(JmapRfc8621Configuration.LOCALHOST_CONFIGURATION).generate(USERNAME).block().username should equal(USERNAME)
+      new SessionSupplier(JmapRfc8621Configuration.LOCALHOST_CONFIGURATION).generate(USERNAME).toOption.get.username should equal(USERNAME)
     }
 
     "return correct account" which {
-      val accounts = new SessionSupplier(JmapRfc8621Configuration.LOCALHOST_CONFIGURATION).generate(USERNAME).block().accounts
+      val accounts = new SessionSupplier(JmapRfc8621Configuration.LOCALHOST_CONFIGURATION).generate(USERNAME).toOption.get.accounts
 
       "has size" in {
         accounts should have size 1


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org


[james-project] 14/18: JAMES-3450 Email/query reject invalid FilterOperator

Posted by bt...@apache.org.
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 6d18bde689800ad2b4b24e228f5d09bd757c45e7
Author: LanKhuat <dl...@linagora.com>
AuthorDate: Wed Nov 18 15:15:50 2020 +0700

    JAMES-3450 Email/query reject invalid FilterOperator
---
 .../contract/EmailQueryMethodContract.scala        | 100 +++++++++++++++++++++
 .../james/jmap/json/EmailQuerySerializer.scala     |   3 +-
 2 files changed, 102 insertions(+), 1 deletion(-)

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 ff8ec97..69297ed 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
@@ -5496,6 +5496,106 @@ trait EmailQueryMethodContract {
   }
 
   @Test
+  def emailQueryShouldRejectFilterOperatorWithExtraFields(server: GuiceJamesServer): Unit = {
+    val mailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.inbox(BOB))
+    val requestDate = ZonedDateTime.now().minusDays(1)
+
+    val request =
+      s"""{
+         |  "using": [
+         |    "urn:ietf:params:jmap:core",
+         |    "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [[
+         |    "Email/query",
+         |    {
+         |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |      "filter" : {
+         |        "inMailbox": "${mailboxId.serialize}",
+         |        "before": "${UTCDate(requestDate.plusHours(1)).asUTC.format(UTC_DATE_FORMAT)}",
+         |        "operator": "AND",
+         |        "conditions": [
+         |          { "hasKeyword": "custom" }, { "hasKeyword": "another_custom" }
+         |        ]
+         |      }
+         |    },
+         |    "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": [
+                    |        [
+                    |            "error",
+                    |            {
+                    |                "type": "invalidArguments",
+                    |                "description": "{\\"errors\\":[{\\"path\\":\\"obj.filter\\",\\"messages\\":[\\"Expecting filterOperator to contain only operator and conditions\\"]}]}"
+                    |            },
+                    |            "c1"
+                    |        ]
+                    |    ]
+                    |}""".stripMargin)
+  }
+
+  @Test
+  def emailQueryShouldRejectOperatorWithoutCondition(server: GuiceJamesServer): Unit = {
+    val request =
+      s"""{
+         |  "using": [
+         |    "urn:ietf:params:jmap:core",
+         |    "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [[
+         |    "Email/query",
+         |    {
+         |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |      "filter" : {
+         |        "operator": "AND"
+         |      }
+         |    },
+         |    "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": [
+                    |        [
+                    |            "error",
+                    |            {
+                    |                "type": "invalidArguments",
+                    |                "description": "{\\"errors\\":[{\\"path\\":\\"obj.filter\\",\\"messages\\":[\\"Expecting filterOperator to contain only operator and conditions\\"]}]}"
+                    |            },
+                    |            "c1"
+                    |        ]
+                    |    ]
+                    |}""".stripMargin)
+  }
+
+  @Test
   def inMailboxShouldBeRejectedWhenInOperator(server: GuiceJamesServer): Unit = {
     val message: Message = buildTestMessage
     val mailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.inbox(BOB))
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailQuerySerializer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailQuerySerializer.scala
index 7a12c2b..11ff719 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailQuerySerializer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailQuerySerializer.scala
@@ -101,7 +101,8 @@ class EmailQuerySerializer @Inject()(mailboxIdFactory: MailboxId.Factory) {
   private implicit val filterOperatorReads: Reads[FilterOperator] = Json.reads[FilterOperator]
 
   private implicit def filterQueryReads: Reads[FilterQuery] = {
-    case jsValue@JsObject(underlying) if underlying.contains("operator") => filterOperatorReads.reads(jsValue)
+    case JsObject(underlying) if underlying.contains("operator") && (!underlying.contains("conditions") || underlying.contains("conditions") && underlying.size > 2) => JsError("Expecting filterOperator to contain only operator and conditions")
+    case jsValue@JsObject(underlying) if underlying.contains("operator") && underlying.contains("conditions") && underlying.size.equals(2) => filterOperatorReads.reads(jsValue)
     case jsValue => filterConditionReads.reads(jsValue)
   }
 


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org