You are viewing a plain text version of this content. The canonical link for it is here.
Posted to server-dev@james.apache.org by bt...@apache.org on 2019/12/10 02:30:03 UTC

[james-project] branch master updated (8ae9166 -> 4ffa7e8)

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 8ae9166  [Refactoring] Move SystemMailboxesProviderImplTest to JUnit 5
     new de105cc  JAMES-2813 ADR about the new distributed workqueue design
     new ca518e6  JAMES-2979 Disable FileMailQueue tests
     new 927c907  JAMES-2979 Deprecate FileMailQueue
     new 16265ce  JAMES-2979 Architecture Decision Record: Deprecate FileMailQueue
     new 59d0840  JAMES-2078 ADR for ElasticSearch disable dynamic mapping
     new 426e93f  JAMES-2917 ADR for ElasticSearch enable routing
     new 38e564b  JAMES-2906 ADR for _source removal rejection
     new 11ed566  JAMES-2919 Validate JMAP partial read ADR after performance tests
     new b4e1876  [Refactoring] port DecoderUtilsTest to junit 5
     new 8ab23e7  [Refactoring] use LocaleDatetime instead of Date to carry knowledge about the timezone
     new 7c3b95f  [Refactoring] More self described test in MessageFastViewProjectionContract
     new c56f2be  [Refactoring] BodyFetchElement should match bean contract
     new cb30feb  [Refactoring] Avoid variable allocation in FetchResponseBuilder::bodyContent
     new 56820c2  [Refactoring] Avoid variable allocation in FetchCommandParser::getSectionType
     new 4e68b91  [Refactoring] Strongly type SectionType
     new 54771a1  JAMES-3002 Demonstrate James respond 400 upon unavailable ElasticSearch
     new cdbc0ee  JAMES-2576 Rework HealthChecks routes status code
     new eef3d71  JAMES-2979 move MailsShouldBeWellReceived test to guice-common
     new a5bb8eb  JAMES-2979 add mail should be well received integration test for guice-cassandra
     new 1c67aca  JAMES-2979 add test to demonstrate spooler issue with activeMQ
     new 20b1353  JAMES-2979 fix spooler reactor usage
     new fb23ed7  JAMES-2979 retry storing mail with a boundedElastic scheduler in Maildispatcher
     new 1306233  JAMES-2979 uncomment test for spooling 200 mails on the cassandra backend
     new e215a4d  JAMES-2979 enhance assertions for IMAPMessageReader
     new 84386f6  JAMES-2979 avoid message to be detected as spam
     new b541012  JAMES-2979 reuse oneHundredMailsShouldBeWellReceived for implementing mailsShouldBeWellReceived
     new 4ffa7e8  JAMES-2979 try to simplify Spooler code

The 27 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:
 CHANGELOG.md                                       |   3 +-
 .../james/imap/api/message/BodyFetchElement.java   | 110 +--
 .../apache/james/imap/api/message/SectionType.java |  13 +-
 .../org/apache/james/imap/decode/DecoderUtils.java |   7 +-
 .../james/imap/decode/FetchPartPathDecoder.java    |  64 +-
 .../james/imap/decode/ImapRequestLineReader.java   |   4 +-
 .../imap/decode/parser/AppendCommandParser.java    |  10 +-
 .../imap/decode/parser/FetchCommandParser.java     |  31 +-
 .../imap/processor/fetch/FetchDataConverter.java   |  15 +-
 .../imap/processor/fetch/FetchResponseBuilder.java |  48 +-
 .../imap/api/message/BodyFetchElementTest.java     |   6 +-
 .../imap/decode/DecoderUtilsLocaleDateTest.java    | 397 ++++------
 .../apache/james/imap/decode/DecoderUtilsTest.java | 816 ++++++++++-----------
 .../imap/decode/FetchPartPathDecoderTest.java      |  73 +-
 .../parser/FetchCommandParserPartialFetchTest.java |   5 +-
 .../processor/fetch/FetchDataConverterTest.java    |   9 +-
 server/container/guice/cassandra-guice/pom.xml     |   6 +
 .../org/apache/james/CassandraJmapTestRule.java    |   4 +
 .../apache/james/WithCassandraBlobStoreTest.java}  |  31 +-
 .../apache/james/MailsShouldBeWellReceived.java    |  66 --
 server/container/guice/guice-common/pom.xml        |  16 +
 .../apache/james/MailsShouldBeWellReceived.java    | 131 ++++
 .../apache/james/GuiceLifecycleHeathCheckTest.java |   2 +-
 .../MessageFastViewProjectionContract.java         |  19 +-
 .../mailetcontainer/impl/JamesMailSpooler.java     |  46 +-
 .../transport/mailets/delivery/MailDispatcher.java |  26 +-
 .../CassandraGetMessageListMethodTest.java         |  34 +
 .../src/test/resources/listeners.xml               |   3 +
 .../integration/GetMessageListMethodTest.java      |   4 +-
 .../methods/integration/SpamAssassinModule.java    |  11 -
 .../memory/MemorySpamAssassinContractTest.java     |   3 +-
 .../src/test/resources/listeners.xml               |   3 +
 .../src/test/resources/listeners.xml               |   3 +
 .../james/webadmin/routes/HealthCheckRoutes.java   |  14 +-
 .../webadmin/routes/HealthCheckRoutesTest.java     |  14 +-
 server/queue/queue-file/pom.xml                    |   3 +
 .../org/apache/james/queue/file/FileMailQueue.java |   5 +
 .../james/queue/file/FileMailQueueFactory.java     |   4 +
 .../james/queue/file/FileMailQueueFactoryTest.java |   7 +
 .../apache/james/queue/file/FileMailQueueTest.java |  70 +-
 .../org/apache/james/utils/IMAPMessageReader.java  |  13 +-
 src/adr/0003-distributed-workqueue.md              |   2 +
 .../0009-disable-elasticsearch-dynamic-mapping.md  |  36 +
 src/adr/0010-enable-elasticsearch-routing.md       |  46 ++
 .../0011-remove-elasticsearch-document-source.md   |  38 +
 src/adr/0012-jmap-partial-reads.md                 |  23 +-
 src/adr/0013-precompute-jmap-preview.md            |  19 +-
 src/adr/0016-distributed-workqueue.md              |  29 +
 src/adr/0017-file-mail-queue-deprecation.md        |  44 ++
 src/site/markdown/server/manage-webadmin.md        |   8 +-
 50 files changed, 1223 insertions(+), 1171 deletions(-)
 copy backends-common/elasticsearch/src/main/java/org/apache/james/backends/es/ReadAliasName.java => protocols/imap/src/main/java/org/apache/james/imap/api/message/SectionType.java (88%)
 copy mailbox/store/src/test/java/org/apache/james/mailbox/store/mail/model/PropertyTest.java => protocols/imap/src/test/java/org/apache/james/imap/api/message/BodyFetchElementTest.java (91%)
 copy server/container/guice/{cassandra-rabbitmq-guice/src/test/java/org/apache/james/CassandraRabbitMQJamesServerFixture.java => cassandra-guice/src/test/java/org/apache/james/WithCassandraBlobStoreTest.java} (57%)
 delete mode 100644 server/container/guice/cassandra-rabbitmq-guice/src/test/java/org/apache/james/MailsShouldBeWellReceived.java
 create mode 100644 server/container/guice/guice-common/src/test/java/org/apache/james/MailsShouldBeWellReceived.java
 create mode 100644 src/adr/0009-disable-elasticsearch-dynamic-mapping.md
 create mode 100644 src/adr/0010-enable-elasticsearch-routing.md
 create mode 100644 src/adr/0011-remove-elasticsearch-document-source.md
 create mode 100644 src/adr/0016-distributed-workqueue.md
 create mode 100644 src/adr/0017-file-mail-queue-deprecation.md


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


[james-project] 23/27: JAMES-2979 uncomment test for spooling 200 mails on the cassandra backend

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 13062337fe3bec2c94afc6d5120d40ff22e5d23e
Author: Rémi KOWALSKI <rk...@linagora.com>
AuthorDate: Thu Nov 21 14:16:10 2019 +0100

    JAMES-2979 uncomment test for spooling 200 mails on the cassandra backend
---
 .../src/test/java/org/apache/james/WithCassandraBlobStoreTest.java  | 6 ------
 1 file changed, 6 deletions(-)

diff --git a/server/container/guice/cassandra-guice/src/test/java/org/apache/james/WithCassandraBlobStoreTest.java b/server/container/guice/cassandra-guice/src/test/java/org/apache/james/WithCassandraBlobStoreTest.java
index 39790a7..7157928 100644
--- a/server/container/guice/cassandra-guice/src/test/java/org/apache/james/WithCassandraBlobStoreTest.java
+++ b/server/container/guice/cassandra-guice/src/test/java/org/apache/james/WithCassandraBlobStoreTest.java
@@ -22,7 +22,6 @@ package org.apache.james;
 import org.apache.james.jmap.draft.JmapJamesServerContract;
 import org.apache.james.jmap.draft.methods.integration.SpamAssassinModuleExtension;
 import org.apache.james.modules.TestJMAPServerModule;
-import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.extension.RegisterExtension;
 
 class WithCassandraBlobStoreTest implements JmapJamesServerContract, MailsShouldBeWellReceived, JamesServerContract {
@@ -41,9 +40,4 @@ class WithCassandraBlobStoreTest implements JmapJamesServerContract, MailsShould
             .overrideWith(JmapJamesServerContract.DOMAIN_LIST_CONFIGURATION_MODULE))
         .build();
 
-
-    @Override
-    @Disabled("Fail to spool some mail at the same time with activeMQ")
-    public void twoHundredMailsShouldBeWellReceived(GuiceJamesServer server) throws Exception {
-    }
 }


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


[james-project] 05/27: JAMES-2078 ADR for ElasticSearch disable dynamic mapping

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 59d0840bad3cdeb57caa9bae4822733df99eb717
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Thu Oct 17 09:15:30 2019 +0700

    JAMES-2078 ADR for ElasticSearch disable dynamic mapping
---
 .../0009-disable-elasticsearch-dynamic-mapping.md  | 36 ++++++++++++++++++++++
 1 file changed, 36 insertions(+)

diff --git a/src/adr/0009-disable-elasticsearch-dynamic-mapping.md b/src/adr/0009-disable-elasticsearch-dynamic-mapping.md
new file mode 100644
index 0000000..5d75a43
--- /dev/null
+++ b/src/adr/0009-disable-elasticsearch-dynamic-mapping.md
@@ -0,0 +1,36 @@
+# 9. Disable ElasticSearch dynamic mapping
+
+Date: 2019-10-10
+
+## Status
+
+Accepted (lazy consensus)
+
+## Context
+
+We rely on dynamic mappings to expose our mail headers as a JSON map. Dynamic mapping is enabled for adding not yet encountered headers in the mapping.
+
+This causes a serie of functional issues:
+ - Maximum field count can easily be exceeded
+ - Field type 'guess' can be wrong, leading to subsequent headers omissions [1]
+ - Document indexation needs to be paused at the index level during mapping changes to avoid concurrent changes, impacting negatively performance.
+
+## Decision
+
+Rely on nested objects to represent mail headers within a mapping
+
+## Consequences
+
+The index needs to be re-created. Document reIndexation is needed.
+
+This solves the aforementionned bugs [1].
+
+Regarding performance:
+ - Default message list performance is unimpacted
+ - We noticed a 4% performance improvment upon indexing throughput
+ - We noticed a 7% increase regarding space per message
+
+## References
+
+ - [1]: https://github.com/linagora/james-project/pull/2726 JAMES-2078 Add an integration test to prove that dynamic mapping can lead to ignored header fields
+ - [JIRA](https://issues.apache.org/jira/browse/JAMES-2078)


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


[james-project] 21/27: JAMES-2979 fix spooler reactor usage

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 20b135341052d2630f46940de1eb7ea679c4ea95
Author: Rémi KOWALSKI <rk...@linagora.com>
AuthorDate: Thu Nov 21 13:32:59 2019 +0100

    JAMES-2979 fix spooler reactor usage
---
 .../mailetcontainer/impl/JamesMailSpooler.java      | 21 ++++++++++++---------
 1 file changed, 12 insertions(+), 9 deletions(-)

diff --git a/server/mailet/mailetcontainer-camel/src/main/java/org/apache/james/mailetcontainer/impl/JamesMailSpooler.java b/server/mailet/mailetcontainer-camel/src/main/java/org/apache/james/mailetcontainer/impl/JamesMailSpooler.java
index 2bd7ed0..2fb1799 100644
--- a/server/mailet/mailetcontainer-camel/src/main/java/org/apache/james/mailetcontainer/impl/JamesMailSpooler.java
+++ b/server/mailet/mailetcontainer-camel/src/main/java/org/apache/james/mailetcontainer/impl/JamesMailSpooler.java
@@ -78,6 +78,7 @@ public class JamesMailSpooler implements Disposable, Configurable, MailSpoolerMB
     private final MailQueueFactory<?> queueFactory;
     private reactor.core.Disposable disposable;
     private Scheduler spooler;
+    private int parallelismLevel;
 
     @Inject
     public JamesMailSpooler(MetricFactory metricFactory, MailProcessor mailProcessor, MailQueueFactory<?> queueFactory) {
@@ -89,6 +90,9 @@ public class JamesMailSpooler implements Disposable, Configurable, MailSpoolerMB
     @Override
     public void configure(HierarchicalConfiguration<ImmutableNode> config) {
         numThreads = config.getInt("threads", 100);
+        //Reactor helps us run things in parallel but we have to ensure there are always threads available
+        //in the threadpool to avoid starvation.
+        parallelismLevel = Math.max(1, numThreads - 2);
     }
 
     /**
@@ -105,19 +109,18 @@ public class JamesMailSpooler implements Disposable, Configurable, MailSpoolerMB
 
     private void run() {
         LOGGER.info("Queue={}", queue);
-
         disposable = Flux.from(queue.deQueue())
-            .flatMap(item -> handleOnQueueItem(item).subscribeOn(spooler))
+            .flatMap(item -> handleOnQueueItem(item).subscribeOn(spooler), parallelismLevel)
             .onErrorContinue((throwable, item) -> LOGGER.error("Exception processing mail while spooling {}", item, throwable))
-            .subscribeOn(Schedulers.boundedElastic())
+            .subscribeOn(spooler)
             .subscribe();
     }
 
     private Mono<Void> handleOnQueueItem(MailQueueItem queueItem) {
         TimeMetric timeMetric = metricFactory.timer(SPOOL_PROCESSING);
         try {
-            processingActive.incrementAndGet();
-            return processMail(queueItem)
+            return Mono.fromCallable(processingActive::incrementAndGet)
+                .flatMap(ignore -> processMail(queueItem))
                 .doOnSuccess(any -> timeMetric.stopAndPublish())
                 .doOnSuccess(any -> processingActive.decrementAndGet());
         } catch (Throwable e) {
@@ -126,14 +129,14 @@ public class JamesMailSpooler implements Disposable, Configurable, MailSpoolerMB
     }
 
     private Mono<Void> processMail(MailQueueItem queueItem) {
-        Mail mail = queueItem.getMail();
-        return Mono.fromRunnable(() -> LOGGER.debug("==== Begin processing mail {} ====", mail.getName()))
-            .subscribeOn(Schedulers.boundedElastic())
+        Mono<Mail> mailPublisher = Mono.fromSupplier(queueItem::getMail);
+        return mailPublisher.flatMap(mail -> Mono.fromRunnable(() -> LOGGER.debug("==== Begin processing mail {} ====", mail.getName()))
+            .subscribeOn(spooler)
             .then(Mono.fromCallable(() -> performProcessMail(mail)))
             .flatMap(any -> acknowledgeItem(queueItem, true))
             .onErrorResume(any -> acknowledgeItem(queueItem, false))
             .then(Mono.fromRunnable(() -> LOGGER.debug("==== End processing mail {} ====", mail.getName())))
-            .then(Mono.fromRunnable(() -> LifecycleUtil.dispose(mail)));
+            .then(Mono.fromRunnable(() -> LifecycleUtil.dispose(mail))));
     }
 
     private Mono<Void> acknowledgeItem(MailQueueItem queueItem, boolean success) {


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


[james-project] 16/27: JAMES-3002 Demonstrate James respond 400 upon unavailable ElasticSearch

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 54771a13375a45d80b1202f8119e001cc2f070ff
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Tue Dec 3 12:41:04 2019 +0700

    JAMES-3002 Demonstrate James respond 400 upon unavailable ElasticSearch
---
 .../org/apache/james/CassandraJmapTestRule.java    |  4 +++
 .../CassandraGetMessageListMethodTest.java         | 34 ++++++++++++++++++++++
 .../integration/GetMessageListMethodTest.java      |  4 +--
 3 files changed, 40 insertions(+), 2 deletions(-)

diff --git a/server/container/guice/cassandra-guice/src/test/java/org/apache/james/CassandraJmapTestRule.java b/server/container/guice/cassandra-guice/src/test/java/org/apache/james/CassandraJmapTestRule.java
index ac072e2..7d55af2 100644
--- a/server/container/guice/cassandra-guice/src/test/java/org/apache/james/CassandraJmapTestRule.java
+++ b/server/container/guice/cassandra-guice/src/test/java/org/apache/james/CassandraJmapTestRule.java
@@ -84,4 +84,8 @@ public class CassandraJmapTestRule implements TestRule {
     public void await() {
         guiceModuleTestRule.await();
     }
+
+    public DockerElasticSearchRule getDockerElasticSearchRule() {
+        return dockerElasticSearchRule;
+    }
 }
diff --git a/server/protocols/jmap-draft-integration-testing/cassandra-jmap-draft-integration-testing/src/test/java/org/apache/james/jmap/cassandra/CassandraGetMessageListMethodTest.java b/server/protocols/jmap-draft-integration-testing/cassandra-jmap-draft-integration-testing/src/test/java/org/apache/james/jmap/cassandra/CassandraGetMessageListMethodTest.java
index 664c3d2..fa9b3d7 100644
--- a/server/protocols/jmap-draft-integration-testing/cassandra-jmap-draft-integration-testing/src/test/java/org/apache/james/jmap/cassandra/CassandraGetMessageListMethodTest.java
+++ b/server/protocols/jmap-draft-integration-testing/cassandra-jmap-draft-integration-testing/src/test/java/org/apache/james/jmap/cassandra/CassandraGetMessageListMethodTest.java
@@ -19,14 +19,23 @@
 
 package org.apache.james.jmap.cassandra;
 
+import static io.restassured.RestAssured.given;
+import static org.apache.james.jmap.TestingConstants.ALICE;
+
 import java.io.IOException;
+import java.util.Date;
+
+import javax.mail.Flags;
 
 import org.apache.james.CassandraJmapTestRule;
 import org.apache.james.DockerCassandraRule;
 import org.apache.james.GuiceJamesServer;
 import org.apache.james.jmap.draft.methods.integration.GetMessageListMethodTest;
+import org.apache.james.mailbox.model.MailboxConstants;
 import org.apache.james.modules.TestJMAPServerModule;
+import org.junit.Ignore;
 import org.junit.Rule;
+import org.junit.Test;
 
 public class CassandraGetMessageListMethodTest extends GetMessageListMethodTest {
 
@@ -46,4 +55,29 @@ public class CassandraGetMessageListMethodTest extends GetMessageListMethodTest
     protected void await() {
         rule.await();
     }
+
+    @Ignore("Demonstrate James respond 400 upon unavailable ElasticSearch")
+    @Test
+    public void getMessageListShouldReturn503WhenElasticSearchIsDown() throws Exception {
+        mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, ALICE.asString(), "mailbox");
+
+        mailboxProbe.appendMessage(ALICE.asString(), ALICE_MAILBOX,
+            ClassLoader.getSystemResourceAsStream("eml/twoAttachments.eml"), new Date(), false, new Flags(Flags.Flag.FLAGGED));
+
+        await();
+
+        rule.getDockerElasticSearchRule().getDockerEs().pause();
+
+        try {
+            given()
+                .header("Authorization", aliceAccessToken.serialize())
+                .body("[[\"getMessageList\", {\"filter\":{\"operator\":\"AND\",\"conditions\":[{\"isFlagged\":\"true\"},{\"isUnread\":\"true\"}]}}, \"#0\"]]")
+            .when()
+                .post("/jmap")
+            .then()
+                .statusCode(503);
+        } finally {
+            rule.getDockerElasticSearchRule().getDockerEs().unpause();
+        }
+    }
 }
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 8dce7c1..2537cc3 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
@@ -112,10 +112,10 @@ public abstract class GetMessageListMethodTest {
 
     protected abstract void await();
 
-    private AccessToken aliceAccessToken;
+    protected AccessToken aliceAccessToken;
     private AccessToken bobAccessToken;
     private GuiceJamesServer jmapServer;
-    private MailboxProbeImpl mailboxProbe;
+    protected MailboxProbeImpl mailboxProbe;
     private DataProbe dataProbe;
 
     @Before


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


[james-project] 11/27: [Refactoring] More self described test in MessageFastViewProjectionContract

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 7c3b95f58fc7713bd3c0fdc1f4166ebb75d949a1
Author: Tran Tien Duc <dt...@linagora.com>
AuthorDate: Fri Dec 6 15:44:16 2019 +0700

    [Refactoring] More self described test in MessageFastViewProjectionContract
    
    By less mathematics involved
---
 .../MessageFastViewProjectionContract.java            | 19 +++++++++----------
 1 file changed, 9 insertions(+), 10 deletions(-)

diff --git a/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/projections/MessageFastViewProjectionContract.java b/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/projections/MessageFastViewProjectionContract.java
index 1e5fa66..f57ff23 100644
--- a/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/projections/MessageFastViewProjectionContract.java
+++ b/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/projections/MessageFastViewProjectionContract.java
@@ -234,27 +234,26 @@ public interface MessageFastViewProjectionContract {
     }
 
     @Test
-    default void storeShouldBeConsistentUponSingleKeyOperation() throws Exception {
+    default void storeShouldReturnAnyLatestPreviewOnConcurrentUpdate() throws Exception {
         MessageId messageId = newMessageId();
-        int threadCount = 10;
-        int operationCount = 100;
 
         ConcurrentTestRunner.builder()
             .reactorOperation((thread, step) -> testee()
                 .store(messageId, MessageFastViewPrecomputedProperties.builder()
-                    .preview(Preview.from(String.valueOf(step * threadCount + thread)))
+                    .preview(Preview.from(thread + "-" + step))
                     .hasAttachment()
                     .build()))
-            .threadCount(threadCount)
-            .operationCount(operationCount)
+            .threadCount(10)
+            .operationCount(100)
             .runSuccessfullyWithin(Duration.ofMinutes(1));
 
-        MessageFastViewPrecomputedProperties preview = Mono.from(testee().retrieve(messageId)).block();
-        Integer previewAsInt = Integer.valueOf(preview.getPreview().getValue());
+        String previewAsString = Mono.from(testee().retrieve(messageId)).block()
+            .getPreview()
+            .getValue();
 
-        assertThat(previewAsInt)
+        assertThat(previewAsString)
             .describedAs("Ensure the stored result was generated by the last operation of one of the threads")
-            .isBetween(threadCount * (operationCount - 1), threadCount * operationCount);
+            .isIn("0-99", "1-99", "2-99", "3-99", "4-99", "5-99", "6-99", "7-99", "8-99", "9-99");
     }
 
     @Test


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


[james-project] 14/27: [Refactoring] Avoid variable allocation in FetchCommandParser::getSectionType

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 56820c22fb28b5b84159bc82cf3e334cf13322a3
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Mon Dec 9 14:15:34 2019 +0700

    [Refactoring] Avoid variable allocation in FetchCommandParser::getSectionType
---
 .../imap/decode/parser/FetchCommandParser.java     | 38 +++++++++-------------
 1 file changed, 15 insertions(+), 23 deletions(-)

diff --git a/protocols/imap/src/main/java/org/apache/james/imap/decode/parser/FetchCommandParser.java b/protocols/imap/src/main/java/org/apache/james/imap/decode/parser/FetchCommandParser.java
index 9f4d33a..eaae520 100644
--- a/protocols/imap/src/main/java/org/apache/james/imap/decode/parser/FetchCommandParser.java
+++ b/protocols/imap/src/main/java/org/apache/james/imap/decode/parser/FetchCommandParser.java
@@ -221,31 +221,23 @@ public class FetchCommandParser extends AbstractUidCommandParser {
     }
 
     private int getSectionType(FetchPartPathDecoder decoder) throws DecodingException {
-        final int specifier = decoder.getSpecifier();
-        final int sectionType;
+        int specifier = decoder.getSpecifier();
         switch (specifier) {
-        case FetchPartPathDecoder.CONTENT:
-            sectionType = BodyFetchElement.CONTENT;
-            break;
-        case FetchPartPathDecoder.HEADER:
-            sectionType = BodyFetchElement.HEADER;
-            break;
-        case FetchPartPathDecoder.HEADER_FIELDS:
-            sectionType = BodyFetchElement.HEADER_FIELDS;
-            break;
-        case FetchPartPathDecoder.HEADER_NOT_FIELDS:
-            sectionType = BodyFetchElement.HEADER_NOT_FIELDS;
-            break;
-        case FetchPartPathDecoder.MIME:
-            sectionType = BodyFetchElement.MIME;
-            break;
-        case FetchPartPathDecoder.TEXT:
-            sectionType = BodyFetchElement.TEXT;
-            break;
-        default:
-            throw new DecodingException(HumanReadableText.ILLEGAL_ARGUMENTS, "Section type is unsupported.");
+            case FetchPartPathDecoder.CONTENT:
+                return BodyFetchElement.CONTENT;
+            case FetchPartPathDecoder.HEADER:
+                return BodyFetchElement.HEADER;
+            case FetchPartPathDecoder.HEADER_FIELDS:
+                return BodyFetchElement.HEADER_FIELDS;
+            case FetchPartPathDecoder.HEADER_NOT_FIELDS:
+                return BodyFetchElement.HEADER_NOT_FIELDS;
+            case FetchPartPathDecoder.MIME:
+                return BodyFetchElement.MIME;
+            case FetchPartPathDecoder.TEXT:
+                return BodyFetchElement.TEXT;
+            default:
+                throw new DecodingException(HumanReadableText.ILLEGAL_ARGUMENTS, "Section type is unsupported.");
         }
-        return sectionType;
     }
 
     private String readWord(ImapRequestLineReader request, String terminator) throws DecodingException {


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


[james-project] 07/27: JAMES-2906 ADR for _source removal rejection

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 38e564b35a44ab739107794e2de4f8b052fd08df
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Thu Oct 17 09:16:05 2019 +0700

    JAMES-2906 ADR for _source removal rejection
---
 .../0011-remove-elasticsearch-document-source.md   | 38 ++++++++++++++++++++++
 1 file changed, 38 insertions(+)

diff --git a/src/adr/0011-remove-elasticsearch-document-source.md b/src/adr/0011-remove-elasticsearch-document-source.md
new file mode 100644
index 0000000..30882f3
--- /dev/null
+++ b/src/adr/0011-remove-elasticsearch-document-source.md
@@ -0,0 +1,38 @@
+# 11. Disable ElasticSearch source
+
+Date: 2019-10-17
+
+## Status
+
+Rejected
+
+The benefits do not outweigh the costs.
+
+## Context
+
+Though very handy to have around, the source field does incur storage overhead within the index. 
+
+## Decision
+
+Disable `_source` for ElasticSearch indexed documents.
+
+## Consequences
+
+Given a dataset composed of small text/plain messages, we notice a 20% space reduction of data stored on ElasticSearch.
+
+However, patch updates can no longer be performed upon flags updates. Upon flag update we need to fully read the mail 
+content, then mime-parse it, potentially html parse it, extract attachment content again and finally index again the full 
+document.
+
+Without `_source` field, flags update is two times slower, 99 percentile 4 times slower, and this impact negatively other 
+requests.
+
+Note please that `_source` allows admin flexibility like performing index level changes without downtime, amongst others:
+ - Increase shards
+ - Modifying replication factor
+ - Changing analysers (IE allows an admin to configure FR analyser instead of EN analyser)
+
+## References
+
+ - https://www.elastic.co/guide/en/elasticsearch/reference/6.3/mapping-source-field.html
+ - [JIRA](https://issues.apache.org/jira/browse/JAMES-2906)


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


[james-project] 18/27: JAMES-2979 move MailsShouldBeWellReceived test to guice-common

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 eef3d71f5ca3c1107dc2432aed967993f634ef78
Author: Rémi KOWALSKI <rk...@linagora.com>
AuthorDate: Wed Nov 20 15:36:19 2019 +0100

    JAMES-2979 move MailsShouldBeWellReceived test to guice-common
---
 server/container/guice/guice-common/pom.xml                   | 11 +++++++++++
 .../test/java/org/apache/james/MailsShouldBeWellReceived.java |  8 ++++----
 2 files changed, 15 insertions(+), 4 deletions(-)

diff --git a/server/container/guice/guice-common/pom.xml b/server/container/guice/guice-common/pom.xml
index b09cc05..0ca1dbd 100644
--- a/server/container/guice/guice-common/pom.xml
+++ b/server/container/guice/guice-common/pom.xml
@@ -110,6 +110,17 @@
         </dependency>
         <dependency>
             <groupId>${james.groupId}</groupId>
+            <artifactId>james-server-testing</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
+            <artifactId>james-server-util</artifactId>
+            <type>test-jar</type>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
             <artifactId>metrics-api</artifactId>
         </dependency>
         <dependency>
diff --git a/server/container/guice/cassandra-rabbitmq-guice/src/test/java/org/apache/james/MailsShouldBeWellReceived.java b/server/container/guice/guice-common/src/test/java/org/apache/james/MailsShouldBeWellReceived.java
similarity index 93%
rename from server/container/guice/cassandra-rabbitmq-guice/src/test/java/org/apache/james/MailsShouldBeWellReceived.java
rename to server/container/guice/guice-common/src/test/java/org/apache/james/MailsShouldBeWellReceived.java
index 9c8adbf..99822cc 100644
--- a/server/container/guice/cassandra-rabbitmq-guice/src/test/java/org/apache/james/MailsShouldBeWellReceived.java
+++ b/server/container/guice/guice-common/src/test/java/org/apache/james/MailsShouldBeWellReceived.java
@@ -19,8 +19,6 @@
 
 package org.apache.james;
 
-import static org.awaitility.Duration.ONE_HUNDRED_MILLISECONDS;
-
 import org.apache.james.core.Domain;
 import org.apache.james.modules.protocols.ImapGuiceProbe;
 import org.apache.james.modules.protocols.SmtpGuiceProbe;
@@ -29,6 +27,7 @@ import org.apache.james.utils.IMAPMessageReader;
 import org.apache.james.utils.SMTPMessageSender;
 import org.apache.james.utils.SpoolerProbe;
 import org.awaitility.Awaitility;
+import org.awaitility.Duration;
 import org.awaitility.core.ConditionFactory;
 import org.junit.jupiter.api.Test;
 
@@ -39,8 +38,8 @@ interface MailsShouldBeWellReceived {
     String JAMES_USER = "james-user@" + DOMAIN;
     String PASSWORD = "secret";
     ConditionFactory CALMLY_AWAIT = Awaitility
-        .with().pollInterval(ONE_HUNDRED_MILLISECONDS)
-        .and().pollDelay(ONE_HUNDRED_MILLISECONDS)
+        .with().pollInterval(Duration.ONE_HUNDRED_MILLISECONDS)
+        .and().pollDelay(Duration.ONE_HUNDRED_MILLISECONDS)
         .await();
 
     @Test
@@ -63,4 +62,5 @@ interface MailsShouldBeWellReceived {
                 .awaitMessage(CALMLY_AWAIT);
         }
     }
+
 }


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


[james-project] 27/27: JAMES-2979 try to simplify Spooler code

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 4ffa7e89d101732669d8f5af4ef21806d33bad3e
Author: Matthieu Baechler <ma...@apache.org>
AuthorDate: Fri Dec 6 11:54:18 2019 +0100

    JAMES-2979 try to simplify Spooler code
---
 .../mailetcontainer/impl/JamesMailSpooler.java     | 35 +++++++++++-----------
 1 file changed, 18 insertions(+), 17 deletions(-)

diff --git a/server/mailet/mailetcontainer-camel/src/main/java/org/apache/james/mailetcontainer/impl/JamesMailSpooler.java b/server/mailet/mailetcontainer-camel/src/main/java/org/apache/james/mailetcontainer/impl/JamesMailSpooler.java
index 2fb1799..63a67e7 100644
--- a/server/mailet/mailetcontainer-camel/src/main/java/org/apache/james/mailetcontainer/impl/JamesMailSpooler.java
+++ b/server/mailet/mailetcontainer-camel/src/main/java/org/apache/james/mailetcontainer/impl/JamesMailSpooler.java
@@ -43,8 +43,6 @@ import org.apache.mailet.Mail;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import com.github.fge.lambdas.Throwing;
-
 import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 import reactor.core.scheduler.Scheduler;
@@ -120,7 +118,7 @@ public class JamesMailSpooler implements Disposable, Configurable, MailSpoolerMB
         TimeMetric timeMetric = metricFactory.timer(SPOOL_PROCESSING);
         try {
             return Mono.fromCallable(processingActive::incrementAndGet)
-                .flatMap(ignore -> processMail(queueItem))
+                .flatMap(ignore -> processMail(queueItem).subscribeOn(spooler))
                 .doOnSuccess(any -> timeMetric.stopAndPublish())
                 .doOnSuccess(any -> processingActive.decrementAndGet());
         } catch (Throwable e) {
@@ -129,31 +127,34 @@ public class JamesMailSpooler implements Disposable, Configurable, MailSpoolerMB
     }
 
     private Mono<Void> processMail(MailQueueItem queueItem) {
-        Mono<Mail> mailPublisher = Mono.fromSupplier(queueItem::getMail);
-        return mailPublisher.flatMap(mail -> Mono.fromRunnable(() -> LOGGER.debug("==== Begin processing mail {} ====", mail.getName()))
-            .subscribeOn(spooler)
-            .then(Mono.fromCallable(() -> performProcessMail(mail)))
-            .flatMap(any -> acknowledgeItem(queueItem, true))
-            .onErrorResume(any -> acknowledgeItem(queueItem, false))
-            .then(Mono.fromRunnable(() -> LOGGER.debug("==== End processing mail {} ====", mail.getName())))
-            .then(Mono.fromRunnable(() -> LifecycleUtil.dispose(mail))));
+        return Mono
+            .using(
+                queueItem::getMail,
+                resource -> Mono.just(resource)
+                    .doOnNext(mail -> LOGGER.debug("==== Begin processing mail {} ====", mail.getName()))
+                    .map(mail -> performProcessMail(queueItem, mail))
+                    .doOnNext(mail -> LOGGER.debug("==== End processing mail {} ====", mail.getName())),
+                LifecycleUtil::dispose)
+            .then();
     }
 
-    private Mono<Void> acknowledgeItem(MailQueueItem queueItem, boolean success) {
-        return Mono.fromRunnable(Throwing.runnable(() -> queueItem.done(success)).sneakyThrow());
-    }
 
-    private boolean performProcessMail(Mail mail) {
+    private Mail performProcessMail(MailQueueItem queueItem, Mail mail) {
         try {
             mailProcessor.service(mail);
 
             if (Thread.currentThread().isInterrupted()) {
                 throw new InterruptedException("Thread has been interrupted");
             }
-            return true;
+            queueItem.done(true);
         } catch (Exception e) {
-            throw new RuntimeException(e);
+            try {
+                queueItem.done(false);
+            } catch (MailQueue.MailQueueException ex) {
+                throw new RuntimeException(e);
+            }
         }
+        return mail;
     }
 
     /**


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


[james-project] 24/27: JAMES-2979 enhance assertions for IMAPMessageReader

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 e215a4d7870a4ae5cafebe07e8fe6a4c0457d233
Author: Matthieu Baechler <ma...@apache.org>
AuthorDate: Fri Nov 29 15:09:27 2019 +0100

    JAMES-2979 enhance assertions for IMAPMessageReader
---
 .../main/java/org/apache/james/utils/IMAPMessageReader.java | 13 +++++--------
 1 file changed, 5 insertions(+), 8 deletions(-)

diff --git a/server/testing/src/main/java/org/apache/james/utils/IMAPMessageReader.java b/server/testing/src/main/java/org/apache/james/utils/IMAPMessageReader.java
index 311c873..c9f841e 100644
--- a/server/testing/src/main/java/org/apache/james/utils/IMAPMessageReader.java
+++ b/server/testing/src/main/java/org/apache/james/utils/IMAPMessageReader.java
@@ -28,6 +28,7 @@ import java.util.stream.Stream;
 
 import org.apache.commons.net.imap.IMAPClient;
 import org.apache.james.core.Username;
+import org.assertj.core.api.Assertions;
 import org.awaitility.core.ConditionFactory;
 import org.junit.jupiter.api.extension.AfterEachCallback;
 import org.junit.jupiter.api.extension.ExtensionContext;
@@ -83,7 +84,7 @@ public class IMAPMessageReader extends ExternalResource implements Closeable, Af
     }
 
     public boolean hasAMessage() throws IOException {
-        imapClient.fetch("1:1", "ALL");
+        imapClient.fetch("1", "UID");
         return imapClient.getReplyString()
             .contains("OK FETCH completed");
     }
@@ -94,13 +95,9 @@ public class IMAPMessageReader extends ExternalResource implements Closeable, Af
     }
 
     public IMAPMessageReader awaitMessageCount(ConditionFactory conditionFactory, int messageCount) {
-        conditionFactory.until(() -> {
-            try {
-                imapClient.fetch("1:*", "ALL");
-                return countFetchedEntries() == messageCount;
-            } catch (Exception e) {
-                throw new RuntimeException(e);
-            }
+        conditionFactory.untilAsserted(() -> {
+            imapClient.fetch("1:*", "UID");
+            Assertions.assertThat(countFetchedEntries()).isEqualTo(messageCount);
         });
         return this;
     }


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


[james-project] 01/27: JAMES-2813 ADR about the new distributed workqueue design

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 de105cc63048d7594b3626065d5c1a503afe6f53
Author: Raphael Ouazana <ra...@linagora.com>
AuthorDate: Tue Dec 3 15:05:46 2019 +0100

    JAMES-2813 ADR about the new distributed workqueue design
---
 src/adr/0003-distributed-workqueue.md |  2 ++
 src/adr/0016-distributed-workqueue.md | 29 +++++++++++++++++++++++++++++
 2 files changed, 31 insertions(+)

diff --git a/src/adr/0003-distributed-workqueue.md b/src/adr/0003-distributed-workqueue.md
index 832a93e..7bf3337 100644
--- a/src/adr/0003-distributed-workqueue.md
+++ b/src/adr/0003-distributed-workqueue.md
@@ -6,6 +6,8 @@ Date: 2019-10-02
 
 Accepted (lazy consensus)
 
+Superceded by [16. Distributed WorkQueue](0016-distributed-workqueue.md)
+
 ## Context
 
 By switching the task manager to a distributed implementation, we need to be able to run a `Task` on any node of the cluster.
diff --git a/src/adr/0016-distributed-workqueue.md b/src/adr/0016-distributed-workqueue.md
new file mode 100644
index 0000000..f2db6b9
--- /dev/null
+++ b/src/adr/0016-distributed-workqueue.md
@@ -0,0 +1,29 @@
+# 16. Distributed WorkQueue
+
+Date: 2019-12-03
+
+## Status
+
+Accepted (lazy consensus)
+
+Supercedes [3. Distributed WorkQueue](0003-distributed-workqueue.md)
+
+## Context
+
+By switching the task manager to a distributed implementation, we need to be able to run a `Task` on any node of the cluster.
+
+## Decision
+
+  For the time being we will keep the sequential execution property of the task manager.
+  This is an intermediate milestone toward the final implementation which will drop this property.
+
+ * Use a RabbitMQ queue as a workqueue where only the `Created` events are pushed into.
+   Instead of using the brittle exclusive queue mechanism described in [3. Distributed WorkQueue](0003-distributed-workqueue.md), we will
+   now use the natively supported [Single Active Consumer](https://www.rabbitmq.com/consumers.html#single-active-consumer) mechanism. 
+
+
+## Consequences
+
+ * This solution is safer to use in production: if the active consumer dies, an other one is promoted instead.
+ * This change needs RabbitMQ version to be at least 3.8.0.
+ * The serial execution of tasks still does not leverage cluster scalability.


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


[james-project] 20/27: JAMES-2979 add test to demonstrate spooler issue with activeMQ

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 1c67aca05f4a3904317fc3e3cde3488bb71fe53d
Author: Rémi KOWALSKI <rk...@linagora.com>
AuthorDate: Wed Nov 20 16:16:11 2019 +0100

    JAMES-2979 add test to demonstrate spooler issue with activeMQ
---
 .../apache/james/WithCassandraBlobStoreTest.java   |  6 ++++
 .../apache/james/MailsShouldBeWellReceived.java    | 34 ++++++++++++++++++++++
 2 files changed, 40 insertions(+)

diff --git a/server/container/guice/cassandra-guice/src/test/java/org/apache/james/WithCassandraBlobStoreTest.java b/server/container/guice/cassandra-guice/src/test/java/org/apache/james/WithCassandraBlobStoreTest.java
index 7157928..39790a7 100644
--- a/server/container/guice/cassandra-guice/src/test/java/org/apache/james/WithCassandraBlobStoreTest.java
+++ b/server/container/guice/cassandra-guice/src/test/java/org/apache/james/WithCassandraBlobStoreTest.java
@@ -22,6 +22,7 @@ package org.apache.james;
 import org.apache.james.jmap.draft.JmapJamesServerContract;
 import org.apache.james.jmap.draft.methods.integration.SpamAssassinModuleExtension;
 import org.apache.james.modules.TestJMAPServerModule;
+import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.extension.RegisterExtension;
 
 class WithCassandraBlobStoreTest implements JmapJamesServerContract, MailsShouldBeWellReceived, JamesServerContract {
@@ -40,4 +41,9 @@ class WithCassandraBlobStoreTest implements JmapJamesServerContract, MailsShould
             .overrideWith(JmapJamesServerContract.DOMAIN_LIST_CONFIGURATION_MODULE))
         .build();
 
+
+    @Override
+    @Disabled("Fail to spool some mail at the same time with activeMQ")
+    public void twoHundredMailsShouldBeWellReceived(GuiceJamesServer server) throws Exception {
+    }
 }
diff --git a/server/container/guice/guice-common/src/test/java/org/apache/james/MailsShouldBeWellReceived.java b/server/container/guice/guice-common/src/test/java/org/apache/james/MailsShouldBeWellReceived.java
index 99822cc..0088658 100644
--- a/server/container/guice/guice-common/src/test/java/org/apache/james/MailsShouldBeWellReceived.java
+++ b/server/container/guice/guice-common/src/test/java/org/apache/james/MailsShouldBeWellReceived.java
@@ -19,6 +19,8 @@
 
 package org.apache.james;
 
+import java.util.UUID;
+
 import org.apache.james.core.Domain;
 import org.apache.james.modules.protocols.ImapGuiceProbe;
 import org.apache.james.modules.protocols.SmtpGuiceProbe;
@@ -31,6 +33,10 @@ import org.awaitility.Duration;
 import org.awaitility.core.ConditionFactory;
 import org.junit.jupiter.api.Test;
 
+import com.github.fge.lambdas.runnable.ThrowingRunnable;
+import reactor.core.publisher.Mono;
+import reactor.core.scheduler.Schedulers;
+
 interface MailsShouldBeWellReceived {
 
     String JAMES_SERVER_HOST = "127.0.0.1";
@@ -42,6 +48,8 @@ interface MailsShouldBeWellReceived {
         .and().pollDelay(Duration.ONE_HUNDRED_MILLISECONDS)
         .await();
 
+    ConditionFactory CALMLY_AWAIT_FIVE_MINUTE = CALMLY_AWAIT.timeout(Duration.FIVE_MINUTES);
+
     @Test
     default void mailsShouldBeWellReceived(GuiceJamesServer server) throws Exception {
         server.getProbe(DataProbeImpl.class).fluent()
@@ -63,4 +71,30 @@ interface MailsShouldBeWellReceived {
         }
     }
 
+    @Test
+    default void twoHundredMailsShouldBeWellReceived(GuiceJamesServer server) throws Exception {
+        server.getProbe(DataProbeImpl.class).fluent()
+            .addDomain(DOMAIN)
+            .addUser(JAMES_USER, PASSWORD);
+
+        int messageCount = 200;
+
+        try (SMTPMessageSender sender = new SMTPMessageSender(Domain.LOCALHOST.asString())) {
+            Mono.fromRunnable(((ThrowingRunnable) () ->
+                sender.connect(JAMES_SERVER_HOST, server.getProbe(SmtpGuiceProbe.class).getSmtpPort())
+                    .sendMessageWithHeaders("bob@any.com", JAMES_USER, "UUID " + UUID.randomUUID().toString())))
+                .repeat(messageCount - 1)
+                .subscribeOn(Schedulers.elastic())
+                .blockLast();
+        }
+
+        CALMLY_AWAIT_FIVE_MINUTE.until(() -> server.getProbe(SpoolerProbe.class).processingFinished());
+
+        try (IMAPMessageReader reader = new IMAPMessageReader()) {
+            reader.connect(JAMES_SERVER_HOST, server.getProbe(ImapGuiceProbe.class).getImapPort())
+                .login(JAMES_USER, PASSWORD)
+                .select(IMAPMessageReader.INBOX)
+                .awaitMessageCount(CALMLY_AWAIT_FIVE_MINUTE, messageCount);
+        }
+    }
 }


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


[james-project] 13/27: [Refactoring] Avoid variable allocation in FetchResponseBuilder::bodyContent

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 cb30feb2a4f0f045b057caad37383f6366d1720c
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Mon Dec 9 14:11:56 2019 +0700

    [Refactoring] Avoid variable allocation in FetchResponseBuilder::bodyContent
---
 .../imap/processor/fetch/FetchResponseBuilder.java | 43 +++++++---------------
 1 file changed, 14 insertions(+), 29 deletions(-)

diff --git a/protocols/imap/src/main/java/org/apache/james/imap/processor/fetch/FetchResponseBuilder.java b/protocols/imap/src/main/java/org/apache/james/imap/processor/fetch/FetchResponseBuilder.java
index 4e9906c..f83c2cc 100644
--- a/protocols/imap/src/main/java/org/apache/james/imap/processor/fetch/FetchResponseBuilder.java
+++ b/protocols/imap/src/main/java/org/apache/james/imap/processor/fetch/FetchResponseBuilder.java
@@ -228,37 +228,22 @@ public final class FetchResponseBuilder {
     }
 
     private FetchResponse.BodyElement bodyContent(MessageResult messageResult, String name, int specifier, int[] path, Collection<String> names, boolean isBase) throws MailboxException {
-        final FetchResponse.BodyElement fullResult;
-
         switch (specifier) {
-        case BodyFetchElement.CONTENT:
-            fullResult = content(messageResult, name, path, isBase);
-            break;
-
-        case BodyFetchElement.HEADER_FIELDS:
-            fullResult = fields(messageResult, name, path, names, isBase);
-            break;
-
-        case BodyFetchElement.HEADER_NOT_FIELDS:
-            fullResult = fieldsNot(messageResult, name, path, names, isBase);
-            break;
-
-        case BodyFetchElement.MIME:
-            fullResult = mimeHeaders(messageResult, name, path, isBase);
-            break;
-        case BodyFetchElement.HEADER:
-            fullResult = headers(messageResult, name, path, isBase);
-            break;
-
-        case BodyFetchElement.TEXT:
-            fullResult = text(messageResult, name, path, isBase);
-            break;
-
-        default:
-            fullResult = null;
-            break;
+            case BodyFetchElement.CONTENT:
+                return content(messageResult, name, path, isBase);
+            case BodyFetchElement.HEADER_FIELDS:
+                return fields(messageResult, name, path, names, isBase);
+            case BodyFetchElement.HEADER_NOT_FIELDS:
+                return fieldsNot(messageResult, name, path, names, isBase);
+            case BodyFetchElement.MIME:
+                return mimeHeaders(messageResult, name, path, isBase);
+            case BodyFetchElement.HEADER:
+                return headers(messageResult, name, path, isBase);
+            case BodyFetchElement.TEXT:
+                return text(messageResult, name, path, isBase);
+            default:
+                return null;
         }
-        return fullResult;
     }
 
     private FetchResponse.BodyElement wrapIfPartialFetch(Long firstOctet, Long numberOfOctets, FetchResponse.BodyElement fullResult) {


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


[james-project] 02/27: JAMES-2979 Disable FileMailQueue tests

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 ca518e6e2db7066e31298e57af3ad0628c470779
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Mon Dec 2 16:52:28 2019 +0700

    JAMES-2979 Disable FileMailQueue tests
---
 .../james/queue/file/FileMailQueueFactoryTest.java |  7 +++
 .../apache/james/queue/file/FileMailQueueTest.java | 70 ++--------------------
 2 files changed, 13 insertions(+), 64 deletions(-)

diff --git a/server/queue/queue-file/src/test/java/org/apache/james/queue/file/FileMailQueueFactoryTest.java b/server/queue/queue-file/src/test/java/org/apache/james/queue/file/FileMailQueueFactoryTest.java
index 4dc1ebf..926092f 100644
--- a/server/queue/queue-file/src/test/java/org/apache/james/queue/file/FileMailQueueFactoryTest.java
+++ b/server/queue/queue-file/src/test/java/org/apache/james/queue/file/FileMailQueueFactoryTest.java
@@ -27,7 +27,14 @@ import org.apache.james.queue.api.ManageableMailQueueFactoryContract;
 import org.apache.james.queue.api.RawMailQueueItemDecoratorFactory;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Disabled;
 
+@Disabled("FileMailQueue is an outdated unmaintained component suffering incomplete features and is not thread safe" +
+    "This includes: " +
+    " - JAMES-2298 Unsupported remove management feature" +
+    " - JAMES-2954 Incomplete browse implementation" +
+    " - JAMES-2544 Mixing concurrent operation might lead to a deadlock and missing fields" +
+    " - JAMES-2979 dequeue is not thread safe")
 public class FileMailQueueFactoryTest implements MailQueueFactoryContract<ManageableMailQueue>, ManageableMailQueueFactoryContract {
     private FileMailQueueFactory mailQueueFactory;
     private MockFileSystem fileSystem;
diff --git a/server/queue/queue-file/src/test/java/org/apache/james/queue/file/FileMailQueueTest.java b/server/queue/queue-file/src/test/java/org/apache/james/queue/file/FileMailQueueTest.java
index bfc9bd7..f2659d6 100644
--- a/server/queue/queue-file/src/test/java/org/apache/james/queue/file/FileMailQueueTest.java
+++ b/server/queue/queue-file/src/test/java/org/apache/james/queue/file/FileMailQueueTest.java
@@ -26,9 +26,14 @@ import org.apache.james.queue.api.RawMailQueueItemDecoratorFactory;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Disabled;
-import org.junit.jupiter.api.Test;
 import org.junit.rules.TemporaryFolder;
 
+@Disabled("FileMailQueue is an outdated unmaintained component suffering incomplete features and is not thread safe" +
+    "This includes: " +
+    " - JAMES-2298 Unsupported remove management feature" +
+    " - JAMES-2954 Incomplete browse implementation" +
+    " - JAMES-2544 Mixing concurrent operation might lead to a deadlock and missing fields" +
+    " - JAMES-2979 dequeue is not thread safe")
 public class FileMailQueueTest implements DelayedManageableMailQueueContract {
     private static final boolean SYNC = true;
 
@@ -55,67 +60,4 @@ public class FileMailQueueTest implements DelayedManageableMailQueueContract {
     public ManageableMailQueue getManageableMailQueue() {
         return mailQueue;
     }
-
-    @Test
-    @Override
-    @Disabled("JAMES-2298 Not supported yet")
-    public void removeBySenderShouldRemoveSpecificEmail() {
-
-    }
-
-    @Test
-    @Override
-    @Disabled("JAMES-2298 Not supported yet")
-    public void removeByNameShouldRemoveSpecificEmail() {
-
-    }
-
-    @Test
-    @Override
-    @Disabled("JAMES-2298 Not supported yet")
-    public void removeByRecipientShouldRemoveSpecificEmail() {
-
-    }
-
-    @Test
-    @Override
-    @Disabled("JAMES-2298 Not supported yet")
-    public void removeByRecipientShouldNotFailWhenQueueIsEmpty() {
-
-    }
-
-    @Test
-    @Override
-    @Disabled("JAMES-2298 Not supported yet")
-    public void removeBySenderShouldNotFailWhenQueueIsEmpty() {
-
-    }
-
-    @Test
-    @Override
-    @Disabled("JAMES-2298 Not supported yet")
-    public void removeByRecipientShouldRemoveSpecificEmailWhenMultipleRecipients() {
-
-    }
-
-    @Test
-    @Override
-    @Disabled("JAMES-2544 Not supported yet")
-    public void browseShouldReturnMailsWithMimeMessage() {
-
-    }
-
-    @Test
-    @Override
-    @Disabled("JAMES-2544 Mixing concurent operation might lead to a deadlock and missing fiels")
-    public void concurrentEnqueueDequeueWithAckNackShouldNotFail() {
-
-    }
-
-    @Test
-    @Override
-    @Disabled("JAMES-2544 Not supported yet")
-    public void deletedElementsShouldNotBeDequeued() {
-
-    }
 }


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


[james-project] 04/27: JAMES-2979 Architecture Decision Record: Deprecate FileMailQueue

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 16265ce86d68dc52e5cf0dc318f2e251e71159c0
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Wed Dec 4 14:24:42 2019 +0700

    JAMES-2979 Architecture Decision Record: Deprecate FileMailQueue
---
 src/adr/0017-file-mail-queue-deprecation.md | 44 +++++++++++++++++++++++++++++
 1 file changed, 44 insertions(+)

diff --git a/src/adr/0017-file-mail-queue-deprecation.md b/src/adr/0017-file-mail-queue-deprecation.md
new file mode 100644
index 0000000..e51e12f
--- /dev/null
+++ b/src/adr/0017-file-mail-queue-deprecation.md
@@ -0,0 +1,44 @@
+# 15. FileMailQueue deprecation
+
+Date: 2019-12-04
+
+## Status
+
+Proposed
+
+## Context
+
+James offers several implementation for MailQueue, a component allowing asynchronous mail processing upon smtp mail 
+reception. These includes:
+ - Default embedded ActiveMQ mail queue implementation, leveraging the JMS APIs and using the filesystem.
+ - RabbitMQMailQueue allowing several James instances to share their MailQueue content.
+ - And FileMailQueue directly leveraging the file system.
+
+We introduced a junit5 test contract regarding management features, concurrency issues, and FileMailQueue do not meet this 
+contract. This results in some tests being disabled and in an unstable test suite.
+
+FileMailQueue tries to implement a message queue within James code, which does not really makes sense as some other projects
+already provides one.
+
+## Decision
+
+Deprecate FileMailQueue components.
+
+Disable FileMailQueue tests.
+
+Target a removal as part of 3.6.0.
+
+## Consequences
+
+FileMailQueue is not exposed to the end user, be it over Spring or Guice, the impact of this deprecation + removal should
+be limited.
+
+We also expect our test suite to be more stable.
+
+## Reference
+
+Issues listing FileMailQueue defects:
+
+ - https://issues.apache.org/jira/browse/JAMES-2298 Unsupported remove management feature
+ - https://issues.apache.org/jira/browse/JAMES-2954 Incomplete browse implementation + Mixing concurrent operation might lead to a deadlock and missing fields
+ - https://issues.apache.org/jira/browse/JAMES-2979 dequeue is not thread safe


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


[james-project] 25/27: JAMES-2979 avoid message to be detected as spam

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 84386f6537125cf4dc45669066c5a0564c9afb52
Author: Matthieu Baechler <ma...@apache.org>
AuthorDate: Fri Nov 29 15:29:55 2019 +0100

    JAMES-2979 avoid message to be detected as spam
    
    	Also, send only 100 mails as it's already long enough
    	and create Inbox before starting to avoid race conditions
    	for Inbox creation
---
 server/container/guice/guice-common/pom.xml        |  5 ++++
 .../apache/james/MailsShouldBeWellReceived.java    | 35 +++++++++++++++++-----
 2 files changed, 32 insertions(+), 8 deletions(-)

diff --git a/server/container/guice/guice-common/pom.xml b/server/container/guice/guice-common/pom.xml
index 0ca1dbd..f3622a6 100644
--- a/server/container/guice/guice-common/pom.xml
+++ b/server/container/guice/guice-common/pom.xml
@@ -62,6 +62,11 @@
         </dependency>
         <dependency>
             <groupId>${james.groupId}</groupId>
+            <artifactId>james-server-guice-mailbox</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
             <artifactId>james-server-guice-imap</artifactId>
             <scope>test</scope>
         </dependency>
diff --git a/server/container/guice/guice-common/src/test/java/org/apache/james/MailsShouldBeWellReceived.java b/server/container/guice/guice-common/src/test/java/org/apache/james/MailsShouldBeWellReceived.java
index 0088658..e06e1b5 100644
--- a/server/container/guice/guice-common/src/test/java/org/apache/james/MailsShouldBeWellReceived.java
+++ b/server/container/guice/guice-common/src/test/java/org/apache/james/MailsShouldBeWellReceived.java
@@ -19,11 +19,16 @@
 
 package org.apache.james;
 
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
 import java.util.UUID;
 
 import org.apache.james.core.Domain;
+import org.apache.james.mailbox.DefaultMailboxes;
+import org.apache.james.modules.MailboxProbeImpl;
 import org.apache.james.modules.protocols.ImapGuiceProbe;
 import org.apache.james.modules.protocols.SmtpGuiceProbe;
+import org.apache.james.util.Port;
 import org.apache.james.utils.DataProbeImpl;
 import org.apache.james.utils.IMAPMessageReader;
 import org.apache.james.utils.SMTPMessageSender;
@@ -33,14 +38,15 @@ import org.awaitility.Duration;
 import org.awaitility.core.ConditionFactory;
 import org.junit.jupiter.api.Test;
 
-import com.github.fge.lambdas.runnable.ThrowingRunnable;
+import com.github.fge.lambdas.Throwing;
+import com.google.common.io.Resources;
 import reactor.core.publisher.Mono;
 import reactor.core.scheduler.Schedulers;
 
 interface MailsShouldBeWellReceived {
 
     String JAMES_SERVER_HOST = "127.0.0.1";
-    String DOMAIN = "domain";
+    String DOMAIN = "apache.org";
     String JAMES_USER = "james-user@" + DOMAIN;
     String PASSWORD = "secret";
     ConditionFactory CALMLY_AWAIT = Awaitility
@@ -72,17 +78,25 @@ interface MailsShouldBeWellReceived {
     }
 
     @Test
-    default void twoHundredMailsShouldBeWellReceived(GuiceJamesServer server) throws Exception {
+    default void oneHundredMailsShouldBeWellReceived(GuiceJamesServer server) throws Exception {
         server.getProbe(DataProbeImpl.class).fluent()
             .addDomain(DOMAIN)
             .addUser(JAMES_USER, PASSWORD);
 
-        int messageCount = 200;
+        MailboxProbeImpl mailboxProbe = server.getProbe(MailboxProbeImpl.class);
+        mailboxProbe.createMailbox("#private", JAMES_USER, DefaultMailboxes.INBOX);
+
+        int messageCount = 100;
+
+        Port smtpPort = server.getProbe(SmtpGuiceProbe.class).getSmtpPort();
+        String message = Resources.toString(Resources.getResource("eml/htmlMail.eml"), StandardCharsets.UTF_8);
 
         try (SMTPMessageSender sender = new SMTPMessageSender(Domain.LOCALHOST.asString())) {
-            Mono.fromRunnable(((ThrowingRunnable) () ->
-                sender.connect(JAMES_SERVER_HOST, server.getProbe(SmtpGuiceProbe.class).getSmtpPort())
-                    .sendMessageWithHeaders("bob@any.com", JAMES_USER, "UUID " + UUID.randomUUID().toString())))
+            Mono.fromRunnable(
+                Throwing.runnable(() -> {
+                    sender.connect(JAMES_SERVER_HOST, smtpPort);
+                    sendUniqueMessage(sender, message);
+            }))
                 .repeat(messageCount - 1)
                 .subscribeOn(Schedulers.elastic())
                 .blockLast();
@@ -94,7 +108,12 @@ interface MailsShouldBeWellReceived {
             reader.connect(JAMES_SERVER_HOST, server.getProbe(ImapGuiceProbe.class).getImapPort())
                 .login(JAMES_USER, PASSWORD)
                 .select(IMAPMessageReader.INBOX)
-                .awaitMessageCount(CALMLY_AWAIT_FIVE_MINUTE, messageCount);
+                .awaitMessageCount(CALMLY_AWAIT, messageCount);
         }
     }
+
+    default void sendUniqueMessage(SMTPMessageSender sender, String message) throws IOException {
+        String uniqueMessage = message.replace("banana", "UUID " + UUID.randomUUID().toString());
+        sender.sendMessageWithHeaders("bob@apache.org", JAMES_USER, uniqueMessage);
+    }
 }


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


[james-project] 06/27: JAMES-2917 ADR for ElasticSearch enable routing

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 426e93f9ff4793631d6557b198448e79e7ffd4f7
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Thu Oct 17 09:15:42 2019 +0700

    JAMES-2917 ADR for ElasticSearch enable routing
---
 src/adr/0010-enable-elasticsearch-routing.md | 46 ++++++++++++++++++++++++++++
 1 file changed, 46 insertions(+)

diff --git a/src/adr/0010-enable-elasticsearch-routing.md b/src/adr/0010-enable-elasticsearch-routing.md
new file mode 100644
index 0000000..b74f458
--- /dev/null
+++ b/src/adr/0010-enable-elasticsearch-routing.md
@@ -0,0 +1,46 @@
+# 10 Enable ElasticSearch routing
+
+Date: 2019-10-17
+
+## Status
+
+Accepted (lazy consensus)
+
+Additional performance testing is required for adoption.
+
+## Context
+
+Our queries are mostly bounded to a mailbox or a user. We can easily
+limit the number of ElasticSearch nodes involved in a given query by
+grouping the underlying documents on the same node using a routing key.
+
+Without a routing key, each shard needs to execute the query. The coordinator
+needs also to be waiting for the slowest shard.
+
+Using the routing key unlocks significant throughput enhancement (proportional
+to the number of shards) and also a possible high percentile latency enhancement.
+
+As most requests are restricted to a single coordination, most search requests will
+hit a single shard, as opposed to non routed searches which would have hit each shards 
+(each shard would return the number of searched documents, to be ordered and limited 
+again in the coordination node). This allows to be more linearly scalable.
+
+## Decision
+
+Enable ElasticSearch routing.
+
+Messages should be indexed by mailbox.
+
+Quota Ratio should be indexed by user.
+
+## Consequences
+
+A data reindex is needed.
+
+On a single ElasticSearch node with 5 shards, we noticed latency reduction for mailbox search (2x mean time and 3x 99 
+percentile reduction)
+
+## References
+
+ - https://www.elastic.co/guide/en/elasticsearch/reference/6.3/mapping-routing-field.html
+ - [JIRA](https://issues.apache.org/jira/browse/JAMES-2917)


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


[james-project] 09/27: [Refactoring] port DecoderUtilsTest to junit 5

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 b4e1876ed0f4172607fab1db5c557c129dd37329
Author: Matthieu Baechler <ma...@apache.org>
AuthorDate: Thu Nov 28 20:55:44 2019 +0100

    [Refactoring] port DecoderUtilsTest to junit 5
---
 .../apache/james/imap/decode/DecoderUtilsTest.java | 803 ++++++++++-----------
 1 file changed, 382 insertions(+), 421 deletions(-)

diff --git a/protocols/imap/src/test/java/org/apache/james/imap/decode/DecoderUtilsTest.java b/protocols/imap/src/test/java/org/apache/james/imap/decode/DecoderUtilsTest.java
index 677f44c..82bccee 100644
--- a/protocols/imap/src/test/java/org/apache/james/imap/decode/DecoderUtilsTest.java
+++ b/protocols/imap/src/test/java/org/apache/james/imap/decode/DecoderUtilsTest.java
@@ -20,501 +20,462 @@
 package org.apache.james.imap.decode;
 
 import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Fail.fail;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 import java.util.Calendar;
 import java.util.Date;
 import java.util.Locale;
 import java.util.TimeZone;
+import java.util.stream.Collectors;
+import java.util.stream.LongStream;
+import java.util.stream.Stream;
 
 import javax.mail.Flags;
 
 import org.apache.commons.lang3.time.FastDateFormat;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.ValueSource;
 
-public class DecoderUtilsTest {
+import com.google.common.collect.Sets;
 
-    private static final String EXTENSION_FLAG = "\\Extension";
+class DecoderUtilsTest {
 
-    private static final String A_CUSTOM_FLAG = "Another";
+    static final String EXTENSION_FLAG = "\\Extension";
+    static final String A_CUSTOM_FLAG = "Another";
 
-
-    
     @Test
-    public void testSetRecentFlag() {
-        Flags flags = new Flags();
-        try {
-            DecoderUtils.setFlag("\\Recent", flags);
-            fail("DecodingException was expect");
-        } catch (DecodingException e) {
-            // expected
-        }
+    void setFlagShouldRejectRecentFlag() {
+        assertThatThrownBy(() -> DecoderUtils.setFlag("\\Recent", new Flags()))
+            .isInstanceOf(DecodingException.class);
     }
 
     @Test
-    public void testSetOtherFlag() throws Exception {
+    void customFlagShouldBeAdded() throws Exception {
         Flags flags = new Flags();
         DecoderUtils.setFlag(A_CUSTOM_FLAG, flags);
-        assertThat(flags
-                .contains(A_CUSTOM_FLAG)).describedAs("Unknown flags should be added").isTrue();
+        assertThat(flags.contains(A_CUSTOM_FLAG))
+            .describedAs("Unknown flags should be added")
+            .isTrue();
     }
 
     @Test
-    public void testExtensionFlag() throws Exception {
+    void extensionFlagShouldBeAdded() throws Exception {
         Flags flags = new Flags();
         DecoderUtils.setFlag(EXTENSION_FLAG, flags);
-        assertThat(flags
-                .contains(EXTENSION_FLAG)).describedAs("Extension flags should be added").isTrue();
+        assertThat(flags.contains(EXTENSION_FLAG))
+            .describedAs("Extension flags should be added")
+            .isTrue();
+    }
+    
+    @ParameterizedTest
+    @ValueSource(strings = {"",
+        "This is a string long enough to be too big", 
+        "1", 
+        "12", 
+        "123", 
+        "1234", 
+        "12345", 
+        "123456", 
+        "1234567", 
+        "12345678", 
+        "123456789", 
+        "1234567890", 
+        "12345678901", 
+        "123456789012", 
+        "1234567890123", 
+        "12345678901234", 
+        "123456789012345", 
+        "1234567890123456", 
+        "12345678901234567", 
+        "123456789012345678", 
+        "1234567890123456789", 
+        "12345678901234567890", 
+        "123456789012345678901", 
+        "1234567890123456789012", 
+        "12345678901234567890123", 
+        "123456789012345678901234", 
+        "1234567890123456789012345", 
+        "12345678901234567890123456", 
+        "123456789012345678901234567"
+    })
+    void decodeShouldThrowOnBadDateTime(String datetime) {
+        assertThatThrownBy(() -> DecoderUtils.decodeDateTime(datetime))
+            .isInstanceOf(DecodingException.class);
     }
 
     @Test
-    public void testBadDateTime() throws Exception {
-        checkDateTime(null);
-        checkDateTime("");
-        checkDateTime("This is a string long enough to be too big");
-        checkDateTime("1");
-        checkDateTime("12");
-        checkDateTime("123");
-        checkDateTime("1234");
-        checkDateTime("12345");
-        checkDateTime("123456");
-        checkDateTime("1234567");
-        checkDateTime("12345678");
-        checkDateTime("123456789");
-        checkDateTime("1234567890");
-        checkDateTime("12345678901");
-        checkDateTime("123456789012");
-        checkDateTime("1234567890123");
-        checkDateTime("12345678901234");
-        checkDateTime("123456789012345");
-        checkDateTime("1234567890123456");
-        checkDateTime("12345678901234567");
-        checkDateTime("123456789012345678");
-        checkDateTime("1234567890123456789");
-        checkDateTime("12345678901234567890");
-        checkDateTime("123456789012345678901");
-        checkDateTime("1234567890123456789012");
-        checkDateTime("12345678901234567890123");
-        checkDateTime("123456789012345678901234");
-        checkDateTime("1234567890123456789012345");
-        checkDateTime("12345678901234567890123456");
-        checkDateTime("123456789012345678901234567");
+    void decodeDateTimeShouldThrowOnNull() {
+        assertThatThrownBy(() -> DecoderUtils.decodeDateTime(null))
+            .isInstanceOf(DecodingException.class);
     }
 
-    private void checkDateTime(String datetime) {
-        try {
-            DecoderUtils.decodeDateTime(datetime);
-            fail("Bad date-time" + datetime);
-        } catch (DecodingException e) {
-            // expected
-        }
+    static Stream<Arguments> nominalDecodeDateTime() {
+        return Stream.of(
+        Arguments.of("21-Oct-1972 20:00:00 +0000", "21 Oct 1972 20:00:00 GMT"),
+        Arguments.of("21-Oct-1972 20:00:00 +0100", "21 Oct 1972 19:00:00 GMT"),
+        Arguments.of("21-Oct-1972 20:00:00 +0200", "21 Oct 1972 18:00:00 GMT"),
+        Arguments.of("21-Oct-1972 20:00:00 +0300", "21 Oct 1972 17:00:00 GMT"),
+        Arguments.of("21-Oct-1972 20:00:00 +0400", "21 Oct 1972 16:00:00 GMT"),
+        Arguments.of("21-Oct-1972 20:00:00 +0500", "21 Oct 1972 15:00:00 GMT"),
+        Arguments.of("21-Oct-1972 20:00:00 +0600", "21 Oct 1972 14:00:00 GMT"),
+        Arguments.of("21-Oct-1972 20:00:00 +0700", "21 Oct 1972 13:00:00 GMT"),
+        Arguments.of("21-Oct-1972 20:00:00 +0800", "21 Oct 1972 12:00:00 GMT"),
+        Arguments.of("21-Oct-1972 20:00:00 +0900", "21 Oct 1972 11:00:00 GMT"),
+        Arguments.of("21-Oct-1972 20:00:00 +1000", "21 Oct 1972 10:00:00 GMT"),
+        Arguments.of("21-Oct-1972 20:00:00 +1100", "21 Oct 1972 09:00:00 GMT"),
+        Arguments.of("21-Oct-1972 20:00:00 +1200", "21 Oct 1972 08:00:00 GMT"),
+
+        Arguments.of("21-Oct-1972 20:00:00 +1000", "21 Oct 1972 10:00:00 GMT"),
+        Arguments.of("21-Oct-1972 20:00:00 -0100", "21 Oct 1972 21:00:00 GMT"),
+        Arguments.of("21-Oct-1972 20:00:00 -0200", "21 Oct 1972 22:00:00 GMT"),
+        Arguments.of("21-Oct-1972 20:00:00 -0300", "21 Oct 1972 23:00:00 GMT"),
+        Arguments.of("21-Oct-1972 20:00:00 -0400", "22 Oct 1972 00:00:00 GMT"),
+        Arguments.of("21-Oct-1972 20:00:00 -0500", "22 Oct 1972 01:00:00 GMT"),
+        Arguments.of("21-Oct-1972 20:00:00 -0600", "22 Oct 1972 02:00:00 GMT"),
+        Arguments.of("21-Oct-1972 20:00:00 -0700", "22 Oct 1972 03:00:00 GMT"),
+        Arguments.of("21-Oct-1972 20:00:00 -0800", "22 Oct 1972 04:00:00 GMT"),
+        Arguments.of("21-Oct-1972 20:00:00 -0900", "22 Oct 1972 05:00:00 GMT"),
+        Arguments.of("21-Oct-1972 20:00:00 -1000", "22 Oct 1972 06:00:00 GMT"),
+
+        Arguments.of("21-Oct-1972 20:00:00 +0030", "21 Oct 1972 19:30:00 GMT"),
+        Arguments.of("21-Oct-1972 20:00:00 -0030", "21 Oct 1972 20:30:00 GMT"),
+
+        Arguments.of("21-Oct-1972 20:00:00 -1000", "22 Oct 1972 06:00:00 GMT"),
+        Arguments.of("21-Oct-1972 06:00:00 +1000", "20 Oct 1972 20:00:00 GMT"),
+        Arguments.of("21-Oct-1972 06:00:00 -1000", "21 Oct 1972 16:00:00 GMT"));
     }
 
-    @Test
+    @ParameterizedTest
+    @MethodSource
     @SuppressWarnings("deprecation")
-    public void testSimpleDecodeDateTime() throws Exception {
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 +0000").toGMTString()).isEqualTo("21 Oct 1972 20:00:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 +0100").toGMTString()).isEqualTo("21 Oct 1972 19:00:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 +0200").toGMTString()).isEqualTo("21 Oct 1972 18:00:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 +0300").toGMTString()).isEqualTo("21 Oct 1972 17:00:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 +0400").toGMTString()).isEqualTo("21 Oct 1972 16:00:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 +0500").toGMTString()).isEqualTo("21 Oct 1972 15:00:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 +0600").toGMTString()).isEqualTo("21 Oct 1972 14:00:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 +0700").toGMTString()).isEqualTo("21 Oct 1972 13:00:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 +0800").toGMTString()).isEqualTo("21 Oct 1972 12:00:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 +0900").toGMTString()).isEqualTo("21 Oct 1972 11:00:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 +1000").toGMTString()).isEqualTo("21 Oct 1972 10:00:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 +1100").toGMTString()).isEqualTo("21 Oct 1972 09:00:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 +1200").toGMTString()).isEqualTo("21 Oct 1972 08:00:00 GMT");
-
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 +1000").toGMTString()).isEqualTo("21 Oct 1972 10:00:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 -0100").toGMTString()).isEqualTo("21 Oct 1972 21:00:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 -0200").toGMTString()).isEqualTo("21 Oct 1972 22:00:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 -0300").toGMTString()).isEqualTo("21 Oct 1972 23:00:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 -0400").toGMTString()).isEqualTo("22 Oct 1972 00:00:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 -0500").toGMTString()).isEqualTo("22 Oct 1972 01:00:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 -0600").toGMTString()).isEqualTo("22 Oct 1972 02:00:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 -0700").toGMTString()).isEqualTo("22 Oct 1972 03:00:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 -0800").toGMTString()).isEqualTo("22 Oct 1972 04:00:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 -0900").toGMTString()).isEqualTo("22 Oct 1972 05:00:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 -1000").toGMTString()).isEqualTo("22 Oct 1972 06:00:00 GMT");
-
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 +0030").toGMTString()).isEqualTo("21 Oct 1972 19:30:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 -0030").toGMTString()).isEqualTo("21 Oct 1972 20:30:00 GMT");
-
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 -1000").toGMTString()).isEqualTo("22 Oct 1972 06:00:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 06:00:00 +1000").toGMTString()).isEqualTo("20 Oct 1972 20:00:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 06:00:00 -1000").toGMTString()).isEqualTo("21 Oct 1972 16:00:00 GMT");
+    void nominalDecodeDateTime(String input, String parsed) throws DecodingException {
+        assertThat(DecoderUtils.decodeDateTime(input).toGMTString()).isEqualTo(parsed);
     }
 
     @Test
     @SuppressWarnings("deprecation")
-    public void testAppleMailPrependsZeroNotSpace() throws Exception {
-        assertThat(DecoderUtils.decodeDateTime(
-                "09-Apr-2008 15:17:51 +0200").toGMTString()).isEqualTo("9 Apr 2008 13:17:51 GMT");
+    void decodeDatetimeShouldAllowAppleMailPrependsZeroNotSpace() throws Exception {
+        assertThat(DecoderUtils.decodeDateTime("09-Apr-2008 15:17:51 +0200").toGMTString())
+            .isEqualTo("9 Apr 2008 13:17:51 GMT");
     }
 
-    @Test
-    public void testDecodeDateTime() throws Exception {
-        runTimeZoneTest(TimeZone.getTimeZone("GMT+0"));
-        runTimeZoneTest(TimeZone.getTimeZone("GMT+1"));
-        runTimeZoneTest(TimeZone.getTimeZone("GMT-1"));
-        runTimeZoneTest(TimeZone.getTimeZone("GMT+2"));
-        runTimeZoneTest(TimeZone.getTimeZone("GMT-2"));
-        runTimeZoneTest(TimeZone.getTimeZone("GMT+3"));
-        runTimeZoneTest(TimeZone.getTimeZone("GMT-3"));
-        runTimeZoneTest(TimeZone.getTimeZone("GMT+11"));
-        runTimeZoneTest(TimeZone.getTimeZone("GMT-11"));
-        runTimeZoneTest(TimeZone.getTimeZone("GMT+1030"));
-        runTimeZoneTest(TimeZone.getTimeZone("GMT-1030"));
-        runTimeZoneTest(TimeZone.getTimeZone("GMT+0045"));
-        runTimeZoneTest(TimeZone.getTimeZone("GMT-0045"));
+    static Stream<Arguments> nominalDecodeDateTimeWithTimezone() {
+        return Sets.cartesianProduct(
+            Stream.of(
+                "GMT+0",
+                "GMT+1",
+                "GMT-1",
+                "GMT+2",
+                "GMT-2",
+                "GMT+3",
+                "GMT-3",
+                "GMT+11",
+                "GMT-11",
+                "GMT+1030",
+                "GMT-1030",
+                "GMT+0045",
+                "GMT-0045")
+            .map(TimeZone::getTimeZone)
+            .collect(Collectors.toSet()),
+            LongStream.of(
+                10000000,
+                100000000,
+                1000000000,
+                10000000000L,
+                100000000000L,
+                1000000000000L,
+                1194168899658L,
+                1912093499271L,
+                1526720308423L,
+                1487487260757L,
+                1584040720026L,
+                1983293490921L,
+                1179806572669L,
+                1194038035064L,
+                1057865248366L,
+                1052797936633L,
+                1075268253439L,
+                1033938440306L,
+                1031614051298L,
+                1059929345305L,
+                1162582627756L,
+                1185747232134L,
+                1151301821303L,
+                1116091684805L,
+                1159599194961L,
+                1222523245646L,
+                1219556266559L,
+                1290015730272L,
+                1221694598854L,
+                1212132783343L,
+                1221761134897L,
+                1270941981377L,
+                1224491731327L,
+                1268571556436L,
+                1246838821081L,
+                1226795970848L,
+                1260254185119L
+            ).mapToObj(Date::new)
+            .collect(Collectors.toSet())
+        ).stream()
+            .map(list -> Arguments.of(list.toArray()));
     }
 
-    private void runTimeZoneTest(TimeZone zone) throws Exception {
-        runDecodeDateTimeTest(new Date(10000000), zone);
-        runDecodeDateTimeTest(new Date(100000000), zone);
-        runDecodeDateTimeTest(new Date(1000000000), zone);
-        runDecodeDateTimeTest(new Date(10000000000L), zone);
-        runDecodeDateTimeTest(new Date(100000000000L), zone);
-        runDecodeDateTimeTest(new Date(1000000000000L), zone);
-        runDecodeDateTimeTest(new Date(1194168899658L), zone);
-        runDecodeDateTimeTest(new Date(1912093499271L), zone);
-        runDecodeDateTimeTest(new Date(1526720308423L), zone);
-        runDecodeDateTimeTest(new Date(1487487260757L), zone);
-        runDecodeDateTimeTest(new Date(1584040720026L), zone);
-        runDecodeDateTimeTest(new Date(1983293490921L), zone);
-        runDecodeDateTimeTest(new Date(1179806572669L), zone);
-        runDecodeDateTimeTest(new Date(1194038035064L), zone);
-        runDecodeDateTimeTest(new Date(1057865248366L), zone);
-        runDecodeDateTimeTest(new Date(1052797936633L), zone);
-        runDecodeDateTimeTest(new Date(1075268253439L), zone);
-        runDecodeDateTimeTest(new Date(1033938440306L), zone);
-        runDecodeDateTimeTest(new Date(1031614051298L), zone);
-        runDecodeDateTimeTest(new Date(1059929345305L), zone);
-        runDecodeDateTimeTest(new Date(1162582627756L), zone);
-        runDecodeDateTimeTest(new Date(1185747232134L), zone);
-        runDecodeDateTimeTest(new Date(1151301821303L), zone);
-        runDecodeDateTimeTest(new Date(1116091684805L), zone);
-        runDecodeDateTimeTest(new Date(1159599194961L), zone);
-        runDecodeDateTimeTest(new Date(1222523245646L), zone);
-        runDecodeDateTimeTest(new Date(1219556266559L), zone);
-        runDecodeDateTimeTest(new Date(1290015730272L), zone);
-        runDecodeDateTimeTest(new Date(1221694598854L), zone);
-        runDecodeDateTimeTest(new Date(1212132783343L), zone);
-        runDecodeDateTimeTest(new Date(1221761134897L), zone);
-        runDecodeDateTimeTest(new Date(1270941981377L), zone);
-        runDecodeDateTimeTest(new Date(1224491731327L), zone);
-        runDecodeDateTimeTest(new Date(1268571556436L), zone);
-        runDecodeDateTimeTest(new Date(1246838821081L), zone);
-        runDecodeDateTimeTest(new Date(1226795970848L), zone);
-        runDecodeDateTimeTest(new Date(1260254185119L), zone);
+    @ParameterizedTest
+    @MethodSource
+    void nominalDecodeDateTimeWithTimezone(TimeZone zone, Date date) throws Exception {
+        FastDateFormat format = FastDateFormat.getInstance("dd-MMM-yyyy hh:mm:ss Z", zone, Locale.US);
+        String in = format.format(date);
+        Date decodedDate = DecoderUtils.decodeDateTime(in);
+        String out = format.format(decodedDate);
+        assertThat(out).describedAs("Round trip").isEqualTo(in);
     }
 
-    private void runDecodeDateTimeTest(Date date, TimeZone zone)
-            throws Exception {
-        dateDecode(formatAsImap(date, zone), zone);
+    static Stream<Arguments> nominalDecodeDigit() {
+        return Stream.of(
+            Arguments.of('0', 0),
+            Arguments.of('1', 1),
+            Arguments.of('2', 2),
+            Arguments.of('3', 3),
+            Arguments.of('4', 4),
+            Arguments.of('5', 5),
+            Arguments.of('6', 6),
+            Arguments.of('7', 7),
+            Arguments.of('8', 8),
+            Arguments.of('9', 9));
     }
 
-    private void dateDecode(String in, TimeZone zone) throws Exception {
-        Date date = DecoderUtils.decodeDateTime(in);
-        String out = formatAsImap(date, zone);
-        assertThat(out).describedAs("Round trip").isEqualTo(in);
+    @ParameterizedTest
+    @MethodSource
+    void nominalDecodeDigit(char digit, int expected) throws DecodingException {
+        assertThat(DecoderUtils.decodeDigit(digit)).isEqualTo(expected);
     }
 
-    private String formatAsImap(Date date, TimeZone zone) {
-        assertThat(date).isNotNull();
-        FastDateFormat format = FastDateFormat.getInstance(
-                "dd-MMM-yyyy hh:mm:ss Z", zone, Locale.US);
-        String out = format.format(date);
-        if (out.charAt(0) == '0') {
-            out = ' ' + out.substring(1, out.length());
-        }
-        return out;
+    @ParameterizedTest
+    @ValueSource(chars = {'/', ':'})
+    void decodeDigitShouldThrowOnNonDigit(char c) {
+        assertThatThrownBy(() -> DecoderUtils.decodeDigit(c))
+            .isInstanceOf(DecodingException.class);
     }
 
-    @Test
-    public void testDecodeDigit() throws Exception {
-        assertThat(DecoderUtils.decodeDigit('0')).isEqualTo(0);
-        assertThat(DecoderUtils.decodeDigit('1')).isEqualTo(1);
-        assertThat(DecoderUtils.decodeDigit('2')).isEqualTo(2);
-        assertThat(DecoderUtils.decodeDigit('3')).isEqualTo(3);
-        assertThat(DecoderUtils.decodeDigit('4')).isEqualTo(4);
-        assertThat(DecoderUtils.decodeDigit('5')).isEqualTo(5);
-        assertThat(DecoderUtils.decodeDigit('6')).isEqualTo(6);
-        assertThat(DecoderUtils.decodeDigit('7')).isEqualTo(7);
-        assertThat(DecoderUtils.decodeDigit('8')).isEqualTo(8);
-        assertThat(DecoderUtils.decodeDigit('9')).isEqualTo(9);
-
-        try {
-            DecoderUtils.decodeDigit('/');
-            fail("/ is not a digit");
-        } catch (DecodingException e) {
-            // expected
-        }
-
-        try {
-            DecoderUtils.decodeDigit(':');
-            fail(": is not a digit");
-        } catch (DecodingException e) {
-            // expected
-        }
+    static Stream<Arguments> nominalDecodeMonth() {
+        return Stream.of(
+            Arguments.of('J', 'A', 'N', Calendar.JANUARY),
+            Arguments.of('j', 'a', 'n', Calendar.JANUARY),
+            Arguments.of('F', 'E', 'B', Calendar.FEBRUARY),
+            Arguments.of('f', 'e', 'b', Calendar.FEBRUARY),
+            Arguments.of('M', 'A', 'R', Calendar.MARCH),
+            Arguments.of('m', 'a', 'r', Calendar.MARCH),
+            Arguments.of('A', 'P', 'R', Calendar.APRIL),
+            Arguments.of('a', 'p', 'r', Calendar.APRIL),
+            Arguments.of('M', 'A', 'Y', Calendar.MAY),
+            Arguments.of('m', 'a', 'y', Calendar.MAY),
+            Arguments.of('J', 'U', 'N', Calendar.JUNE),
+            Arguments.of('j', 'u', 'n', Calendar.JUNE),
+            Arguments.of('J', 'U', 'L', Calendar.JULY),
+            Arguments.of('j', 'u', 'l', Calendar.JULY),
+            Arguments.of('A', 'U', 'G', Calendar.AUGUST),
+            Arguments.of('a', 'u', 'g', Calendar.AUGUST),
+            Arguments.of('S', 'E', 'P', Calendar.SEPTEMBER),
+            Arguments.of('s', 'e', 'p', Calendar.SEPTEMBER),
+            Arguments.of('O', 'C', 'T', Calendar.OCTOBER),
+            Arguments.of('o', 'c', 't', Calendar.OCTOBER),
+            Arguments.of('N', 'O', 'V', Calendar.NOVEMBER),
+            Arguments.of('n', 'o', 'v', Calendar.NOVEMBER),
+            Arguments.of('D', 'E', 'C', Calendar.DECEMBER),
+            Arguments.of('d', 'e', 'c', Calendar.DECEMBER));
     }
 
-    @Test
-    public void testDecodeMonth() throws Exception {
-        assertThat(DecoderUtils.decodeMonth('J', 'A', 'N')).isEqualTo(Calendar.JANUARY);
-        assertThat(DecoderUtils.decodeMonth('j', 'a', 'n')).isEqualTo(Calendar.JANUARY);
-        assertThat(DecoderUtils.decodeMonth('F', 'E', 'B')).isEqualTo(Calendar.FEBRUARY);
-        assertThat(DecoderUtils.decodeMonth('f', 'e', 'b')).isEqualTo(Calendar.FEBRUARY);
-        assertThat(DecoderUtils.decodeMonth('M', 'A', 'R')).isEqualTo(Calendar.MARCH);
-        assertThat(DecoderUtils.decodeMonth('m', 'a', 'r')).isEqualTo(Calendar.MARCH);
-        assertThat(DecoderUtils.decodeMonth('A', 'P', 'R')).isEqualTo(Calendar.APRIL);
-        assertThat(DecoderUtils.decodeMonth('a', 'p', 'r')).isEqualTo(Calendar.APRIL);
-        assertThat(DecoderUtils.decodeMonth('M', 'A', 'Y')).isEqualTo(Calendar.MAY);
-        assertThat(DecoderUtils.decodeMonth('m', 'a', 'y')).isEqualTo(Calendar.MAY);
-        assertThat(DecoderUtils.decodeMonth('J', 'U', 'N')).isEqualTo(Calendar.JUNE);
-        assertThat(DecoderUtils.decodeMonth('j', 'u', 'n')).isEqualTo(Calendar.JUNE);
-        assertThat(DecoderUtils.decodeMonth('J', 'U', 'L')).isEqualTo(Calendar.JULY);
-        assertThat(DecoderUtils.decodeMonth('j', 'u', 'l')).isEqualTo(Calendar.JULY);
-        assertThat(DecoderUtils.decodeMonth('A', 'U', 'G')).isEqualTo(Calendar.AUGUST);
-        assertThat(DecoderUtils.decodeMonth('a', 'u', 'g')).isEqualTo(Calendar.AUGUST);
-        assertThat(DecoderUtils
-                .decodeMonth('S', 'E', 'P')).isEqualTo(Calendar.SEPTEMBER);
-        assertThat(DecoderUtils
-                .decodeMonth('s', 'e', 'p')).isEqualTo(Calendar.SEPTEMBER);
-        assertThat(DecoderUtils.decodeMonth('O', 'C', 'T')).isEqualTo(Calendar.OCTOBER);
-        assertThat(DecoderUtils.decodeMonth('o', 'c', 't')).isEqualTo(Calendar.OCTOBER);
-        assertThat(DecoderUtils.decodeMonth('N', 'O', 'V')).isEqualTo(Calendar.NOVEMBER);
-        assertThat(DecoderUtils.decodeMonth('n', 'o', 'v')).isEqualTo(Calendar.NOVEMBER);
-        assertThat(DecoderUtils.decodeMonth('D', 'E', 'C')).isEqualTo(Calendar.DECEMBER);
-        assertThat(DecoderUtils.decodeMonth('d', 'e', 'c')).isEqualTo(Calendar.DECEMBER);
+    @ParameterizedTest
+    @MethodSource
+    void nominalDecodeMonth(char c1, char c2, char c3, int expected) throws Exception {
+        assertThat(DecoderUtils.decodeMonth(c1, c2, c3)).isEqualTo(expected);
     }
 
-    @Test
-    public void testRejectBogusMonths() {
-        checkReject('N', 'O', 'C');
-        checkReject('A', 'N', 'T');
-        checkReject('Z', 'Z', 'Z');
-        checkReject('S', 'I', 'P');
-        checkReject('D', 'E', 'P');
+
+    static Stream<Arguments> decodeMonthShouldRejectBogusMonths() {
+        return Stream.of(
+            Arguments.of('N', 'O', 'C'),
+            Arguments.of('A', 'N', 'T'),
+            Arguments.of('Z', 'Z', 'Z'),
+            Arguments.of('S', 'I', 'P'),
+            Arguments.of('D', 'E', 'P'));
     }
 
-    private void checkReject(char one, char two, char three) {
-        try {
-            DecoderUtils.decodeMonth(one, two, three);
-            fail(one + two + three + "is not a month");
-        } catch (DecodingException e) {
-            // expected
-        }
+    @ParameterizedTest
+    @MethodSource
+    void decodeMonthShouldRejectBogusMonths(char one, char two, char three) {
+        assertThatThrownBy(() -> DecoderUtils.decodeMonth(one, two, three))
+            .isInstanceOf(DecodingException.class);
     }
 
-    @Test
-    public void testDecodeYear() throws Exception {
-        assertThat(DecoderUtils.decodeYear('1', '9', '9', '9')).isEqualTo(1999);
-        assertThat(DecoderUtils.decodeYear('0', '7', '4', '7')).isEqualTo(747);
-        assertThat(DecoderUtils.decodeYear('2', '5', '2', '5')).isEqualTo(2525);
-        assertThat(DecoderUtils.decodeYear('5', '6', '7', '8')).isEqualTo(5678);
-        assertThat(DecoderUtils.decodeYear('2', '4', '5', '3')).isEqualTo(2453);
-        assertThat(DecoderUtils.decodeYear('2', '0', '0', '0')).isEqualTo(2000);
-        assertThat(DecoderUtils.decodeYear('2', '0', '0', '7')).isEqualTo(2007);
-        assertThat(DecoderUtils.decodeYear('2', '0', '0', '8')).isEqualTo(2008);
-        assertThat(DecoderUtils.decodeYear('2', '0', '1', '0')).isEqualTo(2010);
-        assertThat(DecoderUtils.decodeYear('2', '0', '2', '0')).isEqualTo(2020);
+    static Stream<Arguments> nominalDecodeYear() {
+        return Stream.of(
+            Arguments.of('1', '9', '9', '9', 1999),
+            Arguments.of('0', '7', '4', '7', 747),
+            Arguments.of('2', '5', '2', '5', 2525),
+            Arguments.of('5', '6', '7', '8', 5678),
+            Arguments.of('2', '4', '5', '3', 2453),
+            Arguments.of('2', '0', '0', '0', 2000),
+            Arguments.of('2', '0', '0', '7', 2007),
+            Arguments.of('2', '0', '0', '8', 2008),
+            Arguments.of('2', '0', '1', '0', 2010),
+            Arguments.of('2', '0', '2', '0', 2020));
     }
 
-    @Test
-    public void testRejectBogusYear() {
-        checkRejectYear('D', '0', '2', '3');
-        checkRejectYear('1', 'A', '2', '3');
-        checkRejectYear('1', '5', 'B', '3');
-        checkRejectYear('9', '8', '2', 'C');
-        checkRejectYear('S', 'A', 'F', 'd');
+    @ParameterizedTest
+    @MethodSource
+    void nominalDecodeYear(char c1, char c2, char c3, char c4, int exptected) throws Exception {
+        assertThat(DecoderUtils.decodeYear(c1, c2, c3, c4)).isEqualTo(exptected);
     }
 
-    private void checkRejectYear(char one, char two, char three, char four) {
-        try {
-            DecoderUtils.decodeYear(one, two, three, four);
-            fail(one + two + three + four + "is not a month");
-        } catch (DecodingException e) {
-            // expected
-        }
+    static Stream<Arguments> decodeShouldRejectBogusYear() {
+        return Stream.of(
+            Arguments.of('D', '0', '2', '3'),
+            Arguments.of('1', 'A', '2', '3'),
+            Arguments.of('1', '5', 'B', '3'),
+            Arguments.of('9', '8', '2', 'C'),
+            Arguments.of('S', 'A', 'F', 'd'));
     }
 
-    @Test
-    public void testDecodeZone() throws Exception {
-        assertThat(DecoderUtils.decodeZone('+', '0', '0', '0', '0')).isEqualTo(0);
-        assertThat(DecoderUtils.decodeZone('+', '0', '1', '0', '0')).isEqualTo(100);
-        assertThat(DecoderUtils.decodeZone('+', '0', '2', '0', '0')).isEqualTo(200);
-        assertThat(DecoderUtils.decodeZone('+', '0', '3', '0', '0')).isEqualTo(300);
-        assertThat(DecoderUtils.decodeZone('+', '0', '4', '0', '0')).isEqualTo(400);
-        assertThat(DecoderUtils.decodeZone('+', '0', '5', '0', '0')).isEqualTo(500);
-        assertThat(DecoderUtils.decodeZone('+', '0', '6', '0', '0')).isEqualTo(600);
-        assertThat(DecoderUtils.decodeZone('+', '0', '7', '0', '0')).isEqualTo(700);
-        assertThat(DecoderUtils.decodeZone('+', '0', '8', '0', '0')).isEqualTo(800);
-        assertThat(DecoderUtils.decodeZone('+', '0', '9', '0', '0')).isEqualTo(900);
-        assertThat(DecoderUtils.decodeZone('+', '1', '0', '0', '0')).isEqualTo(1000);
-        assertThat(DecoderUtils.decodeZone('+', '1', '1', '0', '0')).isEqualTo(1100);
-        assertThat(DecoderUtils.decodeZone('+', '1', '2', '0', '0')).isEqualTo(1200);
-        assertThat(DecoderUtils.decodeZone('+', '0', '0', '3', '0')).isEqualTo(30);
-        assertThat(DecoderUtils.decodeZone('+', '0', '1', '3', '0')).isEqualTo(130);
-        assertThat(DecoderUtils.decodeZone('+', '0', '2', '3', '0')).isEqualTo(230);
-        assertThat(DecoderUtils.decodeZone('+', '0', '3', '3', '0')).isEqualTo(330);
-        assertThat(DecoderUtils.decodeZone('+', '0', '4', '3', '0')).isEqualTo(430);
-        assertThat(DecoderUtils.decodeZone('+', '0', '5', '3', '0')).isEqualTo(530);
-        assertThat(DecoderUtils.decodeZone('+', '0', '6', '3', '0')).isEqualTo(630);
-        assertThat(DecoderUtils.decodeZone('+', '0', '7', '3', '0')).isEqualTo(730);
-        assertThat(DecoderUtils.decodeZone('+', '0', '8', '3', '0')).isEqualTo(830);
-        assertThat(DecoderUtils.decodeZone('+', '0', '9', '3', '0')).isEqualTo(930);
-        assertThat(DecoderUtils.decodeZone('+', '1', '0', '3', '0')).isEqualTo(1030);
-        assertThat(DecoderUtils.decodeZone('+', '1', '1', '3', '0')).isEqualTo(1130);
-        assertThat(DecoderUtils.decodeZone('+', '1', '1', '1', '1')).isEqualTo(1111);
-        assertThat(DecoderUtils.decodeZone('-', '0', '0', '0', '0')).isEqualTo(0);
-        assertThat(DecoderUtils.decodeZone('-', '0', '1', '0', '0')).isEqualTo(-100);
-        assertThat(DecoderUtils.decodeZone('-', '0', '2', '0', '0')).isEqualTo(-200);
-        assertThat(DecoderUtils.decodeZone('-', '0', '3', '0', '0')).isEqualTo(-300);
-        assertThat(DecoderUtils.decodeZone('-', '0', '4', '0', '0')).isEqualTo(-400);
-        assertThat(DecoderUtils.decodeZone('-', '0', '5', '0', '0')).isEqualTo(-500);
-        assertThat(DecoderUtils.decodeZone('-', '0', '6', '0', '0')).isEqualTo(-600);
-        assertThat(DecoderUtils.decodeZone('-', '0', '7', '0', '0')).isEqualTo(-700);
-        assertThat(DecoderUtils.decodeZone('-', '0', '8', '0', '0')).isEqualTo(-800);
-        assertThat(DecoderUtils.decodeZone('-', '0', '9', '0', '0')).isEqualTo(-900);
-        assertThat(DecoderUtils.decodeZone('-', '1', '0', '0', '0')).isEqualTo(-1000);
-        assertThat(DecoderUtils.decodeZone('-', '1', '1', '0', '0')).isEqualTo(-1100);
-        assertThat(DecoderUtils.decodeZone('-', '1', '2', '0', '0')).isEqualTo(-1200);
-        assertThat(DecoderUtils.decodeZone('-', '0', '0', '3', '0')).isEqualTo(-30);
-        assertThat(DecoderUtils.decodeZone('-', '0', '1', '3', '0')).isEqualTo(-130);
-        assertThat(DecoderUtils.decodeZone('-', '0', '2', '3', '0')).isEqualTo(-230);
-        assertThat(DecoderUtils.decodeZone('-', '0', '3', '3', '0')).isEqualTo(-330);
-        assertThat(DecoderUtils.decodeZone('-', '0', '4', '3', '0')).isEqualTo(-430);
-        assertThat(DecoderUtils.decodeZone('-', '0', '5', '3', '0')).isEqualTo(-530);
-        assertThat(DecoderUtils.decodeZone('-', '0', '6', '3', '0')).isEqualTo(-630);
-        assertThat(DecoderUtils.decodeZone('-', '0', '7', '3', '0')).isEqualTo(-730);
-        assertThat(DecoderUtils.decodeZone('-', '0', '8', '3', '0')).isEqualTo(-830);
-        assertThat(DecoderUtils.decodeZone('-', '0', '9', '3', '0')).isEqualTo(-930);
-        assertThat(DecoderUtils.decodeZone('-', '1', '0', '3', '0')).isEqualTo(-1030);
-        assertThat(DecoderUtils.decodeZone('-', '1', '1', '3', '0')).isEqualTo(-1130);
-        assertThat(DecoderUtils.decodeZone('-', '1', '1', '1', '1')).isEqualTo(-1111);
+    @ParameterizedTest
+    @MethodSource
+    void decodeShouldRejectBogusYear(char one, char two, char three, char four) {
+        assertThatThrownBy(() -> DecoderUtils.decodeYear(one, two, three, four))
+            .isInstanceOf(DecodingException.class);
+    }
 
+    static Stream<Arguments> nominalDecodeZone() {
+        return Stream.of(
+            Arguments.of('+', '0', '0', '0', '0', 0),
+            Arguments.of('+', '0', '1', '0', '0', 100),
+            Arguments.of('+', '0', '2', '0', '0', 200),
+            Arguments.of('+', '0', '3', '0', '0', 300),
+            Arguments.of('+', '0', '4', '0', '0', 400),
+            Arguments.of('+', '0', '5', '0', '0', 500),
+            Arguments.of('+', '0', '6', '0', '0', 600),
+            Arguments.of('+', '0', '7', '0', '0', 700),
+            Arguments.of('+', '0', '8', '0', '0', 800),
+            Arguments.of('+', '0', '9', '0', '0', 900),
+            Arguments.of('+', '1', '0', '0', '0', 1000),
+            Arguments.of('+', '1', '1', '0', '0', 1100),
+            Arguments.of('+', '1', '2', '0', '0', 1200),
+            Arguments.of('+', '0', '0', '3', '0', 30),
+            Arguments.of('+', '0', '1', '3', '0', 130),
+            Arguments.of('+', '0', '2', '3', '0', 230),
+            Arguments.of('+', '0', '3', '3', '0', 330),
+            Arguments.of('+', '0', '4', '3', '0', 430),
+            Arguments.of('+', '0', '5', '3', '0', 530),
+            Arguments.of('+', '0', '6', '3', '0', 630),
+            Arguments.of('+', '0', '7', '3', '0', 730),
+            Arguments.of('+', '0', '8', '3', '0', 830),
+            Arguments.of('+', '0', '9', '3', '0', 930),
+            Arguments.of('+', '1', '0', '3', '0', 1030),
+            Arguments.of('+', '1', '1', '3', '0', 1130),
+            Arguments.of('+', '1', '1', '1', '1', 1111),
+            Arguments.of('-', '0', '0', '0', '0', 0),
+            Arguments.of('-', '0', '1', '0', '0', -100),
+            Arguments.of('-', '0', '2', '0', '0', -200),
+            Arguments.of('-', '0', '3', '0', '0', -300),
+            Arguments.of('-', '0', '4', '0', '0', -400),
+            Arguments.of('-', '0', '5', '0', '0', -500),
+            Arguments.of('-', '0', '6', '0', '0', -600),
+            Arguments.of('-', '0', '7', '0', '0', -700),
+            Arguments.of('-', '0', '8', '0', '0', -800),
+            Arguments.of('-', '0', '9', '0', '0', -900),
+            Arguments.of('-', '1', '0', '0', '0', -1000),
+            Arguments.of('-', '1', '1', '0', '0', -1100),
+            Arguments.of('-', '1', '2', '0', '0', -1200),
+            Arguments.of('-', '0', '0', '3', '0', -30),
+            Arguments.of('-', '0', '1', '3', '0', -130),
+            Arguments.of('-', '0', '2', '3', '0', -230),
+            Arguments.of('-', '0', '3', '3', '0', -330),
+            Arguments.of('-', '0', '4', '3', '0', -430),
+            Arguments.of('-', '0', '5', '3', '0', -530),
+            Arguments.of('-', '0', '6', '3', '0', -630),
+            Arguments.of('-', '0', '7', '3', '0', -730),
+            Arguments.of('-', '0', '8', '3', '0', -830),
+            Arguments.of('-', '0', '9', '3', '0', -930),
+            Arguments.of('-', '1', '0', '3', '0', -1030),
+            Arguments.of('-', '1', '1', '3', '0', -1130),
+            Arguments.of('-', '1', '1', '1', '1', -1111));
     }
 
-    @Test
-    public void testBogusZones() {
-        checkRejectZone(" 0000");
-        checkRejectZone(" GMT ");
-        checkRejectZone("DANG!");
-        checkRejectZone("+a000");
-        checkRejectZone("+0b00");
-        checkRejectZone("+00c0");
-        checkRejectZone("+000d");
-        checkRejectZone("-a000");
-        checkRejectZone("-0b00");
-        checkRejectZone("-00c0");
-        checkRejectZone("-000d");
+    @ParameterizedTest
+    @MethodSource
+    void nominalDecodeZone(char c1, char c2, char c3, char c4, char c5, int exptected) throws Exception {
+        assertThat(DecoderUtils.decodeZone(c1, c2, c3, c4, c5)).isEqualTo(exptected);
     }
 
-    private void checkRejectZone(String zone) {
-        try {
-            DecoderUtils.decodeZone(zone.charAt(0), zone.charAt(1), zone
-                    .charAt(2), zone.charAt(3), zone.charAt(4));
-            fail(zone + "is not a timezone");
-        } catch (DecodingException e) {
-            // expected
-        }
+    static Stream<Arguments> decodeShouldRejectBogusZone() {
+        return Stream.of(
+            Arguments.of(' ', '0', '0', '0', '0'),
+            Arguments.of(' ', 'G', 'M', 'T', ' '),
+            Arguments.of('D', 'A', 'N', 'G', '!'),
+            Arguments.of('+', 'a', '0', '0', '0'),
+            Arguments.of('+', '0', 'b', '0', '0'),
+            Arguments.of('+', '0', '0', 'c', '0'),
+            Arguments.of('+', '0', '0', '0', 'd'),
+            Arguments.of('-', 'a', '0', '0', '0'),
+            Arguments.of('-', '0', 'b', '0', '0'),
+            Arguments.of('-', '0', '0', 'c', '0'),
+            Arguments.of('-', '0', '0', '0', 'd'));
     }
 
-    @Test
-    public void testIsSimpleDigit() {
-        assertThat(DecoderUtils.isSimpleDigit('0')).isTrue();
-        assertThat(DecoderUtils.isSimpleDigit('1')).isTrue();
-        assertThat(DecoderUtils.isSimpleDigit('2')).isTrue();
-        assertThat(DecoderUtils.isSimpleDigit('3')).isTrue();
-        assertThat(DecoderUtils.isSimpleDigit('4')).isTrue();
-        assertThat(DecoderUtils.isSimpleDigit('5')).isTrue();
-        assertThat(DecoderUtils.isSimpleDigit('6')).isTrue();
-        assertThat(DecoderUtils.isSimpleDigit('7')).isTrue();
-        assertThat(DecoderUtils.isSimpleDigit('8')).isTrue();
-        assertThat(DecoderUtils.isSimpleDigit('9')).isTrue();
-
-        assertThat(DecoderUtils.isSimpleDigit('/')).isFalse();
-        assertThat(DecoderUtils.isSimpleDigit('.')).isFalse();
-        assertThat(DecoderUtils.isSimpleDigit('-')).isFalse();
-        assertThat(DecoderUtils.isSimpleDigit('+')).isFalse();
-        assertThat(DecoderUtils.isSimpleDigit(',')).isFalse();
-        assertThat(DecoderUtils.isSimpleDigit('*')).isFalse();
-        assertThat(DecoderUtils.isSimpleDigit(':')).isFalse();
-        assertThat(DecoderUtils.isSimpleDigit(';')).isFalse();
-        assertThat(DecoderUtils.isSimpleDigit('<')).isFalse();
-        assertThat(DecoderUtils.isSimpleDigit('=')).isFalse();
-        assertThat(DecoderUtils.isSimpleDigit('>')).isFalse();
-        assertThat(DecoderUtils.isSimpleDigit('A')).isFalse();
-        assertThat(DecoderUtils.isSimpleDigit('B')).isFalse();
+    @ParameterizedTest
+    @MethodSource
+    void decodeShouldRejectBogusZone(char c1, char c2, char c3, char c4, char c5) {
+        assertThatThrownBy(() -> DecoderUtils.decodeZone(c1, c2, c3, c4, c5))
+            .isInstanceOf(DecodingException.class);
     }
 
-    @Test
-    public void testDecodeNumber() throws Exception {
-        assertThat(DecoderUtils.decodeNumber('0', '1')).isEqualTo(1);
-        assertThat(DecoderUtils.decodeNumber('0', '2')).isEqualTo(2);
-        assertThat(DecoderUtils.decodeNumber('0', '3')).isEqualTo(3);
-        assertThat(DecoderUtils.decodeNumber('0', '4')).isEqualTo(4);
-        assertThat(DecoderUtils.decodeNumber('0', '5')).isEqualTo(5);
-        assertThat(DecoderUtils.decodeNumber('0', '6')).isEqualTo(6);
-        assertThat(DecoderUtils.decodeNumber('0', '7')).isEqualTo(7);
-        assertThat(DecoderUtils.decodeNumber('0', '8')).isEqualTo(8);
-        assertThat(DecoderUtils.decodeNumber('0', '9')).isEqualTo(9);
-        assertThat(DecoderUtils.decodeNumber('1', '9')).isEqualTo(19);
-        assertThat(DecoderUtils.decodeNumber('2', '8')).isEqualTo(28);
-        assertThat(DecoderUtils.decodeNumber('3', '7')).isEqualTo(37);
-        assertThat(DecoderUtils.decodeNumber('4', '6')).isEqualTo(46);
-        assertThat(DecoderUtils.decodeNumber('5', '5')).isEqualTo(55);
-        assertThat(DecoderUtils.decodeNumber('6', '4')).isEqualTo(64);
-        assertThat(DecoderUtils.decodeNumber('7', '3')).isEqualTo(73);
-        assertThat(DecoderUtils.decodeNumber('8', '2')).isEqualTo(82);
-        assertThat(DecoderUtils.decodeNumber('9', '1')).isEqualTo(91);
+    @ParameterizedTest
+    @ValueSource(chars = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'})
+    void nominalSimpleDigit(char c) {
+        assertThat(DecoderUtils.isSimpleDigit(c)).isTrue();
     }
 
-    @Test
-    public void testRejectNumber() {
-        checkRejectNumber("A1");
-        checkRejectNumber("1A");
-        checkRejectNumber("AA");
+    @ParameterizedTest
+    @ValueSource(chars = {'/', '.', '-', '+', ',', '*', ':', ';', '<', '=', '>', 'A', 'B'})
+    void simpleDigitShouldReturnFalseOnNonDigit(char c) {
+        assertThat(DecoderUtils.isSimpleDigit(c)).isFalse();
+    }
+
+    static Stream<Arguments> nominalDecodeNumber() {
+        return Stream.of(
+            Arguments.of('0', '1', 1),
+            Arguments.of('0', '2', 2),
+            Arguments.of('0', '3', 3),
+            Arguments.of('0', '4', 4),
+            Arguments.of('0', '5', 5),
+            Arguments.of('0', '6', 6),
+            Arguments.of('0', '7', 7),
+            Arguments.of('0', '8', 8),
+            Arguments.of('0', '9', 9),
+            Arguments.of('1', '9', 19),
+            Arguments.of('2', '8', 28),
+            Arguments.of('3', '7', 37),
+            Arguments.of('4', '6', 46),
+            Arguments.of('5', '5', 55),
+            Arguments.of('6', '4', 64),
+            Arguments.of('7', '3', 73),
+            Arguments.of('8', '2', 82),
+            Arguments.of('9', '1', 91));
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void nominalDecodeNumber(char c1, char c2, int expected) throws DecodingException {
+        assertThat(DecoderUtils.decodeNumber(c1, c2)).isEqualTo(expected);
     }
 
-    private void checkRejectNumber(String number) {
-        try {
-            DecoderUtils.decodeNumber(number.charAt(0), number.charAt(1));
-            fail(number + "is not a number");
-        } catch (DecodingException e) {
-            // expected
-        }
+    @ParameterizedTest
+    @ValueSource(strings = {"A1", "1A", "AA"})
+    void decodeShouldRejectBogusNumbers(String number) {
+        assertThatThrownBy(() -> DecoderUtils.decodeNumber(number.charAt(0), number.charAt(1)))
+            .isInstanceOf(DecodingException.class);
     }
 }


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


[james-project] 22/27: JAMES-2979 retry storing mail with a boundedElastic scheduler in Maildispatcher

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 fb23ed7c703a6b8afdb0bcb51c5b6446815ba2fc
Author: Rémi KOWALSKI <rk...@linagora.com>
AuthorDate: Thu Nov 21 14:07:25 2019 +0100

    JAMES-2979 retry storing mail with a boundedElastic scheduler in Maildispatcher
---
 .../transport/mailets/delivery/MailDispatcher.java | 26 ++++++++++++++++++----
 1 file changed, 22 insertions(+), 4 deletions(-)

diff --git a/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/delivery/MailDispatcher.java b/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/delivery/MailDispatcher.java
index 339323f..4737012 100644
--- a/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/delivery/MailDispatcher.java
+++ b/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/delivery/MailDispatcher.java
@@ -18,6 +18,7 @@
  ****************************************************************/
 package org.apache.james.transport.mailets.delivery;
 
+import java.time.Duration;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
@@ -36,20 +37,27 @@ import org.apache.mailet.base.RFC2822Headers;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.github.fge.lambdas.runnable.ThrowingRunnable;
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import reactor.core.publisher.Mono;
+import reactor.core.scheduler.Scheduler;
+import reactor.core.scheduler.Schedulers;
 
 public class MailDispatcher {
     private static final Logger LOGGER = LoggerFactory.getLogger(MailDispatcher.class);
-    public static final String[] NO_HEADERS = {};
+    private static final String[] NO_HEADERS = {};
+    private static final int RETRIES = 3;
+    private static final Duration FIRST_BACKOFF = Duration.ofMillis(200);
+    private static final Duration MAX_BACKOFF = Duration.ofSeconds(1);
 
     public static Builder builder() {
         return new Builder();
     }
 
     public static class Builder {
-        public static final boolean CONSUME = true;
+        static final boolean CONSUME = true;
         private MailStore mailStore;
         private Optional<Boolean> consume = Optional.empty();
         private MailetContext mailetContext;
@@ -80,11 +88,13 @@ public class MailDispatcher {
     private final MailStore mailStore;
     private final boolean consume;
     private final MailetContext mailetContext;
+    private final Scheduler scheduler;
 
     private MailDispatcher(MailStore mailStore, boolean consume, MailetContext mailetContext) {
         this.mailStore = mailStore;
         this.consume = consume;
         this.mailetContext = mailetContext;
+        this.scheduler = Schedulers.boundedElastic();
     }
 
     public void dispatch(Mail mail) throws MessagingException {
@@ -130,8 +140,8 @@ public class MailDispatcher {
                 Map<String, List<String>> savedHeaders = saveHeaders(mail, recipient);
 
                 addSpecificHeadersForRecipient(mail, message, recipient);
-                mailStore.storeMail(recipient, mail);
-                
+                storeMailWithRetry(mail, recipient).block();
+
                 restoreHeaders(mail.getMessage(), savedHeaders);
             } catch (Exception ex) {
                 LOGGER.error("Error while storing mail.", ex);
@@ -141,6 +151,14 @@ public class MailDispatcher {
         return errors;
     }
 
+    private Mono<Void> storeMailWithRetry(Mail mail, MailAddress recipient) {
+       return Mono.fromRunnable((ThrowingRunnable)() -> mailStore.storeMail(recipient, mail))
+           .doOnError(error -> LOGGER.error("Error While storing mail.", error))
+           .subscribeOn(scheduler)
+           .retryBackoff(RETRIES, FIRST_BACKOFF, MAX_BACKOFF, scheduler)
+           .then();
+    }
+
     private Map<String, List<String>> saveHeaders(Mail mail, MailAddress recipient) throws MessagingException {
         ImmutableMap.Builder<String, List<String>> backup = ImmutableMap.builder();
         Collection<String> headersToSave = mail.getPerRecipientSpecificHeaders().getHeaderNamesForRecipient(recipient);


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


[james-project] 17/27: JAMES-2576 Rework HealthChecks routes status code

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 cdbc0ee65faa04a60c4de6fa8bbc65fd89a650be
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Thu Dec 5 12:43:32 2019 +0700

    JAMES-2576 Rework HealthChecks routes status code
    
    Return 503 upon unhealthy checks.
    
    Return 200 upon degraded checks as service can still be used.
---
 CHANGELOG.md                                               |  3 ++-
 .../org/apache/james/GuiceLifecycleHeathCheckTest.java     |  2 +-
 .../apache/james/webadmin/routes/HealthCheckRoutes.java    | 14 ++++++++------
 .../james/webadmin/routes/HealthCheckRoutesTest.java       | 14 +++++++-------
 src/site/markdown/server/manage-webadmin.md                |  8 ++++----
 5 files changed, 22 insertions(+), 19 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8a9ed39..2860550 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -22,7 +22,8 @@ of tasks being currently executed.
 - JAMES-2855 Multiple library/plugin/docker images/build tool upgrades
 - By default the cassandra keyspace creation by James is now disabled by default. This allow to have credentials limited to a keyspace. It can be enabled by setting cassandra.keyspace.create=true in the cassandra.properties file.
 - Usernames are assumed to be always lower cased. Many users recently complained about mails non received when sending to upper cased local recipients. We decided to simplify the handling of case for local recipients and users by always storing them lower cased.
-  
+- Unhealthy health checks now return HTTP 503 instead of 500, degraded now returns 200 instead of 500. See JAMES-2576.
+
 ### Fixed
 - JAMES-2828 & JAMES-2929 bugs affecting JDBCMailRepository usage with PostgresSQL thanks to Jörg Thomas & Sergey B
 - JAMES-2936 Creating a mailbox using consecutive delimiter character leads to creation of list of unnamed mailbox
diff --git a/server/container/guice/memory-guice/src/test/java/org/apache/james/GuiceLifecycleHeathCheckTest.java b/server/container/guice/memory-guice/src/test/java/org/apache/james/GuiceLifecycleHeathCheckTest.java
index 785925f..d2af157 100644
--- a/server/container/guice/memory-guice/src/test/java/org/apache/james/GuiceLifecycleHeathCheckTest.java
+++ b/server/container/guice/memory-guice/src/test/java/org/apache/james/GuiceLifecycleHeathCheckTest.java
@@ -128,7 +128,7 @@ class GuiceLifecycleHeathCheckTest {
                 when()
                     .get("/healthcheck")
                     .then()
-                    .statusCode(HttpStatus.INTERNAL_SERVER_ERROR_500);
+                    .statusCode(HttpStatus.SERVICE_UNAVAILABLE_503);
             } finally {
                 latch.countDown();
                 stopMono.block();
diff --git a/server/protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/routes/HealthCheckRoutes.java b/server/protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/routes/HealthCheckRoutes.java
index 8d53407..45d9543 100644
--- a/server/protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/routes/HealthCheckRoutes.java
+++ b/server/protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/routes/HealthCheckRoutes.java
@@ -26,6 +26,7 @@ import javax.inject.Inject;
 import javax.ws.rs.GET;
 import javax.ws.rs.Path;
 
+import org.apache.commons.lang3.NotImplementedException;
 import org.apache.james.core.healthcheck.HealthCheck;
 import org.apache.james.core.healthcheck.Result;
 import org.apache.james.core.healthcheck.ResultStatus;
@@ -138,12 +139,13 @@ public class HealthCheckRoutes implements PublicRoutes {
     
     private int getCorrespondingStatusCode(ResultStatus resultStatus) {
         switch (resultStatus) {
-        case HEALTHY:
-            return HttpStatus.OK_200;
-        case DEGRADED:
-        case UNHEALTHY:
-        default:
-            return HttpStatus.INTERNAL_SERVER_ERROR_500;
+            case HEALTHY:
+            case DEGRADED:
+                return HttpStatus.OK_200;
+            case UNHEALTHY:
+                return HttpStatus.SERVICE_UNAVAILABLE_503;
+            default:
+                throw new NotImplementedException(resultStatus + " is not supported");
         }
     }
 
diff --git a/server/protocols/webadmin/webadmin-core/src/test/java/org/apache/james/webadmin/routes/HealthCheckRoutesTest.java b/server/protocols/webadmin/webadmin-core/src/test/java/org/apache/james/webadmin/routes/HealthCheckRoutesTest.java
index 869a029..359724d 100644
--- a/server/protocols/webadmin/webadmin-core/src/test/java/org/apache/james/webadmin/routes/HealthCheckRoutesTest.java
+++ b/server/protocols/webadmin/webadmin-core/src/test/java/org/apache/james/webadmin/routes/HealthCheckRoutesTest.java
@@ -162,7 +162,7 @@ public class HealthCheckRoutesTest {
         String retrieveBody = when()
                 .get()
             .then()
-                .statusCode(HttpStatus.INTERNAL_SERVER_ERROR_500)
+                .statusCode(HttpStatus.SERVICE_UNAVAILABLE_503)
                 .extract()
                 .body().asString();
 
@@ -194,7 +194,7 @@ public class HealthCheckRoutesTest {
         String retrieveBody = when()
                 .get()
             .then()
-                .statusCode(HttpStatus.INTERNAL_SERVER_ERROR_500)
+                .statusCode(HttpStatus.SERVICE_UNAVAILABLE_503)
                 .extract()
                 .body().asString();
 
@@ -226,7 +226,7 @@ public class HealthCheckRoutesTest {
         String retrieveBody = when()
                 .get()
             .then()
-                .statusCode(HttpStatus.INTERNAL_SERVER_ERROR_500)
+                .statusCode(HttpStatus.OK_200)
                 .extract()
                 .body().asString();
 
@@ -258,7 +258,7 @@ public class HealthCheckRoutesTest {
         String retrieveBody = when()
                 .get()
             .then()
-                .statusCode(HttpStatus.INTERNAL_SERVER_ERROR_500)
+                .statusCode(HttpStatus.OK_200)
                 .extract()
                 .body().asString();
 
@@ -289,7 +289,7 @@ public class HealthCheckRoutesTest {
         String retrieveBody = when()
                 .get()
             .then()
-                .statusCode(HttpStatus.INTERNAL_SERVER_ERROR_500)
+                .statusCode(HttpStatus.SERVICE_UNAVAILABLE_503)
                 .extract()
                 .body().asString();
 
@@ -338,7 +338,7 @@ public class HealthCheckRoutesTest {
         .when()
             .get("/checks/{componentName}")
         .then()
-            .statusCode(HttpStatus.INTERNAL_SERVER_ERROR_500)
+            .statusCode(HttpStatus.OK_200)
             .body("componentName", equalTo(NAME_1))
             .body("escapedComponentName", equalTo(NAME_1))
             .body("status", equalTo(ResultStatus.DEGRADED.getValue()))
@@ -354,7 +354,7 @@ public class HealthCheckRoutesTest {
         .when()
             .get("/checks/{componentName}")
         .then()
-            .statusCode(HttpStatus.INTERNAL_SERVER_ERROR_500)
+            .statusCode(HttpStatus.SERVICE_UNAVAILABLE_503)
             .body("componentName", equalTo(NAME_1))
             .body("escapedComponentName", equalTo(NAME_1))
             .body("status", equalTo(ResultStatus.UNHEALTHY.getValue()))
diff --git a/src/site/markdown/server/manage-webadmin.md b/src/site/markdown/server/manage-webadmin.md
index 746ba37..a4ad547 100644
--- a/src/site/markdown/server/manage-webadmin.md
+++ b/src/site/markdown/server/manage-webadmin.md
@@ -99,8 +99,8 @@ Supported health checks include:
 
 Response codes:
 
- - 200: All checks have answered with a Healthy status
- - 500: At least one check have answered with a Unhealthy or Degraded status
+ - 200: All checks have answered with a Healthy or Degraded status. James services can still be used.
+ - 503: At least one check have answered with a Unhealthy status
 
 ### Check single component
 
@@ -123,9 +123,9 @@ Will return the component's name, the component's escaped name, the health statu
 
 Response codes:
 
- - 200: The check has answered with a Healthy status.
+ - 200: The check has answered with a Healthy or Degraded status.
  - 404: A component with the given name was not found.
- - 500: The check has anwered with a Unhealthy or Degraded status.
+ - 503: The check has anwered with a Unhealthy status.
  
 ### List all health checks
  


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


[james-project] 19/27: JAMES-2979 add mail should be well received integration test for guice-cassandra

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 a5bb8ebe9439bc03e9f70f172a550191bb93f1f7
Author: Rémi KOWALSKI <rk...@linagora.com>
AuthorDate: Wed Nov 20 15:56:36 2019 +0100

    JAMES-2979 add mail should be well received integration test for guice-cassandra
---
 server/container/guice/cassandra-guice/pom.xml     |  6 +++
 .../apache/james/WithCassandraBlobStoreTest.java   | 43 ++++++++++++++++++++++
 .../src/test/resources/listeners.xml               |  3 ++
 .../methods/integration/SpamAssassinModule.java    | 11 ------
 .../memory/MemorySpamAssassinContractTest.java     |  3 +-
 .../src/test/resources/listeners.xml               |  3 ++
 .../src/test/resources/listeners.xml               |  3 ++
 7 files changed, 59 insertions(+), 13 deletions(-)

diff --git a/server/container/guice/cassandra-guice/pom.xml b/server/container/guice/cassandra-guice/pom.xml
index 76b08f7..347c693 100644
--- a/server/container/guice/cassandra-guice/pom.xml
+++ b/server/container/guice/cassandra-guice/pom.xml
@@ -105,6 +105,12 @@
         </dependency>
         <dependency>
             <groupId>${james.groupId}</groupId>
+            <artifactId>apache-james-spamassassin</artifactId>
+            <type>test-jar</type>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
             <artifactId>apache-mailet-icalendar</artifactId>
             <version>${project.version}</version>
         </dependency>
diff --git a/server/container/guice/cassandra-guice/src/test/java/org/apache/james/WithCassandraBlobStoreTest.java b/server/container/guice/cassandra-guice/src/test/java/org/apache/james/WithCassandraBlobStoreTest.java
new file mode 100644
index 0000000..7157928
--- /dev/null
+++ b/server/container/guice/cassandra-guice/src/test/java/org/apache/james/WithCassandraBlobStoreTest.java
@@ -0,0 +1,43 @@
+/*
+ * 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 org.apache.james.jmap.draft.JmapJamesServerContract;
+import org.apache.james.jmap.draft.methods.integration.SpamAssassinModuleExtension;
+import org.apache.james.modules.TestJMAPServerModule;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+class WithCassandraBlobStoreTest implements JmapJamesServerContract, MailsShouldBeWellReceived, JamesServerContract {
+
+    static int LIMIT_TO_10_MESSAGES = 10;
+
+    @RegisterExtension
+    static JamesServerExtension jamesServerExtension = new JamesServerBuilder()
+        .extension(new DockerElasticSearchExtension())
+        .extension(new CassandraExtension())
+        .extension(new SpamAssassinModuleExtension())
+        .server(configuration -> GuiceJamesServer
+            .forConfiguration(configuration)
+            .combineWith(CassandraJamesServerMain.ALL_BUT_JMX_CASSANDRA_MODULE)
+            .overrideWith(new TestJMAPServerModule(LIMIT_TO_10_MESSAGES))
+            .overrideWith(JmapJamesServerContract.DOMAIN_LIST_CONFIGURATION_MODULE))
+        .build();
+
+}
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..cac2777 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,7 @@
       <name>second</name>
     </configuration>
   </listener>
+  <listener>
+    <class>org.apache.james.mailbox.spamassassin.SpamAssassinListener</class>
+  </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/SpamAssassinModule.java b/server/protocols/jmap-draft-integration-testing/jmap-draft-integration-testing-common/src/test/java/org/apache/james/jmap/draft/methods/integration/SpamAssassinModule.java
index c14f979..c1edd25 100644
--- a/server/protocols/jmap-draft-integration-testing/jmap-draft-integration-testing-common/src/test/java/org/apache/james/jmap/draft/methods/integration/SpamAssassinModule.java
+++ b/server/protocols/jmap-draft-integration-testing/jmap-draft-integration-testing-common/src/test/java/org/apache/james/jmap/draft/methods/integration/SpamAssassinModule.java
@@ -55,17 +55,6 @@ public class SpamAssassinModule extends AbstractModule {
                 new MailetConfigurationOverride(
                     org.apache.james.transport.mailets.SpamAssassin.class,
                     spamAssassinMailetConfig()));
-
-        Multibinder.newSetBinder(binder(), MailboxListener.GroupMailboxListener.class)
-            .addBinding()
-            .to(SpamAssassinListener.class);
-    }
-
-    @Provides
-    @Singleton
-    SpamAssassinListener provideSpamAssassinListener(SpamAssassin spamAssassin, SystemMailboxesProvider systemMailboxesProvider, MailboxManager mailboxManager, MailboxSessionMapperFactory mapperFactory) {
-        return new SpamAssassinListener(spamAssassin, systemMailboxesProvider, mailboxManager, mapperFactory,
-            MailboxListener.ExecutionMode.SYNCHRONOUS);
     }
 
     @Provides
diff --git a/server/protocols/jmap-draft-integration-testing/memory-jmap-draft-integration-testing/src/test/java/org/apache/james/jmap/memory/MemorySpamAssassinContractTest.java b/server/protocols/jmap-draft-integration-testing/memory-jmap-draft-integration-testing/src/test/java/org/apache/james/jmap/memory/MemorySpamAssassinContractTest.java
index 01f0ef6..60b84de 100644
--- a/server/protocols/jmap-draft-integration-testing/memory-jmap-draft-integration-testing/src/test/java/org/apache/james/jmap/memory/MemorySpamAssassinContractTest.java
+++ b/server/protocols/jmap-draft-integration-testing/memory-jmap-draft-integration-testing/src/test/java/org/apache/james/jmap/memory/MemorySpamAssassinContractTest.java
@@ -35,10 +35,9 @@ class MemorySpamAssassinContractTest implements SpamAssassinContract {
 
     private static final int LIMIT_TO_20_MESSAGES = 20;
 
-    private static final SpamAssassinModuleExtension spamAssassinExtension = new SpamAssassinModuleExtension();
     @RegisterExtension
     static JamesServerExtension testExtension = new JamesServerBuilder()
-        .extension(spamAssassinExtension)
+        .extension(new SpamAssassinModuleExtension())
         .server(configuration -> GuiceJamesServer.forConfiguration(configuration)
             .combineWith(MemoryJamesServerMain.IN_MEMORY_SERVER_AGGREGATE_MODULE)
             .overrideWith(new TestJMAPServerModule(LIMIT_TO_20_MESSAGES))
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 59e3fec..ae2e80a 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
@@ -43,4 +43,7 @@
       <name>second</name>
     </configuration>
   </listener>
+  <listener>
+    <class>org.apache.james.mailbox.spamassassin.SpamAssassinListener</class>
+  </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 ff2e517..cac2777 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
@@ -46,4 +46,7 @@
       <name>second</name>
     </configuration>
   </listener>
+  <listener>
+    <class>org.apache.james.mailbox.spamassassin.SpamAssassinListener</class>
+  </listener>
 </listeners>
\ No newline at end of file


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


[james-project] 15/27: [Refactoring] Strongly type SectionType

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 4e68b9154be319c1a206480180f53c002e8f4e8c
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Mon Dec 9 14:34:59 2019 +0700

    [Refactoring] Strongly type SectionType
    
    This integer representation is common between FetchPartPartDecoder and
    BodyFetchElement thus no conversion logic is required.
---
 .../james/imap/api/message/BodyFetchElement.java   | 37 +++--------
 .../apache/james/imap/api/message/SectionType.java | 29 +++++++++
 .../james/imap/decode/FetchPartPathDecoder.java    | 64 +++++++------------
 .../imap/decode/parser/FetchCommandParser.java     | 23 +------
 .../imap/processor/fetch/FetchDataConverter.java   | 15 ++---
 .../imap/processor/fetch/FetchResponseBuilder.java | 17 ++---
 .../imap/decode/FetchPartPathDecoderTest.java      | 73 +++++++++++-----------
 .../parser/FetchCommandParserPartialFetchTest.java |  5 +-
 .../processor/fetch/FetchDataConverterTest.java    |  9 ++-
 9 files changed, 123 insertions(+), 149 deletions(-)

diff --git a/protocols/imap/src/main/java/org/apache/james/imap/api/message/BodyFetchElement.java b/protocols/imap/src/main/java/org/apache/james/imap/api/message/BodyFetchElement.java
index 3990ed9..9dee961 100644
--- a/protocols/imap/src/main/java/org/apache/james/imap/api/message/BodyFetchElement.java
+++ b/protocols/imap/src/main/java/org/apache/james/imap/api/message/BodyFetchElement.java
@@ -29,24 +29,11 @@ import com.google.common.base.MoreObjects;
 import com.google.common.collect.ImmutableList;
 
 public class BodyFetchElement {
+    private static final BodyFetchElement rfc822 = new BodyFetchElement(ImapConstants.FETCH_RFC822, SectionType.CONTENT, null, null, null, null);
 
-    public static final int TEXT = 0;
+    private static final BodyFetchElement rfc822Header = new BodyFetchElement(ImapConstants.FETCH_RFC822_HEADER, SectionType.HEADER, null, null, null, null);
 
-    public static final int MIME = 1;
-
-    public static final int HEADER = 2;
-
-    public static final int HEADER_FIELDS = 3;
-
-    public static final int HEADER_NOT_FIELDS = 4;
-
-    public static final int CONTENT = 5;
-
-    private static final BodyFetchElement rfc822 = new BodyFetchElement(ImapConstants.FETCH_RFC822, CONTENT, null, null, null, null);
-
-    private static final BodyFetchElement rfc822Header = new BodyFetchElement(ImapConstants.FETCH_RFC822_HEADER, HEADER, null, null, null, null);
-
-    private static final BodyFetchElement rfc822Text = new BodyFetchElement(ImapConstants.FETCH_RFC822_TEXT, TEXT, null, null, null, null);
+    private static final BodyFetchElement rfc822Text = new BodyFetchElement(ImapConstants.FETCH_RFC822_TEXT, SectionType.TEXT, null, null, null, null);
 
     public static final BodyFetchElement createRFC822() {
         return rfc822;
@@ -61,18 +48,13 @@ public class BodyFetchElement {
     }
 
     private final Long firstOctet;
-
     private final Long numberOfOctets;
-
     private final String name;
-
-    private final int sectionType;
-
+    private final SectionType sectionType;
     private final int[] path;
-
     private final Collection<String> fieldNames;
 
-    public BodyFetchElement(String name, int sectionType, int[] path, Collection<String> fieldNames, Long firstOctet, Long numberOfOctets) {
+    public BodyFetchElement(String name, SectionType sectionType, int[] path, Collection<String> fieldNames, Long firstOctet, Long numberOfOctets) {
         this.name = name;
         this.sectionType = sectionType;
         this.fieldNames = fieldNames;
@@ -88,8 +70,8 @@ public class BodyFetchElement {
     /**
      * Gets field names.
      * 
-     * @return <code>String</code> collection, when {@link #HEADER_FIELDS} or
-     *         {@link #HEADER_NOT_FIELDS} or null otherwise
+     * @return <code>String</code> collection, when {@link SectionType#HEADER_FIELDS} or
+     *         {@link SectionType#HEADER_NOT_FIELDS} or null otherwise
      */
     public final Collection<String> getFieldNames() {
         return fieldNames;
@@ -106,11 +88,8 @@ public class BodyFetchElement {
 
     /**
      * Gets the type of section.
-     * 
-     * @return {@link #HEADER_FIELDS}, {@link #TEXT}, {@link #CONTENT},
-     *         {@link #HEADER}, {@link #MIME} or {@link #HEADER_NOT_FIELDS}
      */
-    public final int getSectionType() {
+    public final SectionType getSectionType() {
         return sectionType;
     }
 
diff --git a/protocols/imap/src/main/java/org/apache/james/imap/api/message/SectionType.java b/protocols/imap/src/main/java/org/apache/james/imap/api/message/SectionType.java
new file mode 100644
index 0000000..d2b4239
--- /dev/null
+++ b/protocols/imap/src/main/java/org/apache/james/imap/api/message/SectionType.java
@@ -0,0 +1,29 @@
+/****************************************************************
+ * 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.imap.api.message;
+
+public enum SectionType {
+    TEXT,
+    MIME,
+    HEADER,
+    HEADER_FIELDS,
+    HEADER_NOT_FIELDS,
+    CONTENT
+}
diff --git a/protocols/imap/src/main/java/org/apache/james/imap/decode/FetchPartPathDecoder.java b/protocols/imap/src/main/java/org/apache/james/imap/decode/FetchPartPathDecoder.java
index ebc8099..c4c757b 100644
--- a/protocols/imap/src/main/java/org/apache/james/imap/decode/FetchPartPathDecoder.java
+++ b/protocols/imap/src/main/java/org/apache/james/imap/decode/FetchPartPathDecoder.java
@@ -23,21 +23,9 @@ import java.util.ArrayList;
 import java.util.List;
 
 import org.apache.james.imap.api.display.HumanReadableText;
+import org.apache.james.imap.api.message.SectionType;
 
 public class FetchPartPathDecoder {
-
-    public static final int TEXT = 0;
-
-    public static final int MIME = 1;
-
-    public static final int HEADER = 2;
-
-    public static final int HEADER_FIELDS = 3;
-
-    public static final int HEADER_NOT_FIELDS = 4;
-
-    public static final int CONTENT = 5;
-
     /**
      * Going to need to make one array copy so might as well ensure plenty of
      * space
@@ -47,7 +35,7 @@ public class FetchPartPathDecoder {
     /** Embedded RFC882 messages are rare so start size one array */
     private static final int ARRAY_INITIAL_SIZE = 1;
 
-    private int sectionType;
+    private SectionType sectionType;
 
     private int[] path;
 
@@ -60,7 +48,7 @@ public class FetchPartPathDecoder {
     public FetchPartPathDecoder() {
     }
 
-    public int decode(CharSequence sectionSpecification) throws DecodingException {
+    public SectionType decode(CharSequence sectionSpecification) throws DecodingException {
         init();
         sectionType = decode(0, sectionSpecification);
         prunePath();
@@ -78,8 +66,8 @@ public class FetchPartPathDecoder {
         }
     }
 
-    private int decode(int at, CharSequence sectionSpecification) throws DecodingException {
-        int result;
+    private SectionType decode(int at, CharSequence sectionSpecification) throws DecodingException {
+        SectionType result;
         int length = sectionSpecification.length();
         if (at < length) {
             final char next = sectionSpecification.charAt(at);
@@ -149,12 +137,12 @@ public class FetchPartPathDecoder {
             }
         } else {
             storePartial();
-            result = CONTENT;
+            result = SectionType.CONTENT;
         }
         return result;
     }
 
-    private int mime(int at, CharSequence sectionSpecification) throws DecodingException {
+    private SectionType mime(int at, CharSequence sectionSpecification) throws DecodingException {
         if (sectionSpecification.length() == at + 4) {
             mustBeI(sectionSpecification, at + 1);
             mustBeM(sectionSpecification, at + 2);
@@ -163,7 +151,7 @@ public class FetchPartPathDecoder {
         } else {
             throw new DecodingException(HumanReadableText.ILLEGAL_ARGUMENTS, "Unknown body specification");
         }
-        return MIME;
+        return SectionType.MIME;
     }
 
     private void mustBeI(CharSequence sectionSpecification, int position) throws DecodingException {
@@ -271,8 +259,8 @@ public class FetchPartPathDecoder {
         }
     }
 
-    private int header(int at, CharSequence sectionSpecification) throws DecodingException {
-        int result;
+    private SectionType header(int at, CharSequence sectionSpecification) throws DecodingException {
+        SectionType result;
         int length = sectionSpecification.length();
         if (length > at + 5) {
             mustBeE(sectionSpecification, at + 1);
@@ -282,7 +270,7 @@ public class FetchPartPathDecoder {
             mustBeR(sectionSpecification, at + 5);
             storePartial();
             if (length == at + 6) {
-                result = HEADER;
+                result = SectionType.HEADER;
             } else {
                 result = headerFields(at + 6, sectionSpecification);
             }
@@ -292,8 +280,8 @@ public class FetchPartPathDecoder {
         return result;
     }
 
-    private int headerFields(int at, CharSequence sectionSpecification) throws DecodingException {
-        int result;
+    private SectionType headerFields(int at, CharSequence sectionSpecification) throws DecodingException {
+        SectionType result;
         int length = sectionSpecification.length();
         if (length > at + 7) {
             mustBeDot(sectionSpecification, at);
@@ -307,7 +295,7 @@ public class FetchPartPathDecoder {
             int namesStartAt;
             switch (next) {
             case ' ':
-                result = HEADER_FIELDS;
+                result = SectionType.HEADER_FIELDS;
                 namesStartAt = skipSpaces(at + 7, sectionSpecification);
                 break;
             case '.':
@@ -315,7 +303,7 @@ public class FetchPartPathDecoder {
                     mustBeN(sectionSpecification, at + 8);
                     mustBeO(sectionSpecification, at + 9);
                     mustBeT(sectionSpecification, at + 10);
-                    result = HEADER_NOT_FIELDS;
+                    result = SectionType.HEADER_NOT_FIELDS;
                     namesStartAt = skipSpaces(at + 11, sectionSpecification);
                 } else {
                     throw new DecodingException(HumanReadableText.ILLEGAL_ARGUMENTS, "Unknown body specification");
@@ -382,7 +370,7 @@ public class FetchPartPathDecoder {
         return result;
     }
 
-    private int text(int at, CharSequence sectionSpecification) throws DecodingException {
+    private SectionType text(int at, CharSequence sectionSpecification) throws DecodingException {
         if (sectionSpecification.length() == at + 4) {
             mustBeE(sectionSpecification, at + 1);
             mustBeX(sectionSpecification, at + 2);
@@ -391,18 +379,16 @@ public class FetchPartPathDecoder {
         } else {
             throw new DecodingException(HumanReadableText.ILLEGAL_ARGUMENTS, "Unknown body specification");
         }
-        return TEXT;
+        return SectionType.TEXT;
     }
 
-    private int digit(int at, CharSequence sectionSpecification, int digit) throws DecodingException {
-        int result;
+    private SectionType digit(int at, CharSequence sectionSpecification, int digit) throws DecodingException {
         digit(digit);
-        result = decode(at + 1, sectionSpecification);
-        return result;
+        return decode(at + 1, sectionSpecification);
     }
 
     private void init() {
-        sectionType = CONTENT;
+        sectionType = SectionType.CONTENT;
         resetPartial();
         path = null;
         used = 0;
@@ -452,13 +438,9 @@ public class FetchPartPathDecoder {
     }
 
     /**
-     * Gets the
-     * 
-     * @return {@link #TEXT}, {@link #MIME}, {@link #HEADER},
-     *         {@link #HEADER_FIELDS}, {@link #HEADER_NOT_FIELDS} or
-     *         {@link #CONTENT}
+     * Gets the{@link SectionType}
      */
-    public final int getSpecifier() {
+    public final SectionType getSpecifier() {
         return sectionType;
     }
 
@@ -466,7 +448,7 @@ public class FetchPartPathDecoder {
      * Gets field names.
      * 
      * @return <code>List</code> of <code>String</code> names when
-     *         {@link #HEADER_FIELDS} or {@link #HEADER_NOT_FIELDS}, null
+     *         {@link SectionType#HEADER_FIELDS} or {@link SectionType#HEADER_NOT_FIELDS}, null
      *         otherwise
      */
     public final List<String> getNames() {
diff --git a/protocols/imap/src/main/java/org/apache/james/imap/decode/parser/FetchCommandParser.java b/protocols/imap/src/main/java/org/apache/james/imap/decode/parser/FetchCommandParser.java
index eaae520..f929325 100644
--- a/protocols/imap/src/main/java/org/apache/james/imap/decode/parser/FetchCommandParser.java
+++ b/protocols/imap/src/main/java/org/apache/james/imap/decode/parser/FetchCommandParser.java
@@ -28,6 +28,7 @@ import org.apache.james.imap.api.display.HumanReadableText;
 import org.apache.james.imap.api.message.BodyFetchElement;
 import org.apache.james.imap.api.message.FetchData;
 import org.apache.james.imap.api.message.IdRange;
+import org.apache.james.imap.api.message.SectionType;
 import org.apache.james.imap.api.process.ImapSession;
 import org.apache.james.imap.decode.DecodingException;
 import org.apache.james.imap.decode.FetchPartPathDecoder;
@@ -213,33 +214,13 @@ public class FetchCommandParser extends AbstractUidCommandParser {
         final String responseName = "BODY[" + parameter + "]";
         FetchPartPathDecoder decoder = new FetchPartPathDecoder();
         decoder.decode(parameter);
-        final int sectionType = getSectionType(decoder);
+        final SectionType sectionType = decoder.getSpecifier();
 
         final List<String> names = decoder.getNames();
         final int[] path = decoder.getPath();
         return new BodyFetchElement(responseName, sectionType, path, names, firstOctet, numberOfOctets);
     }
 
-    private int getSectionType(FetchPartPathDecoder decoder) throws DecodingException {
-        int specifier = decoder.getSpecifier();
-        switch (specifier) {
-            case FetchPartPathDecoder.CONTENT:
-                return BodyFetchElement.CONTENT;
-            case FetchPartPathDecoder.HEADER:
-                return BodyFetchElement.HEADER;
-            case FetchPartPathDecoder.HEADER_FIELDS:
-                return BodyFetchElement.HEADER_FIELDS;
-            case FetchPartPathDecoder.HEADER_NOT_FIELDS:
-                return BodyFetchElement.HEADER_NOT_FIELDS;
-            case FetchPartPathDecoder.MIME:
-                return BodyFetchElement.MIME;
-            case FetchPartPathDecoder.TEXT:
-                return BodyFetchElement.TEXT;
-            default:
-                throw new DecodingException(HumanReadableText.ILLEGAL_ARGUMENTS, "Section type is unsupported.");
-        }
-    }
-
     private String readWord(ImapRequestLineReader request, String terminator) throws DecodingException {
         StringBuilder builder = new StringBuilder();
         char next = request.nextChar();
diff --git a/protocols/imap/src/main/java/org/apache/james/imap/processor/fetch/FetchDataConverter.java b/protocols/imap/src/main/java/org/apache/james/imap/processor/fetch/FetchDataConverter.java
index 8596c88..a6333e0 100644
--- a/protocols/imap/src/main/java/org/apache/james/imap/processor/fetch/FetchDataConverter.java
+++ b/protocols/imap/src/main/java/org/apache/james/imap/processor/fetch/FetchDataConverter.java
@@ -23,6 +23,7 @@ import java.util.Collection;
 
 import org.apache.james.imap.api.message.BodyFetchElement;
 import org.apache.james.imap.api.message.FetchData;
+import org.apache.james.imap.api.message.SectionType;
 import org.apache.james.mailbox.model.FetchGroup;
 import org.apache.james.mailbox.model.MimePath;
 
@@ -41,26 +42,26 @@ class FetchDataConverter {
         Collection<BodyFetchElement> bodyElements = fetch.getBodyElements();
         if (bodyElements != null) {
             for (BodyFetchElement element : bodyElements) {
-                final int sectionType = element.getSectionType();
+                final SectionType sectionType = element.getSectionType();
                 final int[] path = element.getPath();
                 final boolean isBase = (path == null || path.length == 0);
                 switch (sectionType) {
-                    case BodyFetchElement.CONTENT:
+                    case CONTENT:
                         if (isBase) {
                             result = addContent(result, path, isBase, FetchGroup.Profile.FULL_CONTENT);
                         } else {
                             result = addContent(result, path, isBase, FetchGroup.Profile.MIME_CONTENT);
                         }
                         break;
-                    case BodyFetchElement.HEADER:
-                    case BodyFetchElement.HEADER_NOT_FIELDS:
-                    case BodyFetchElement.HEADER_FIELDS:
+                    case HEADER:
+                    case HEADER_NOT_FIELDS:
+                    case HEADER_FIELDS:
                         result = addContent(result, path, isBase, FetchGroup.Profile.HEADERS);
                         break;
-                    case BodyFetchElement.MIME:
+                    case MIME:
                         result = addContent(result, path, isBase, FetchGroup.Profile.MIME_HEADERS);
                         break;
-                    case BodyFetchElement.TEXT:
+                    case TEXT:
                         result = addContent(result, path, isBase, FetchGroup.Profile.BODY_CONTENT);
                         break;
                     default:
diff --git a/protocols/imap/src/main/java/org/apache/james/imap/processor/fetch/FetchResponseBuilder.java b/protocols/imap/src/main/java/org/apache/james/imap/processor/fetch/FetchResponseBuilder.java
index f83c2cc..0b2e938 100644
--- a/protocols/imap/src/main/java/org/apache/james/imap/processor/fetch/FetchResponseBuilder.java
+++ b/protocols/imap/src/main/java/org/apache/james/imap/processor/fetch/FetchResponseBuilder.java
@@ -34,6 +34,7 @@ import javax.mail.Flags;
 import org.apache.james.imap.api.ImapSessionUtils;
 import org.apache.james.imap.api.message.BodyFetchElement;
 import org.apache.james.imap.api.message.FetchData;
+import org.apache.james.imap.api.message.SectionType;
 import org.apache.james.imap.api.process.ImapSession;
 import org.apache.james.imap.api.process.SelectedMailbox;
 import org.apache.james.imap.message.response.FetchResponse;
@@ -218,7 +219,7 @@ public final class FetchResponseBuilder {
         final Long firstOctet = fetchElement.getFirstOctet();
         final Long numberOfOctets = fetchElement.getNumberOfOctets();
         final String name = fetchElement.getResponseName();
-        final int specifier = fetchElement.getSectionType();
+        final SectionType specifier = fetchElement.getSectionType();
         final int[] path = fetchElement.getPath();
         final Collection<String> names = fetchElement.getFieldNames();
         final boolean isBase = (path == null || path.length == 0);
@@ -227,19 +228,19 @@ public final class FetchResponseBuilder {
 
     }
 
-    private FetchResponse.BodyElement bodyContent(MessageResult messageResult, String name, int specifier, int[] path, Collection<String> names, boolean isBase) throws MailboxException {
+    private FetchResponse.BodyElement bodyContent(MessageResult messageResult, String name, SectionType specifier, int[] path, Collection<String> names, boolean isBase) throws MailboxException {
         switch (specifier) {
-            case BodyFetchElement.CONTENT:
+            case CONTENT:
                 return content(messageResult, name, path, isBase);
-            case BodyFetchElement.HEADER_FIELDS:
+            case HEADER_FIELDS:
                 return fields(messageResult, name, path, names, isBase);
-            case BodyFetchElement.HEADER_NOT_FIELDS:
+            case HEADER_NOT_FIELDS:
                 return fieldsNot(messageResult, name, path, names, isBase);
-            case BodyFetchElement.MIME:
+            case MIME:
                 return mimeHeaders(messageResult, name, path, isBase);
-            case BodyFetchElement.HEADER:
+            case HEADER:
                 return headers(messageResult, name, path, isBase);
-            case BodyFetchElement.TEXT:
+            case TEXT:
                 return text(messageResult, name, path, isBase);
             default:
                 return null;
diff --git a/protocols/imap/src/test/java/org/apache/james/imap/decode/FetchPartPathDecoderTest.java b/protocols/imap/src/test/java/org/apache/james/imap/decode/FetchPartPathDecoderTest.java
index 7f78757..851202b 100644
--- a/protocols/imap/src/test/java/org/apache/james/imap/decode/FetchPartPathDecoderTest.java
+++ b/protocols/imap/src/test/java/org/apache/james/imap/decode/FetchPartPathDecoderTest.java
@@ -25,6 +25,7 @@ import static org.assertj.core.api.Fail.fail;
 import java.util.Collection;
 import java.util.Iterator;
 
+import org.apache.james.imap.api.message.SectionType;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -40,77 +41,77 @@ public class FetchPartPathDecoderTest {
 
     @Test
     public void testShouldDetectText() throws Exception {
-        assertThat(decoder.decode("TEXT")).isEqualTo(FetchPartPathDecoder.TEXT);
-        assertThat(decoder.decode("3.TEXT")).isEqualTo(FetchPartPathDecoder.TEXT);
-        assertThat(decoder.decode("3.1.TEXT")).isEqualTo(FetchPartPathDecoder.TEXT);
+        assertThat(decoder.decode("TEXT")).isEqualTo(SectionType.TEXT);
+        assertThat(decoder.decode("3.TEXT")).isEqualTo(SectionType.TEXT);
+        assertThat(decoder.decode("3.1.TEXT")).isEqualTo(SectionType.TEXT);
         assertThat(decoder
-                .decode("3.2.5.7.8.TEXT")).isEqualTo(FetchPartPathDecoder.TEXT);
+                .decode("3.2.5.7.8.TEXT")).isEqualTo(SectionType.TEXT);
     }
 
     @Test
     public void testShouldDetectHeader() throws Exception {
-        assertThat(decoder.decode("HEADER")).isEqualTo(FetchPartPathDecoder.HEADER);
-        assertThat(decoder.decode("4.HEADER")).isEqualTo(FetchPartPathDecoder.HEADER);
-        assertThat(decoder.decode("10.1.HEADER")).isEqualTo(FetchPartPathDecoder.HEADER);
+        assertThat(decoder.decode("HEADER")).isEqualTo(SectionType.HEADER);
+        assertThat(decoder.decode("4.HEADER")).isEqualTo(SectionType.HEADER);
+        assertThat(decoder.decode("10.1.HEADER")).isEqualTo(SectionType.HEADER);
         assertThat(decoder
-                .decode("8.3.5.11.HEADER")).isEqualTo(FetchPartPathDecoder.HEADER);
+                .decode("8.3.5.11.HEADER")).isEqualTo(SectionType.HEADER);
     }
 
     @Test
     public void testShouldDetectHeaderFields() throws Exception {
         assertThat(decoder
-                .decode("HEADER.FIELDS ()")).isEqualTo(FetchPartPathDecoder.HEADER_FIELDS);
+                .decode("HEADER.FIELDS ()")).isEqualTo(SectionType.HEADER_FIELDS);
         assertThat(decoder
-                .decode("4.HEADER.FIELDS ()")).isEqualTo(FetchPartPathDecoder.HEADER_FIELDS);
+                .decode("4.HEADER.FIELDS ()")).isEqualTo(SectionType.HEADER_FIELDS);
         assertThat(decoder
-                .decode("10.1.HEADER.FIELDS ()")).isEqualTo(FetchPartPathDecoder.HEADER_FIELDS);
+                .decode("10.1.HEADER.FIELDS ()")).isEqualTo(SectionType.HEADER_FIELDS);
         assertThat(decoder
-                .decode("8.3.5.11.HEADER.FIELDS ()")).isEqualTo(FetchPartPathDecoder.HEADER_FIELDS);
+                .decode("8.3.5.11.HEADER.FIELDS ()")).isEqualTo(SectionType.HEADER_FIELDS);
     }
 
     @Test
     public void testShouldDetectHeaderFieldsNot() throws Exception {
         assertThat(decoder
-                .decode("HEADER.FIELDS.NOT ()")).isEqualTo(FetchPartPathDecoder.HEADER_NOT_FIELDS);
+                .decode("HEADER.FIELDS.NOT ()")).isEqualTo(SectionType.HEADER_NOT_FIELDS);
         assertThat(decoder
-                .decode("4.HEADER.FIELDS.NOT ()")).isEqualTo(FetchPartPathDecoder.HEADER_NOT_FIELDS);
+                .decode("4.HEADER.FIELDS.NOT ()")).isEqualTo(SectionType.HEADER_NOT_FIELDS);
         assertThat(decoder
-                .decode("10.1.HEADER.FIELDS.NOT ()")).isEqualTo(FetchPartPathDecoder.HEADER_NOT_FIELDS);
+                .decode("10.1.HEADER.FIELDS.NOT ()")).isEqualTo(SectionType.HEADER_NOT_FIELDS);
         assertThat(decoder
-                .decode("8.3.5.11.HEADER.FIELDS.NOT ()")).isEqualTo(FetchPartPathDecoder.HEADER_NOT_FIELDS);
+                .decode("8.3.5.11.HEADER.FIELDS.NOT ()")).isEqualTo(SectionType.HEADER_NOT_FIELDS);
     }
 
     @Test
     public void testShouldDetectMime() throws Exception {
-        assertThat(decoder.decode("MIME")).isEqualTo(FetchPartPathDecoder.MIME);
-        assertThat(decoder.decode("6.MIME")).isEqualTo(FetchPartPathDecoder.MIME);
-        assertThat(decoder.decode("2.88.MIME")).isEqualTo(FetchPartPathDecoder.MIME);
+        assertThat(decoder.decode("MIME")).isEqualTo(SectionType.MIME);
+        assertThat(decoder.decode("6.MIME")).isEqualTo(SectionType.MIME);
+        assertThat(decoder.decode("2.88.MIME")).isEqualTo(SectionType.MIME);
         assertThat(decoder
-                .decode("32.3.15.11.MIME")).isEqualTo(FetchPartPathDecoder.MIME);
+                .decode("32.3.15.11.MIME")).isEqualTo(SectionType.MIME);
     }
 
     @Test
     public void testShouldDetectContent() throws Exception {
-        assertThat(decoder.decode("34")).isEqualTo(FetchPartPathDecoder.CONTENT);
-        assertThat(decoder.decode("6")).isEqualTo(FetchPartPathDecoder.CONTENT);
-        assertThat(decoder.decode("9.88")).isEqualTo(FetchPartPathDecoder.CONTENT);
-        assertThat(decoder.decode("17.3.15.11")).isEqualTo(FetchPartPathDecoder.CONTENT);
+        assertThat(decoder.decode("34")).isEqualTo(SectionType.CONTENT);
+        assertThat(decoder.decode("6")).isEqualTo(SectionType.CONTENT);
+        assertThat(decoder.decode("9.88")).isEqualTo(SectionType.CONTENT);
+        assertThat(decoder.decode("17.3.15.11")).isEqualTo(SectionType.CONTENT);
     }
 
     @Test
     public void testShouldIgnoreCase() throws Exception {
-        assertThat(decoder.decode("6.MIME")).isEqualTo(FetchPartPathDecoder.MIME);
-        assertThat(decoder.decode("6.mime")).isEqualTo(FetchPartPathDecoder.MIME);
-        assertThat(decoder.decode("6.miME")).isEqualTo(FetchPartPathDecoder.MIME);
-        assertThat(decoder.decode("6.MIme")).isEqualTo(FetchPartPathDecoder.MIME);
-        assertThat(decoder.decode("6.HEADER")).isEqualTo(FetchPartPathDecoder.HEADER);
-        assertThat(decoder.decode("6.header")).isEqualTo(FetchPartPathDecoder.HEADER);
-        assertThat(decoder.decode("6.HEadER")).isEqualTo(FetchPartPathDecoder.HEADER);
-        assertThat(decoder.decode("6.heADEr")).isEqualTo(FetchPartPathDecoder.HEADER);
-        assertThat(decoder.decode("6.TEXT")).isEqualTo(FetchPartPathDecoder.TEXT);
-        assertThat(decoder.decode("6.text")).isEqualTo(FetchPartPathDecoder.TEXT);
-        assertThat(decoder.decode("6.TExt")).isEqualTo(FetchPartPathDecoder.TEXT);
-        assertThat(decoder.decode("6.teXT")).isEqualTo(FetchPartPathDecoder.TEXT);
+        assertThat(decoder.decode("6.MIME")).isEqualTo(SectionType.MIME);
+        assertThat(decoder.decode("6.mime")).isEqualTo(SectionType.MIME);
+        assertThat(decoder.decode("6.miME")).isEqualTo(SectionType.MIME);
+        assertThat(decoder.decode("6.MIme")).isEqualTo(SectionType.MIME);
+        assertThat(decoder.decode("6.HEADER")).isEqualTo(SectionType.HEADER);
+        assertThat(decoder.decode("6.header")).isEqualTo(SectionType.HEADER);
+        assertThat(decoder.decode("6.HEadER")).isEqualTo(SectionType.HEADER);
+        assertThat(decoder.decode("6.heADEr")).isEqualTo(SectionType.HEADER);
+        assertThat(decoder.decode("6.TEXT")).isEqualTo(SectionType.TEXT);
+        assertThat(decoder.decode("6.text")).isEqualTo(SectionType.TEXT);
+        assertThat(decoder.decode("6.TExt")).isEqualTo(SectionType.TEXT);
+        assertThat(decoder.decode("6.teXT")).isEqualTo(SectionType.TEXT);
     }
 
     @Test
diff --git a/protocols/imap/src/test/java/org/apache/james/imap/decode/parser/FetchCommandParserPartialFetchTest.java b/protocols/imap/src/test/java/org/apache/james/imap/decode/parser/FetchCommandParserPartialFetchTest.java
index 4f78aff..17cc5f0 100644
--- a/protocols/imap/src/test/java/org/apache/james/imap/decode/parser/FetchCommandParserPartialFetchTest.java
+++ b/protocols/imap/src/test/java/org/apache/james/imap/decode/parser/FetchCommandParserPartialFetchTest.java
@@ -32,6 +32,7 @@ import org.apache.james.imap.api.Tag;
 import org.apache.james.imap.api.message.BodyFetchElement;
 import org.apache.james.imap.api.message.FetchData;
 import org.apache.james.imap.api.message.IdRange;
+import org.apache.james.imap.api.message.SectionType;
 import org.apache.james.imap.api.process.ImapSession;
 import org.apache.james.imap.decode.DecodingException;
 import org.apache.james.imap.decode.ImapRequestLineReader;
@@ -56,7 +57,7 @@ public class FetchCommandParserPartialFetchTest  {
     public void testShouldParseZeroAndLength() throws Exception {
         IdRange[] ranges = { new IdRange(1) };
         FetchData data = new FetchData();
-        data.add(new BodyFetchElement("BODY[]", BodyFetchElement.CONTENT, null,
+        data.add(new BodyFetchElement("BODY[]", SectionType.CONTENT, null,
                 null, 0L, 100L), false);
         check("1 (BODY[]<0.100>)\r\n", ranges, false, data, TAG);
     }
@@ -65,7 +66,7 @@ public class FetchCommandParserPartialFetchTest  {
     public void testShouldParseNonZeroAndLength() throws Exception {
         IdRange[] ranges = { new IdRange(1) };
         FetchData data = new FetchData();
-        data.add(new BodyFetchElement("BODY[]", BodyFetchElement.CONTENT, null,
+        data.add(new BodyFetchElement("BODY[]", SectionType.CONTENT, null,
                 null, 20L, 12342348L), false);
         check("1 (BODY[]<20.12342348>)\r\n", ranges, false, data, TAG);
     }
diff --git a/protocols/imap/src/test/java/org/apache/james/imap/processor/fetch/FetchDataConverterTest.java b/protocols/imap/src/test/java/org/apache/james/imap/processor/fetch/FetchDataConverterTest.java
index 63e772e..0050bfa 100644
--- a/protocols/imap/src/test/java/org/apache/james/imap/processor/fetch/FetchDataConverterTest.java
+++ b/protocols/imap/src/test/java/org/apache/james/imap/processor/fetch/FetchDataConverterTest.java
@@ -19,13 +19,12 @@
 
 package org.apache.james.imap.processor.fetch;
 
-import static org.apache.james.imap.api.message.BodyFetchElement.CONTENT;
-import static org.apache.james.imap.api.message.BodyFetchElement.HEADER;
-import static org.apache.james.imap.api.message.BodyFetchElement.MIME;
-import static org.apache.james.imap.api.message.BodyFetchElement.TEXT;
+import static org.apache.james.imap.api.message.SectionType.CONTENT;
+import static org.apache.james.imap.api.message.SectionType.HEADER;
+import static org.apache.james.imap.api.message.SectionType.MIME;
+import static org.apache.james.imap.api.message.SectionType.TEXT;
 import static org.assertj.core.api.Assertions.assertThat;
 
-import java.util.EnumSet;
 import java.util.stream.Stream;
 
 import org.apache.james.imap.api.ImapConstants;


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


[james-project] 08/27: JAMES-2919 Validate JMAP partial read ADR after performance tests

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 11ed566c7689f9363cb803a61eb9765723b634d2
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Wed Dec 4 18:23:37 2019 +0700

    JAMES-2919 Validate JMAP partial read ADR after performance tests
---
 src/adr/0012-jmap-partial-reads.md      | 23 ++++++++++++++++-------
 src/adr/0013-precompute-jmap-preview.md | 19 ++++++++++++++-----
 2 files changed, 30 insertions(+), 12 deletions(-)

diff --git a/src/adr/0012-jmap-partial-reads.md b/src/adr/0012-jmap-partial-reads.md
index 310cd2e..ef945b6 100644
--- a/src/adr/0012-jmap-partial-reads.md
+++ b/src/adr/0012-jmap-partial-reads.md
@@ -4,9 +4,7 @@ Date: 2019-10-09
 
 ## Status
 
-Proposed
-
-Adoption needs to be backed by some performance tests.
+Accepted
 
 ## Context
 
@@ -29,10 +27,21 @@ Some performance tests will be run in order to evaluate the improvements.
 
 ## Consequences
 
-GetMessages with a limited set of requested properties will no longer result necessarily in full database message read. We
-thus expect a significant improvement, for instance when only metadata are requested.
-
-In case of a less than 5% improvement, the code will not be added to the codebase and the proposal will get the status 'rejected'.
+GetMessages with a limited set of requested properties no longer result necessarily in full database message read. We
+thus have a significant improvement, for instance when only metadata are requested.
+
+Given the following scenario played by 5000 users per hour (constant rate)
+ - Authenticate
+ - List mailboxes
+ - List messages in one of their mailboxes
+ - Get 10 times the mailboxIds and keywords of the given messages
+
+We went from:
+ - A 20% failure and timeout rate before this change to no failure
+ - Mean time for GetMessages went from 27 159 ms to 27 ms (1000 time improvment), for all operation from
+ 27 591 ms to 60 ms (460 time improvment)
+ - P99 is a metric that did not make sense because the initial simulation exceeded Gatling (the performance measuring tool 
+ we use) timeout (60s) at the p50 percentile. After this proposal p99 for the entire scenario is of 1 383 ms
 
 ## References
 
diff --git a/src/adr/0013-precompute-jmap-preview.md b/src/adr/0013-precompute-jmap-preview.md
index e683ab2..8a1092f 100644
--- a/src/adr/0013-precompute-jmap-preview.md
+++ b/src/adr/0013-precompute-jmap-preview.md
@@ -4,9 +4,7 @@ Date: 2019-10-09
 
 ## Status
 
-Proposed
-
-Adoption needs to be backed by some performance tests.
+Accepted
 
 ## Context
 
@@ -35,9 +33,20 @@ Some performance tests will be run in order to evaluate the improvements.
 
 ## Consequences
 
-We expect a huge performance enhancement for JMAP clients relying on preview for listing mails.
+Given the following scenario played by 2500 users per hour (constant rate)
+ - Authenticate
+ - List mailboxes
+ - List messages in one of their mailboxes
+ - Get 8 times the properties expected to be fast to fetch with JMAP
+
+We went from:
+ - A 7% failure and timeout rate before this change to almost no failure
+ - Mean time for GetMessages went from 9 710 ms to 434 ms (22 time improvment), for all operation from
+ 12 802 ms to 407 ms (31 time improvment)
+ - P99 is a metric that did not make sense because the initial simulation exceeded Gatling (the performance measuring tool 
+ we use) timeout (60s) at the p95 percentile. After this proposal p99 for the entire scenario is of 1 747 ms
 
-In case of a less than 5% improvement, the code will not be added to the codebase and the proposal will get the status 'rejected'.
+As such, this changeset significantly increases the JMAP performance.
 
 ## References
 


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


[james-project] 12/27: [Refactoring] BodyFetchElement should match bean contract

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 c56f2bed648ab0678a5bc67ea13f06d5f7dc31cf
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Mon Dec 9 14:07:18 2019 +0700

    [Refactoring] BodyFetchElement should match bean contract
    
    Use standard template for this.
    
    Note that numerous EqualsVerifier issues were solved along the way...
---
 .../james/imap/api/message/BodyFetchElement.java   | 73 +++++-----------------
 .../imap/api/message/BodyFetchElementTest.java     | 32 ++++++++++
 2 files changed, 49 insertions(+), 56 deletions(-)

diff --git a/protocols/imap/src/main/java/org/apache/james/imap/api/message/BodyFetchElement.java b/protocols/imap/src/main/java/org/apache/james/imap/api/message/BodyFetchElement.java
index fe7531b..3990ed9 100644
--- a/protocols/imap/src/main/java/org/apache/james/imap/api/message/BodyFetchElement.java
+++ b/protocols/imap/src/main/java/org/apache/james/imap/api/message/BodyFetchElement.java
@@ -20,6 +20,7 @@ package org.apache.james.imap.api.message;
 
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.Objects;
 import java.util.Optional;
 
 import org.apache.james.imap.api.ImapConstants;
@@ -131,64 +132,24 @@ public class BodyFetchElement {
         return numberOfOctets;
     }
 
-    public int hashCode() {
-        final int PRIME = 31;
-        int result = 1;
-        result = PRIME * result + ((fieldNames == null) ? 0 : fieldNames.hashCode());
-        result = PRIME * result + ((firstOctet == null) ? 0 : firstOctet.hashCode());
-        result = PRIME * result + ((name == null) ? 0 : name.hashCode());
-        result = PRIME * result + ((numberOfOctets == null) ? 0 : numberOfOctets.hashCode());
-        result = PRIME * result + ((path == null) ? 0 : path.length);
-        result = PRIME * result + sectionType;
-        return result;
+    @Override
+    public final boolean equals(Object o) {
+        if (o instanceof BodyFetchElement) {
+            BodyFetchElement that = (BodyFetchElement) o;
+
+            return Objects.equals(this.sectionType, that.sectionType)
+                && Objects.equals(this.firstOctet, that.firstOctet)
+                && Objects.equals(this.numberOfOctets, that.numberOfOctets)
+                && Objects.equals(this.name, that.name)
+                && Arrays.equals(this.path, that.path)
+                && Objects.equals(this.fieldNames, that.fieldNames);
+        }
+        return false;
     }
 
-    public boolean equals(Object obj) {
-        if (this == obj) {
-            return true;
-        }
-        if (obj == null) {
-            return false;
-        }
-        if (getClass() != obj.getClass()) {
-            return false;
-        }
-        final BodyFetchElement other = (BodyFetchElement) obj;
-        if (fieldNames == null) {
-            if (other.fieldNames != null) {
-                return false;
-            }
-        } else if (!fieldNames.equals(other.fieldNames)) {
-            return false;
-        }
-        if (firstOctet == null) {
-            if (other.firstOctet != null) {
-                return false;
-            }
-        } else if (!firstOctet.equals(other.firstOctet)) {
-            return false;
-        }
-        if (name == null) {
-            if (other.name != null) {
-                return false;
-            }
-        } else if (!name.equals(other.name)) {
-            return false;
-        }
-        if (numberOfOctets == null) {
-            if (other.numberOfOctets != null) {
-                return false;
-            }
-        } else if (!numberOfOctets.equals(other.numberOfOctets)) {
-            return false;
-        }
-        if (!Arrays.equals(path, other.path)) {
-            return false;
-        }
-        if (sectionType != other.sectionType) {
-            return false;
-        }
-        return true;
+    @Override
+    public final int hashCode() {
+        return Objects.hash(firstOctet, numberOfOctets, name, sectionType, Arrays.hashCode(path), fieldNames);
     }
 
     @Override
diff --git a/protocols/imap/src/test/java/org/apache/james/imap/api/message/BodyFetchElementTest.java b/protocols/imap/src/test/java/org/apache/james/imap/api/message/BodyFetchElementTest.java
new file mode 100644
index 0000000..2a5bad9
--- /dev/null
+++ b/protocols/imap/src/test/java/org/apache/james/imap/api/message/BodyFetchElementTest.java
@@ -0,0 +1,32 @@
+/****************************************************************
+ * 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.imap.api.message;
+
+import org.junit.jupiter.api.Test;
+
+import nl.jqno.equalsverifier.EqualsVerifier;
+
+class BodyFetchElementTest {
+    @Test
+    void shouldMatchBeanContract() {
+        EqualsVerifier.forClass(BodyFetchElement.class)
+            .verify();
+    }
+}
\ No newline at end of file


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


[james-project] 10/27: [Refactoring] use LocaleDatetime instead of Date to carry knowledge about the timezone

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 8ab23e7db3cec919027612d7213c3f795df58f93
Author: Matthieu Baechler <ma...@apache.org>
AuthorDate: Thu Nov 28 22:13:00 2019 +0100

    [Refactoring] use LocaleDatetime instead of Date to carry knowledge about the timezone
---
 .../org/apache/james/imap/decode/DecoderUtils.java |   7 +-
 .../james/imap/decode/ImapRequestLineReader.java   |   4 +-
 .../imap/decode/parser/AppendCommandParser.java    |  10 +-
 .../imap/decode/DecoderUtilsLocaleDateTest.java    | 397 ++++++++-------------
 .../apache/james/imap/decode/DecoderUtilsTest.java |  33 +-
 5 files changed, 172 insertions(+), 279 deletions(-)

diff --git a/protocols/imap/src/main/java/org/apache/james/imap/decode/DecoderUtils.java b/protocols/imap/src/main/java/org/apache/james/imap/decode/DecoderUtils.java
index 3ce137c..5207b71 100644
--- a/protocols/imap/src/main/java/org/apache/james/imap/decode/DecoderUtils.java
+++ b/protocols/imap/src/main/java/org/apache/james/imap/decode/DecoderUtils.java
@@ -19,8 +19,9 @@
 
 package org.apache.james.imap.decode;
 
+import java.time.LocalDateTime;
+import java.time.ZoneId;
 import java.util.Calendar;
-import java.util.Date;
 import java.util.GregorianCalendar;
 import java.util.Locale;
 import java.util.TimeZone;
@@ -97,7 +98,7 @@ public final class DecoderUtils {
      * @throws DecodingException
      *             when this conversion fails
      */
-    public static Date decodeDateTime(CharSequence chars) throws DecodingException {
+    public static LocalDateTime decodeDateTime(CharSequence chars) throws DecodingException {
         if (isDateTime(chars)) {
             char dayHigh = chars.charAt(0);
             char dayLow = chars.charAt(1);
@@ -136,7 +137,7 @@ public final class DecoderUtils {
             GregorianCalendar calendar = new GregorianCalendar(TimeZone.getTimeZone("GMT"), Locale.US);
             calendar.clear();
             calendar.set(year, month, day, hour, minute, second);
-            return calendar.getTime();
+            return LocalDateTime.ofInstant(calendar.getTime().toInstant(), ZoneId.systemDefault());
         } else {
             final String message;
             if (chars == null) {
diff --git a/protocols/imap/src/main/java/org/apache/james/imap/decode/ImapRequestLineReader.java b/protocols/imap/src/main/java/org/apache/james/imap/decode/ImapRequestLineReader.java
index b4f5e75..a5b5bbe 100644
--- a/protocols/imap/src/main/java/org/apache/james/imap/decode/ImapRequestLineReader.java
+++ b/protocols/imap/src/main/java/org/apache/james/imap/decode/ImapRequestLineReader.java
@@ -28,8 +28,8 @@ import java.nio.charset.Charset;
 import java.nio.charset.CharsetDecoder;
 import java.nio.charset.CoderResult;
 import java.nio.charset.CodingErrorAction;
+import java.time.LocalDateTime;
 import java.util.ArrayList;
-import java.util.Date;
 import java.util.List;
 
 import javax.mail.Flags;
@@ -313,7 +313,7 @@ public abstract class ImapRequestLineReader {
     /**
      * Reads a "date-time" argument from the request.
      */
-    public Date dateTime() throws DecodingException {
+    public LocalDateTime dateTime() throws DecodingException {
         char next = nextWordChar();
         String dateString;
         if (next == '"') {
diff --git a/protocols/imap/src/main/java/org/apache/james/imap/decode/parser/AppendCommandParser.java b/protocols/imap/src/main/java/org/apache/james/imap/decode/parser/AppendCommandParser.java
index 468fa87..2fbfc63 100644
--- a/protocols/imap/src/main/java/org/apache/james/imap/decode/parser/AppendCommandParser.java
+++ b/protocols/imap/src/main/java/org/apache/james/imap/decode/parser/AppendCommandParser.java
@@ -18,6 +18,8 @@
  ****************************************************************/
 package org.apache.james.imap.decode.parser;
 
+import java.time.LocalDateTime;
+import java.time.ZoneId;
 import java.util.Date;
 
 import javax.mail.Flags;
@@ -59,7 +61,7 @@ public class AppendCommandParser extends AbstractImapCommandParser {
      * If the next character in the request is a '"', tries to read a DateTime
      * argument. If not, returns null.
      */
-    public Date optionalDateTime(ImapRequestLineReader request) throws DecodingException {
+    public LocalDateTime optionalDateTime(ImapRequestLineReader request) throws DecodingException {
         char next = request.nextWordChar();
         if (next == '"') {
             return request.dateTime();
@@ -75,12 +77,12 @@ public class AppendCommandParser extends AbstractImapCommandParser {
         if (flags == null) {
             flags = new Flags();
         }
-        Date datetime = optionalDateTime(request);
+        LocalDateTime datetime = optionalDateTime(request);
         if (datetime == null) {
-            datetime = new Date();
+            datetime = LocalDateTime.now();
         }
         request.nextWordChar();
 
-        return new AppendRequest(command, mailboxName, flags, datetime, request.consumeLiteral(true), tag);
+        return new AppendRequest(command, mailboxName, flags, Date.from(datetime.atZone(ZoneId.systemDefault()).toInstant()), request.consumeLiteral(true), tag);
     }
 }
diff --git a/protocols/imap/src/test/java/org/apache/james/imap/decode/DecoderUtilsLocaleDateTest.java b/protocols/imap/src/test/java/org/apache/james/imap/decode/DecoderUtilsLocaleDateTest.java
index dea3c1b..683618f 100644
--- a/protocols/imap/src/test/java/org/apache/james/imap/decode/DecoderUtilsLocaleDateTest.java
+++ b/protocols/imap/src/test/java/org/apache/james/imap/decode/DecoderUtilsLocaleDateTest.java
@@ -21,278 +21,159 @@ package org.apache.james.imap.decode;
 
 import static org.assertj.core.api.Assertions.assertThat;
 
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
 import java.util.Locale;
+import java.util.stream.Stream;
 
 import org.junit.After;
-import org.junit.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
 
-public class DecoderUtilsLocaleDateTest  {
+import reactor.util.function.Tuples;
 
-    private static final Locale BASE_DEFAULT_LOCALE = Locale.getDefault();
 
+public class DecoderUtilsLocaleDateTest {
+
+    private static final Locale BASE_DEFAULT_LOCALE = Locale.getDefault();
 
     @After
     public void tearDown() throws Exception {
         Locale.setDefault(BASE_DEFAULT_LOCALE);
     }
 
-    @Test
-    public void testUS() throws Exception {
-        runTests(Locale.US);
-    }
-
-    @Test
-    public void testITALY() throws Exception {
-        runTests(Locale.ITALY);
-    }
-
-    @Test
-    public void testTAIWAN() throws Exception {
-        runTests(Locale.TAIWAN);
-    }
-
-    @Test
-    public void testCHINA() throws Exception {
-        runTests(Locale.CHINA);
-    }
-
-    @Test
-    public void testKOREA() throws Exception {
-        runTests(Locale.KOREA);
-    }
-
-    @Test
-    public void testJAPAN() throws Exception {
-        runTests(Locale.JAPAN);
-    }
-
-    @Test
-    public void testUK() throws Exception {
-        runTests(Locale.UK);
+    static Stream<Arguments> decodeDateTimeWithVariousDefaultLocale() {
+        return
+            Stream.of(
+                Tuples.of("21-Oct-1972 20:00:00 +0000", "21 Oct 1972 20:00:00 GMT"),
+                Tuples.of("21-Oct-1972 20:00:00 +0100", "21 Oct 1972 19:00:00 GMT"),
+                Tuples.of("21-Oct-1972 20:00:00 +0200", "21 Oct 1972 18:00:00 GMT"),
+                Tuples.of("21-Oct-1972 20:00:00 +0300", "21 Oct 1972 17:00:00 GMT"),
+                Tuples.of("21-Oct-1972 20:00:00 +0400", "21 Oct 1972 16:00:00 GMT"),
+                Tuples.of("21-Oct-1972 20:00:00 +0500", "21 Oct 1972 15:00:00 GMT"),
+                Tuples.of("21-Oct-1972 20:00:00 +0600", "21 Oct 1972 14:00:00 GMT"),
+                Tuples.of("21-Oct-1972 20:00:00 +0700", "21 Oct 1972 13:00:00 GMT"),
+                Tuples.of("21-Oct-1972 20:00:00 +0800", "21 Oct 1972 12:00:00 GMT"),
+                Tuples.of("21-Oct-1972 20:00:00 +0900", "21 Oct 1972 11:00:00 GMT"),
+                Tuples.of("21-Oct-1972 20:00:00 +1000", "21 Oct 1972 10:00:00 GMT"),
+                Tuples.of("21-Oct-1972 20:00:00 +1100", "21 Oct 1972 09:00:00 GMT"),
+                Tuples.of("21-Oct-1972 20:00:00 +1200", "21 Oct 1972 08:00:00 GMT"),
+
+                Tuples.of("21-Oct-1972 20:00:00 +1000", "21 Oct 1972 10:00:00 GMT"),
+                Tuples.of("21-Oct-1972 20:00:00 -0100", "21 Oct 1972 21:00:00 GMT"),
+                Tuples.of("21-Oct-1972 20:00:00 -0200", "21 Oct 1972 22:00:00 GMT"),
+                Tuples.of("21-Oct-1972 20:00:00 -0300", "21 Oct 1972 23:00:00 GMT"),
+                Tuples.of("21-Oct-1972 20:00:00 -0400", "22 Oct 1972 00:00:00 GMT"),
+                Tuples.of("21-Oct-1972 20:00:00 -0500", "22 Oct 1972 01:00:00 GMT"),
+                Tuples.of("21-Oct-1972 20:00:00 -0600", "22 Oct 1972 02:00:00 GMT"),
+                Tuples.of("21-Oct-1972 20:00:00 -0700", "22 Oct 1972 03:00:00 GMT"),
+                Tuples.of("21-Oct-1972 20:00:00 -0800", "22 Oct 1972 04:00:00 GMT"),
+                Tuples.of("21-Oct-1972 20:00:00 -0900", "22 Oct 1972 05:00:00 GMT"),
+                Tuples.of("21-Oct-1972 20:00:00 -1000", "22 Oct 1972 06:00:00 GMT"),
+
+                Tuples.of("21-Oct-1972 20:00:00 +0030", "21 Oct 1972 19:30:00 GMT"),
+                Tuples.of("21-Oct-1972 20:00:00 +0130", "21 Oct 1972 18:30:00 GMT"),
+                Tuples.of("21-Oct-1972 20:00:00 +0230", "21 Oct 1972 17:30:00 GMT"),
+                Tuples.of("21-Oct-1972 20:00:00 +0330", "21 Oct 1972 16:30:00 GMT"),
+                Tuples.of("21-Oct-1972 20:00:00 +0430", "21 Oct 1972 15:30:00 GMT"),
+                Tuples.of("21-Oct-1972 20:00:00 +0530", "21 Oct 1972 14:30:00 GMT"),
+                Tuples.of("21-Oct-1972 20:00:00 +0630", "21 Oct 1972 13:30:00 GMT"),
+                Tuples.of("21-Oct-1972 20:00:00 +0730", "21 Oct 1972 12:30:00 GMT"),
+                Tuples.of("21-Oct-1972 20:00:00 +0830", "21 Oct 1972 11:30:00 GMT"),
+                Tuples.of("21-Oct-1972 20:00:00 +0930", "21 Oct 1972 10:30:00 GMT"),
+                Tuples.of("21-Oct-1972 20:00:00 +1030", "21 Oct 1972 09:30:00 GMT"),
+                Tuples.of("21-Oct-1972 20:00:00 +1130", "21 Oct 1972 08:30:00 GMT"),
+                Tuples.of("21-Oct-1972 20:00:00 +1230", "21 Oct 1972 07:30:00 GMT"),
+
+                Tuples.of("21-Oct-1972 20:00:00 -0030", "21 Oct 1972 20:30:00 GMT"),
+                Tuples.of("21-Oct-1972 20:00:00 -0130", "21 Oct 1972 21:30:00 GMT"),
+                Tuples.of("21-Oct-1972 20:00:00 -0230", "21 Oct 1972 22:30:00 GMT"),
+                Tuples.of("21-Oct-1972 20:00:00 -0330", "21 Oct 1972 23:30:00 GMT"),
+                Tuples.of("21-Oct-1972 20:00:00 -0430", "22 Oct 1972 00:30:00 GMT"),
+                Tuples.of("21-Oct-1972 20:00:00 -0530", "22 Oct 1972 01:30:00 GMT"),
+                Tuples.of("21-Oct-1972 20:00:00 -0630", "22 Oct 1972 02:30:00 GMT"),
+                Tuples.of("21-Oct-1972 20:00:00 -0730", "22 Oct 1972 03:30:00 GMT"),
+                Tuples.of("21-Oct-1972 20:00:00 -0830", "22 Oct 1972 04:30:00 GMT"),
+                Tuples.of("21-Oct-1972 20:00:00 -0930", "22 Oct 1972 05:30:00 GMT"),
+                Tuples.of("21-Oct-1972 20:00:00 -1030", "22 Oct 1972 06:30:00 GMT"),
+
+                Tuples.of("21-Oct-1972 20:16:27 +0000", "21 Oct 1972 20:16:27 GMT"),
+                Tuples.of("21-Oct-1972 20:16:27 +0100", "21 Oct 1972 19:16:27 GMT"),
+                Tuples.of("21-Oct-1972 20:16:27 +0200", "21 Oct 1972 18:16:27 GMT"),
+                Tuples.of("21-Oct-1972 20:16:27 +0300", "21 Oct 1972 17:16:27 GMT"),
+                Tuples.of("21-Oct-1972 20:16:27 +0400", "21 Oct 1972 16:16:27 GMT"),
+                Tuples.of("21-Oct-1972 20:16:27 +0500", "21 Oct 1972 15:16:27 GMT"),
+                Tuples.of("21-Oct-1972 20:16:27 +0600", "21 Oct 1972 14:16:27 GMT"),
+                Tuples.of("21-Oct-1972 20:16:27 +0700", "21 Oct 1972 13:16:27 GMT"),
+                Tuples.of("21-Oct-1972 20:16:27 +0800", "21 Oct 1972 12:16:27 GMT"),
+                Tuples.of("21-Oct-1972 20:16:27 +0900", "21 Oct 1972 11:16:27 GMT"),
+                Tuples.of("21-Oct-1972 20:16:27 +1000", "21 Oct 1972 10:16:27 GMT"),
+                Tuples.of("21-Oct-1972 20:16:27 +1100", "21 Oct 1972 09:16:27 GMT"),
+                Tuples.of("21-Oct-1972 20:16:27 +1200", "21 Oct 1972 08:16:27 GMT"),
+
+                Tuples.of("21-Oct-1972 20:16:27 -0000", "21 Oct 1972 20:16:27 GMT"),
+                Tuples.of("21-Oct-1972 20:16:27 -0100", "21 Oct 1972 21:16:27 GMT"),
+                Tuples.of("21-Oct-1972 20:16:27 -0200", "21 Oct 1972 22:16:27 GMT"),
+                Tuples.of("21-Oct-1972 20:16:27 -0300", "21 Oct 1972 23:16:27 GMT"),
+                Tuples.of("21-Oct-1972 20:16:27 -0400", "22 Oct 1972 00:16:27 GMT"),
+                Tuples.of("21-Oct-1972 20:16:27 -0500", "22 Oct 1972 01:16:27 GMT"),
+                Tuples.of("21-Oct-1972 20:16:27 -0600", "22 Oct 1972 02:16:27 GMT"),
+                Tuples.of("21-Oct-1972 20:16:27 -0700", "22 Oct 1972 03:16:27 GMT"),
+                Tuples.of("21-Oct-1972 20:16:27 -0800", "22 Oct 1972 04:16:27 GMT"),
+                Tuples.of("21-Oct-1972 20:16:27 -0900", "22 Oct 1972 05:16:27 GMT"),
+                Tuples.of("21-Oct-1972 20:16:27 -1000", "22 Oct 1972 06:16:27 GMT"),
+
+                Tuples.of("21-Oct-1972 20:16:27 +0030", "21 Oct 1972 19:46:27 GMT"),
+                Tuples.of("21-Oct-1972 20:16:27 +0130", "21 Oct 1972 18:46:27 GMT"),
+                Tuples.of("21-Oct-1972 20:16:27 +0230", "21 Oct 1972 17:46:27 GMT"),
+                Tuples.of("21-Oct-1972 20:16:27 +0330", "21 Oct 1972 16:46:27 GMT"),
+                Tuples.of("21-Oct-1972 20:16:27 +0430", "21 Oct 1972 15:46:27 GMT"),
+                Tuples.of("21-Oct-1972 20:16:27 +0530", "21 Oct 1972 14:46:27 GMT"),
+                Tuples.of("21-Oct-1972 20:16:27 +0630", "21 Oct 1972 13:46:27 GMT"),
+                Tuples.of("21-Oct-1972 20:16:27 +0730", "21 Oct 1972 12:46:27 GMT"),
+                Tuples.of("21-Oct-1972 20:16:27 +0830", "21 Oct 1972 11:46:27 GMT"),
+                Tuples.of("21-Oct-1972 20:16:27 +0930", "21 Oct 1972 10:46:27 GMT"),
+                Tuples.of("21-Oct-1972 20:16:27 +1030", "21 Oct 1972 09:46:27 GMT"),
+                Tuples.of("21-Oct-1972 20:16:27 +1130", "21 Oct 1972 08:46:27 GMT"),
+                Tuples.of("21-Oct-1972 20:16:27 +1230", "21 Oct 1972 07:46:27 GMT"),
+
+                Tuples.of("21-Oct-1972 20:16:27 -0030", "21 Oct 1972 20:46:27 GMT"),
+                Tuples.of("21-Oct-1972 20:16:27 -0130", "21 Oct 1972 21:46:27 GMT"),
+                Tuples.of("21-Oct-1972 20:16:27 -0230", "21 Oct 1972 22:46:27 GMT"),
+                Tuples.of("21-Oct-1972 20:16:27 -0330", "21 Oct 1972 23:46:27 GMT"),
+                Tuples.of("21-Oct-1972 20:16:27 -0430", "22 Oct 1972 00:46:27 GMT"),
+                Tuples.of("21-Oct-1972 20:16:27 -0530", "22 Oct 1972 01:46:27 GMT"),
+                Tuples.of("21-Oct-1972 20:16:27 -0630", "22 Oct 1972 02:46:27 GMT"),
+                Tuples.of("21-Oct-1972 20:16:27 -0730", "22 Oct 1972 03:46:27 GMT"),
+                Tuples.of("21-Oct-1972 20:16:27 -0830", "22 Oct 1972 04:46:27 GMT"),
+                Tuples.of("21-Oct-1972 20:16:27 -0930", "22 Oct 1972 05:46:27 GMT"),
+                Tuples.of("21-Oct-1972 20:16:27 -1030", "22 Oct 1972 06:46:27 GMT"))
+                .flatMap(tuple ->
+                    Stream.of(
+                        Locale.US,
+                        Locale.FRANCE,
+                        Locale.GERMANY,
+                        Locale.UK,
+                        Locale.CANADA,
+                        Locale.JAPAN,
+                        Locale.KOREA,
+                        Locale.CHINA,
+                        Locale.TAIWAN,
+                        Locale.ITALY
+                    ).map(locale -> Arguments.of(tuple.getT1(), tuple.getT2(), locale)));
     }
 
-    @Test
-    public void testCANADA() throws Exception {
-        runTests(Locale.CANADA);
-    }
-
-    @Test
-    public void testGERMANY() throws Exception {
-        runTests(Locale.GERMANY);
-    }
-
-    @Test
-    public void testFRANCH() throws Exception {
-        runTests(Locale.FRANCE);
-    }
-
-    
-    private void runTests(Locale locale) throws Exception {
+    @ParameterizedTest
+    @MethodSource
+    void decodeDateTimeWithVariousDefaultLocale(String input, String expected, Locale locale) throws Exception {
         Locale.setDefault(locale);
-        decodeDateTime();
-    }
-
-    @SuppressWarnings("deprecation")
-    private void decodeDateTime() throws Exception {
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 +0000").toGMTString()).isEqualTo("21 Oct 1972 20:00:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 +0100").toGMTString()).isEqualTo("21 Oct 1972 19:00:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 +0200").toGMTString()).isEqualTo("21 Oct 1972 18:00:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 +0300").toGMTString()).isEqualTo("21 Oct 1972 17:00:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 +0400").toGMTString()).isEqualTo("21 Oct 1972 16:00:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 +0500").toGMTString()).isEqualTo("21 Oct 1972 15:00:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 +0600").toGMTString()).isEqualTo("21 Oct 1972 14:00:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 +0700").toGMTString()).isEqualTo("21 Oct 1972 13:00:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 +0800").toGMTString()).isEqualTo("21 Oct 1972 12:00:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 +0900").toGMTString()).isEqualTo("21 Oct 1972 11:00:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 +1000").toGMTString()).isEqualTo("21 Oct 1972 10:00:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 +1100").toGMTString()).isEqualTo("21 Oct 1972 09:00:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 +1200").toGMTString()).isEqualTo("21 Oct 1972 08:00:00 GMT");
-
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 +1000").toGMTString()).isEqualTo("21 Oct 1972 10:00:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 -0100").toGMTString()).isEqualTo("21 Oct 1972 21:00:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 -0200").toGMTString()).isEqualTo("21 Oct 1972 22:00:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 -0300").toGMTString()).isEqualTo("21 Oct 1972 23:00:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 -0400").toGMTString()).isEqualTo("22 Oct 1972 00:00:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 -0500").toGMTString()).isEqualTo("22 Oct 1972 01:00:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 -0600").toGMTString()).isEqualTo("22 Oct 1972 02:00:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 -0700").toGMTString()).isEqualTo("22 Oct 1972 03:00:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 -0800").toGMTString()).isEqualTo("22 Oct 1972 04:00:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 -0900").toGMTString()).isEqualTo("22 Oct 1972 05:00:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 -1000").toGMTString()).isEqualTo("22 Oct 1972 06:00:00 GMT");
-
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 +0030").toGMTString()).isEqualTo("21 Oct 1972 19:30:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 +0130").toGMTString()).isEqualTo("21 Oct 1972 18:30:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 +0230").toGMTString()).isEqualTo("21 Oct 1972 17:30:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 +0330").toGMTString()).isEqualTo("21 Oct 1972 16:30:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 +0430").toGMTString()).isEqualTo("21 Oct 1972 15:30:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 +0530").toGMTString()).isEqualTo("21 Oct 1972 14:30:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 +0630").toGMTString()).isEqualTo("21 Oct 1972 13:30:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 +0730").toGMTString()).isEqualTo("21 Oct 1972 12:30:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 +0830").toGMTString()).isEqualTo("21 Oct 1972 11:30:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 +0930").toGMTString()).isEqualTo("21 Oct 1972 10:30:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 +1030").toGMTString()).isEqualTo("21 Oct 1972 09:30:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 +1130").toGMTString()).isEqualTo("21 Oct 1972 08:30:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 +1230").toGMTString()).isEqualTo("21 Oct 1972 07:30:00 GMT");
-
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 -0030").toGMTString()).isEqualTo("21 Oct 1972 20:30:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 -0130").toGMTString()).isEqualTo("21 Oct 1972 21:30:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 -0230").toGMTString()).isEqualTo("21 Oct 1972 22:30:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 -0330").toGMTString()).isEqualTo("21 Oct 1972 23:30:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 -0430").toGMTString()).isEqualTo("22 Oct 1972 00:30:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 -0530").toGMTString()).isEqualTo("22 Oct 1972 01:30:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 -0630").toGMTString()).isEqualTo("22 Oct 1972 02:30:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 -0730").toGMTString()).isEqualTo("22 Oct 1972 03:30:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 -0830").toGMTString()).isEqualTo("22 Oct 1972 04:30:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 -0930").toGMTString()).isEqualTo("22 Oct 1972 05:30:00 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:00:00 -1030").toGMTString()).isEqualTo("22 Oct 1972 06:30:00 GMT");
-
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:16:27 +0000").toGMTString()).isEqualTo("21 Oct 1972 20:16:27 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:16:27 +0100").toGMTString()).isEqualTo("21 Oct 1972 19:16:27 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:16:27 +0200").toGMTString()).isEqualTo("21 Oct 1972 18:16:27 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:16:27 +0300").toGMTString()).isEqualTo("21 Oct 1972 17:16:27 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:16:27 +0400").toGMTString()).isEqualTo("21 Oct 1972 16:16:27 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:16:27 +0500").toGMTString()).isEqualTo("21 Oct 1972 15:16:27 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:16:27 +0600").toGMTString()).isEqualTo("21 Oct 1972 14:16:27 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:16:27 +0700").toGMTString()).isEqualTo("21 Oct 1972 13:16:27 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:16:27 +0800").toGMTString()).isEqualTo("21 Oct 1972 12:16:27 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:16:27 +0900").toGMTString()).isEqualTo("21 Oct 1972 11:16:27 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:16:27 +1000").toGMTString()).isEqualTo("21 Oct 1972 10:16:27 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:16:27 +1100").toGMTString()).isEqualTo("21 Oct 1972 09:16:27 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:16:27 +1200").toGMTString()).isEqualTo("21 Oct 1972 08:16:27 GMT");
-
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:16:27 -0000").toGMTString()).isEqualTo("21 Oct 1972 20:16:27 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:16:27 -0100").toGMTString()).isEqualTo("21 Oct 1972 21:16:27 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:16:27 -0200").toGMTString()).isEqualTo("21 Oct 1972 22:16:27 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:16:27 -0300").toGMTString()).isEqualTo("21 Oct 1972 23:16:27 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:16:27 -0400").toGMTString()).isEqualTo("22 Oct 1972 00:16:27 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:16:27 -0500").toGMTString()).isEqualTo("22 Oct 1972 01:16:27 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:16:27 -0600").toGMTString()).isEqualTo("22 Oct 1972 02:16:27 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:16:27 -0700").toGMTString()).isEqualTo("22 Oct 1972 03:16:27 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:16:27 -0800").toGMTString()).isEqualTo("22 Oct 1972 04:16:27 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:16:27 -0900").toGMTString()).isEqualTo("22 Oct 1972 05:16:27 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:16:27 -1000").toGMTString()).isEqualTo("22 Oct 1972 06:16:27 GMT");
-
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:16:27 +0030").toGMTString()).isEqualTo("21 Oct 1972 19:46:27 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:16:27 +0130").toGMTString()).isEqualTo("21 Oct 1972 18:46:27 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:16:27 +0230").toGMTString()).isEqualTo("21 Oct 1972 17:46:27 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:16:27 +0330").toGMTString()).isEqualTo("21 Oct 1972 16:46:27 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:16:27 +0430").toGMTString()).isEqualTo("21 Oct 1972 15:46:27 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:16:27 +0530").toGMTString()).isEqualTo("21 Oct 1972 14:46:27 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:16:27 +0630").toGMTString()).isEqualTo("21 Oct 1972 13:46:27 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:16:27 +0730").toGMTString()).isEqualTo("21 Oct 1972 12:46:27 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:16:27 +0830").toGMTString()).isEqualTo("21 Oct 1972 11:46:27 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:16:27 +0930").toGMTString()).isEqualTo("21 Oct 1972 10:46:27 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:16:27 +1030").toGMTString()).isEqualTo("21 Oct 1972 09:46:27 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:16:27 +1130").toGMTString()).isEqualTo("21 Oct 1972 08:46:27 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:16:27 +1230").toGMTString()).isEqualTo("21 Oct 1972 07:46:27 GMT");
-
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:16:27 -0030").toGMTString()).isEqualTo("21 Oct 1972 20:46:27 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:16:27 -0130").toGMTString()).isEqualTo("21 Oct 1972 21:46:27 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:16:27 -0230").toGMTString()).isEqualTo("21 Oct 1972 22:46:27 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:16:27 -0330").toGMTString()).isEqualTo("21 Oct 1972 23:46:27 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:16:27 -0430").toGMTString()).isEqualTo("22 Oct 1972 00:46:27 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:16:27 -0530").toGMTString()).isEqualTo("22 Oct 1972 01:46:27 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:16:27 -0630").toGMTString()).isEqualTo("22 Oct 1972 02:46:27 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:16:27 -0730").toGMTString()).isEqualTo("22 Oct 1972 03:46:27 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:16:27 -0830").toGMTString()).isEqualTo("22 Oct 1972 04:46:27 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:16:27 -0930").toGMTString()).isEqualTo("22 Oct 1972 05:46:27 GMT");
-        assertThat(DecoderUtils.decodeDateTime(
-                "21-Oct-1972 20:16:27 -1030").toGMTString()).isEqualTo("22 Oct 1972 06:46:27 GMT");
-
+        LocalDateTime localDateTime = DecoderUtils.decodeDateTime(input);
+        assertThat(ZonedDateTime.of(localDateTime, ZoneId.systemDefault())
+            .withZoneSameInstant(ZoneId.of("GMT"))
+            .format(DateTimeFormatter.ofPattern("dd MMM YYYY HH:mm:ss VV", Locale.US)))
+            .isEqualTo(expected);
     }
 }
diff --git a/protocols/imap/src/test/java/org/apache/james/imap/decode/DecoderUtilsTest.java b/protocols/imap/src/test/java/org/apache/james/imap/decode/DecoderUtilsTest.java
index 82bccee..19ad193 100644
--- a/protocols/imap/src/test/java/org/apache/james/imap/decode/DecoderUtilsTest.java
+++ b/protocols/imap/src/test/java/org/apache/james/imap/decode/DecoderUtilsTest.java
@@ -22,6 +22,11 @@ package org.apache.james.imap.decode;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
 import java.util.Calendar;
 import java.util.Date;
 import java.util.Locale;
@@ -150,16 +155,21 @@ class DecoderUtilsTest {
 
     @ParameterizedTest
     @MethodSource
-    @SuppressWarnings("deprecation")
     void nominalDecodeDateTime(String input, String parsed) throws DecodingException {
-        assertThat(DecoderUtils.decodeDateTime(input).toGMTString()).isEqualTo(parsed);
+        LocalDateTime localDateTime = DecoderUtils.decodeDateTime(input);
+        assertThat(ZonedDateTime.of(localDateTime, ZoneId.systemDefault())
+                .withZoneSameInstant(ZoneId.of("GMT"))
+                .format(DateTimeFormatter.ofPattern("dd MMM YYYY HH:mm:ss VV", Locale.US)))
+            .isEqualTo(parsed);
     }
 
     @Test
-    @SuppressWarnings("deprecation")
     void decodeDatetimeShouldAllowAppleMailPrependsZeroNotSpace() throws Exception {
-        assertThat(DecoderUtils.decodeDateTime("09-Apr-2008 15:17:51 +0200").toGMTString())
-            .isEqualTo("9 Apr 2008 13:17:51 GMT");
+        LocalDateTime localDateTime = DecoderUtils.decodeDateTime("09-Apr-2008 15:17:51 +0200");
+        assertThat(ZonedDateTime.of(localDateTime, ZoneId.systemDefault())
+                .withZoneSameInstant(ZoneId.of("GMT"))
+                .format(DateTimeFormatter.ofPattern("dd MMM YYYY HH:mm:ss VV", Locale.US)))
+            .isEqualTo("09 Apr 2008 13:17:51 GMT");
     }
 
     static Stream<Arguments> nominalDecodeDateTimeWithTimezone() {
@@ -218,7 +228,7 @@ class DecoderUtilsTest {
                 1246838821081L,
                 1226795970848L,
                 1260254185119L
-            ).mapToObj(Date::new)
+            ).mapToObj(Instant::ofEpochMilli)
             .collect(Collectors.toSet())
         ).stream()
             .map(list -> Arguments.of(list.toArray()));
@@ -226,12 +236,11 @@ class DecoderUtilsTest {
 
     @ParameterizedTest
     @MethodSource
-    void nominalDecodeDateTimeWithTimezone(TimeZone zone, Date date) throws Exception {
-        FastDateFormat format = FastDateFormat.getInstance("dd-MMM-yyyy hh:mm:ss Z", zone, Locale.US);
-        String in = format.format(date);
-        Date decodedDate = DecoderUtils.decodeDateTime(in);
-        String out = format.format(decodedDate);
-        assertThat(out).describedAs("Round trip").isEqualTo(in);
+    void nominalDecodeDateTimeWithTimezone(TimeZone zone, Instant date) throws Exception {
+        FastDateFormat format = FastDateFormat.getInstance("dd-MMM-yyyy HH:mm:ss Z", zone, Locale.US);
+        String in = format.format(Date.from(date));
+        LocalDateTime decodedDate = DecoderUtils.decodeDateTime(in);
+        assertThat(decodedDate.atZone(ZoneId.systemDefault())).describedAs("Round trip").isEqualToIgnoringNanos(ZonedDateTime.ofInstant(date, zone.toZoneId()));
     }
 
     static Stream<Arguments> nominalDecodeDigit() {


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


[james-project] 26/27: JAMES-2979 reuse oneHundredMailsShouldBeWellReceived for implementing mailsShouldBeWellReceived

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 b5410122d67093f50ff946d2c6c81e1a390672a1
Author: Matthieu Baechler <ma...@apache.org>
AuthorDate: Fri Dec 6 11:50:42 2019 +0100

    JAMES-2979 reuse oneHundredMailsShouldBeWellReceived for implementing mailsShouldBeWellReceived
---
 .../org/apache/james/MailsShouldBeWellReceived.java    | 18 +++++++++++++++---
 1 file changed, 15 insertions(+), 3 deletions(-)

diff --git a/server/container/guice/guice-common/src/test/java/org/apache/james/MailsShouldBeWellReceived.java b/server/container/guice/guice-common/src/test/java/org/apache/james/MailsShouldBeWellReceived.java
index e06e1b5..45f71fc 100644
--- a/server/container/guice/guice-common/src/test/java/org/apache/james/MailsShouldBeWellReceived.java
+++ b/server/container/guice/guice-common/src/test/java/org/apache/james/MailsShouldBeWellReceived.java
@@ -62,9 +62,20 @@ interface MailsShouldBeWellReceived {
             .addDomain(DOMAIN)
             .addUser(JAMES_USER, PASSWORD);
 
+        MailboxProbeImpl mailboxProbe = server.getProbe(MailboxProbeImpl.class);
+        mailboxProbe.createMailbox("#private", JAMES_USER, DefaultMailboxes.INBOX);
+
+        Port smtpPort = server.getProbe(SmtpGuiceProbe.class).getSmtpPort();
+        String message = Resources.toString(Resources.getResource("eml/htmlMail.eml"), StandardCharsets.UTF_8);
+
         try (SMTPMessageSender sender = new SMTPMessageSender(Domain.LOCALHOST.asString())) {
-            sender.connect(JAMES_SERVER_HOST, server.getProbe(SmtpGuiceProbe.class).getSmtpPort())
-                .sendMessage("bob@any.com", JAMES_USER);
+            Mono.fromRunnable(
+                Throwing.runnable(() -> {
+                    sender.connect(JAMES_SERVER_HOST, smtpPort);
+                    sendUniqueMessage(sender, message);
+                }))
+                .subscribeOn(Schedulers.elastic())
+                .block();
         }
 
         CALMLY_AWAIT.until(() -> server.getProbe(SpoolerProbe.class).processingFinished());
@@ -73,8 +84,9 @@ interface MailsShouldBeWellReceived {
             reader.connect(JAMES_SERVER_HOST, server.getProbe(ImapGuiceProbe.class).getImapPort())
                 .login(JAMES_USER, PASSWORD)
                 .select(IMAPMessageReader.INBOX)
-                .awaitMessage(CALMLY_AWAIT);
+                .awaitMessageCount(CALMLY_AWAIT, 1);
         }
+
     }
 
     @Test


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


[james-project] 03/27: JAMES-2979 Deprecate FileMailQueue

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 927c907c5d5f1895cc44fbfc527a8cbab70dbcc0
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Wed Dec 4 14:09:01 2019 +0700

    JAMES-2979 Deprecate FileMailQueue
---
 server/queue/queue-file/pom.xml                                      | 3 +++
 .../src/main/java/org/apache/james/queue/file/FileMailQueue.java     | 5 +++++
 .../main/java/org/apache/james/queue/file/FileMailQueueFactory.java  | 4 ++++
 3 files changed, 12 insertions(+)

diff --git a/server/queue/queue-file/pom.xml b/server/queue/queue-file/pom.xml
index 07ee0a0..33d70e5 100644
--- a/server/queue/queue-file/pom.xml
+++ b/server/queue/queue-file/pom.xml
@@ -31,6 +31,9 @@
     <packaging>jar</packaging>
 
     <name>Apache James :: Server :: Mail Queue :: File</name>
+    <description>Provides a MailQueue implementation directly leveraging the file system.
+    Deprecated as many management features are missing, as this implementation is not thread safe.
+    Use embedded ActiveMQMailQueue instead.</description>
 
     <dependencies>
         <dependency>
diff --git a/server/queue/queue-file/src/main/java/org/apache/james/queue/file/FileMailQueue.java b/server/queue/queue-file/src/main/java/org/apache/james/queue/file/FileMailQueue.java
index 3f54528..0bcb954 100644
--- a/server/queue/queue-file/src/main/java/org/apache/james/queue/file/FileMailQueue.java
+++ b/server/queue/queue-file/src/main/java/org/apache/james/queue/file/FileMailQueue.java
@@ -64,6 +64,7 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import com.github.fge.lambdas.Throwing;
+
 import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 
@@ -72,7 +73,11 @@ import reactor.core.publisher.Mono;
  * <p/>
  * On create of the {@link FileMailQueue} the {@link #init()} will get called. This takes care of
  * loading the needed meta-data into memory for fast access.
+ *
+ * @deprecated FileMailQueue implementation is unmaintained, incomplete and not thread safe
+ * We recommend using embedded ActiveMQMailQueue implementation instead
  */
+@Deprecated
 public class FileMailQueue implements ManageableMailQueue {
     private static final Logger LOGGER = LoggerFactory.getLogger(FileMailQueue.class);
 
diff --git a/server/queue/queue-file/src/main/java/org/apache/james/queue/file/FileMailQueueFactory.java b/server/queue/queue-file/src/main/java/org/apache/james/queue/file/FileMailQueueFactory.java
index 16e0bc8..87d88a8 100644
--- a/server/queue/queue-file/src/main/java/org/apache/james/queue/file/FileMailQueueFactory.java
+++ b/server/queue/queue-file/src/main/java/org/apache/james/queue/file/FileMailQueueFactory.java
@@ -35,7 +35,11 @@ import com.google.common.collect.ImmutableSet;
 
 /**
  * {@link MailQueueFactory} implementation which returns {@link FileMailQueue} instances
+ *
+ * @deprecated FileMailQueue implementation is unmaintained, incomplete and not thread safe
+ * We recommend using embedded ActiveMQMailQueue implementation instead
  */
+@Deprecated
 public class FileMailQueueFactory implements MailQueueFactory<ManageableMailQueue> {
 
     private final Map<String, ManageableMailQueue> queues = new HashMap<>();


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