You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@james.apache.org by bt...@apache.org on 2020/12/21 07:29:25 UTC

[james-project] branch master updated (94c1847 -> 24dc4ec)

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 94c1847  JAMES-3400 Update Webadmin-cli README.md following separation of reindex from mailbox commands group
     new bb3e426  JAMES-3431 [REFACTORING] Rely on Bouncer DELIVERY_ERROR attribute name
     new 4515e25  JAMES-3431 Fix a JavaDoc error
     new c9b5a40  JAMES-3431 Add action configuration option for DSNBounce
     new 1baa5d2  JAMES-3431 Only include diagnostic code for failed & delayed actions
     new 5c8d248  JAMES-3431 DSN RET parameter should be taken into account when specified
     new da28d75  JAMES-3431 DSNBounce should include diagnostic only if relevant
     new 62d00f1  JAMES-3431 DSNBounce: Action should customize error text footer
     new a728442  JAMES-3431 DSNBounce should carry over Original-Envelope-Id field
     new ec5ac03  JAMES-3431 DSNBounceTest: organize tests in nested classes
     new e4923e8  JAMES-3431 DSNBounceTest: more tests
     new 1c5597d  JAMES-3431 DSNBounce: Add the original subject as part of human readable message
     new 3635e02  JAMES-3431 DSNBounce: Allow configuration of default status code
     new 6c8b2af  JAMES-3431 DSNBounce javaDoc should point to https://tools.ietf.org/html/rfc3464
     new 833710a  JAMES-3481 Mailbox/change handle delegated mailbox
     new 7bf36b3  JAMES-3481 Add capability for fetching delegated change
     new c1d50f9  JAMES-3481 Use Factory to create MailboxChange & State instance
     new 24dc4ec  JAMES-3481 Move common JMAP methods to a dedicated object

The 17 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:
 .../james/modules/mailbox/MemoryMailboxModule.java |    2 +
 .../change/CassandraMailboxChangeRepository.java   |    8 +-
 .../james/jmap/api/change/MailboxChange.java       |  276 ++++-
 .../jmap/api/change/MailboxChangeRepository.java   |    4 +-
 .../change/MemoryMailboxChangeRepository.java      |   20 +
 .../change/MailboxChangeRepositoryContract.java    |  263 ++--
 .../change/MemoryMailboxChangeRepositoryTest.java  |    8 +
 .../apache/james/transport/mailets/DSNBounce.java  |  108 +-
 .../james/transport/mailets/DSNBounceTest.java     | 1298 +++++++++++++++-----
 .../james/jmap/rfc8621/contract/JmapRequests.scala |  159 +++
 .../contract/MailboxChangesMethodContract.scala    |  726 ++++++++++-
 .../memory/MemoryMailboxChangesMethodTest.java     |    6 +
 .../james/jmap/change/MailboxChangeListener.scala  |   21 +-
 .../james/jmap/method/MailboxChangesMethod.scala   |   11 +-
 .../jmap/change/MailboxChangeListenerTest.scala    |   55 +-
 15 files changed, 2413 insertions(+), 552 deletions(-)
 create mode 100644 server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/JmapRequests.scala


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


[james-project] 09/17: JAMES-3431 DSNBounceTest: organize tests in nested classes

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 ec5ac032cc4aeab5ff3fc8dcd9788b69a55a5ba5
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Thu Dec 17 09:07:22 2020 +0700

    JAMES-3431 DSNBounceTest: organize tests in nested classes
---
 .../james/transport/mailets/DSNBounceTest.java     | 728 +++++++++++----------
 1 file changed, 368 insertions(+), 360 deletions(-)

diff --git a/server/mailet/mailets/src/test/java/org/apache/james/transport/mailets/DSNBounceTest.java b/server/mailet/mailets/src/test/java/org/apache/james/transport/mailets/DSNBounceTest.java
index 62e0628..6faa417 100644
--- a/server/mailet/mailets/src/test/java/org/apache/james/transport/mailets/DSNBounceTest.java
+++ b/server/mailet/mailets/src/test/java/org/apache/james/transport/mailets/DSNBounceTest.java
@@ -54,6 +54,7 @@ import org.apache.mailet.base.test.FakeMailContext;
 import org.apache.mailet.base.test.FakeMailContext.SentMail;
 import org.apache.mailet.base.test.FakeMailetConfig;
 import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.Test;
 
 public class DSNBounceTest {
@@ -80,381 +81,324 @@ public class DSNBounceTest {
             .thenReturn("myhost");
     }
 
-    @Test
-    void getMailetInfoShouldReturnValue() {
-        assertThat(dsnBounce.getMailetInfo()).isEqualTo("DSNBounce Mailet");
-    }
-
-    @Test
-    void getAllowedInitParametersShouldReturnTheParameters() {
-        assertThat(dsnBounce.getAllowedInitParameters()).containsOnly("debug", "passThrough", "messageString", "attachment", "sender", "prefix", "action");
-    }
-
-    @Test
-    void initShouldFailWhenUnknownParameterIsConfigured() {
-        FakeMailetConfig mailetConfig = FakeMailetConfig.builder()
+    @Nested
+    class Configuration {
+        @Test
+        void getMailetInfoShouldReturnValue() {
+            assertThat(dsnBounce.getMailetInfo()).isEqualTo("DSNBounce Mailet");
+        }
+
+        @Test
+        void getAllowedInitParametersShouldReturnTheParameters() {
+            assertThat(dsnBounce.getAllowedInitParameters()).containsOnly("debug", "passThrough", "messageString", "attachment", "sender", "prefix", "action");
+        }
+
+        @Test
+        void initShouldFailWhenUnknownParameterIsConfigured() {
+            FakeMailetConfig mailetConfig = FakeMailetConfig.builder()
                 .mailetName(MAILET_NAME)
                 .mailetContext(fakeMailContext)
                 .setProperty("unknwon", "value")
                 .build();
 
-        assertThatThrownBy(() -> dsnBounce.init(mailetConfig))
-            .isInstanceOf(MessagingException.class);
-    }
+            assertThatThrownBy(() -> dsnBounce.init(mailetConfig))
+                .isInstanceOf(MessagingException.class);
+        }
 
-    @Test
-    void getRecipientsShouldReturnReversePathOnly() {
-        assertThat(dsnBounce.getRecipients()).containsOnly(SpecialAddress.REVERSE_PATH);
-    }
+        @Test
+        void getRecipientsShouldReturnReversePathOnly() {
+            assertThat(dsnBounce.getRecipients()).containsOnly(SpecialAddress.REVERSE_PATH);
+        }
 
-    @Test
-    void getToShouldReturnReversePathOnly() {
-        assertThat(dsnBounce.getTo()).containsOnly(SpecialAddress.REVERSE_PATH.toInternetAddress());
-    }
+        @Test
+        void getToShouldReturnReversePathOnly() {
+            assertThat(dsnBounce.getTo()).containsOnly(SpecialAddress.REVERSE_PATH.toInternetAddress());
+        }
 
-    @Test
-    void getReversePathShouldReturnNullSpecialAddress() {
-        Mail mail = null;
-        assertThat(dsnBounce.getReversePath(mail)).contains(SpecialAddress.NULL);
+        @Test
+        void getReversePathShouldReturnNullSpecialAddress() {
+            Mail mail = null;
+            assertThat(dsnBounce.getReversePath(mail)).contains(SpecialAddress.NULL);
+        }
     }
 
-    @Test
-    void serviceShouldSendMultipartMailToTheSender() throws Exception {
-        FakeMailetConfig mailetConfig = FakeMailetConfig.builder()
+    @Nested
+    class GenericTests {
+        @Test
+        void serviceShouldUpdateTheMailStateWhenNoSenderAndPassThroughIsFalse() throws Exception {
+            FakeMailetConfig mailetConfig = FakeMailetConfig.builder()
                 .mailetName(MAILET_NAME)
                 .mailetContext(fakeMailContext)
+                .setProperty("passThrough", "false")
                 .build();
-        dsnBounce.init(mailetConfig);
+            dsnBounce.init(mailetConfig);
 
-        MailAddress senderMailAddress = new MailAddress("sender@domain.com");
-        FakeMail mail = FakeMail.builder()
+            FakeMail mail = FakeMail.builder()
                 .name(MAILET_NAME)
-                .sender(senderMailAddress)
+                .attribute(DELIVERY_ERROR_ATTRIBUTE)
                 .mimeMessage(MimeMessageBuilder.mimeMessageBuilder()
                     .setText("My content"))
                 .recipient("recipient@domain.com")
                 .lastUpdated(Date.from(Instant.parse("2016-09-08T14:25:52.000Z")))
+                .remoteAddr("remoteHost")
                 .build();
 
-        dsnBounce.service(mail);
+            dsnBounce.service(mail);
 
-        List<SentMail> sentMails = fakeMailContext.getSentMails();
-        assertThat(sentMails).hasSize(1);
-        SentMail sentMail = sentMails.get(0);
-        assertThat(sentMail.getSender()).isNull();
-        assertThat(sentMail.getRecipients()).containsOnly(senderMailAddress);
-        MimeMessage sentMessage = sentMail.getMsg();
-        assertThat(sentMessage.getContentType()).contains("multipart/report;");
-        assertThat(sentMessage.getContentType()).contains("report-type=delivery-status");
-    }
+            assertThat(mail.getState()).isEqualTo(Mail.GHOST);
+        }
 
-    @Test
-    void serviceShouldSendMultipartMailContainingTextPart() throws Exception {
-        FakeMailetConfig mailetConfig = FakeMailetConfig.builder()
+        @Test
+        void serviceShouldNotUpdateTheMailStateWhenNoSenderPassThroughHasDefaultValue() throws Exception {
+            FakeMailetConfig mailetConfig = FakeMailetConfig.builder()
                 .mailetName(MAILET_NAME)
                 .mailetContext(fakeMailContext)
                 .build();
-        dsnBounce.init(mailetConfig);
+            dsnBounce.init(mailetConfig);
 
-        MailAddress senderMailAddress = new MailAddress("sender@domain.com");
-        FakeMail mail = FakeMail.builder()
+            FakeMail mail = FakeMail.builder()
                 .name(MAILET_NAME)
-                .sender(senderMailAddress)
                 .attribute(DELIVERY_ERROR_ATTRIBUTE)
                 .mimeMessage(MimeMessageBuilder.mimeMessageBuilder()
                     .setText("My content"))
                 .recipient("recipient@domain.com")
                 .lastUpdated(Date.from(Instant.parse("2016-09-08T14:25:52.000Z")))
+                .remoteAddr("remoteHost")
                 .build();
 
-        dsnBounce.service(mail);
-
-        String hostname = InetAddress.getLocalHost().getHostName();
-        String expectedContent = "Hi. This is the James mail server at " + hostname + ".\nI'm afraid I wasn't able to deliver your message to the following addresses.\nThis is a permanent error; I've given up. Sorry it didn't work out.  Below\nI include the list of recipients and the reason why I was unable to deliver\nyour message.\n\n" +
-                "Failed recipient(s):\n" + 
-                "recipient@domain.com\n" +
-                "\n" +
-                "Error message:\n" +
-                "Delivery error\n" + 
-                "\n";
+            dsnBounce.service(mail);
 
-        List<SentMail> sentMails = fakeMailContext.getSentMails();
-        assertThat(sentMails).hasSize(1);
-        SentMail sentMail = sentMails.get(0);
-        MimeMessage sentMessage = sentMail.getMsg();
-        MimeMultipart content = (MimeMultipart) sentMessage.getContent();
-        BodyPart bodyPart = content.getBodyPart(0);
-        assertThat(bodyPart.getContentType()).isEqualTo("text/plain; charset=us-ascii");
-        assertThat(bodyPart.getContent()).isEqualTo(expectedContent);
-    }
+            assertThat(mail.getState()).isNull();
+        }
 
-    @Test
-    void serviceShouldSendMultipartMailContainingTextPartWhenCustomMessageIsConfigured() throws Exception {
-        FakeMailetConfig mailetConfig = FakeMailetConfig.builder()
+        @Test
+        void serviceShouldNotUpdateTheMailStateWhenNoSenderPassThroughIsTrue() throws Exception {
+            FakeMailetConfig mailetConfig = FakeMailetConfig.builder()
                 .mailetName(MAILET_NAME)
                 .mailetContext(fakeMailContext)
-                .setProperty("messageString", "My custom message\n")
+                .setProperty("passThrough", "true")
                 .build();
-        dsnBounce.init(mailetConfig);
+            dsnBounce.init(mailetConfig);
 
-        MailAddress senderMailAddress = new MailAddress("sender@domain.com");
-        FakeMail mail = FakeMail.builder()
+            FakeMail mail = FakeMail.builder()
                 .name(MAILET_NAME)
-                .sender(senderMailAddress)
                 .attribute(DELIVERY_ERROR_ATTRIBUTE)
                 .mimeMessage(MimeMessageBuilder.mimeMessageBuilder()
                     .setText("My content"))
                 .recipient("recipient@domain.com")
                 .lastUpdated(Date.from(Instant.parse("2016-09-08T14:25:52.000Z")))
+                .remoteAddr("remoteHost")
                 .build();
 
-        dsnBounce.service(mail);
+            dsnBounce.service(mail);
 
-        String expectedContent = "My custom message\n\n" +
-                "Failed recipient(s):\n" + 
-                "recipient@domain.com\n" +
-                "\n" +
-                "Error message:\n" +
-                "Delivery error\n" + 
-                "\n";
-
-        List<SentMail> sentMails = fakeMailContext.getSentMails();
-        assertThat(sentMails).hasSize(1);
-        SentMail sentMail = sentMails.get(0);
-        MimeMessage sentMessage = sentMail.getMsg();
-        MimeMultipart content = (MimeMultipart) sentMessage.getContent();
-        BodyPart bodyPart = content.getBodyPart(0);
-        assertThat(bodyPart.getContentType()).isEqualTo("text/plain; charset=us-ascii");
-        assertThat(bodyPart.getContent()).isEqualTo(expectedContent);
-    }
+            assertThat(mail.getState()).isNull();
+        }
 
-    @Test
-    void serviceShouldSendMultipartMailContainingDSNPart() throws Exception {
-        FakeMailetConfig mailetConfig = FakeMailetConfig.builder()
+        @Test
+        void serviceShouldSetTheDateHeaderWhenNone() throws Exception {
+            FakeMailetConfig mailetConfig = FakeMailetConfig.builder()
                 .mailetName(MAILET_NAME)
                 .mailetContext(fakeMailContext)
                 .build();
-        dsnBounce.init(mailetConfig);
+            dsnBounce.init(mailetConfig);
 
-        MailAddress senderMailAddress = new MailAddress("sender@domain.com");
-        FakeMail mail = FakeMail.builder()
+            MailAddress senderMailAddress = new MailAddress("sender@domain.com");
+            FakeMail mail = FakeMail.builder()
                 .name(MAILET_NAME)
                 .sender(senderMailAddress)
-                .attribute(DELIVERY_ERROR_ATTRIBUTE)
                 .mimeMessage(MimeMessageBuilder.mimeMessageBuilder()
                     .setText("My content"))
                 .recipient("recipient@domain.com")
                 .lastUpdated(Date.from(Instant.parse("2016-09-08T14:25:52.000Z")))
-                .remoteAddr("remoteHost")
                 .build();
 
-        dsnBounce.service(mail);
+            dsnBounce.service(mail);
 
-        String expectedContent = "Reporting-MTA: dns; myhost\n" +
-                "Received-From-MTA: dns; 111.222.333.444\n" +
-                "\n" +
-                "Final-Recipient: rfc822; recipient@domain.com\n" +
-                "Action: failed\n" +
-                "Status: Delivery error\n" +
-                "Diagnostic-Code: X-James; Delivery error\n" +
-                "Last-Attempt-Date: Thu, 8 Sep 2016 14:25:52 XXXXX (UTC)\n";
+            List<SentMail> sentMails = fakeMailContext.getSentMails();
+            assertThat(sentMails).hasSize(1);
+            SentMail sentMail = sentMails.get(0);
+            assertThat(sentMail.getSender()).isNull();
+            assertThat(sentMail.getRecipients()).containsOnly(senderMailAddress);
+            MimeMessage sentMessage = sentMail.getMsg();
+            assertThat(sentMessage.getHeader(RFC2822Headers.DATE)).isNotNull();
+        }
 
-        List<SentMail> sentMails = fakeMailContext.getSentMails();
-        assertThat(sentMails).hasSize(1);
-        SentMail sentMail = sentMails.get(0);
-        MimeMessage sentMessage = sentMail.getMsg();
-        MimeMultipart content = (MimeMultipart) sentMessage.getContent();
-        SharedByteArrayInputStream actualContent = (SharedByteArrayInputStream) content.getBodyPart(1).getContent();
-        assertThat(IOUtils.toString(actualContent, StandardCharsets.UTF_8)).isEqualTo(expectedContent);
-    }
-
-    @Test
-    void serviceShouldUpdateTheMailStateWhenNoSenderAndPassThroughIsFalse() throws Exception {
-        FakeMailetConfig mailetConfig = FakeMailetConfig.builder()
+        @Test
+        void serviceShouldNotModifyTheDateHeaderWhenAlreadyPresent() throws Exception {
+            FakeMailetConfig mailetConfig = FakeMailetConfig.builder()
                 .mailetName(MAILET_NAME)
                 .mailetContext(fakeMailContext)
-                .setProperty("passThrough", "false")
                 .build();
-        dsnBounce.init(mailetConfig);
+            dsnBounce.init(mailetConfig);
 
-        FakeMail mail = FakeMail.builder()
+            MailAddress senderMailAddress = new MailAddress("sender@domain.com");
+            String expectedDate = "Wed, 28 Sep 2016 14:25:52 +0000 (UTC)";
+            FakeMail mail = FakeMail.builder()
                 .name(MAILET_NAME)
-                .attribute(DELIVERY_ERROR_ATTRIBUTE)
+                .sender(senderMailAddress)
                 .mimeMessage(MimeMessageBuilder.mimeMessageBuilder()
-                    .setText("My content"))
+                    .setText("My content")
+                    .addHeader(RFC2822Headers.DATE, expectedDate))
                 .recipient("recipient@domain.com")
                 .lastUpdated(Date.from(Instant.parse("2016-09-08T14:25:52.000Z")))
-                .remoteAddr("remoteHost")
                 .build();
 
-        dsnBounce.service(mail);
+            dsnBounce.service(mail);
 
-        assertThat(mail.getState()).isEqualTo(Mail.GHOST);
-    }
+            List<SentMail> sentMails = fakeMailContext.getSentMails();
+            assertThat(sentMails).hasSize(1);
+            SentMail sentMail = sentMails.get(0);
+            assertThat(sentMail.getSender()).isNull();
+            assertThat(sentMail.getRecipients()).containsOnly(senderMailAddress);
+            MimeMessage sentMessage = sentMail.getMsg();
+            assertThat(sentMessage.getHeader(RFC2822Headers.DATE)[0]).isEqualTo(expectedDate);
+        }
 
-    @Test
-    void serviceShouldNotUpdateTheMailStateWhenNoSenderPassThroughHasDefaultValue() throws Exception {
-        FakeMailetConfig mailetConfig = FakeMailetConfig.builder()
+        @Test
+        void dsnBounceShouldAddPrefixToSubjectWhenPrefixIsConfigured() throws Exception {
+            FakeMailetConfig mailetConfig = FakeMailetConfig.builder()
                 .mailetName(MAILET_NAME)
                 .mailetContext(fakeMailContext)
+                .setProperty("prefix", "pre")
                 .build();
-        dsnBounce.init(mailetConfig);
+            dsnBounce.init(mailetConfig);
 
-        FakeMail mail = FakeMail.builder()
+            FakeMail mail = FakeMail.builder()
                 .name(MAILET_NAME)
-                .attribute(DELIVERY_ERROR_ATTRIBUTE)
+                .sender(MailAddressFixture.ANY_AT_JAMES)
                 .mimeMessage(MimeMessageBuilder.mimeMessageBuilder()
-                    .setText("My content"))
-                .recipient("recipient@domain.com")
-                .lastUpdated(Date.from(Instant.parse("2016-09-08T14:25:52.000Z")))
-                .remoteAddr("remoteHost")
+                    .setSubject("My subject"))
                 .build();
 
-        dsnBounce.service(mail);
+            dsnBounce.service(mail);
 
-        assertThat(mail.getState()).isNull();
-    }
+            assertThat(fakeMailContext.getSentMails()).hasSize(1).allSatisfy(
+                sentMail -> assertThat(sentMail.getSubject()).contains("pre My subject"));
+        }
 
-    @Test
-    void serviceShouldNotUpdateTheMailStateWhenNoSenderPassThroughIsTrue() throws Exception {
-        FakeMailetConfig mailetConfig = FakeMailetConfig.builder()
+        @Test
+        void dsnBounceShouldAllowSenderSpecialPostmaster() throws Exception {
+            FakeMailetConfig mailetConfig = FakeMailetConfig.builder()
                 .mailetName(MAILET_NAME)
                 .mailetContext(fakeMailContext)
-                .setProperty("passThrough", "true")
+                .setProperty("sender", "postmaster")
                 .build();
-        dsnBounce.init(mailetConfig);
+            dsnBounce.init(mailetConfig);
 
-        FakeMail mail = FakeMail.builder()
+            FakeMail mail = FakeMail.builder()
                 .name(MAILET_NAME)
-                .attribute(DELIVERY_ERROR_ATTRIBUTE)
+                .sender(MailAddressFixture.ANY_AT_JAMES)
                 .mimeMessage(MimeMessageBuilder.mimeMessageBuilder()
-                    .setText("My content"))
-                .recipient("recipient@domain.com")
-                .lastUpdated(Date.from(Instant.parse("2016-09-08T14:25:52.000Z")))
-                .remoteAddr("remoteHost")
+                    .setSubject("My subject"))
                 .build();
 
-        dsnBounce.service(mail);
+            dsnBounce.service(mail);
 
-        assertThat(mail.getState()).isNull();
-    }
+            List<SentMail> sentMails = fakeMailContext.getSentMails();
+            assertThat(sentMails).hasSize(1);
+            SentMail sentMail = sentMails.get(0);
+
+            assertThat(sentMail.getMsg().getFrom())
+                .containsOnly(fakeMailContext.getPostmaster().toInternetAddress());
+            assertThat(sentMail.getRecipients()).containsOnly(mail.getSender());
+        }
 
-    @Test
-    void serviceShouldNotAttachTheOriginalMailWhenAttachmentIsEqualToNone() throws Exception {
-        FakeMailetConfig mailetConfig = FakeMailetConfig.builder()
+        @Test
+        void dsnBounceShouldAllowSenderSpecialSender() throws Exception {
+            FakeMailetConfig mailetConfig = FakeMailetConfig.builder()
                 .mailetName(MAILET_NAME)
                 .mailetContext(fakeMailContext)
-                .setProperty("attachment", "none")
+                .setProperty("sender", "sender")
+                .setProperty("prefix", "pre")
                 .build();
-        dsnBounce.init(mailetConfig);
+            dsnBounce.init(mailetConfig);
 
-        MailAddress senderMailAddress = new MailAddress("sender@domain.com");
-        FakeMail mail = FakeMail.builder()
+            FakeMail mail = FakeMail.builder()
                 .name(MAILET_NAME)
-                .sender(senderMailAddress)
+                .sender(MailAddressFixture.ANY_AT_JAMES)
                 .mimeMessage(MimeMessageBuilder.mimeMessageBuilder()
-                    .setText("My content"))
-                .recipient("recipient@domain.com")
-                .lastUpdated(Date.from(Instant.parse("2016-09-08T14:25:52.000Z")))
+                    .setSubject("My subject"))
                 .build();
 
-        dsnBounce.service(mail);
+            dsnBounce.service(mail);
 
-        List<SentMail> sentMails = fakeMailContext.getSentMails();
-        assertThat(sentMails).hasSize(1);
-        SentMail sentMail = sentMails.get(0);
-        assertThat(sentMail.getSender()).isNull();
-        assertThat(sentMail.getRecipients()).containsOnly(senderMailAddress);
-        MimeMessage sentMessage = sentMail.getMsg();
-        MimeMultipart content = (MimeMultipart) sentMessage.getContent();
-        assertThat(content.getCount()).isEqualTo(2);
-    }
+            List<SentMail> sentMails = fakeMailContext.getSentMails();
+            assertThat(sentMails).hasSize(1);
+            SentMail sentMail = sentMails.get(0);
+
+            assertThat(sentMail.getMsg().getFrom()).containsOnly(mail.getSender().toInternetAddress());
+            assertThat(sentMail.getRecipients()).containsOnly(mail.getSender());
+        }
 
-    @Test
-    void serviceShouldAttachTheOriginalMailWhenAttachmentIsEqualToAll() throws Exception {
-        FakeMailetConfig mailetConfig = FakeMailetConfig.builder()
+        @Test
+        void dsnBounceShouldAllowSenderSpecialUnaltered() throws Exception {
+
+            FakeMailetConfig mailetConfig = FakeMailetConfig.builder()
                 .mailetName(MAILET_NAME)
                 .mailetContext(fakeMailContext)
-                .setProperty("attachment", "all")
+                .setProperty("sender", "unaltered")
                 .build();
-        dsnBounce.init(mailetConfig);
-
-        MailAddress senderMailAddress = new MailAddress("sender@domain.com");
-        MimeMessage mimeMessage = MimeMessageBuilder.mimeMessageBuilder()
-            .setText("My content")
-            .build();
-        FakeMail mail = FakeMail.builder()
-            .name(MAILET_NAME)
-            .sender(senderMailAddress)
-            .recipient("recipient@domain.com")
-            .lastUpdated(Date.from(Instant.parse("2016-09-08T14:25:52.000Z")))
-            .mimeMessage(mimeMessage)
-            .build();
-        MimeMessage mimeMessageCopy = new MimeMessage(mimeMessage);
-
-        dsnBounce.service(mail);
-
-        List<SentMail> sentMails = fakeMailContext.getSentMails();
-        assertThat(sentMails).hasSize(1);
-        SentMail sentMail = sentMails.get(0);
-        assertThat(sentMail.getSender()).isNull();
-        assertThat(sentMail.getRecipients()).containsOnly(senderMailAddress);
-        MimeMessage sentMessage = sentMail.getMsg();
-        MimeMultipart content = (MimeMultipart) sentMessage.getContent();
-
-        assertThat(sentMail.getMsg().getContentType()).startsWith("multipart/report;");
-        assertThat(MimeMessageUtil.asString((MimeMessage) content.getBodyPart(2).getContent()))
-            .isEqualTo(MimeMessageUtil.asString(mimeMessageCopy));
-    }
+            dsnBounce.init(mailetConfig);
+
+            FakeMail mail = FakeMail.builder()
+                .name(MAILET_NAME)
+                .sender(MailAddressFixture.ANY_AT_JAMES)
+                .mimeMessage(MimeMessageBuilder.mimeMessageBuilder()
+                    .setSubject("My subject"))
+                .build();
+
+            dsnBounce.service(mail);
 
-    @Test
-    void serviceShouldAttachTheOriginalMailHeadersOnlyWhenAttachmentIsEqualToHeads() throws Exception {
-        FakeMailetConfig mailetConfig = FakeMailetConfig.builder()
+            List<SentMail> sentMails = fakeMailContext.getSentMails();
+            assertThat(sentMails).hasSize(1);
+            SentMail sentMail = sentMails.get(0);
+
+            assertThat(sentMail.getMsg().getFrom()).containsOnly(mail.getSender().toInternetAddress());
+            assertThat(sentMail.getRecipients()).containsOnly(mail.getSender());
+        }
+
+        @Test
+        void dsnBounceShouldAllowSenderSpecialAddress() throws Exception {
+
+            MailAddress bounceSender = new MailAddress("bounces@domain.com");
+            FakeMailetConfig mailetConfig = FakeMailetConfig.builder()
                 .mailetName(MAILET_NAME)
                 .mailetContext(fakeMailContext)
-                .setProperty("attachment", "heads")
+                .setProperty("sender", bounceSender.asPrettyString())
                 .build();
-        dsnBounce.init(mailetConfig);
+            dsnBounce.init(mailetConfig);
 
-        MailAddress senderMailAddress = new MailAddress("sender@domain.com");
-        FakeMail mail = FakeMail.builder()
+            FakeMail mail = FakeMail.builder()
                 .name(MAILET_NAME)
-                .sender(senderMailAddress)
+                .sender(MailAddressFixture.ANY_AT_JAMES)
                 .mimeMessage(MimeMessageBuilder.mimeMessageBuilder()
-                    .setText("My content")
-                    .addHeader("myHeader", "myValue")
-                    .setSubject("mySubject"))
-                .recipient("recipient@domain.com")
-                .lastUpdated(Date.from(Instant.parse("2016-09-08T14:25:52.000Z")))
+                    .setSubject("My subject"))
                 .build();
 
-        dsnBounce.service(mail);
-
-        List<SentMail> sentMails = fakeMailContext.getSentMails();
-        assertThat(sentMails).hasSize(1);
-        SentMail sentMail = sentMails.get(0);
-        assertThat(sentMail.getSender()).isNull();
-        assertThat(sentMail.getRecipients()).containsOnly(senderMailAddress);
-        MimeMessage sentMessage = sentMail.getMsg();
-        MimeMultipart content = (MimeMultipart) sentMessage.getContent();
-        BodyPart bodyPart = content.getBodyPart(2);
-        SharedByteArrayInputStream actualContent = (SharedByteArrayInputStream) bodyPart.getContent();
-        assertThat(IOUtils.toString(actualContent, StandardCharsets.UTF_8))
-            .contains("Subject: mySubject")
-            .contains("myHeader: myValue");
-        assertThat(bodyPart.getContentType()).isEqualTo("text/rfc822-headers; name=mySubject");
+            dsnBounce.service(mail);
+
+            List<SentMail> sentMails = fakeMailContext.getSentMails();
+            assertThat(sentMails).hasSize(1);
+            SentMail sentMail = sentMails.get(0);
+
+            assertThat(sentMail.getMsg().getFrom()).containsOnly(bounceSender.toInternetAddress());
+            assertThat(sentMail.getRecipients()).containsOnly(mail.getSender());
+        }
     }
 
-    @Test
-    void serviceShouldSetTheDateHeaderWhenNone() throws Exception {
-        FakeMailetConfig mailetConfig = FakeMailetConfig.builder()
+    @Nested
+    class FailedAction {
+        @Test
+        void serviceShouldSendMultipartMailToTheSender() throws Exception {
+            FakeMailetConfig mailetConfig = FakeMailetConfig.builder()
                 .mailetName(MAILET_NAME)
                 .mailetContext(fakeMailContext)
                 .build();
-        dsnBounce.init(mailetConfig);
+            dsnBounce.init(mailetConfig);
 
-        MailAddress senderMailAddress = new MailAddress("sender@domain.com");
-        FakeMail mail = FakeMail.builder()
+            MailAddress senderMailAddress = new MailAddress("sender@domain.com");
+            FakeMail mail = FakeMail.builder()
                 .name(MAILET_NAME)
                 .sender(senderMailAddress)
                 .mimeMessage(MimeMessageBuilder.mimeMessageBuilder()
@@ -463,178 +407,242 @@ public class DSNBounceTest {
                 .lastUpdated(Date.from(Instant.parse("2016-09-08T14:25:52.000Z")))
                 .build();
 
-        dsnBounce.service(mail);
+            dsnBounce.service(mail);
 
-        List<SentMail> sentMails = fakeMailContext.getSentMails();
-        assertThat(sentMails).hasSize(1);
-        SentMail sentMail = sentMails.get(0);
-        assertThat(sentMail.getSender()).isNull();
-        assertThat(sentMail.getRecipients()).containsOnly(senderMailAddress);
-        MimeMessage sentMessage = sentMail.getMsg();
-        assertThat(sentMessage.getHeader(RFC2822Headers.DATE)).isNotNull();
-    }
+            List<SentMail> sentMails = fakeMailContext.getSentMails();
+            assertThat(sentMails).hasSize(1);
+            SentMail sentMail = sentMails.get(0);
+            assertThat(sentMail.getSender()).isNull();
+            assertThat(sentMail.getRecipients()).containsOnly(senderMailAddress);
+            MimeMessage sentMessage = sentMail.getMsg();
+            assertThat(sentMessage.getContentType()).contains("multipart/report;");
+            assertThat(sentMessage.getContentType()).contains("report-type=delivery-status");
+        }
 
-    @Test
-    void serviceShouldNotModifyTheDateHeaderWhenAlreadyPresent() throws Exception {
-        FakeMailetConfig mailetConfig = FakeMailetConfig.builder()
+        @Test
+        void serviceShouldSendMultipartMailContainingTextPart() throws Exception {
+            FakeMailetConfig mailetConfig = FakeMailetConfig.builder()
                 .mailetName(MAILET_NAME)
                 .mailetContext(fakeMailContext)
                 .build();
-        dsnBounce.init(mailetConfig);
+            dsnBounce.init(mailetConfig);
 
-        MailAddress senderMailAddress = new MailAddress("sender@domain.com");
-        String expectedDate = "Wed, 28 Sep 2016 14:25:52 +0000 (UTC)";
-        FakeMail mail = FakeMail.builder()
+            MailAddress senderMailAddress = new MailAddress("sender@domain.com");
+            FakeMail mail = FakeMail.builder()
                 .name(MAILET_NAME)
                 .sender(senderMailAddress)
+                .attribute(DELIVERY_ERROR_ATTRIBUTE)
                 .mimeMessage(MimeMessageBuilder.mimeMessageBuilder()
-                    .setText("My content")
-                    .addHeader(RFC2822Headers.DATE, expectedDate))
+                    .setText("My content"))
                 .recipient("recipient@domain.com")
                 .lastUpdated(Date.from(Instant.parse("2016-09-08T14:25:52.000Z")))
                 .build();
 
-        dsnBounce.service(mail);
+            dsnBounce.service(mail);
 
-        List<SentMail> sentMails = fakeMailContext.getSentMails();
-        assertThat(sentMails).hasSize(1);
-        SentMail sentMail = sentMails.get(0);
-        assertThat(sentMail.getSender()).isNull();
-        assertThat(sentMail.getRecipients()).containsOnly(senderMailAddress);
-        MimeMessage sentMessage = sentMail.getMsg();
-        assertThat(sentMessage.getHeader(RFC2822Headers.DATE)[0]).isEqualTo(expectedDate);
-    }
+            String hostname = InetAddress.getLocalHost().getHostName();
+            String expectedContent = "Hi. This is the James mail server at " + hostname + ".\nI'm afraid I wasn't able to deliver your message to the following addresses.\nThis is a permanent error; I've given up. Sorry it didn't work out.  Below\nI include the list of recipients and the reason why I was unable to deliver\nyour message.\n\n" +
+                "Failed recipient(s):\n" +
+                "recipient@domain.com\n" +
+                "\n" +
+                "Error message:\n" +
+                "Delivery error\n" +
+                "\n";
 
-    @Test
-    void dsnBounceShouldAddPrefixToSubjectWhenPrefixIsConfigured() throws Exception {
-        FakeMailetConfig mailetConfig = FakeMailetConfig.builder()
+            List<SentMail> sentMails = fakeMailContext.getSentMails();
+            assertThat(sentMails).hasSize(1);
+            SentMail sentMail = sentMails.get(0);
+            MimeMessage sentMessage = sentMail.getMsg();
+            MimeMultipart content = (MimeMultipart) sentMessage.getContent();
+            BodyPart bodyPart = content.getBodyPart(0);
+            assertThat(bodyPart.getContentType()).isEqualTo("text/plain; charset=us-ascii");
+            assertThat(bodyPart.getContent()).isEqualTo(expectedContent);
+        }
+
+        @Test
+        void serviceShouldSendMultipartMailContainingTextPartWhenCustomMessageIsConfigured() throws Exception {
+            FakeMailetConfig mailetConfig = FakeMailetConfig.builder()
                 .mailetName(MAILET_NAME)
                 .mailetContext(fakeMailContext)
-                .setProperty("prefix", "pre")
+                .setProperty("messageString", "My custom message\n")
                 .build();
-        dsnBounce.init(mailetConfig);
+            dsnBounce.init(mailetConfig);
 
-        FakeMail mail = FakeMail.builder()
+            MailAddress senderMailAddress = new MailAddress("sender@domain.com");
+            FakeMail mail = FakeMail.builder()
                 .name(MAILET_NAME)
-                .sender(MailAddressFixture.ANY_AT_JAMES)
+                .sender(senderMailAddress)
+                .attribute(DELIVERY_ERROR_ATTRIBUTE)
                 .mimeMessage(MimeMessageBuilder.mimeMessageBuilder()
-                    .setSubject("My subject"))
+                    .setText("My content"))
+                .recipient("recipient@domain.com")
+                .lastUpdated(Date.from(Instant.parse("2016-09-08T14:25:52.000Z")))
                 .build();
 
-        dsnBounce.service(mail);
+            dsnBounce.service(mail);
 
-        assertThat(fakeMailContext.getSentMails()).hasSize(1).allSatisfy(
-                sentMail -> assertThat(sentMail.getSubject()).contains("pre My subject"));
-    }
+            String expectedContent = "My custom message\n\n" +
+                "Failed recipient(s):\n" +
+                "recipient@domain.com\n" +
+                "\n" +
+                "Error message:\n" +
+                "Delivery error\n" +
+                "\n";
 
-    @Test
-    void dsnBounceShouldAllowSenderSpecialPostmaster() throws Exception {
-        
-        FakeMailetConfig mailetConfig = FakeMailetConfig.builder()
+            List<SentMail> sentMails = fakeMailContext.getSentMails();
+            assertThat(sentMails).hasSize(1);
+            SentMail sentMail = sentMails.get(0);
+            MimeMessage sentMessage = sentMail.getMsg();
+            MimeMultipart content = (MimeMultipart) sentMessage.getContent();
+            BodyPart bodyPart = content.getBodyPart(0);
+            assertThat(bodyPart.getContentType()).isEqualTo("text/plain; charset=us-ascii");
+            assertThat(bodyPart.getContent()).isEqualTo(expectedContent);
+        }
+
+        @Test
+        void serviceShouldSendMultipartMailContainingDSNPart() throws Exception {
+            FakeMailetConfig mailetConfig = FakeMailetConfig.builder()
                 .mailetName(MAILET_NAME)
                 .mailetContext(fakeMailContext)
-                .setProperty("sender", "postmaster")
                 .build();
-        dsnBounce.init(mailetConfig);
+            dsnBounce.init(mailetConfig);
 
-        FakeMail mail = FakeMail.builder()
+            MailAddress senderMailAddress = new MailAddress("sender@domain.com");
+            FakeMail mail = FakeMail.builder()
                 .name(MAILET_NAME)
-                .sender(MailAddressFixture.ANY_AT_JAMES)
+                .sender(senderMailAddress)
+                .attribute(DELIVERY_ERROR_ATTRIBUTE)
                 .mimeMessage(MimeMessageBuilder.mimeMessageBuilder()
-                    .setSubject("My subject"))
+                    .setText("My content"))
+                .recipient("recipient@domain.com")
+                .lastUpdated(Date.from(Instant.parse("2016-09-08T14:25:52.000Z")))
+                .remoteAddr("remoteHost")
                 .build();
 
-        dsnBounce.service(mail);
-
-        List<SentMail> sentMails = fakeMailContext.getSentMails();
-        assertThat(sentMails).hasSize(1);
-        SentMail sentMail = sentMails.get(0);
-
-        assertThat(sentMail.getMsg().getFrom())
-                .containsOnly(fakeMailContext.getPostmaster().toInternetAddress());
-        assertThat(sentMail.getRecipients()).containsOnly(mail.getSender());
-    }
+            dsnBounce.service(mail);
 
-    @Test
-    void dsnBounceShouldAllowSenderSpecialSender() throws Exception {
+            String expectedContent = "Reporting-MTA: dns; myhost\n" +
+                "Received-From-MTA: dns; 111.222.333.444\n" +
+                "\n" +
+                "Final-Recipient: rfc822; recipient@domain.com\n" +
+                "Action: failed\n" +
+                "Status: Delivery error\n" +
+                "Diagnostic-Code: X-James; Delivery error\n" +
+                "Last-Attempt-Date: Thu, 8 Sep 2016 14:25:52 XXXXX (UTC)\n";
 
-        FakeMailetConfig mailetConfig = FakeMailetConfig.builder()
+            List<SentMail> sentMails = fakeMailContext.getSentMails();
+            assertThat(sentMails).hasSize(1);
+            SentMail sentMail = sentMails.get(0);
+            MimeMessage sentMessage = sentMail.getMsg();
+            MimeMultipart content = (MimeMultipart) sentMessage.getContent();
+            SharedByteArrayInputStream actualContent = (SharedByteArrayInputStream) content.getBodyPart(1).getContent();
+            assertThat(IOUtils.toString(actualContent, StandardCharsets.UTF_8)).isEqualTo(expectedContent);
+        }
+
+        @Test
+        void serviceShouldNotAttachTheOriginalMailWhenAttachmentIsEqualToNone() throws Exception {
+            FakeMailetConfig mailetConfig = FakeMailetConfig.builder()
                 .mailetName(MAILET_NAME)
                 .mailetContext(fakeMailContext)
-                .setProperty("sender", "sender")
-                .setProperty("prefix", "pre")
+                .setProperty("attachment", "none")
                 .build();
-        dsnBounce.init(mailetConfig);
+            dsnBounce.init(mailetConfig);
 
-        FakeMail mail = FakeMail.builder()
+            MailAddress senderMailAddress = new MailAddress("sender@domain.com");
+            FakeMail mail = FakeMail.builder()
                 .name(MAILET_NAME)
-                .sender(MailAddressFixture.ANY_AT_JAMES)
+                .sender(senderMailAddress)
                 .mimeMessage(MimeMessageBuilder.mimeMessageBuilder()
-                        .setSubject("My subject"))
+                    .setText("My content"))
+                .recipient("recipient@domain.com")
+                .lastUpdated(Date.from(Instant.parse("2016-09-08T14:25:52.000Z")))
                 .build();
 
-        dsnBounce.service(mail);
-
-        List<SentMail> sentMails = fakeMailContext.getSentMails();
-        assertThat(sentMails).hasSize(1);
-        SentMail sentMail = sentMails.get(0);
+            dsnBounce.service(mail);
 
-        assertThat(sentMail.getMsg().getFrom()).containsOnly(mail.getSender().toInternetAddress());
-        assertThat(sentMail.getRecipients()).containsOnly(mail.getSender());
-    }
-
-    @Test
-    void dsnBounceShouldAllowSenderSpecialUnaltered() throws Exception {
+            List<SentMail> sentMails = fakeMailContext.getSentMails();
+            assertThat(sentMails).hasSize(1);
+            SentMail sentMail = sentMails.get(0);
+            assertThat(sentMail.getSender()).isNull();
+            assertThat(sentMail.getRecipients()).containsOnly(senderMailAddress);
+            MimeMessage sentMessage = sentMail.getMsg();
+            MimeMultipart content = (MimeMultipart) sentMessage.getContent();
+            assertThat(content.getCount()).isEqualTo(2);
+        }
 
-        FakeMailetConfig mailetConfig = FakeMailetConfig.builder()
+        @Test
+        void serviceShouldAttachTheOriginalMailWhenAttachmentIsEqualToAll() throws Exception {
+            FakeMailetConfig mailetConfig = FakeMailetConfig.builder()
                 .mailetName(MAILET_NAME)
                 .mailetContext(fakeMailContext)
-                .setProperty("sender", "unaltered")
+                .setProperty("attachment", "all")
                 .build();
-        dsnBounce.init(mailetConfig);
+            dsnBounce.init(mailetConfig);
 
-        FakeMail mail = FakeMail.builder()
+            MailAddress senderMailAddress = new MailAddress("sender@domain.com");
+            MimeMessage mimeMessage = MimeMessageBuilder.mimeMessageBuilder()
+                .setText("My content")
+                .build();
+            FakeMail mail = FakeMail.builder()
                 .name(MAILET_NAME)
-                .sender(MailAddressFixture.ANY_AT_JAMES)
-                .mimeMessage(MimeMessageBuilder.mimeMessageBuilder()
-                        .setSubject("My subject"))
+                .sender(senderMailAddress)
+                .recipient("recipient@domain.com")
+                .lastUpdated(Date.from(Instant.parse("2016-09-08T14:25:52.000Z")))
+                .mimeMessage(mimeMessage)
                 .build();
+            MimeMessage mimeMessageCopy = new MimeMessage(mimeMessage);
 
-        dsnBounce.service(mail);
-
-        List<SentMail> sentMails = fakeMailContext.getSentMails();
-        assertThat(sentMails).hasSize(1);
-        SentMail sentMail = sentMails.get(0);
+            dsnBounce.service(mail);
 
-        assertThat(sentMail.getMsg().getFrom()).containsOnly(mail.getSender().toInternetAddress());
-        assertThat(sentMail.getRecipients()).containsOnly(mail.getSender());
-    }
+            List<SentMail> sentMails = fakeMailContext.getSentMails();
+            assertThat(sentMails).hasSize(1);
+            SentMail sentMail = sentMails.get(0);
+            assertThat(sentMail.getSender()).isNull();
+            assertThat(sentMail.getRecipients()).containsOnly(senderMailAddress);
+            MimeMessage sentMessage = sentMail.getMsg();
+            MimeMultipart content = (MimeMultipart) sentMessage.getContent();
 
-    @Test
-    void dsnBounceShouldAllowSenderSpecialAddress() throws Exception {
+            assertThat(sentMail.getMsg().getContentType()).startsWith("multipart/report;");
+            assertThat(MimeMessageUtil.asString((MimeMessage) content.getBodyPart(2).getContent()))
+                .isEqualTo(MimeMessageUtil.asString(mimeMessageCopy));
+        }
 
-        MailAddress bounceSender = new MailAddress("bounces@domain.com");
-        FakeMailetConfig mailetConfig = FakeMailetConfig.builder()
+        @Test
+        void serviceShouldAttachTheOriginalMailHeadersOnlyWhenAttachmentIsEqualToHeads() throws Exception {
+            FakeMailetConfig mailetConfig = FakeMailetConfig.builder()
                 .mailetName(MAILET_NAME)
                 .mailetContext(fakeMailContext)
-                .setProperty("sender", bounceSender.asPrettyString())
+                .setProperty("attachment", "heads")
                 .build();
-        dsnBounce.init(mailetConfig);
+            dsnBounce.init(mailetConfig);
 
-        FakeMail mail = FakeMail.builder()
+            MailAddress senderMailAddress = new MailAddress("sender@domain.com");
+            FakeMail mail = FakeMail.builder()
                 .name(MAILET_NAME)
-                .sender(MailAddressFixture.ANY_AT_JAMES)
+                .sender(senderMailAddress)
                 .mimeMessage(MimeMessageBuilder.mimeMessageBuilder()
-                        .setSubject("My subject"))
+                    .setText("My content")
+                    .addHeader("myHeader", "myValue")
+                    .setSubject("mySubject"))
+                .recipient("recipient@domain.com")
+                .lastUpdated(Date.from(Instant.parse("2016-09-08T14:25:52.000Z")))
                 .build();
 
-        dsnBounce.service(mail);
-
-        List<SentMail> sentMails = fakeMailContext.getSentMails();
-        assertThat(sentMails).hasSize(1);
-        SentMail sentMail = sentMails.get(0);
-
-        assertThat(sentMail.getMsg().getFrom()).containsOnly(bounceSender.toInternetAddress());
-        assertThat(sentMail.getRecipients()).containsOnly(mail.getSender());
+            dsnBounce.service(mail);
+
+            List<SentMail> sentMails = fakeMailContext.getSentMails();
+            assertThat(sentMails).hasSize(1);
+            SentMail sentMail = sentMails.get(0);
+            assertThat(sentMail.getSender()).isNull();
+            assertThat(sentMail.getRecipients()).containsOnly(senderMailAddress);
+            MimeMessage sentMessage = sentMail.getMsg();
+            MimeMultipart content = (MimeMultipart) sentMessage.getContent();
+            BodyPart bodyPart = content.getBodyPart(2);
+            SharedByteArrayInputStream actualContent = (SharedByteArrayInputStream) bodyPart.getContent();
+            assertThat(IOUtils.toString(actualContent, StandardCharsets.UTF_8))
+                .contains("Subject: mySubject")
+                .contains("myHeader: myValue");
+            assertThat(bodyPart.getContentType()).isEqualTo("text/rfc822-headers; name=mySubject");
+        }
     }
 }
\ No newline at end of file


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


[james-project] 06/17: JAMES-3431 DSNBounce should include diagnostic only if relevant

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 da28d755c5b0026539637a91fcbaa766f5ed3184
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Thu Dec 17 08:47:41 2020 +0700

    JAMES-3431 DSNBounce should include diagnostic only if relevant
---
 .../java/org/apache/james/transport/mailets/DSNBounce.java  | 13 +++++++------
 1 file changed, 7 insertions(+), 6 deletions(-)

diff --git a/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/DSNBounce.java b/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/DSNBounce.java
index a5eb82e..3862048 100755
--- a/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/DSNBounce.java
+++ b/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/DSNBounce.java
@@ -20,7 +20,6 @@
 package org.apache.james.transport.mailets;
 
 import static org.apache.james.transport.mailets.remote.delivery.Bouncer.DELIVERY_ERROR;
-import static org.apache.mailet.DsnParameters.Ret.HDRS;
 
 import java.net.InetAddress;
 import java.net.UnknownHostException;
@@ -145,7 +144,7 @@ public class DSNBounce extends GenericMailet implements RedirectNotify {
             return value;
         }
 
-        public boolean shouldIncludeDiagnosticCode() {
+        public boolean shouldIncludeDiagnostic() {
             return shouldIncludeDiagnosticCode;
         }
     }
@@ -418,9 +417,11 @@ public class DSNBounce extends GenericMailet implements RedirectNotify {
                 .map(MailAddress::asString)
                 .collect(Collectors.joining(", ")));
         builder.append(LINE_BREAK).append(LINE_BREAK);
-        builder.append("Error message:").append(LINE_BREAK);
-        builder.append(AttributeUtils.getValueAndCastFromMail(originalMail, DELIVERY_ERROR, String.class).orElse("")).append(LINE_BREAK);
-        builder.append(LINE_BREAK);
+        if (action.shouldIncludeDiagnostic()) {
+            builder.append("Error message:").append(LINE_BREAK);
+            builder.append(AttributeUtils.getValueAndCastFromMail(originalMail, DELIVERY_ERROR, String.class).orElse("")).append(LINE_BREAK);
+            builder.append(LINE_BREAK);
+        }
 
         MimeBodyPart bodyPart = new MimeBodyPart();
         bodyPart.setText(builder.toString());
@@ -477,7 +478,7 @@ public class DSNBounce extends GenericMailet implements RedirectNotify {
         buffer.append("Final-Recipient: rfc822; " + mailAddress.toString()).append(LINE_BREAK);
         buffer.append("Action: ").append(action.asString()).append(LINE_BREAK);
         buffer.append("Status: " + deliveryError).append(LINE_BREAK);
-        if (action.shouldIncludeDiagnosticCode()) {
+        if (action.shouldIncludeDiagnostic()) {
             buffer.append("Diagnostic-Code: " + getDiagnosticType(deliveryError) + "; " + deliveryError).append(LINE_BREAK);
         }
         buffer.append("Last-Attempt-Date: " + dateFormatter.format(ZonedDateTime.ofInstant(lastUpdated.toInstant(), ZoneId.systemDefault())))


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


[james-project] 11/17: JAMES-3431 DSNBounce: Add the original subject as part of human readable message

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 1c5597d87ade7bd2787525308dc1cea917e7446d
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Thu Dec 17 09:51:45 2020 +0700

    JAMES-3431 DSNBounce: Add the original subject as part of human readable message
    
    This will help the received to correlate the DSN message with the mail he sent.
---
 .../apache/james/transport/mailets/DSNBounce.java  |  2 ++
 .../james/transport/mailets/DSNBounceTest.java     | 42 ++++++++++++++++++++++
 2 files changed, 44 insertions(+)

diff --git a/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/DSNBounce.java b/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/DSNBounce.java
index 081caa3..11be842 100755
--- a/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/DSNBounce.java
+++ b/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/DSNBounce.java
@@ -412,6 +412,8 @@ public class DSNBounce extends GenericMailet implements RedirectNotify {
         StringBuilder builder = new StringBuilder();
 
         builder.append(bounceMessage()).append(LINE_BREAK);
+        Optional.ofNullable(originalMail.getMessage().getSubject())
+            .ifPresent(subject -> builder.append("Original email subject: ").append(subject).append(LINE_BREAK).append(LINE_BREAK));
         builder.append(action.asString()).append(" recipient(s):").append(LINE_BREAK);
         builder.append(originalMail.getRecipients()
                 .stream()
diff --git a/server/mailet/mailets/src/test/java/org/apache/james/transport/mailets/DSNBounceTest.java b/server/mailet/mailets/src/test/java/org/apache/james/transport/mailets/DSNBounceTest.java
index f3e31d8..e05a81f 100644
--- a/server/mailet/mailets/src/test/java/org/apache/james/transport/mailets/DSNBounceTest.java
+++ b/server/mailet/mailets/src/test/java/org/apache/james/transport/mailets/DSNBounceTest.java
@@ -648,6 +648,48 @@ public class DSNBounceTest {
         }
 
         @Test
+        void originalSubjectShouldBeCarriedOver() throws Exception {
+            FakeMailetConfig mailetConfig = FakeMailetConfig.builder()
+                .mailetName(MAILET_NAME)
+                .mailetContext(fakeMailContext)
+                .build();
+            dsnBounce.init(mailetConfig);
+
+            MailAddress senderMailAddress = new MailAddress("sender@domain.com");
+            FakeMail mail = FakeMail.builder()
+                .name(MAILET_NAME)
+                .sender(senderMailAddress)
+                .attribute(DELIVERY_ERROR_ATTRIBUTE)
+                .mimeMessage(MimeMessageBuilder.mimeMessageBuilder()
+                    .setSubject("Banana power!")
+                    .setText("My content"))
+                .recipient("recipient@domain.com")
+                .lastUpdated(Date.from(Instant.parse("2016-09-08T14:25:52.000Z")))
+                .build();
+
+            dsnBounce.service(mail);
+
+            String hostname = InetAddress.getLocalHost().getHostName();
+            String expectedContent = "Hi. This is the James mail server at " + hostname + ".\nI'm afraid I wasn't able to deliver your message to the following addresses.\nThis is a permanent error; I've given up. Sorry it didn't work out.  Below\nI include the list of recipients and the reason why I was unable to deliver\nyour message.\n\n" +
+                "Original email subject: Banana power!\n\n" +
+                "Failed recipient(s):\n" +
+                "recipient@domain.com\n" +
+                "\n" +
+                "Error message:\n" +
+                "Delivery error\n" +
+                "\n";
+
+            List<SentMail> sentMails = fakeMailContext.getSentMails();
+            assertThat(sentMails).hasSize(1);
+            SentMail sentMail = sentMails.get(0);
+            MimeMessage sentMessage = sentMail.getMsg();
+            MimeMultipart content = (MimeMultipart) sentMessage.getContent();
+            BodyPart bodyPart = content.getBodyPart(0);
+            assertThat(bodyPart.getContentType()).isEqualTo("text/plain; charset=us-ascii");
+            assertThat(bodyPart.getContent()).isEqualTo(expectedContent);
+        }
+
+        @Test
         void serviceShouldSendMultipartMailContainingTextPartWhenCustomMessageIsConfigured() throws Exception {
             FakeMailetConfig mailetConfig = FakeMailetConfig.builder()
                 .mailetName(MAILET_NAME)


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


[james-project] 13/17: JAMES-3431 DSNBounce javaDoc should point to https://tools.ietf.org/html/rfc3464

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 6c8b2af7f41ef1a2f211bf42efae6a7d057bc5e7
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Thu Dec 17 10:10:05 2020 +0700

    JAMES-3431 DSNBounce javaDoc should point to https://tools.ietf.org/html/rfc3464
    
    Then the code reader is easily handed the implemented specification
---
 .../main/java/org/apache/james/transport/mailets/DSNBounce.java    | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/DSNBounce.java b/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/DSNBounce.java
index c3ffb8d..05319f5 100755
--- a/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/DSNBounce.java
+++ b/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/DSNBounce.java
@@ -78,9 +78,10 @@ import com.google.common.collect.ImmutableSet;
 
 /**
  * <p>
- * Generates a Delivery Status Notification (DSN) Note that this is different
- * than a mail-client's reply, which would use the Reply-To or From header.
- * </p>
+ * Generates a Delivery Status Notification (DSN) as per RFC-3464 An Extensible Message Format for Delivery Status
+ * Notifications (https://tools.ietf.org/html/rfc3464).</p>
+ *
+ * <p>Note that this is different than a mail-client's reply, which would use the Reply-To or From header.</p>
  * <p>
  * Bounced messages are attached in their entirety (headers and content) and the
  * resulting MIME part type is "message/rfc822".<br>


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


[james-project] 08/17: JAMES-3431 DSNBounce should carry over Original-Envelope-Id field

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 a728442a1016ce7fb2a64beda13174b7bb4826bf
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Thu Dec 17 08:58:08 2020 +0700

    JAMES-3431 DSNBounce should carry over Original-Envelope-Id field
---
 .../src/main/java/org/apache/james/transport/mailets/DSNBounce.java | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/DSNBounce.java b/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/DSNBounce.java
index 1c247b1..081caa3 100755
--- a/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/DSNBounce.java
+++ b/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/DSNBounce.java
@@ -453,6 +453,12 @@ public class DSNBounce extends GenericMailet implements RedirectNotify {
         buffer.append("Received-From-MTA: dns; " + originalMail.getRemoteHost())
             .append(LINE_BREAK);
 
+        originalMail.dsnParameters()
+            .flatMap(DsnParameters::getEnvIdParameter)
+            .ifPresent(envId -> buffer.append("Original-Envelope-Id: ")
+                .append(envId.asString())
+                .append(LINE_BREAK));
+
         for (MailAddress rec : originalMail.getRecipients()) {
             appendRecipient(buffer, rec, getDeliveryError(originalMail), originalMail.getLastUpdated());
         }


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


[james-project] 04/17: JAMES-3431 Only include diagnostic code for failed & delayed actions

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 1baa5d2cc55a0da936994d9c25bafda8fe941a2e
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Wed Dec 16 17:45:04 2020 +0700

    JAMES-3431 Only include diagnostic code for failed & delayed actions
---
 .../apache/james/transport/mailets/DSNBounce.java  | 22 +++++++++++++++-------
 1 file changed, 15 insertions(+), 7 deletions(-)

diff --git a/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/DSNBounce.java b/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/DSNBounce.java
index 4a620ee..916c736 100755
--- a/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/DSNBounce.java
+++ b/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/DSNBounce.java
@@ -118,11 +118,11 @@ public class DSNBounce extends GenericMailet implements RedirectNotify {
     private static final Logger LOGGER = LoggerFactory.getLogger(DSNBounce.class);
 
     enum Action {
-        DELIVERED("delivered"),
-        DELAYED("delayed"),
-        FAILED("failed"),
-        EXPANDED("expanded"),
-        RELAYED("relayed");
+        DELIVERED("delivered", false),
+        DELAYED("delayed", true),
+        FAILED("failed", true),
+        EXPANDED("expanded", false),
+        RELAYED("relayed", false);
 
         public static Optional<Action> parse(String serialized) {
             return Stream.of(Action.values())
@@ -131,14 +131,20 @@ public class DSNBounce extends GenericMailet implements RedirectNotify {
         }
 
         private final String value;
+        private final boolean shouldIncludeDiagnosticCode;
 
-        Action(String value) {
+        Action(String value, boolean shouldIncludeDiagnosticCode) {
             this.value = value;
+            this.shouldIncludeDiagnosticCode = shouldIncludeDiagnosticCode;
         }
 
         public String asString() {
             return value;
         }
+
+        public boolean shouldIncludeDiagnosticCode() {
+            return shouldIncludeDiagnosticCode;
+        }
     }
 
     private static final ImmutableSet<String> CONFIGURABLE_PARAMETERS = ImmutableSet.of("debug", "passThrough", "messageString", "attachment", "sender", "prefix", "action");
@@ -452,7 +458,9 @@ public class DSNBounce extends GenericMailet implements RedirectNotify {
         buffer.append("Final-Recipient: rfc822; " + mailAddress.toString()).append(LINE_BREAK);
         buffer.append("Action: ").append(action.asString()).append(LINE_BREAK);
         buffer.append("Status: " + deliveryError).append(LINE_BREAK);
-        buffer.append("Diagnostic-Code: " + getDiagnosticType(deliveryError) + "; " + deliveryError).append(LINE_BREAK);
+        if (action.shouldIncludeDiagnosticCode()) {
+            buffer.append("Diagnostic-Code: " + getDiagnosticType(deliveryError) + "; " + deliveryError).append(LINE_BREAK);
+        }
         buffer.append("Last-Attempt-Date: " + dateFormatter.format(ZonedDateTime.ofInstant(lastUpdated.toInstant(), ZoneId.systemDefault())))
             .append(LINE_BREAK);
     }


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


[james-project] 15/17: JAMES-3481 Add capability for fetching delegated change

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 7bf36b33bfee86afb56eade0ffbc57b3d931cb2e
Author: LanKhuat <dl...@linagora.com>
AuthorDate: Wed Dec 16 18:15:25 2020 +0700

    JAMES-3481 Add capability for fetching delegated change
---
 .../change/CassandraMailboxChangeRepository.java   |  8 ++-
 .../jmap/api/change/MailboxChangeRepository.java   |  2 +
 .../change/MemoryMailboxChangeRepository.java      | 20 ++++++
 .../change/MailboxChangeRepositoryContract.java    |  2 +-
 .../contract/MailboxChangesMethodContract.scala    | 83 ++++++++++++++++++++--
 .../james/jmap/method/MailboxChangesMethod.scala   |  9 ++-
 6 files changed, 114 insertions(+), 10 deletions(-)

diff --git a/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/change/CassandraMailboxChangeRepository.java b/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/change/CassandraMailboxChangeRepository.java
index 738d9c2..72410b6 100644
--- a/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/change/CassandraMailboxChangeRepository.java
+++ b/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/change/CassandraMailboxChangeRepository.java
@@ -22,6 +22,7 @@ package org.apache.james.jmap.cassandra.change;
 import java.util.Optional;
 
 import org.apache.james.jmap.api.change.MailboxChange;
+import org.apache.james.jmap.api.change.MailboxChange.Limit;
 import org.apache.james.jmap.api.change.MailboxChangeRepository;
 import org.apache.james.jmap.api.change.MailboxChanges;
 import org.apache.james.jmap.api.model.AccountId;
@@ -36,7 +37,12 @@ public class CassandraMailboxChangeRepository implements MailboxChangeRepository
     }
 
     @Override
-    public Mono<MailboxChanges> getSinceState(AccountId accountId, MailboxChange.State state, Optional<MailboxChange.Limit> maxIdsToReturn) {
+    public Mono<MailboxChanges> getSinceState(AccountId accountId, MailboxChange.State state, Optional<Limit> maxChanges) {
+        return Mono.empty();
+    }
+
+    @Override
+    public Mono<MailboxChanges> getSinceStateWithDelegation(AccountId accountId, MailboxChange.State state, Optional<Limit> maxChanges) {
         return Mono.empty();
     }
 
diff --git a/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/MailboxChangeRepository.java b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/MailboxChangeRepository.java
index 006316b..201e022 100644
--- a/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/MailboxChangeRepository.java
+++ b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/MailboxChangeRepository.java
@@ -33,5 +33,7 @@ public interface MailboxChangeRepository {
 
     Mono<MailboxChanges> getSinceState(AccountId accountId, State state, Optional<Limit> maxChanges);
 
+    Mono<MailboxChanges> getSinceStateWithDelegation(AccountId accountId, State state, Optional<Limit> maxChanges);
+
     Mono<State> getLatestState(AccountId accountId);
 }
diff --git a/server/data/data-jmap/src/main/java/org/apache/james/jmap/memory/change/MemoryMailboxChangeRepository.java b/server/data/data-jmap/src/main/java/org/apache/james/jmap/memory/change/MemoryMailboxChangeRepository.java
index 5dad517..ab91994 100644
--- a/server/data/data-jmap/src/main/java/org/apache/james/jmap/memory/change/MemoryMailboxChangeRepository.java
+++ b/server/data/data-jmap/src/main/java/org/apache/james/jmap/memory/change/MemoryMailboxChangeRepository.java
@@ -21,6 +21,7 @@ package org.apache.james.jmap.memory.change;
 
 import java.util.Comparator;
 import java.util.Optional;
+import java.util.function.Predicate;
 
 import org.apache.james.jmap.api.change.MailboxChange;
 import org.apache.james.jmap.api.change.MailboxChange.Limit;
@@ -69,6 +70,25 @@ public class MemoryMailboxChangeRepository implements MailboxChangeRepository {
         return findByState(accountId, state)
             .flatMapMany(currentState -> Flux.fromIterable(mailboxChangeMap.get(accountId))
                 .filter(change -> change.getDate().isAfter(currentState.getDate()))
+                .filter(Predicate.not(MailboxChange::isDelegated))
+                .sort(Comparator.comparing(MailboxChange::getDate)))
+            .collect(new MailboxChangeCollector(state, maxChanges.orElse(DEFAULT_NUMBER_OF_CHANGES)));
+    }
+
+    @Override
+    public Mono<MailboxChanges> getSinceStateWithDelegation(AccountId accountId, State state, Optional<Limit> maxChanges) {
+        Preconditions.checkNotNull(accountId);
+        Preconditions.checkNotNull(state);
+        maxChanges.ifPresent(limit -> Preconditions.checkArgument(limit.getValue() > 0, "maxChanges must be a positive integer"));
+        if (state.equals(State.INITIAL)) {
+            return Flux.fromIterable(mailboxChangeMap.get(accountId))
+                .sort(Comparator.comparing(MailboxChange::getDate))
+                .collect(new MailboxChangeCollector(state, maxChanges.orElse(DEFAULT_NUMBER_OF_CHANGES)));
+        }
+
+        return findByState(accountId, state)
+            .flatMapMany(currentState -> Flux.fromIterable(mailboxChangeMap.get(accountId))
+                .filter(change -> change.getDate().isAfter(currentState.getDate()))
                 .sort(Comparator.comparing(MailboxChange::getDate)))
             .collect(new MailboxChangeCollector(state, maxChanges.orElse(DEFAULT_NUMBER_OF_CHANGES)));
     }
diff --git a/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/change/MailboxChangeRepositoryContract.java b/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/change/MailboxChangeRepositoryContract.java
index 26c04ba..293d448 100644
--- a/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/change/MailboxChangeRepositoryContract.java
+++ b/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/change/MailboxChangeRepositoryContract.java
@@ -351,7 +351,7 @@ public interface MailboxChangeRepositoryContract {
         repository.save(oldState);
         repository.save(change1);
 
-        assertThat(repository.getSinceState(ACCOUNT_ID, STATE_0, Optional.empty()).block().getUpdated())
+        assertThat(repository.getSinceStateWithDelegation(ACCOUNT_ID, STATE_0, Optional.empty()).block().getUpdated())
             .containsExactly(TestId.of(1));
 
     }
diff --git a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxChangesMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxChangesMethodContract.scala
index e07eef1..4467a54 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxChangesMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxChangesMethodContract.scala
@@ -489,7 +489,7 @@ trait MailboxChangesMethodContract {
 
       val request =
         s"""{
-           |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+           |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail", "urn:apache:james:params:jmap:mail:shares"],
            |  "methodCalls": [[
            |    "Mailbox/changes",
            |    {
@@ -553,7 +553,7 @@ trait MailboxChangesMethodContract {
 
       val request =
         s"""{
-           |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+           |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail", "urn:apache:james:params:jmap:mail:shares"],
            |  "methodCalls": [[
            |    "Mailbox/changes",
            |    {
@@ -624,7 +624,7 @@ trait MailboxChangesMethodContract {
 
       val request =
         s"""{
-           |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+           |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail", "urn:apache:james:params:jmap:mail:shares"],
            |  "methodCalls": [[
            |    "Mailbox/changes",
            |    {
@@ -694,7 +694,7 @@ trait MailboxChangesMethodContract {
 
       val request =
         s"""{
-           |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+           |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail", "urn:apache:james:params:jmap:mail:shares"],
            |  "methodCalls": [[
            |    "Mailbox/changes",
            |    {
@@ -765,7 +765,7 @@ trait MailboxChangesMethodContract {
 
       val request =
         s"""{
-           |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+           |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail", "urn:apache:james:params:jmap:mail:shares"],
            |  "methodCalls": [[
            |    "Mailbox/changes",
            |    {
@@ -810,6 +810,77 @@ trait MailboxChangesMethodContract {
     }
 
     @Test
+    def mailboxChangesShouldNotReturnUpdatedChangesWhenMissingSharesCapability(server: GuiceJamesServer): Unit = {
+      val mailboxProbe: MailboxProbeImpl = server.getProbe(classOf[MailboxProbeImpl])
+
+      provisionSystemMailboxes(server)
+
+      val path = MailboxPath.forUser(BOB, "mailbox1")
+      val mailboxId: String = mailboxProbe
+        .createMailbox(path)
+        .serialize
+
+      server.getProbe(classOf[ACLProbeImpl])
+        .replaceRights(path, ANDRE.asString, new MailboxACL.Rfc4314Rights(Right.Lookup, Right.Read))
+
+      val message: Message = Message.Builder
+        .of
+        .setSubject("test")
+        .setBody("testmail", StandardCharsets.UTF_8)
+        .build
+      val messageId: MessageId = mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message)).getMessageId
+
+      val oldState: State = storeReferenceState(server, ANDRE)
+
+      destroyEmail(messageId)
+
+      val request =
+        s"""{
+           |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+           |  "methodCalls": [[
+           |    "Mailbox/changes",
+           |    {
+           |      "accountId": "$ANDRE_ACCOUNT_ID",
+           |      "sinceState": "${oldState.getValue}"
+           |    },
+           |    "c1"]]
+           |}""".stripMargin
+
+      val response = `given`(
+        baseRequestSpecBuilder(server)
+          .setAuth(authScheme(UserCredential(ANDRE, ANDRE_PASSWORD)))
+          .addHeader(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+          .setBody(request)
+          .build, new ResponseSpecBuilder().build)
+        .post
+        .`then`
+          .statusCode(SC_OK)
+          .contentType(JSON)
+          .extract
+          .body
+          .asString
+
+      assertThatJson(response)
+        .whenIgnoringPaths("methodResponses[0][1].newState")
+        .withOptions(new Options(IGNORING_ARRAY_ORDER))
+        .isEqualTo(
+          s"""{
+             |    "sessionState": "${SESSION_STATE.value}",
+             |    "methodResponses": [
+             |      [ "Mailbox/changes", {
+             |        "accountId": "$ANDRE_ACCOUNT_ID",
+             |        "oldState": "${oldState.getValue}",
+             |        "hasMoreChanges": false,
+             |        "updatedProperties": [],
+             |        "created": [],
+             |        "updated": [],
+             |        "destroyed": []
+             |      }, "c1"]
+             |    ]
+             |}""".stripMargin)
+    }
+
+    @Test
     @Disabled("Not implemented yet")
     def mailboxChangesShouldReturnUpdatedChangesWhenDestroyDelegatedMailbox(server: GuiceJamesServer): Unit = {
       val mailboxProbe: MailboxProbeImpl = server.getProbe(classOf[MailboxProbeImpl])
@@ -830,7 +901,7 @@ trait MailboxChangesMethodContract {
 
       val request =
         s"""{
-           |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+           |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail", "urn:apache:james:params:jmap:mail:shares"],
            |  "methodCalls": [[
            |    "Mailbox/changes",
            |    {
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxChangesMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxChangesMethod.scala
index 8daac10..5bbca81 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxChangesMethod.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxChangesMethod.scala
@@ -26,7 +26,7 @@ import org.apache.james.jmap.api.change.MailboxChangeRepository
 import org.apache.james.jmap.api.model.{AccountId => JavaAccountId}
 import org.apache.james.jmap.core.CapabilityIdentifier.{CapabilityIdentifier, JMAP_MAIL}
 import org.apache.james.jmap.core.Invocation.{Arguments, MethodName}
-import org.apache.james.jmap.core.{Invocation, Properties, State}
+import org.apache.james.jmap.core.{CapabilityIdentifier, Invocation, Properties, State}
 import org.apache.james.jmap.json.{MailboxSerializer, ResponseSerializer}
 import org.apache.james.jmap.mail.{HasMoreChanges, MailboxChangesRequest, MailboxChangesResponse}
 import org.apache.james.jmap.routes.SessionSupplier
@@ -46,7 +46,12 @@ class MailboxChangesMethod @Inject()(mailboxSerializer: MailboxSerializer,
   override val requiredCapabilities: Set[CapabilityIdentifier] = Set(JMAP_MAIL)
 
   override def doProcess(capabilities: Set[CapabilityIdentifier], invocation: InvocationWithContext, mailboxSession: MailboxSession, request: MailboxChangesRequest): SMono[InvocationWithContext] =
-    SMono.fromPublisher(mailboxChangeRepository.getSinceState(JavaAccountId.fromUsername(mailboxSession.getUser), JavaState.of(request.sinceState.value), request.maxChanged.toJava))
+    SMono.fromPublisher(
+      if (capabilities.contains(CapabilityIdentifier.JAMES_SHARES)) {
+        mailboxChangeRepository.getSinceStateWithDelegation(JavaAccountId.fromUsername(mailboxSession.getUser), JavaState.of(request.sinceState.value), request.maxChanged.toJava)
+      } else {
+        mailboxChangeRepository.getSinceState(JavaAccountId.fromUsername(mailboxSession.getUser), JavaState.of(request.sinceState.value), request.maxChanged.toJava)
+      })
       .map(mailboxChanges => MailboxChangesResponse(
         accountId = request.accountId,
         oldState = request.sinceState,


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


[james-project] 17/17: JAMES-3481 Move common JMAP methods to a dedicated object

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 24dc4ec52ec2b57db0c6e145366389f95c3f1236
Author: LanKhuat <dl...@linagora.com>
AuthorDate: Thu Dec 17 16:50:18 2020 +0700

    JAMES-3481 Move common JMAP methods to a dedicated object
---
 .../james/jmap/rfc8621/contract/JmapRequests.scala | 159 +++++++++++++++++++++
 .../contract/MailboxChangesMethodContract.scala    | 152 ++------------------
 2 files changed, 170 insertions(+), 141 deletions(-)

diff --git a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/JmapRequests.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/JmapRequests.scala
new file mode 100644
index 0000000..2ac3cee
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/JmapRequests.scala
@@ -0,0 +1,159 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.jmap.rfc8621.contract
+
+import io.netty.handler.codec.http.HttpHeaderNames.ACCEPT
+import io.restassured.RestAssured.`given`
+import io.restassured.http.ContentType.JSON
+import org.apache.http.HttpStatus.SC_OK
+import org.apache.james.jmap.rfc8621.contract.Fixture.ACCEPT_RFC8621_VERSION_HEADER
+import org.apache.james.mailbox.model.MessageId
+
+object JmapRequests {
+  def renameMailbox(mailboxId: String, name: String): Unit = {
+    val request =
+      s"""
+         |{
+         |  "using": [ "urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail" ],
+         |  "methodCalls": [[
+         |    "Mailbox/set", {
+         |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |      "update": {
+         |        "$mailboxId": {
+         |          "name": "$name"
+         |        }
+         |      }
+         |    }, "c1"]
+         |  ]
+         |}
+         |""".stripMargin
+
+    `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .log().ifValidationFails()
+      .statusCode(SC_OK)
+      .contentType(JSON)
+  }
+
+  def destroyMailbox(mailboxId: String): Unit = {
+    val request =
+      s"""
+         |{
+         |  "using": [ "urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail" ],
+         |  "methodCalls": [[
+         |    "Mailbox/set", {
+         |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |      "destroy": ["$mailboxId"]
+         |    }, "c1"]
+         |  ]
+         |}
+         |""".stripMargin
+
+    `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+      .when
+      .post
+      .`then`
+      .log().ifValidationFails()
+      .statusCode(SC_OK)
+      .contentType(JSON)
+  }
+
+  def markEmailAsSeen(messageId: MessageId): Unit = {
+    val request = String.format(
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [
+         |    ["Email/set", {
+         |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |      "update": {
+         |        "${messageId.serialize}": {
+         |          "keywords": {
+         |             "$$seen": true
+         |          }
+         |        }
+         |      }
+         |    }, "c1"]]
+         |}""".stripMargin)
+
+    `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+      .when
+      .post
+      .`then`
+      .log().ifValidationFails()
+      .statusCode(SC_OK)
+      .contentType(JSON)
+  }
+
+  def markEmailAsNotSeen(messageId: MessageId): Unit = {
+    val request = String.format(
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [
+         |    ["Email/set", {
+         |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |      "update": {
+         |        "${messageId.serialize}": {
+         |          "keywords/$$seen": null
+         |        }
+         |      }
+         |    }, "c1"]]
+         |}""".stripMargin)
+
+    `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+      .when
+      .post
+      .`then`
+      .log().ifValidationFails()
+      .statusCode(SC_OK)
+      .contentType(JSON)
+  }
+
+  def destroyEmail(messageId: MessageId): Unit = {
+    val request = String.format(
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [
+         |    ["Email/set", {
+         |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |      "destroy": ["${messageId.serialize}"]
+         |    }, "c1"]]
+         |}""".stripMargin)
+
+    `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+      .when
+      .post
+      .`then`
+      .log().ifValidationFails()
+      .statusCode(SC_OK)
+      .contentType(JSON)
+  }
+}
diff --git a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxChangesMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxChangesMethodContract.scala
index c230dd5..36cdfad 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxChangesMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxChangesMethodContract.scala
@@ -154,7 +154,7 @@ trait MailboxChangesMethodContract {
 
     val oldState: State = storeReferenceState(server, BOB)
 
-    renameMailbox(mailboxId, "mailbox11")
+    JmapRequests.renameMailbox(mailboxId, "mailbox11")
 
     val request =
       s"""{
@@ -284,7 +284,7 @@ trait MailboxChangesMethodContract {
 
     val oldState: State = storeReferenceState(server, BOB)
 
-    markEmailAsSeen(messageId)
+    JmapRequests.markEmailAsSeen(messageId)
 
     val request =
       s"""{
@@ -352,7 +352,7 @@ trait MailboxChangesMethodContract {
 
     val oldState: State = storeReferenceState(server, BOB)
 
-    markEmailAsNotSeen(messageId)
+    JmapRequests.markEmailAsNotSeen(messageId)
 
     val request =
       s"""{
@@ -418,7 +418,7 @@ trait MailboxChangesMethodContract {
 
     val oldState: State = storeReferenceState(server, BOB)
 
-    destroyEmail(messageId)
+    JmapRequests.destroyEmail(messageId)
 
     val request =
       s"""{
@@ -551,7 +551,7 @@ trait MailboxChangesMethodContract {
 
       val oldState: State = storeReferenceState(server, ANDRE)
 
-      renameMailbox(mailboxId, "mailbox11")
+      JmapRequests.renameMailbox(mailboxId, "mailbox11")
 
       val request =
         s"""{
@@ -622,7 +622,7 @@ trait MailboxChangesMethodContract {
 
       val oldState: State = storeReferenceState(server, ANDRE)
 
-      markEmailAsSeen(messageId)
+      JmapRequests.markEmailAsSeen(messageId)
 
       val request =
         s"""{
@@ -692,7 +692,7 @@ trait MailboxChangesMethodContract {
 
       val oldState: State = storeReferenceState(server, ANDRE)
 
-      markEmailAsNotSeen(messageId)
+      JmapRequests.markEmailAsNotSeen(messageId)
 
       val request =
         s"""{
@@ -763,7 +763,7 @@ trait MailboxChangesMethodContract {
 
       val oldState: State = storeReferenceState(server, ANDRE)
 
-      destroyEmail(messageId)
+      JmapRequests.destroyEmail(messageId)
 
       val request =
         s"""{
@@ -834,7 +834,7 @@ trait MailboxChangesMethodContract {
 
       val oldState: State = storeReferenceState(server, ANDRE)
 
-      destroyEmail(messageId)
+      JmapRequests.destroyEmail(messageId)
 
       val request =
         s"""{
@@ -899,7 +899,7 @@ trait MailboxChangesMethodContract {
 
       val oldState: State = storeReferenceState(server, ANDRE)
 
-      destroyMailbox(mailboxId)
+      JmapRequests.destroyMailbox(mailboxId)
 
       val request =
         s"""{
@@ -1032,7 +1032,7 @@ trait MailboxChangesMethodContract {
     val mailboxId2: String = mailboxProbe
       .createMailbox(path2)
       .serialize
-    renameMailbox(mailboxId2, "mailbox22")
+    JmapRequests.renameMailbox(mailboxId2, "mailbox22")
 
     server.getProbe(classOf[MailboxProbeImpl])
       .deleteMailbox(path1.getNamespace, BOB.asString(), path1.getName)
@@ -1416,136 +1416,6 @@ trait MailboxChangesMethodContract {
            |}""".stripMargin)
   }
 
-  private def renameMailbox(mailboxId: String, name: String): Unit = {
-    val request =
-      s"""
-         |{
-         |  "using": [ "urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail" ],
-         |  "methodCalls": [[
-         |    "Mailbox/set", {
-         |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
-         |      "update": {
-         |        "$mailboxId": {
-         |          "name": "$name"
-         |        }
-         |      }
-         |    }, "c1"]
-         |  ]
-         |}
-         |""".stripMargin
-
-    `given`
-      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
-      .body(request)
-    .when
-      .post
-    .`then`
-      .log().ifValidationFails()
-      .statusCode(SC_OK)
-      .contentType(JSON)
-  }
-
-  private def destroyMailbox(mailboxId: String): Unit = {
-    val request =
-      s"""
-         |{
-         |  "using": [ "urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail" ],
-         |  "methodCalls": [[
-         |    "Mailbox/set", {
-         |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
-         |      "destroy": ["$mailboxId"]
-         |    }, "c1"]
-         |  ]
-         |}
-         |""".stripMargin
-
-    `given`
-      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
-      .body(request)
-    .when
-      .post
-    .`then`
-      .log().ifValidationFails()
-      .statusCode(SC_OK)
-      .contentType(JSON)
-  }
-
-  private def markEmailAsSeen(messageId: MessageId): Unit = {
-    val request = String.format(
-      s"""{
-         |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
-         |  "methodCalls": [
-         |    ["Email/set", {
-         |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
-         |      "update": {
-         |        "${messageId.serialize}": {
-         |          "keywords": {
-         |             "$$seen": true
-         |          }
-         |        }
-         |      }
-         |    }, "c1"]]
-         |}""".stripMargin)
-
-    `given`
-      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
-      .body(request)
-    .when
-      .post
-    .`then`
-      .log().ifValidationFails()
-      .statusCode(SC_OK)
-      .contentType(JSON)
-  }
-
-  private def markEmailAsNotSeen(messageId: MessageId): Unit = {
-    val request = String.format(
-      s"""{
-         |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
-         |  "methodCalls": [
-         |    ["Email/set", {
-         |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
-         |      "update": {
-         |        "${messageId.serialize}": {
-         |          "keywords/$$seen": null
-         |        }
-         |      }
-         |    }, "c1"]]
-         |}""".stripMargin)
-
-    `given`
-      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
-      .body(request)
-    .when
-      .post
-    .`then`
-      .log().ifValidationFails()
-      .statusCode(SC_OK)
-      .contentType(JSON)
-  }
-
-  private def destroyEmail(messageId: MessageId): Unit = {
-    val request = String.format(
-      s"""{
-         |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
-         |  "methodCalls": [
-         |    ["Email/set", {
-         |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
-         |      "destroy": ["${messageId.serialize}"]
-         |    }, "c1"]]
-         |}""".stripMargin)
-
-    `given`
-      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
-      .body(request)
-    .when
-      .post
-    .`then`
-      .log().ifValidationFails()
-      .statusCode(SC_OK)
-      .contentType(JSON)
-  }
-
   private def storeReferenceState(server: GuiceJamesServer, username: Username): State = {
     val state: State = stateFactory.generate()
     val jmapGuiceProbe: JmapGuiceProbe = server.getProbe(classOf[JmapGuiceProbe])


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


[james-project] 12/17: JAMES-3431 DSNBounce: Allow configuration of default 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 3635e022e54a3d9a42439e731ec8802852e608fe
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Thu Dec 17 10:05:58 2020 +0700

    JAMES-3431 DSNBounce: Allow configuration of default status code
    
    This enable positioning a meaningful default value instead of defaulting to 'unknown' - which is not spec compliant.
---
 .../apache/james/transport/mailets/DSNBounce.java  | 13 ++++++-
 .../james/transport/mailets/DSNBounceTest.java     | 44 +++++++++++++++++++++-
 2 files changed, 54 insertions(+), 3 deletions(-)

diff --git a/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/DSNBounce.java b/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/DSNBounce.java
index 11be842..c3ffb8d 100755
--- a/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/DSNBounce.java
+++ b/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/DSNBounce.java
@@ -110,10 +110,17 @@ import com.google.common.collect.ImmutableSet;
  *   &lt;passThrough&gt;<i>true or false, default=true</i>&lt;/passThrough&gt;
  *   &lt;debug&gt;<i>true or false, default=false</i>&lt;/debug&gt;
  *   &lt;action&gt;<i>failed, delayed, delivered, expanded or relayed, default=failed</i>&lt;/action&gt;
+ *   &lt;defaultStatus&gt;<i>failed, delayed, delivered, expanded or relayed, default=unknown</i>&lt;/defaultStatus&gt;  &lt;!-- See https://tools.ietf.org/html/rfc3463 --&gt;
  * &lt;/mailet&gt;
  * </code>
  * </pre>
  *
+ * Possible values for defaultStatus (X being a digit):
+ *  - General structure is X.XXX.XXX
+ *  - 2.XXX.XXX indicates success and is suitable for relayed, delivered and expanded actions. 2.0.0 provides no further information.
+ *  - 4.XXX.XXX indicates transient failures and is suitable for delayed action. 4.0.0 provides no further information.
+ *  - 5.XXX.XXX indicates permanent failures and is suitable for failed. 5.0.0 provides no further information.
+ *
  * @see RedirectNotify
  */
 
@@ -150,7 +157,7 @@ public class DSNBounce extends GenericMailet implements RedirectNotify {
         }
     }
 
-    private static final ImmutableSet<String> CONFIGURABLE_PARAMETERS = ImmutableSet.of("debug", "passThrough", "messageString", "attachment", "sender", "prefix", "action");
+    private static final ImmutableSet<String> CONFIGURABLE_PARAMETERS = ImmutableSet.of("debug", "passThrough", "messageString", "attachment", "sender", "prefix", "action", "defaultStatus");
     private static final List<MailAddress> RECIPIENT_MAIL_ADDRESSES = ImmutableList.of(SpecialAddress.REVERSE_PATH);
     private static final List<InternetAddress> TO_INTERNET_ADDRESSES = ImmutableList.of(SpecialAddress.REVERSE_PATH.toInternetAddress());
 
@@ -163,6 +170,7 @@ public class DSNBounce extends GenericMailet implements RedirectNotify {
     private final DateTimeFormatter dateFormatter;
     private String messageString = null;
     private Action action = null;
+    private String defaultStatus;
 
     @Inject
     public DSNBounce(DNSService dns) {
@@ -195,6 +203,7 @@ public class DSNBounce extends GenericMailet implements RedirectNotify {
             .map(configuredValue -> Action.parse(configuredValue)
                 .orElseThrow(() -> new IllegalArgumentException("Action '" + configuredValue + "' is not supported")))
             .orElse(Action.FAILED);
+        defaultStatus = getInitParameter("defaultStatus", "unknown");
     }
 
     @Override
@@ -497,7 +506,7 @@ public class DSNBounce extends GenericMailet implements RedirectNotify {
     private String getDeliveryError(Mail originalMail) {
         return AttributeUtils
             .getValueAndCastFromMail(originalMail, DELIVERY_ERROR, String.class)
-            .orElse("unknown");
+            .orElse(defaultStatus);
     }
 
     private String getDiagnosticType(String diagnosticCode) {
diff --git a/server/mailet/mailets/src/test/java/org/apache/james/transport/mailets/DSNBounceTest.java b/server/mailet/mailets/src/test/java/org/apache/james/transport/mailets/DSNBounceTest.java
index e05a81f..c06f0ba 100644
--- a/server/mailet/mailets/src/test/java/org/apache/james/transport/mailets/DSNBounceTest.java
+++ b/server/mailet/mailets/src/test/java/org/apache/james/transport/mailets/DSNBounceTest.java
@@ -91,7 +91,7 @@ public class DSNBounceTest {
 
         @Test
         void getAllowedInitParametersShouldReturnTheParameters() {
-            assertThat(dsnBounce.getAllowedInitParameters()).containsOnly("debug", "passThrough", "messageString", "attachment", "sender", "prefix", "action");
+            assertThat(dsnBounce.getAllowedInitParameters()).containsOnly("debug", "passThrough", "messageString", "attachment", "sender", "prefix", "action", "defaultStatus");
         }
 
         @Test
@@ -1275,4 +1275,46 @@ public class DSNBounceTest {
         SharedByteArrayInputStream actualContent = (SharedByteArrayInputStream) content.getBodyPart(1).getContent();
         assertThat(IOUtils.toString(actualContent, StandardCharsets.UTF_8)).isEqualTo(expectedContent);
     }
+
+    @Test
+    void defaultStatusShouldBeUsedWhenNone() throws Exception {
+        FakeMailetConfig mailetConfig = FakeMailetConfig.builder()
+            .mailetName(MAILET_NAME)
+            .mailetContext(fakeMailContext)
+            .setProperty("defaultStatus", "4.0.0")
+            .build();
+        dsnBounce.init(mailetConfig);
+
+        MailAddress senderMailAddress = new MailAddress("sender@domain.com");
+        FakeMail mail = FakeMail.builder()
+            .name(MAILET_NAME)
+            .sender(senderMailAddress)
+            .mimeMessage(MimeMessageBuilder.mimeMessageBuilder()
+                .setText("My content"))
+            .recipient("recipient@domain.com")
+            .lastUpdated(Date.from(Instant.parse("2016-09-08T14:25:52.000Z")))
+            .remoteAddr("remoteHost")
+            .build();
+        mail.setDsnParameters(DsnParameters.builder().envId(DsnParameters.EnvId.of("xyz")).build().get());
+
+        dsnBounce.service(mail);
+
+        String expectedContent = "Reporting-MTA: dns; myhost\n" +
+            "Received-From-MTA: dns; 111.222.333.444\n" +
+            "Original-Envelope-Id: xyz\n" +
+            "\n" +
+            "Final-Recipient: rfc822; recipient@domain.com\n" +
+            "Action: failed\n" +
+            "Status: 4.0.0\n" +
+            "Diagnostic-Code: X-James; 4.0.0\n" +
+            "Last-Attempt-Date: Thu, 8 Sep 2016 14:25:52 XXXXX (UTC)\n";
+
+        List<SentMail> sentMails = fakeMailContext.getSentMails();
+        assertThat(sentMails).hasSize(1);
+        SentMail sentMail = sentMails.get(0);
+        MimeMessage sentMessage = sentMail.getMsg();
+        MimeMultipart content = (MimeMultipart) sentMessage.getContent();
+        SharedByteArrayInputStream actualContent = (SharedByteArrayInputStream) content.getBodyPart(1).getContent();
+        assertThat(IOUtils.toString(actualContent, StandardCharsets.UTF_8)).isEqualTo(expectedContent);
+    }
 }
\ No newline at end of file


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


[james-project] 03/17: JAMES-3431 Add action configuration option for DSNBounce

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 c9b5a403453b0b9eda1bf12374114aa07ec973d2
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Wed Dec 16 16:49:03 2020 +0700

    JAMES-3431 Add action configuration option for DSNBounce
---
 .../apache/james/transport/mailets/DSNBounce.java  | 35 ++++++++++++++++++++--
 .../james/transport/mailets/DSNBounceTest.java     |  2 +-
 2 files changed, 34 insertions(+), 3 deletions(-)

diff --git a/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/DSNBounce.java b/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/DSNBounce.java
index 98c33ce..4a620ee 100755
--- a/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/DSNBounce.java
+++ b/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/DSNBounce.java
@@ -33,6 +33,7 @@ import java.util.Optional;
 import java.util.Set;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 import javax.inject.Inject;
 import javax.mail.MessagingException;
@@ -105,6 +106,7 @@ import com.google.common.collect.ImmutableSet;
  *   &lt;messageString&gt;<i>the message sent in the bounce, the first occurrence of the pattern [machine] is replaced with the name of the executing machine, default=Hi. This is the James mail server at [machine] ... </i>&lt;/messageString&gt;
  *   &lt;passThrough&gt;<i>true or false, default=true</i>&lt;/passThrough&gt;
  *   &lt;debug&gt;<i>true or false, default=false</i>&lt;/debug&gt;
+ *   &lt;action&gt;<i>failed, delayed, delivered, expanded or relayed, default=failed</i>&lt;/action&gt;
  * &lt;/mailet&gt;
  * </code>
  * </pre>
@@ -115,7 +117,31 @@ import com.google.common.collect.ImmutableSet;
 public class DSNBounce extends GenericMailet implements RedirectNotify {
     private static final Logger LOGGER = LoggerFactory.getLogger(DSNBounce.class);
 
-    private static final ImmutableSet<String> CONFIGURABLE_PARAMETERS = ImmutableSet.of("debug", "passThrough", "messageString", "attachment", "sender", "prefix");
+    enum Action {
+        DELIVERED("delivered"),
+        DELAYED("delayed"),
+        FAILED("failed"),
+        EXPANDED("expanded"),
+        RELAYED("relayed");
+
+        public static Optional<Action> parse(String serialized) {
+            return Stream.of(Action.values())
+                .filter(value -> value.asString().equalsIgnoreCase(serialized))
+                .findFirst();
+        }
+
+        private final String value;
+
+        Action(String value) {
+            this.value = value;
+        }
+
+        public String asString() {
+            return value;
+        }
+    }
+
+    private static final ImmutableSet<String> CONFIGURABLE_PARAMETERS = ImmutableSet.of("debug", "passThrough", "messageString", "attachment", "sender", "prefix", "action");
     private static final List<MailAddress> RECIPIENT_MAIL_ADDRESSES = ImmutableList.of(SpecialAddress.REVERSE_PATH);
     private static final List<InternetAddress> TO_INTERNET_ADDRESSES = ImmutableList.of(SpecialAddress.REVERSE_PATH.toInternetAddress());
 
@@ -127,6 +153,7 @@ public class DSNBounce extends GenericMailet implements RedirectNotify {
     private final DNSService dns;
     private final DateTimeFormatter dateFormatter;
     private String messageString = null;
+    private Action action = null;
 
     @Inject
     public DSNBounce(DNSService dns) {
@@ -155,6 +182,10 @@ public class DSNBounce extends GenericMailet implements RedirectNotify {
         }
         messageString = getInitParameter("messageString",
                 "Hi. This is the James mail server at [machine].\nI'm afraid I wasn't able to deliver your message to the following addresses.\nThis is a permanent error; I've given up. Sorry it didn't work out.  Below\nI include the list of recipients and the reason why I was unable to deliver\nyour message.\n");
+        action = Optional.ofNullable(getInitParameter("action", null))
+            .map(configuredValue -> Action.parse(configuredValue)
+                .orElseThrow(() -> new IllegalArgumentException("Action '" + configuredValue + "' is not supported")))
+            .orElse(Action.FAILED);
     }
 
     @Override
@@ -419,7 +450,7 @@ public class DSNBounce extends GenericMailet implements RedirectNotify {
     private void appendRecipient(StringBuffer buffer, MailAddress mailAddress, String deliveryError, Date lastUpdated) {
         buffer.append(LINE_BREAK);
         buffer.append("Final-Recipient: rfc822; " + mailAddress.toString()).append(LINE_BREAK);
-        buffer.append("Action: failed").append(LINE_BREAK);
+        buffer.append("Action: ").append(action.asString()).append(LINE_BREAK);
         buffer.append("Status: " + deliveryError).append(LINE_BREAK);
         buffer.append("Diagnostic-Code: " + getDiagnosticType(deliveryError) + "; " + deliveryError).append(LINE_BREAK);
         buffer.append("Last-Attempt-Date: " + dateFormatter.format(ZonedDateTime.ofInstant(lastUpdated.toInstant(), ZoneId.systemDefault())))
diff --git a/server/mailet/mailets/src/test/java/org/apache/james/transport/mailets/DSNBounceTest.java b/server/mailet/mailets/src/test/java/org/apache/james/transport/mailets/DSNBounceTest.java
index 429dbf6..62e0628 100644
--- a/server/mailet/mailets/src/test/java/org/apache/james/transport/mailets/DSNBounceTest.java
+++ b/server/mailet/mailets/src/test/java/org/apache/james/transport/mailets/DSNBounceTest.java
@@ -87,7 +87,7 @@ public class DSNBounceTest {
 
     @Test
     void getAllowedInitParametersShouldReturnTheParameters() {
-        assertThat(dsnBounce.getAllowedInitParameters()).containsOnly("debug", "passThrough", "messageString", "attachment", "sender", "prefix");
+        assertThat(dsnBounce.getAllowedInitParameters()).containsOnly("debug", "passThrough", "messageString", "attachment", "sender", "prefix", "action");
     }
 
     @Test


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


[james-project] 07/17: JAMES-3431 DSNBounce: Action should customize error text footer

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 62d00f14ff4a6a9790eafb9e30bc3c679d6ac269
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Thu Dec 17 08:51:54 2020 +0700

    JAMES-3431 DSNBounce: Action should customize error text footer
---
 .../org/apache/james/transport/mailets/DSNBounce.java     | 15 ++++++++-------
 1 file changed, 8 insertions(+), 7 deletions(-)

diff --git a/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/DSNBounce.java b/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/DSNBounce.java
index 3862048..1c247b1 100755
--- a/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/DSNBounce.java
+++ b/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/DSNBounce.java
@@ -29,6 +29,7 @@ import java.time.ZonedDateTime;
 import java.time.format.DateTimeFormatter;
 import java.util.Date;
 import java.util.List;
+import java.util.Locale;
 import java.util.Optional;
 import java.util.Set;
 import java.util.regex.Pattern;
@@ -120,11 +121,11 @@ public class DSNBounce extends GenericMailet implements RedirectNotify {
     private static final Logger LOGGER = LoggerFactory.getLogger(DSNBounce.class);
 
     enum Action {
-        DELIVERED("delivered", false),
-        DELAYED("delayed", true),
-        FAILED("failed", true),
-        EXPANDED("expanded", false),
-        RELAYED("relayed", false);
+        DELIVERED("Delivered", false),
+        DELAYED("Delayed", true),
+        FAILED("Failed", true),
+        EXPANDED("Expanded", false),
+        RELAYED("Relayed", false);
 
         public static Optional<Action> parse(String serialized) {
             return Stream.of(Action.values())
@@ -411,7 +412,7 @@ public class DSNBounce extends GenericMailet implements RedirectNotify {
         StringBuilder builder = new StringBuilder();
 
         builder.append(bounceMessage()).append(LINE_BREAK);
-        builder.append("Failed recipient(s):").append(LINE_BREAK);
+        builder.append(action.asString()).append(" recipient(s):").append(LINE_BREAK);
         builder.append(originalMail.getRecipients()
                 .stream()
                 .map(MailAddress::asString)
@@ -476,7 +477,7 @@ public class DSNBounce extends GenericMailet implements RedirectNotify {
     private void appendRecipient(StringBuffer buffer, MailAddress mailAddress, String deliveryError, Date lastUpdated) {
         buffer.append(LINE_BREAK);
         buffer.append("Final-Recipient: rfc822; " + mailAddress.toString()).append(LINE_BREAK);
-        buffer.append("Action: ").append(action.asString()).append(LINE_BREAK);
+        buffer.append("Action: ").append(action.asString().toLowerCase(Locale.US)).append(LINE_BREAK);
         buffer.append("Status: " + deliveryError).append(LINE_BREAK);
         if (action.shouldIncludeDiagnostic()) {
             buffer.append("Diagnostic-Code: " + getDiagnosticType(deliveryError) + "; " + deliveryError).append(LINE_BREAK);


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


[james-project] 10/17: JAMES-3431 DSNBounceTest: more 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 e4923e849a54dd92b89fe4e7bf1282814823c72f
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Thu Dec 17 09:46:34 2020 +0700

    JAMES-3431 DSNBounceTest: more tests
---
 .../james/transport/mailets/DSNBounceTest.java     | 642 ++++++++++++++++++++-
 1 file changed, 615 insertions(+), 27 deletions(-)

diff --git a/server/mailet/mailets/src/test/java/org/apache/james/transport/mailets/DSNBounceTest.java b/server/mailet/mailets/src/test/java/org/apache/james/transport/mailets/DSNBounceTest.java
index 6faa417..f3e31d8 100644
--- a/server/mailet/mailets/src/test/java/org/apache/james/transport/mailets/DSNBounceTest.java
+++ b/server/mailet/mailets/src/test/java/org/apache/james/transport/mailets/DSNBounceTest.java
@@ -45,6 +45,7 @@ import org.apache.james.dnsservice.api.DNSService;
 import org.apache.james.transport.mailets.redirect.SpecialAddress;
 import org.apache.james.util.MimeMessageUtil;
 import org.apache.mailet.Attribute;
+import org.apache.mailet.DsnParameters;
 import org.apache.mailet.Mail;
 import org.apache.mailet.base.DateFormats;
 import org.apache.mailet.base.MailAddressFixture;
@@ -388,6 +389,193 @@ public class DSNBounceTest {
     }
 
     @Nested
+    class Attachments {
+        @Test
+        void serviceShouldNotAttachTheOriginalMailWhenAttachmentIsEqualToNoneAndDsnRet() throws Exception {
+            FakeMailetConfig mailetConfig = FakeMailetConfig.builder()
+                .mailetName(MAILET_NAME)
+                .mailetContext(fakeMailContext)
+                .setProperty("attachment", "none")
+                .build();
+            dsnBounce.init(mailetConfig);
+
+            MailAddress senderMailAddress = new MailAddress("sender@domain.com");
+            FakeMail mail = FakeMail.builder()
+                .name(MAILET_NAME)
+                .sender(senderMailAddress)
+                .mimeMessage(MimeMessageBuilder.mimeMessageBuilder()
+                    .setText("My content"))
+                .recipient("recipient@domain.com")
+                .lastUpdated(Date.from(Instant.parse("2016-09-08T14:25:52.000Z")))
+                .build();
+            mail.setDsnParameters(DsnParameters.builder().ret(DsnParameters.Ret.FULL).build().get());
+
+            dsnBounce.service(mail);
+
+            List<SentMail> sentMails = fakeMailContext.getSentMails();
+            assertThat(sentMails).hasSize(1);
+            SentMail sentMail = sentMails.get(0);
+            assertThat(sentMail.getSender()).isNull();
+            assertThat(sentMail.getRecipients()).containsOnly(senderMailAddress);
+            MimeMessage sentMessage = sentMail.getMsg();
+            MimeMultipart content = (MimeMultipart) sentMessage.getContent();
+            assertThat(content.getCount()).isEqualTo(2);
+        }
+
+        @Test
+        void serviceShouldAttachTheOriginalMailWhenAttachmentIsEqualToAll() throws Exception {
+            FakeMailetConfig mailetConfig = FakeMailetConfig.builder()
+                .mailetName(MAILET_NAME)
+                .mailetContext(fakeMailContext)
+                .setProperty("attachment", "all")
+                .build();
+            dsnBounce.init(mailetConfig);
+
+            MailAddress senderMailAddress = new MailAddress("sender@domain.com");
+            MimeMessage mimeMessage = MimeMessageBuilder.mimeMessageBuilder()
+                .setText("My content")
+                .build();
+            FakeMail mail = FakeMail.builder()
+                .name(MAILET_NAME)
+                .sender(senderMailAddress)
+                .recipient("recipient@domain.com")
+                .lastUpdated(Date.from(Instant.parse("2016-09-08T14:25:52.000Z")))
+                .mimeMessage(mimeMessage)
+                .build();
+            MimeMessage mimeMessageCopy = new MimeMessage(mimeMessage);
+
+            dsnBounce.service(mail);
+
+            List<SentMail> sentMails = fakeMailContext.getSentMails();
+            assertThat(sentMails).hasSize(1);
+            SentMail sentMail = sentMails.get(0);
+            assertThat(sentMail.getSender()).isNull();
+            assertThat(sentMail.getRecipients()).containsOnly(senderMailAddress);
+            MimeMessage sentMessage = sentMail.getMsg();
+            MimeMultipart content = (MimeMultipart) sentMessage.getContent();
+
+            assertThat(sentMail.getMsg().getContentType()).startsWith("multipart/report;");
+            assertThat(MimeMessageUtil.asString((MimeMessage) content.getBodyPart(2).getContent()))
+                .isEqualTo(MimeMessageUtil.asString(mimeMessageCopy));
+        }
+
+        @Test
+        void serviceShouldAttachTheOriginalMailHeadersOnlyWhenAttachmentIsEqualToHeads() throws Exception {
+            FakeMailetConfig mailetConfig = FakeMailetConfig.builder()
+                .mailetName(MAILET_NAME)
+                .mailetContext(fakeMailContext)
+                .setProperty("attachment", "heads")
+                .build();
+            dsnBounce.init(mailetConfig);
+
+            MailAddress senderMailAddress = new MailAddress("sender@domain.com");
+            FakeMail mail = FakeMail.builder()
+                .name(MAILET_NAME)
+                .sender(senderMailAddress)
+                .mimeMessage(MimeMessageBuilder.mimeMessageBuilder()
+                    .setText("My content")
+                    .addHeader("myHeader", "myValue")
+                    .setSubject("mySubject"))
+                .recipient("recipient@domain.com")
+                .lastUpdated(Date.from(Instant.parse("2016-09-08T14:25:52.000Z")))
+                .build();
+
+            dsnBounce.service(mail);
+
+            List<SentMail> sentMails = fakeMailContext.getSentMails();
+            assertThat(sentMails).hasSize(1);
+            SentMail sentMail = sentMails.get(0);
+            assertThat(sentMail.getSender()).isNull();
+            assertThat(sentMail.getRecipients()).containsOnly(senderMailAddress);
+            MimeMessage sentMessage = sentMail.getMsg();
+            MimeMultipart content = (MimeMultipart) sentMessage.getContent();
+            BodyPart bodyPart = content.getBodyPart(2);
+            SharedByteArrayInputStream actualContent = (SharedByteArrayInputStream) bodyPart.getContent();
+            assertThat(IOUtils.toString(actualContent, StandardCharsets.UTF_8))
+                .contains("Subject: mySubject")
+                .contains("myHeader: myValue");
+            assertThat(bodyPart.getContentType()).isEqualTo("text/rfc822-headers; name=mySubject");
+        }
+
+        @Test
+        void serviceShouldAttachTheOriginalMailWhenRequestedByTheSMTPClient() throws Exception {
+            FakeMailetConfig mailetConfig = FakeMailetConfig.builder()
+                .mailetName(MAILET_NAME)
+                .mailetContext(fakeMailContext)
+                .setProperty("attachment", "heads")
+                .build();
+            dsnBounce.init(mailetConfig);
+
+            MailAddress senderMailAddress = new MailAddress("sender@domain.com");
+            MimeMessage mimeMessage = MimeMessageBuilder.mimeMessageBuilder()
+                .setText("My content")
+                .build();
+            FakeMail mail = FakeMail.builder()
+                .name(MAILET_NAME)
+                .sender(senderMailAddress)
+                .recipient("recipient@domain.com")
+                .lastUpdated(Date.from(Instant.parse("2016-09-08T14:25:52.000Z")))
+                .mimeMessage(mimeMessage)
+                .build();
+            mail.setDsnParameters(DsnParameters.builder().ret(DsnParameters.Ret.FULL).build().get());
+            MimeMessage mimeMessageCopy = new MimeMessage(mimeMessage);
+
+            dsnBounce.service(mail);
+
+            List<SentMail> sentMails = fakeMailContext.getSentMails();
+            assertThat(sentMails).hasSize(1);
+            SentMail sentMail = sentMails.get(0);
+            assertThat(sentMail.getSender()).isNull();
+            assertThat(sentMail.getRecipients()).containsOnly(senderMailAddress);
+            MimeMessage sentMessage = sentMail.getMsg();
+            MimeMultipart content = (MimeMultipart) sentMessage.getContent();
+
+            assertThat(sentMail.getMsg().getContentType()).startsWith("multipart/report;");
+            assertThat(MimeMessageUtil.asString((MimeMessage) content.getBodyPart(2).getContent()))
+                .isEqualTo(MimeMessageUtil.asString(mimeMessageCopy));
+        }
+
+        @Test
+        void serviceShouldAttachTheOriginalHeadersWhenRequestedByTheSMTPClient() throws Exception {
+            FakeMailetConfig mailetConfig = FakeMailetConfig.builder()
+                .mailetName(MAILET_NAME)
+                .mailetContext(fakeMailContext)
+                .setProperty("attachment", "all")
+                .build();
+            dsnBounce.init(mailetConfig);
+
+            MailAddress senderMailAddress = new MailAddress("sender@domain.com");
+            FakeMail mail = FakeMail.builder()
+                .name(MAILET_NAME)
+                .sender(senderMailAddress)
+                .mimeMessage(MimeMessageBuilder.mimeMessageBuilder()
+                    .setText("My content")
+                    .addHeader("myHeader", "myValue")
+                    .setSubject("mySubject"))
+                .recipient("recipient@domain.com")
+                .lastUpdated(Date.from(Instant.parse("2016-09-08T14:25:52.000Z")))
+                .build();
+            mail.setDsnParameters(DsnParameters.builder().ret(DsnParameters.Ret.HDRS).build().get());
+
+            dsnBounce.service(mail);
+
+            List<SentMail> sentMails = fakeMailContext.getSentMails();
+            assertThat(sentMails).hasSize(1);
+            SentMail sentMail = sentMails.get(0);
+            assertThat(sentMail.getSender()).isNull();
+            assertThat(sentMail.getRecipients()).containsOnly(senderMailAddress);
+            MimeMessage sentMessage = sentMail.getMsg();
+            MimeMultipart content = (MimeMultipart) sentMessage.getContent();
+            BodyPart bodyPart = content.getBodyPart(2);
+            SharedByteArrayInputStream actualContent = (SharedByteArrayInputStream) bodyPart.getContent();
+            assertThat(IOUtils.toString(actualContent, StandardCharsets.UTF_8))
+                .contains("Subject: mySubject")
+                .contains("myHeader: myValue");
+            assertThat(bodyPart.getContentType()).isEqualTo("text/rfc822-headers; name=mySubject");
+        }
+    }
+
+    @Nested
     class FailedAction {
         @Test
         void serviceShouldSendMultipartMailToTheSender() throws Exception {
@@ -538,13 +726,17 @@ public class DSNBounceTest {
             SharedByteArrayInputStream actualContent = (SharedByteArrayInputStream) content.getBodyPart(1).getContent();
             assertThat(IOUtils.toString(actualContent, StandardCharsets.UTF_8)).isEqualTo(expectedContent);
         }
+    }
 
+    @Nested
+    class DeliveredAction {
         @Test
-        void serviceShouldNotAttachTheOriginalMailWhenAttachmentIsEqualToNone() throws Exception {
+        void serviceShouldSendMultipartMailToTheSender() throws Exception {
             FakeMailetConfig mailetConfig = FakeMailetConfig.builder()
                 .mailetName(MAILET_NAME)
                 .mailetContext(fakeMailContext)
-                .setProperty("attachment", "none")
+                .setProperty("action", "delivered")
+                .setProperty("messageString", "Hi. Your mail was successfully delivered")
                 .build();
             dsnBounce.init(mailetConfig);
 
@@ -566,53 +758,100 @@ public class DSNBounceTest {
             assertThat(sentMail.getSender()).isNull();
             assertThat(sentMail.getRecipients()).containsOnly(senderMailAddress);
             MimeMessage sentMessage = sentMail.getMsg();
-            MimeMultipart content = (MimeMultipart) sentMessage.getContent();
-            assertThat(content.getCount()).isEqualTo(2);
+            assertThat(sentMessage.getContentType()).contains("multipart/report;");
+            assertThat(sentMessage.getContentType()).contains("report-type=delivery-status");
         }
 
         @Test
-        void serviceShouldAttachTheOriginalMailWhenAttachmentIsEqualToAll() throws Exception {
+        void serviceShouldSendMultipartMailContainingTextPart() throws Exception {
             FakeMailetConfig mailetConfig = FakeMailetConfig.builder()
                 .mailetName(MAILET_NAME)
                 .mailetContext(fakeMailContext)
-                .setProperty("attachment", "all")
+                .setProperty("action", "delivered")
+                .setProperty("messageString", "Hi. Your mail was successfully delivered at [machine].\n")
                 .build();
             dsnBounce.init(mailetConfig);
 
             MailAddress senderMailAddress = new MailAddress("sender@domain.com");
-            MimeMessage mimeMessage = MimeMessageBuilder.mimeMessageBuilder()
-                .setText("My content")
-                .build();
             FakeMail mail = FakeMail.builder()
                 .name(MAILET_NAME)
                 .sender(senderMailAddress)
+                .attribute(DELIVERY_ERROR_ATTRIBUTE)
+                .mimeMessage(MimeMessageBuilder.mimeMessageBuilder()
+                    .setText("My content"))
                 .recipient("recipient@domain.com")
                 .lastUpdated(Date.from(Instant.parse("2016-09-08T14:25:52.000Z")))
-                .mimeMessage(mimeMessage)
                 .build();
-            MimeMessage mimeMessageCopy = new MimeMessage(mimeMessage);
 
             dsnBounce.service(mail);
 
+            String hostname = InetAddress.getLocalHost().getHostName();
+            String expectedContent = "Hi. Your mail was successfully delivered at " + hostname + ".\n" +
+                "\n" +
+                "Delivered recipient(s):\n" +
+                "recipient@domain.com\n" +
+                "\n";
+
             List<SentMail> sentMails = fakeMailContext.getSentMails();
             assertThat(sentMails).hasSize(1);
             SentMail sentMail = sentMails.get(0);
-            assertThat(sentMail.getSender()).isNull();
-            assertThat(sentMail.getRecipients()).containsOnly(senderMailAddress);
             MimeMessage sentMessage = sentMail.getMsg();
             MimeMultipart content = (MimeMultipart) sentMessage.getContent();
+            BodyPart bodyPart = content.getBodyPart(0);
+            assertThat(bodyPart.getContentType()).isEqualTo("text/plain; charset=us-ascii");
+            assertThat(bodyPart.getContent()).isEqualTo(expectedContent);
+        }
 
-            assertThat(sentMail.getMsg().getContentType()).startsWith("multipart/report;");
-            assertThat(MimeMessageUtil.asString((MimeMessage) content.getBodyPart(2).getContent()))
-                .isEqualTo(MimeMessageUtil.asString(mimeMessageCopy));
+        @Test
+        void serviceShouldSendMultipartMailContainingDSNPart() throws Exception {
+            FakeMailetConfig mailetConfig = FakeMailetConfig.builder()
+                .mailetName(MAILET_NAME)
+                .mailetContext(fakeMailContext)
+                .setProperty("action", "delivered")
+                .setProperty("messageString", "Hi. Your mail was successfully delivered")
+                .build();
+            dsnBounce.init(mailetConfig);
+
+            MailAddress senderMailAddress = new MailAddress("sender@domain.com");
+            FakeMail mail = FakeMail.builder()
+                .name(MAILET_NAME)
+                .sender(senderMailAddress)
+                .mimeMessage(MimeMessageBuilder.mimeMessageBuilder()
+                    .setText("My content"))
+                .recipient("recipient@domain.com")
+                .lastUpdated(Date.from(Instant.parse("2016-09-08T14:25:52.000Z")))
+                .remoteAddr("remoteHost")
+                .build();
+
+            dsnBounce.service(mail);
+
+            String expectedContent = "Reporting-MTA: dns; myhost\n" +
+                "Received-From-MTA: dns; 111.222.333.444\n" +
+                "\n" +
+                "Final-Recipient: rfc822; recipient@domain.com\n" +
+                "Action: delivered\n" +
+                "Status: unknown\n" +
+                "Last-Attempt-Date: Thu, 8 Sep 2016 14:25:52 XXXXX (UTC)\n";
+
+            List<SentMail> sentMails = fakeMailContext.getSentMails();
+            assertThat(sentMails).hasSize(1);
+            SentMail sentMail = sentMails.get(0);
+            MimeMessage sentMessage = sentMail.getMsg();
+            MimeMultipart content = (MimeMultipart) sentMessage.getContent();
+            SharedByteArrayInputStream actualContent = (SharedByteArrayInputStream) content.getBodyPart(1).getContent();
+            assertThat(IOUtils.toString(actualContent, StandardCharsets.UTF_8)).isEqualTo(expectedContent);
         }
+    }
 
+    @Nested
+    class DelayedAction {
         @Test
-        void serviceShouldAttachTheOriginalMailHeadersOnlyWhenAttachmentIsEqualToHeads() throws Exception {
+        void serviceShouldSendMultipartMailToTheSender() throws Exception {
             FakeMailetConfig mailetConfig = FakeMailetConfig.builder()
                 .mailetName(MAILET_NAME)
                 .mailetContext(fakeMailContext)
-                .setProperty("attachment", "heads")
+                .setProperty("action", "delayed")
+                .setProperty("messageString", "Hi. Your mail was delayed at [machine]\n")
                 .build();
             dsnBounce.init(mailetConfig);
 
@@ -621,9 +860,7 @@ public class DSNBounceTest {
                 .name(MAILET_NAME)
                 .sender(senderMailAddress)
                 .mimeMessage(MimeMessageBuilder.mimeMessageBuilder()
-                    .setText("My content")
-                    .addHeader("myHeader", "myValue")
-                    .setSubject("mySubject"))
+                    .setText("My content"))
                 .recipient("recipient@domain.com")
                 .lastUpdated(Date.from(Instant.parse("2016-09-08T14:25:52.000Z")))
                 .build();
@@ -636,13 +873,364 @@ public class DSNBounceTest {
             assertThat(sentMail.getSender()).isNull();
             assertThat(sentMail.getRecipients()).containsOnly(senderMailAddress);
             MimeMessage sentMessage = sentMail.getMsg();
+            assertThat(sentMessage.getContentType()).contains("multipart/report;");
+            assertThat(sentMessage.getContentType()).contains("report-type=delivery-status");
+        }
+
+        @Test
+        void serviceShouldSendMultipartMailContainingTextPart() throws Exception {
+            FakeMailetConfig mailetConfig = FakeMailetConfig.builder()
+                .mailetName(MAILET_NAME)
+                .mailetContext(fakeMailContext)
+                .setProperty("action", "delayed")
+                .setProperty("messageString", "Hi. Your mail was delayed at [machine].\n")
+                .build();
+            dsnBounce.init(mailetConfig);
+
+            MailAddress senderMailAddress = new MailAddress("sender@domain.com");
+            FakeMail mail = FakeMail.builder()
+                .name(MAILET_NAME)
+                .sender(senderMailAddress)
+                .attribute(DELIVERY_ERROR_ATTRIBUTE)
+                .mimeMessage(MimeMessageBuilder.mimeMessageBuilder()
+                    .setText("My content"))
+                .recipient("recipient@domain.com")
+                .lastUpdated(Date.from(Instant.parse("2016-09-08T14:25:52.000Z")))
+                .build();
+
+            dsnBounce.service(mail);
+
+            String hostname = InetAddress.getLocalHost().getHostName();
+            String expectedContent = "Hi. Your mail was delayed at " + hostname + ".\n" +
+                "\n" +
+                "Delayed recipient(s):\n" +
+                "recipient@domain.com\n" +
+                "\n" +
+                "Error message:\n" +
+                "Delivery error\n\n";
+
+            List<SentMail> sentMails = fakeMailContext.getSentMails();
+            assertThat(sentMails).hasSize(1);
+            SentMail sentMail = sentMails.get(0);
+            MimeMessage sentMessage = sentMail.getMsg();
             MimeMultipart content = (MimeMultipart) sentMessage.getContent();
-            BodyPart bodyPart = content.getBodyPart(2);
-            SharedByteArrayInputStream actualContent = (SharedByteArrayInputStream) bodyPart.getContent();
-            assertThat(IOUtils.toString(actualContent, StandardCharsets.UTF_8))
-                .contains("Subject: mySubject")
-                .contains("myHeader: myValue");
-            assertThat(bodyPart.getContentType()).isEqualTo("text/rfc822-headers; name=mySubject");
+            BodyPart bodyPart = content.getBodyPart(0);
+            assertThat(bodyPart.getContentType()).isEqualTo("text/plain; charset=us-ascii");
+            assertThat(bodyPart.getContent()).isEqualTo(expectedContent);
         }
+
+        @Test
+        void serviceShouldSendMultipartMailContainingDSNPart() throws Exception {
+            FakeMailetConfig mailetConfig = FakeMailetConfig.builder()
+                .mailetName(MAILET_NAME)
+                .mailetContext(fakeMailContext)
+                .setProperty("action", "delayed")
+                .setProperty("messageString", "Hi. Your mail was delayed at [machine]\n")
+                .build();
+            dsnBounce.init(mailetConfig);
+
+            MailAddress senderMailAddress = new MailAddress("sender@domain.com");
+            FakeMail mail = FakeMail.builder()
+                .name(MAILET_NAME)
+                .sender(senderMailAddress)
+                .attribute(DELIVERY_ERROR_ATTRIBUTE)
+                .mimeMessage(MimeMessageBuilder.mimeMessageBuilder()
+                    .setText("My content"))
+                .recipient("recipient@domain.com")
+                .lastUpdated(Date.from(Instant.parse("2016-09-08T14:25:52.000Z")))
+                .remoteAddr("remoteHost")
+                .build();
+
+            dsnBounce.service(mail);
+
+            String expectedContent = "Reporting-MTA: dns; myhost\n" +
+                "Received-From-MTA: dns; 111.222.333.444\n" +
+                "\n" +
+                "Final-Recipient: rfc822; recipient@domain.com\n" +
+                "Action: delayed\n" +
+                "Status: Delivery error\n" +
+                "Diagnostic-Code: X-James; Delivery error\n" +
+                "Last-Attempt-Date: Thu, 8 Sep 2016 14:25:52 XXXXX (UTC)\n";
+
+            List<SentMail> sentMails = fakeMailContext.getSentMails();
+            assertThat(sentMails).hasSize(1);
+            SentMail sentMail = sentMails.get(0);
+            MimeMessage sentMessage = sentMail.getMsg();
+            MimeMultipart content = (MimeMultipart) sentMessage.getContent();
+            SharedByteArrayInputStream actualContent = (SharedByteArrayInputStream) content.getBodyPart(1).getContent();
+            assertThat(IOUtils.toString(actualContent, StandardCharsets.UTF_8)).isEqualTo(expectedContent);
+        }
+    }
+
+    @Nested
+    class RelayedAction {
+        @Test
+        void serviceShouldSendMultipartMailToTheSender() throws Exception {
+            FakeMailetConfig mailetConfig = FakeMailetConfig.builder()
+                .mailetName(MAILET_NAME)
+                .mailetContext(fakeMailContext)
+                .setProperty("action", "relayed")
+                .setProperty("messageString", "Hi. Your mail was relayed at [machine]\n")
+                .build();
+            dsnBounce.init(mailetConfig);
+
+            MailAddress senderMailAddress = new MailAddress("sender@domain.com");
+            FakeMail mail = FakeMail.builder()
+                .name(MAILET_NAME)
+                .sender(senderMailAddress)
+                .mimeMessage(MimeMessageBuilder.mimeMessageBuilder()
+                    .setText("My content"))
+                .recipient("recipient@domain.com")
+                .lastUpdated(Date.from(Instant.parse("2016-09-08T14:25:52.000Z")))
+                .build();
+
+            dsnBounce.service(mail);
+
+            List<SentMail> sentMails = fakeMailContext.getSentMails();
+            assertThat(sentMails).hasSize(1);
+            SentMail sentMail = sentMails.get(0);
+            assertThat(sentMail.getSender()).isNull();
+            assertThat(sentMail.getRecipients()).containsOnly(senderMailAddress);
+            MimeMessage sentMessage = sentMail.getMsg();
+            assertThat(sentMessage.getContentType()).contains("multipart/report;");
+            assertThat(sentMessage.getContentType()).contains("report-type=delivery-status");
+        }
+
+        @Test
+        void serviceShouldSendMultipartMailContainingTextPart() throws Exception {
+            FakeMailetConfig mailetConfig = FakeMailetConfig.builder()
+                .mailetName(MAILET_NAME)
+                .mailetContext(fakeMailContext)
+                .setProperty("action", "relayed")
+                .setProperty("messageString", "Hi. Your mail was relayed at [machine].\n")
+                .build();
+            dsnBounce.init(mailetConfig);
+
+            MailAddress senderMailAddress = new MailAddress("sender@domain.com");
+            FakeMail mail = FakeMail.builder()
+                .name(MAILET_NAME)
+                .sender(senderMailAddress)
+                .attribute(DELIVERY_ERROR_ATTRIBUTE)
+                .mimeMessage(MimeMessageBuilder.mimeMessageBuilder()
+                    .setText("My content"))
+                .recipient("recipient@domain.com")
+                .lastUpdated(Date.from(Instant.parse("2016-09-08T14:25:52.000Z")))
+                .build();
+
+            dsnBounce.service(mail);
+
+            String hostname = InetAddress.getLocalHost().getHostName();
+            String expectedContent = "Hi. Your mail was relayed at " + hostname + ".\n" +
+                "\n" +
+                "Relayed recipient(s):\n" +
+                "recipient@domain.com\n" +
+                "\n";
+
+            List<SentMail> sentMails = fakeMailContext.getSentMails();
+            assertThat(sentMails).hasSize(1);
+            SentMail sentMail = sentMails.get(0);
+            MimeMessage sentMessage = sentMail.getMsg();
+            MimeMultipart content = (MimeMultipart) sentMessage.getContent();
+            BodyPart bodyPart = content.getBodyPart(0);
+            assertThat(bodyPart.getContentType()).isEqualTo("text/plain; charset=us-ascii");
+            assertThat(bodyPart.getContent()).isEqualTo(expectedContent);
+        }
+
+        @Test
+        void serviceShouldSendMultipartMailContainingDSNPart() throws Exception {
+            FakeMailetConfig mailetConfig = FakeMailetConfig.builder()
+                .mailetName(MAILET_NAME)
+                .mailetContext(fakeMailContext)
+                .setProperty("action", "relayed")
+                .setProperty("messageString", "Hi. Your mail was delayed at [machine]\n")
+                .build();
+            dsnBounce.init(mailetConfig);
+
+            MailAddress senderMailAddress = new MailAddress("sender@domain.com");
+            FakeMail mail = FakeMail.builder()
+                .name(MAILET_NAME)
+                .sender(senderMailAddress)
+                .mimeMessage(MimeMessageBuilder.mimeMessageBuilder()
+                    .setText("My content"))
+                .recipient("recipient@domain.com")
+                .lastUpdated(Date.from(Instant.parse("2016-09-08T14:25:52.000Z")))
+                .remoteAddr("remoteHost")
+                .build();
+
+            dsnBounce.service(mail);
+
+            String expectedContent = "Reporting-MTA: dns; myhost\n" +
+                "Received-From-MTA: dns; 111.222.333.444\n" +
+                "\n" +
+                "Final-Recipient: rfc822; recipient@domain.com\n" +
+                "Action: relayed\n" +
+                "Status: unknown\n" +
+                "Last-Attempt-Date: Thu, 8 Sep 2016 14:25:52 XXXXX (UTC)\n";
+
+            List<SentMail> sentMails = fakeMailContext.getSentMails();
+            assertThat(sentMails).hasSize(1);
+            SentMail sentMail = sentMails.get(0);
+            MimeMessage sentMessage = sentMail.getMsg();
+            MimeMultipart content = (MimeMultipart) sentMessage.getContent();
+            SharedByteArrayInputStream actualContent = (SharedByteArrayInputStream) content.getBodyPart(1).getContent();
+            assertThat(IOUtils.toString(actualContent, StandardCharsets.UTF_8)).isEqualTo(expectedContent);
+        }
+    }
+
+    @Nested
+    class ExpandedAction {
+        @Test
+        void serviceShouldSendMultipartMailToTheSender() throws Exception {
+            FakeMailetConfig mailetConfig = FakeMailetConfig.builder()
+                .mailetName(MAILET_NAME)
+                .mailetContext(fakeMailContext)
+                .setProperty("action", "expanded")
+                .setProperty("messageString", "Hi. Your mail was expanded at [machine]\n")
+                .build();
+            dsnBounce.init(mailetConfig);
+
+            MailAddress senderMailAddress = new MailAddress("sender@domain.com");
+            FakeMail mail = FakeMail.builder()
+                .name(MAILET_NAME)
+                .sender(senderMailAddress)
+                .mimeMessage(MimeMessageBuilder.mimeMessageBuilder()
+                    .setText("My content"))
+                .recipient("recipient@domain.com")
+                .lastUpdated(Date.from(Instant.parse("2016-09-08T14:25:52.000Z")))
+                .build();
+
+            dsnBounce.service(mail);
+
+            List<SentMail> sentMails = fakeMailContext.getSentMails();
+            assertThat(sentMails).hasSize(1);
+            SentMail sentMail = sentMails.get(0);
+            assertThat(sentMail.getSender()).isNull();
+            assertThat(sentMail.getRecipients()).containsOnly(senderMailAddress);
+            MimeMessage sentMessage = sentMail.getMsg();
+            assertThat(sentMessage.getContentType()).contains("multipart/report;");
+            assertThat(sentMessage.getContentType()).contains("report-type=delivery-status");
+        }
+
+        @Test
+        void serviceShouldSendMultipartMailContainingTextPart() throws Exception {
+            FakeMailetConfig mailetConfig = FakeMailetConfig.builder()
+                .mailetName(MAILET_NAME)
+                .mailetContext(fakeMailContext)
+                .setProperty("action", "expanded")
+                .setProperty("messageString", "Hi. Your mail was expanded at [machine].\n")
+                .build();
+            dsnBounce.init(mailetConfig);
+
+            MailAddress senderMailAddress = new MailAddress("sender@domain.com");
+            FakeMail mail = FakeMail.builder()
+                .name(MAILET_NAME)
+                .sender(senderMailAddress)
+                .attribute(DELIVERY_ERROR_ATTRIBUTE)
+                .mimeMessage(MimeMessageBuilder.mimeMessageBuilder()
+                    .setText("My content"))
+                .recipient("recipient@domain.com")
+                .lastUpdated(Date.from(Instant.parse("2016-09-08T14:25:52.000Z")))
+                .build();
+
+            dsnBounce.service(mail);
+
+            String hostname = InetAddress.getLocalHost().getHostName();
+            String expectedContent = "Hi. Your mail was expanded at " + hostname + ".\n" +
+                "\n" +
+                "Expanded recipient(s):\n" +
+                "recipient@domain.com\n" +
+                "\n";
+
+            List<SentMail> sentMails = fakeMailContext.getSentMails();
+            assertThat(sentMails).hasSize(1);
+            SentMail sentMail = sentMails.get(0);
+            MimeMessage sentMessage = sentMail.getMsg();
+            MimeMultipart content = (MimeMultipart) sentMessage.getContent();
+            BodyPart bodyPart = content.getBodyPart(0);
+            assertThat(bodyPart.getContentType()).isEqualTo("text/plain; charset=us-ascii");
+            assertThat(bodyPart.getContent()).isEqualTo(expectedContent);
+        }
+
+        @Test
+        void serviceShouldSendMultipartMailContainingDSNPart() throws Exception {
+            FakeMailetConfig mailetConfig = FakeMailetConfig.builder()
+                .mailetName(MAILET_NAME)
+                .mailetContext(fakeMailContext)
+                .setProperty("action", "expanded")
+                .setProperty("messageString", "Hi. Your mail was expanded at [machine]\n")
+                .build();
+            dsnBounce.init(mailetConfig);
+
+            MailAddress senderMailAddress = new MailAddress("sender@domain.com");
+            FakeMail mail = FakeMail.builder()
+                .name(MAILET_NAME)
+                .sender(senderMailAddress)
+                .mimeMessage(MimeMessageBuilder.mimeMessageBuilder()
+                    .setText("My content"))
+                .recipient("recipient@domain.com")
+                .lastUpdated(Date.from(Instant.parse("2016-09-08T14:25:52.000Z")))
+                .remoteAddr("remoteHost")
+                .build();
+
+            dsnBounce.service(mail);
+
+            String expectedContent = "Reporting-MTA: dns; myhost\n" +
+                "Received-From-MTA: dns; 111.222.333.444\n" +
+                "\n" +
+                "Final-Recipient: rfc822; recipient@domain.com\n" +
+                "Action: expanded\n" +
+                "Status: unknown\n" +
+                "Last-Attempt-Date: Thu, 8 Sep 2016 14:25:52 XXXXX (UTC)\n";
+
+            List<SentMail> sentMails = fakeMailContext.getSentMails();
+            assertThat(sentMails).hasSize(1);
+            SentMail sentMail = sentMails.get(0);
+            MimeMessage sentMessage = sentMail.getMsg();
+            MimeMultipart content = (MimeMultipart) sentMessage.getContent();
+            SharedByteArrayInputStream actualContent = (SharedByteArrayInputStream) content.getBodyPart(1).getContent();
+            assertThat(IOUtils.toString(actualContent, StandardCharsets.UTF_8)).isEqualTo(expectedContent);
+        }
+    }
+
+    @Test
+    void envIdShouldBePositioned() throws Exception {
+        FakeMailetConfig mailetConfig = FakeMailetConfig.builder()
+            .mailetName(MAILET_NAME)
+            .mailetContext(fakeMailContext)
+            .build();
+        dsnBounce.init(mailetConfig);
+
+        MailAddress senderMailAddress = new MailAddress("sender@domain.com");
+        FakeMail mail = FakeMail.builder()
+            .name(MAILET_NAME)
+            .sender(senderMailAddress)
+            .attribute(DELIVERY_ERROR_ATTRIBUTE)
+            .mimeMessage(MimeMessageBuilder.mimeMessageBuilder()
+                .setText("My content"))
+            .recipient("recipient@domain.com")
+            .lastUpdated(Date.from(Instant.parse("2016-09-08T14:25:52.000Z")))
+            .remoteAddr("remoteHost")
+            .build();
+        mail.setDsnParameters(DsnParameters.builder().envId(DsnParameters.EnvId.of("xyz")).build().get());
+
+        dsnBounce.service(mail);
+
+        String expectedContent = "Reporting-MTA: dns; myhost\n" +
+            "Received-From-MTA: dns; 111.222.333.444\n" +
+            "Original-Envelope-Id: xyz\n" +
+            "\n" +
+            "Final-Recipient: rfc822; recipient@domain.com\n" +
+            "Action: failed\n" +
+            "Status: Delivery error\n" +
+            "Diagnostic-Code: X-James; Delivery error\n" +
+            "Last-Attempt-Date: Thu, 8 Sep 2016 14:25:52 XXXXX (UTC)\n";
+
+        List<SentMail> sentMails = fakeMailContext.getSentMails();
+        assertThat(sentMails).hasSize(1);
+        SentMail sentMail = sentMails.get(0);
+        MimeMessage sentMessage = sentMail.getMsg();
+        MimeMultipart content = (MimeMultipart) sentMessage.getContent();
+        SharedByteArrayInputStream actualContent = (SharedByteArrayInputStream) content.getBodyPart(1).getContent();
+        assertThat(IOUtils.toString(actualContent, StandardCharsets.UTF_8)).isEqualTo(expectedContent);
     }
 }
\ No newline at end of file


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


[james-project] 05/17: JAMES-3431 DSN RET parameter should be taken into account when specified

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 5c8d248d548bf393f260b2d5d93e5302f62d10be
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Wed Dec 16 17:52:54 2020 +0700

    JAMES-3431 DSN RET parameter should be taken into account when specified
---
 .../apache/james/transport/mailets/DSNBounce.java   | 21 ++++++++++++++++++++-
 1 file changed, 20 insertions(+), 1 deletion(-)

diff --git a/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/DSNBounce.java b/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/DSNBounce.java
index 916c736..a5eb82e 100755
--- a/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/DSNBounce.java
+++ b/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/DSNBounce.java
@@ -20,6 +20,7 @@
 package org.apache.james.transport.mailets;
 
 import static org.apache.james.transport.mailets.remote.delivery.Bouncer.DELIVERY_ERROR;
+import static org.apache.mailet.DsnParameters.Ret.HDRS;
 
 import java.net.InetAddress;
 import java.net.UnknownHostException;
@@ -42,6 +43,7 @@ import javax.mail.internet.InternetAddress;
 import javax.mail.internet.MimeBodyPart;
 import javax.mail.internet.MimeMessage;
 
+import org.apache.commons.lang3.NotImplementedException;
 import org.apache.james.core.MailAddress;
 import org.apache.james.core.MaybeSender;
 import org.apache.james.dnsservice.api.DNSService;
@@ -63,6 +65,7 @@ import org.apache.james.transport.util.SenderUtils;
 import org.apache.james.transport.util.SpecialAddressesUtils;
 import org.apache.james.transport.util.TosUtils;
 import org.apache.mailet.AttributeUtils;
+import org.apache.mailet.DsnParameters;
 import org.apache.mailet.Mail;
 import org.apache.mailet.base.DateFormats;
 import org.apache.mailet.base.GenericMailet;
@@ -384,11 +387,27 @@ public class DSNBounce extends GenericMailet implements RedirectNotify {
         multipart.addBodyPart(createTextMsg(originalMail));
         multipart.addBodyPart(createDSN(originalMail));
         if (!getInitParameters().getAttachmentType().equals(TypeCode.NONE)) {
-            multipart.addBodyPart(createAttachedOriginal(originalMail, getInitParameters().getAttachmentType()));
+            multipart.addBodyPart(createAttachedOriginal(originalMail, getAttachmentType(originalMail)));
         }
         return multipart;
     }
 
+    private TypeCode getAttachmentType(Mail originalMail) {
+        return originalMail.dsnParameters()
+            .flatMap(DsnParameters::getRetParameter)
+            .map(ret -> {
+                switch (ret) {
+                    case HDRS:
+                        return TypeCode.HEADS;
+                    case FULL:
+                        return TypeCode.MESSAGE;
+                    default:
+                        throw new NotImplementedException("Unknown RET parameter: " + ret);
+                }
+            })
+            .orElse(getInitParameters().getAttachmentType());
+    }
+
     private MimeBodyPart createTextMsg(Mail originalMail) throws MessagingException {
         StringBuilder builder = new StringBuilder();
 


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


[james-project] 02/17: JAMES-3431 Fix a JavaDoc error

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 4515e25d4bff220f0e74502569be5dbcc2f2092b
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Wed Dec 16 16:49:38 2020 +0700

    JAMES-3431 Fix a JavaDoc error
---
 .../src/main/java/org/apache/james/transport/mailets/DSNBounce.java     | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/DSNBounce.java b/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/DSNBounce.java
index 211eb67..98c33ce 100755
--- a/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/DSNBounce.java
+++ b/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/DSNBounce.java
@@ -109,7 +109,7 @@ import com.google.common.collect.ImmutableSet;
  * </code>
  * </pre>
  *
- * @see org.apache.james.transport.mailets.AbstractNotify
+ * @see RedirectNotify
  */
 
 public class DSNBounce extends GenericMailet implements RedirectNotify {


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


[james-project] 16/17: JAMES-3481 Use Factory to create MailboxChange & State instance

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 c1d50f9274049e309df7cccb89afdde063e4dedc
Author: LanKhuat <dl...@linagora.com>
AuthorDate: Thu Dec 17 16:41:00 2020 +0700

    JAMES-3481 Use Factory to create MailboxChange & State instance
---
 .../james/modules/mailbox/MemoryMailboxModule.java |   2 +
 .../james/jmap/api/change/MailboxChange.java       | 245 +++++++++++++--------
 .../change/MailboxChangeRepositoryContract.java    | 219 ++++++++++--------
 .../change/MemoryMailboxChangeRepositoryTest.java  |   8 +
 .../contract/MailboxChangesMethodContract.scala    |  91 ++++----
 .../memory/MemoryMailboxChangesMethodTest.java     |   6 +
 .../james/jmap/change/MailboxChangeListener.scala  |  19 +-
 .../james/jmap/method/MailboxChangesMethod.scala   |  10 +-
 .../jmap/change/MailboxChangeListenerTest.scala    |  51 +++--
 9 files changed, 386 insertions(+), 265 deletions(-)

diff --git a/server/container/guice/memory-guice/src/main/java/org/apache/james/modules/mailbox/MemoryMailboxModule.java b/server/container/guice/memory-guice/src/main/java/org/apache/james/modules/mailbox/MemoryMailboxModule.java
index 211efe6..6bd4002 100644
--- a/server/container/guice/memory-guice/src/main/java/org/apache/james/modules/mailbox/MemoryMailboxModule.java
+++ b/server/container/guice/memory-guice/src/main/java/org/apache/james/modules/mailbox/MemoryMailboxModule.java
@@ -25,6 +25,7 @@ import javax.inject.Singleton;
 
 import org.apache.james.adapter.mailbox.UserRepositoryAuthenticator;
 import org.apache.james.adapter.mailbox.UserRepositoryAuthorizator;
+import org.apache.james.jmap.api.change.MailboxChange.State;
 import org.apache.james.jmap.api.change.MailboxChangeRepository;
 import org.apache.james.jmap.memory.change.MemoryMailboxChangeRepository;
 import org.apache.james.mailbox.AttachmentContentLoader;
@@ -95,6 +96,7 @@ public class MemoryMailboxModule extends AbstractModule {
         bind(UidProvider.class).to(InMemoryUidProvider.class);
         bind(MailboxId.Factory.class).to(InMemoryId.Factory.class);
         bind(MessageId.Factory.class).to(InMemoryMessageId.Factory.class);
+        bind(State.Factory.class).to(State.DefaultFactory.class);
 
         bind(BlobManager.class).to(StoreBlobManager.class);
         bind(SubscriptionManager.class).to(StoreSubscriptionManager.class);
diff --git a/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/MailboxChange.java b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/MailboxChange.java
index 5613db9..ee5e1e7 100644
--- a/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/MailboxChange.java
+++ b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/MailboxChange.java
@@ -27,6 +27,7 @@ import java.util.Optional;
 import java.util.UUID;
 import java.util.stream.Stream;
 
+import javax.inject.Inject;
 import javax.mail.Flags;
 
 import org.apache.james.core.Username;
@@ -54,6 +55,18 @@ public class MailboxChange {
     public static class State {
         public static State INITIAL = of(UUID.fromString("2c9f1b12-b35a-43e6-9af2-0106fb53a943"));
 
+        public interface Factory {
+            State generate();
+        }
+
+        public static class DefaultFactory implements Factory {
+
+            @Override
+            public State generate() {
+                return of(UUID.randomUUID());
+            }
+        }
+
         public static State of(UUID value) {
             return new State(value);
         }
@@ -102,17 +115,17 @@ public class MailboxChange {
     }
 
     @FunctionalInterface
-    interface RequiredAccountId {
+    public interface RequiredAccountId {
         RequiredState accountId(AccountId accountId);
     }
 
     @FunctionalInterface
-    interface RequiredState {
+    public interface RequiredState {
         RequiredDate state(State state);
     }
 
     @FunctionalInterface
-    interface RequiredDate {
+    public interface RequiredDate {
         Builder date(ZonedDateTime date);
     }
 
@@ -163,111 +176,156 @@ public class MailboxChange {
         return accountId -> state -> date -> new Builder(accountId, state, date);
     }
 
-    public static Builder created(AccountId accountId, State state, ZonedDateTime date, List<MailboxId> created) {
-        return MailboxChange.builder()
-            .accountId(accountId)
-            .state(state)
-            .date(date)
-            .created(created);
-    }
-
-    public static Builder updated(AccountId accountId, State state, ZonedDateTime date, List<MailboxId> updated) {
-        return MailboxChange.builder()
-            .accountId(accountId)
-            .state(state)
-            .date(date)
-            .updated(updated);
-    }
-
-    public static Builder destroyed(AccountId accountId, State state, ZonedDateTime date, List<MailboxId> destroyed) {
-        return MailboxChange.builder()
-            .accountId(accountId)
-            .state(state)
-            .date(date)
-            .destroyed(destroyed);
-    }
+    public static class Factory {
+        private final Clock clock;
+        private final MailboxManager mailboxManager;
+        private final State.Factory stateFactory;
 
-    public static Optional<List<MailboxChange>> fromEvent(Event event, MailboxManager mailboxManager) {
-        ZonedDateTime now = ZonedDateTime.now(Clock.systemUTC());
-        if (event instanceof MailboxAdded) {
-            MailboxAdded mailboxAdded = (MailboxAdded) event;
-            return Optional.of(ImmutableList.of(MailboxChange.created(AccountId.fromUsername(mailboxAdded.getUsername()), State.of(UUID.randomUUID()), now, ImmutableList.of(mailboxAdded.getMailboxId())).build()));
+        @Inject
+        public Factory(Clock clock, MailboxManager mailboxManager, State.Factory stateFactory) {
+            this.clock = clock;
+            this.mailboxManager = mailboxManager;
+            this.stateFactory = stateFactory;
         }
-        if (event instanceof MailboxRenamed) {
-            MailboxRenamed mailboxRenamed = (MailboxRenamed) event;
 
-            MailboxChange ownerChange = MailboxChange.updated(AccountId.fromUsername(mailboxRenamed.getUsername()), State.of(UUID.randomUUID()), now, ImmutableList.of(mailboxRenamed.getMailboxId())).build();
-            Stream<MailboxChange> shareeChanges = getSharees(mailboxRenamed.getNewPath(), mailboxRenamed.getUsername(), mailboxManager)
-                .map(name -> MailboxChange.updated(AccountId.fromString(name), State.of(UUID.randomUUID()), now, ImmutableList.of(mailboxRenamed.getMailboxId()))
-                    .delegated()
-                    .build());
-
-            List<MailboxChange> blah = Stream.concat(Stream.of(ownerChange), shareeChanges).collect(Guavate.toImmutableList());
-            return Optional.of(blah);
-        }
-        if (event instanceof MailboxACLUpdated) {
-            MailboxACLUpdated mailboxACLUpdated = (MailboxACLUpdated) event;
+        public List<MailboxChange> fromEvent(Event event) {
+            ZonedDateTime now = ZonedDateTime.now(clock);
+            if (event instanceof MailboxAdded) {
+                MailboxAdded mailboxAdded = (MailboxAdded) event;
 
-            MailboxChange ownerChange = MailboxChange.updated(AccountId.fromUsername(mailboxACLUpdated.getUsername()), State.of(UUID.randomUUID()), now, ImmutableList.of(mailboxACLUpdated.getMailboxId())).build();
-            Stream<MailboxChange> shareeChanges = getSharees(mailboxACLUpdated.getMailboxPath(), mailboxACLUpdated.getUsername(), mailboxManager)
-                .map(name -> MailboxChange.updated(AccountId.fromString(name), State.of(UUID.randomUUID()), now, ImmutableList.of(mailboxACLUpdated.getMailboxId()))
-                    .delegated()
+                return ImmutableList.of(MailboxChange.builder()
+                    .accountId(AccountId.fromUsername(mailboxAdded.getUsername()))
+                    .state(stateFactory.generate())
+                    .date(now)
+                    .created(ImmutableList.of(mailboxAdded.getMailboxId()))
                     .build());
+            }
+            if (event instanceof MailboxRenamed) {
+                MailboxRenamed mailboxRenamed = (MailboxRenamed) event;
+
+                MailboxChange ownerChange = MailboxChange.builder()
+                    .accountId(AccountId.fromUsername(mailboxRenamed.getUsername()))
+                    .state(stateFactory.generate())
+                    .date(now)
+                    .updated(ImmutableList.of(mailboxRenamed.getMailboxId()))
+                    .build();
+
+                Stream<MailboxChange> shareeChanges = getSharees(mailboxRenamed.getNewPath(), mailboxRenamed.getUsername(), mailboxManager)
+                    .map(name -> MailboxChange.builder()
+                        .accountId(AccountId.fromString(name))
+                        .state(stateFactory.generate())
+                        .date(now)
+                        .updated(ImmutableList.of(mailboxRenamed.getMailboxId()))
+                        .delegated()
+                        .build());
 
-            return Optional.of(
-                Stream.concat(Stream.of(ownerChange), shareeChanges)
-                    .collect(Guavate.toImmutableList()));
-        }
-        if (event instanceof MailboxDeletion) {
-            MailboxDeletion mailboxDeletion = (MailboxDeletion) event;
-            return Optional.of(ImmutableList.of(MailboxChange.destroyed(AccountId.fromUsername(mailboxDeletion.getUsername()), State.of(UUID.randomUUID()), now, ImmutableList.of(mailboxDeletion.getMailboxId())).build()));
-        }
-        if (event instanceof Added) {
-            Added messageAdded = (Added) event;
+                return Stream.concat(Stream.of(ownerChange), shareeChanges)
+                    .collect(Guavate.toImmutableList());
+            }
+            if (event instanceof MailboxACLUpdated) {
+                MailboxACLUpdated mailboxACLUpdated = (MailboxACLUpdated) event;
+
+                MailboxChange ownerChange = MailboxChange.builder()
+                    .accountId(AccountId.fromUsername(mailboxACLUpdated.getUsername()))
+                    .state(stateFactory.generate())
+                    .date(now)
+                    .updated(ImmutableList.of(mailboxACLUpdated.getMailboxId()))
+                    .build();
+
+                Stream<MailboxChange> shareeChanges = getSharees(mailboxACLUpdated.getMailboxPath(), mailboxACLUpdated.getUsername(), mailboxManager)
+                    .map(name -> MailboxChange.builder()
+                        .accountId(AccountId.fromString(name))
+                        .state(stateFactory.generate())
+                        .date(now)
+                        .updated(ImmutableList.of(mailboxACLUpdated.getMailboxId()))
+                        .delegated()
+                        .build());
 
-            MailboxChange ownerChange = MailboxChange.updated(AccountId.fromUsername(messageAdded.getUsername()), State.of(UUID.randomUUID()), now, ImmutableList.of(messageAdded.getMailboxId())).build();
-            Stream<MailboxChange> shareeChanges = getSharees(messageAdded.getMailboxPath(), messageAdded.getUsername(), mailboxManager)
-                .map(name -> MailboxChange.updated(AccountId.fromString(name), State.of(UUID.randomUUID()), now, ImmutableList.of(messageAdded.getMailboxId()))
-                    .delegated()
+                return Stream.concat(Stream.of(ownerChange), shareeChanges)
+                    .collect(Guavate.toImmutableList());
+            }
+            if (event instanceof MailboxDeletion) {
+                MailboxDeletion mailboxDeletion = (MailboxDeletion) event;
+
+                return ImmutableList.of(MailboxChange.builder()
+                    .accountId(AccountId.fromUsername(mailboxDeletion.getUsername()))
+                    .state(stateFactory.generate())
+                    .date(now)
+                    .destroyed(ImmutableList.of(mailboxDeletion.getMailboxId()))
                     .build());
-
-            return Optional.of(
-                Stream.concat(Stream.of(ownerChange), shareeChanges)
-                    .collect(Guavate.toImmutableList()));
-        }
-        if (event instanceof FlagsUpdated) {
-            FlagsUpdated messageFlagUpdated = (FlagsUpdated) event;
-            boolean isSeenChanged = messageFlagUpdated.getUpdatedFlags()
-                .stream()
-                .anyMatch(flags -> flags.isChanged(Flags.Flag.SEEN));
-            if (isSeenChanged) {
-
-                MailboxChange ownerChange = MailboxChange.updated(AccountId.fromUsername(messageFlagUpdated.getUsername()), State.of(UUID.randomUUID()), now, ImmutableList.of(messageFlagUpdated.getMailboxId())).build();
-                Stream<MailboxChange> shareeChanges = getSharees(messageFlagUpdated.getMailboxPath(), messageFlagUpdated.getUsername(), mailboxManager)
-                    .map(name -> MailboxChange.updated(AccountId.fromString(name), State.of(UUID.randomUUID()), now, ImmutableList.of(messageFlagUpdated.getMailboxId()))
+            }
+            if (event instanceof Added) {
+                Added messageAdded = (Added) event;
+
+                MailboxChange ownerChange = MailboxChange.builder()
+                    .accountId(AccountId.fromUsername(messageAdded.getUsername()))
+                    .state(stateFactory.generate())
+                    .date(now)
+                    .updated(ImmutableList.of(messageAdded.getMailboxId()))
+                    .build();
+
+                Stream<MailboxChange> shareeChanges = getSharees(messageAdded.getMailboxPath(), messageAdded.getUsername(), mailboxManager)
+                    .map(name -> MailboxChange.builder()
+                        .accountId(AccountId.fromString(name))
+                        .state(stateFactory.generate())
+                        .date(now)
+                        .updated(ImmutableList.of(messageAdded.getMailboxId()))
                         .delegated()
                         .build());
 
-                return Optional.of(
-                    Stream.concat(Stream.of(ownerChange), shareeChanges)
-                        .collect(Guavate.toImmutableList()));
+                return Stream.concat(Stream.of(ownerChange), shareeChanges)
+                    .collect(Guavate.toImmutableList());
             }
-        }
-        if (event instanceof Expunged) {
-            Expunged expunged = (Expunged) event;
+            if (event instanceof FlagsUpdated) {
+                FlagsUpdated messageFlagUpdated = (FlagsUpdated) event;
+                boolean isSeenChanged = messageFlagUpdated.getUpdatedFlags()
+                    .stream()
+                    .anyMatch(flags -> flags.isChanged(Flags.Flag.SEEN));
+                if (isSeenChanged) {
+                    MailboxChange ownerChange = MailboxChange.builder()
+                        .accountId(AccountId.fromUsername(messageFlagUpdated.getUsername()))
+                        .state(stateFactory.generate())
+                        .date(now)
+                        .updated(ImmutableList.of(messageFlagUpdated.getMailboxId()))
+                        .build();
+
+                    Stream<MailboxChange> shareeChanges = getSharees(messageFlagUpdated.getMailboxPath(), messageFlagUpdated.getUsername(), mailboxManager)
+                        .map(name -> MailboxChange.builder()
+                            .accountId(AccountId.fromString(name))
+                            .state(stateFactory.generate())
+                            .date(now)
+                            .updated(ImmutableList.of(messageFlagUpdated.getMailboxId()))
+                            .delegated()
+                            .build());
+
+                    return Stream.concat(Stream.of(ownerChange), shareeChanges)
+                        .collect(Guavate.toImmutableList());
+                }
+            }
+            if (event instanceof Expunged) {
+                Expunged expunged = (Expunged) event;
+                MailboxChange ownerChange = MailboxChange.builder()
+                    .accountId(AccountId.fromUsername(expunged.getUsername()))
+                    .state(stateFactory.generate())
+                    .date(now)
+                    .updated(ImmutableList.of(expunged.getMailboxId()))
+                    .build();
+
+                Stream<MailboxChange> shareeChanges = getSharees(expunged.getMailboxPath(), expunged.getUsername(), mailboxManager)
+                    .map(name -> MailboxChange.builder()
+                        .accountId(AccountId.fromString(name))
+                        .state(stateFactory.generate())
+                        .date(now)
+                        .updated(ImmutableList.of(expunged.getMailboxId()))
+                        .delegated()
+                        .build());
 
-            MailboxChange ownerChange = MailboxChange.updated(AccountId.fromUsername(expunged.getUsername()), State.of(UUID.randomUUID()), now, ImmutableList.of(expunged.getMailboxId())).build();
-            Stream<MailboxChange> shareeChanges = getSharees(expunged.getMailboxPath(), expunged.getUsername(), mailboxManager)
-                .map(name -> MailboxChange.updated(AccountId.fromString(name), State.of(UUID.randomUUID()), now, ImmutableList.of(expunged.getMailboxId()))
-                    .delegated()
-                    .build());
+                return Stream.concat(Stream.of(ownerChange), shareeChanges)
+                    .collect(Guavate.toImmutableList());
+            }
 
-            return Optional.of(
-                Stream.concat(Stream.of(ownerChange), shareeChanges)
-                    .collect(Guavate.toImmutableList()));
+            return ImmutableList.of();
         }
-
-        return Optional.empty();
     }
 
     private static Stream<String> getSharees(MailboxPath path, Username username, MailboxManager mailboxManager) {
@@ -277,6 +335,7 @@ public class MailboxChange {
             return mailboxACL.getEntries().keySet()
                 .stream()
                 .filter(rfc4314Rights -> !rfc4314Rights.isNegative())
+                .filter(rfc4314Rights -> rfc4314Rights.getNameType().equals(MailboxACL.NameType.user))
                 .map(MailboxACL.EntryKey::getName);
         } catch (MailboxException e) {
             return Stream.of();
diff --git a/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/change/MailboxChangeRepositoryContract.java b/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/change/MailboxChangeRepositoryContract.java
index 293d448..5a65347 100644
--- a/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/change/MailboxChangeRepositoryContract.java
+++ b/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/change/MailboxChangeRepositoryContract.java
@@ -26,7 +26,6 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 import java.time.ZonedDateTime;
 import java.util.Optional;
-import java.util.UUID;
 
 import org.apache.james.jmap.api.change.MailboxChange.Limit;
 import org.apache.james.jmap.api.change.MailboxChange.State;
@@ -41,15 +40,17 @@ import com.google.common.collect.ImmutableList;
 public interface MailboxChangeRepositoryContract {
     AccountId ACCOUNT_ID = AccountId.fromUsername(BOB);
     ZonedDateTime DATE = ZonedDateTime.now();
-    State STATE_0 = State.of(UUID.randomUUID());
+
+    State.Factory stateFactory();
 
     MailboxChangeRepository mailboxChangeRepository();
 
     @Test
     default void saveChangeShouldSuccess() {
         MailboxChangeRepository repository = mailboxChangeRepository();
+        State state = stateFactory().generate();
 
-        MailboxChange change = MailboxChange.created(ACCOUNT_ID, STATE_0, DATE, ImmutableList.of(TestId.of(1))).build();
+        MailboxChange change = MailboxChange.builder().accountId(ACCOUNT_ID).state(state).date(DATE).created(ImmutableList.of(TestId.of(1))).build();
 
         assertThatCode(() -> repository.save(change).block())
             .doesNotThrowAnyException();
@@ -66,10 +67,11 @@ public interface MailboxChangeRepositoryContract {
     @Test
     default void getLatestStateShouldReturnLastPersistedState() {
         MailboxChangeRepository repository = mailboxChangeRepository();
+        State.Factory stateFactory = stateFactory();
 
-        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE.minusHours(2)).created(ImmutableList.of(TestId.of(2))).build();
-        MailboxChange change2 = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE.minusHours(1)).created(ImmutableList.of(TestId.of(3))).build();
-        MailboxChange change3 = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE).created(ImmutableList.of(TestId.of(4))).build();
+        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE.minusHours(2)).created(ImmutableList.of(TestId.of(2))).build();
+        MailboxChange change2 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE.minusHours(1)).created(ImmutableList.of(TestId.of(3))).build();
+        MailboxChange change3 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE).created(ImmutableList.of(TestId.of(4))).build();
         repository.save(change1).block();
         repository.save(change2).block();
         repository.save(change3).block();
@@ -81,8 +83,9 @@ public interface MailboxChangeRepositoryContract {
     @Test
     default void saveChangeShouldFailWhenNoAccountId() {
         MailboxChangeRepository repository = mailboxChangeRepository();
+        State.Factory stateFactory = stateFactory();
 
-        MailboxChange change = MailboxChange.created(null, STATE_0, DATE, ImmutableList.of(TestId.of(1))).build();
+        MailboxChange change = MailboxChange.builder().accountId(null).state(stateFactory.generate()).date(DATE).created(ImmutableList.of(TestId.of(1))).build();
 
         assertThatThrownBy(() -> repository.save(change).block())
             .isInstanceOf(NullPointerException.class);
@@ -92,7 +95,7 @@ public interface MailboxChangeRepositoryContract {
     default void saveChangeShouldFailWhenNoState() {
         MailboxChangeRepository repository = mailboxChangeRepository();
 
-        MailboxChange change = MailboxChange.created(ACCOUNT_ID, null, DATE, ImmutableList.of(TestId.of(1))).build();
+        MailboxChange change = MailboxChange.builder().accountId(ACCOUNT_ID).state(null).date(DATE).created(ImmutableList.of(TestId.of(1))).build();
 
         assertThatThrownBy(() -> repository.save(change).block())
             .isInstanceOf(NullPointerException.class);
@@ -101,63 +104,73 @@ public interface MailboxChangeRepositoryContract {
     @Test
     default void getChangesShouldSuccess() {
         MailboxChangeRepository repository = mailboxChangeRepository();
+        State.Factory stateFactory = stateFactory();
+        State referenceState = stateFactory.generate();
 
-        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(STATE_0).date(DATE.minusHours(1)).created(ImmutableList.of(TestId.of(1))).build();
-        MailboxChange change = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE).updated(ImmutableList.of(TestId.of(1))).build();
-        repository.save(oldState).block();
-        repository.save(change).block();
+        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(referenceState).date(DATE.minusHours(1)).created(ImmutableList.of(TestId.of(1))).build();
+        MailboxChange change = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE).updated(ImmutableList.of(TestId.of(1))).build();
+        repository.save(oldState);
+        repository.save(change);
 
-        assertThat(repository.getSinceState(ACCOUNT_ID, STATE_0, Optional.empty()).block().getAllChanges())
+        assertThat(repository.getSinceState(ACCOUNT_ID, referenceState, Optional.empty()).block().getAllChanges())
             .hasSameElementsAs(change.getUpdated());
     }
 
     @Test
     default void getChangesShouldReturnEmptyWhenNoNewerState() {
         MailboxChangeRepository repository = mailboxChangeRepository();
+        State.Factory stateFactory = stateFactory();
+        State referenceState = stateFactory.generate();
 
-        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(STATE_0).date(DATE).created(ImmutableList.of(TestId.of(1))).build();
-        repository.save(oldState).block();
+        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(referenceState).date(DATE).created(ImmutableList.of(TestId.of(1))).build();
+        repository.save(oldState);
 
-        assertThat(repository.getSinceState(ACCOUNT_ID, STATE_0, Optional.empty()).block().getAllChanges())
+        assertThat(repository.getSinceState(ACCOUNT_ID, referenceState, Optional.empty()).block().getAllChanges())
             .isEmpty();
     }
 
     @Test
     default void getChangesShouldReturnCurrentStateWhenNoNewerState() {
         MailboxChangeRepository repository = mailboxChangeRepository();
+        State.Factory stateFactory = stateFactory();
+        State referenceState = stateFactory.generate();
 
-        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(STATE_0).date(DATE).created(ImmutableList.of(TestId.of(1))).build();
-        repository.save(oldState).block();
+        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(referenceState).date(DATE).created(ImmutableList.of(TestId.of(1))).build();
+        repository.save(oldState);
 
-        assertThat(repository.getSinceState(ACCOUNT_ID, STATE_0, Optional.empty()).block().getNewState())
+        assertThat(repository.getSinceState(ACCOUNT_ID, referenceState, Optional.empty()).block().getNewState())
             .isEqualTo(oldState.getState());
     }
 
     @Test
     default void getChangesShouldLimitChanges() {
         MailboxChangeRepository repository = mailboxChangeRepository();
+        State.Factory stateFactory = stateFactory();
+        State referenceState = stateFactory.generate();
 
-        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(STATE_0).date(DATE.minusHours(3)).created(ImmutableList.of(TestId.of(1))).build();
-        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE.minusHours(2)).created(ImmutableList.of(TestId.of(2))).build();
-        MailboxChange change2 = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE.minusHours(1)).created(ImmutableList.of(TestId.of(3))).build();
-        MailboxChange change3 = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE).created(ImmutableList.of(TestId.of(4))).build();
-        repository.save(oldState).block();
-        repository.save(change1).block();
-        repository.save(change2).block();
-        repository.save(change3).block();
+        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(referenceState).date(DATE.minusHours(3)).created(ImmutableList.of(TestId.of(1))).build();
+        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE.minusHours(2)).created(ImmutableList.of(TestId.of(2))).build();
+        MailboxChange change2 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE.minusHours(1)).created(ImmutableList.of(TestId.of(3))).build();
+        MailboxChange change3 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE).created(ImmutableList.of(TestId.of(4))).build();
+        repository.save(oldState);
+        repository.save(change1);
+        repository.save(change2);
+        repository.save(change3);
 
-        assertThat(repository.getSinceState(ACCOUNT_ID, STATE_0, Optional.of(Limit.of(3))).block().getCreated())
+        assertThat(repository.getSinceState(ACCOUNT_ID, referenceState, Optional.of(Limit.of(3))).block().getCreated())
             .containsExactlyInAnyOrder(TestId.of(2), TestId.of(3), TestId.of(4));
     }
 
     @Test
     default void getChangesShouldReturnAllFromInitial() {
         MailboxChangeRepository repository = mailboxChangeRepository();
+        State.Factory stateFactory = stateFactory();
+        State referenceState = stateFactory.generate();
 
-        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(STATE_0).date(DATE.minusHours(3)).created(ImmutableList.of(TestId.of(1))).build();
-        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE.minusHours(2)).created(ImmutableList.of(TestId.of(2))).build();
-        MailboxChange change2 = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE.minusHours(1)).created(ImmutableList.of(TestId.of(3))).build();
-        MailboxChange change3 = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE).created(ImmutableList.of(TestId.of(4))).build();
+        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(referenceState).date(DATE.minusHours(3)).created(ImmutableList.of(TestId.of(1))).build();
+        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE.minusHours(2)).created(ImmutableList.of(TestId.of(2))).build();
+        MailboxChange change2 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE.minusHours(1)).created(ImmutableList.of(TestId.of(3))).build();
+        MailboxChange change3 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE).created(ImmutableList.of(TestId.of(4))).build();
         repository.save(oldState).block();
         repository.save(change1).block();
         repository.save(change2).block();
@@ -170,12 +183,14 @@ public interface MailboxChangeRepositoryContract {
     @Test
     default void getChangesFromInitialShouldReturnNewState() {
         MailboxChangeRepository repository = mailboxChangeRepository();
+        State.Factory stateFactory = stateFactory();
+        State referenceState = stateFactory.generate();
 
-        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(STATE_0).date(DATE.minusHours(3)).created(ImmutableList.of(TestId.of(1))).build();
-        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE.minusHours(2)).created(ImmutableList.of(TestId.of(2))).build();
-        State state2 = State.of(UUID.randomUUID());
+        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(referenceState).date(DATE.minusHours(3)).created(ImmutableList.of(TestId.of(1))).build();
+        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE.minusHours(2)).created(ImmutableList.of(TestId.of(2))).build();
+        State state2 = stateFactory.generate();
         MailboxChange change2 = MailboxChange.builder().accountId(ACCOUNT_ID).state(state2).date(DATE.minusHours(1)).created(ImmutableList.of(TestId.of(3))).build();
-        MailboxChange change3 = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE).created(ImmutableList.of(TestId.of(4))).build();
+        MailboxChange change3 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE).created(ImmutableList.of(TestId.of(4))).build();
         repository.save(oldState).block();
         repository.save(change1).block();
         repository.save(change2).block();
@@ -189,108 +204,122 @@ public interface MailboxChangeRepositoryContract {
     @Test
     default void getChangesShouldLimitChangesWhenMaxChangesOmitted() {
         MailboxChangeRepository repository = mailboxChangeRepository();
+        State.Factory stateFactory = stateFactory();
+        State referenceState = stateFactory.generate();
 
-        MailboxChange oldState = MailboxChange.created(ACCOUNT_ID, STATE_0, DATE.minusHours(2), ImmutableList.of(TestId.of(1))).build();
-        MailboxChange change1 = MailboxChange.created(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE.minusHours(1), ImmutableList.of(TestId.of(2), TestId.of(3), TestId.of(4), TestId.of(5), TestId.of(6))).build();
-        MailboxChange change2 = MailboxChange.created(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE, ImmutableList.of(TestId.of(7))).build();
+        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(referenceState).date(DATE.minusHours(2)).created(ImmutableList.of(TestId.of(1))).build();
+        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE.minusHours(1)).created(ImmutableList.of(TestId.of(2), TestId.of(3), TestId.of(4), TestId.of(5), TestId.of(6))).build();
+        MailboxChange change2 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE).created(ImmutableList.of(TestId.of(7))).build();
 
         repository.save(oldState).block();
         repository.save(change1).block();
         repository.save(change2).block();
 
-        assertThat(repository.getSinceState(ACCOUNT_ID, STATE_0, Optional.empty()).block().getAllChanges())
+        assertThat(repository.getSinceState(ACCOUNT_ID, referenceState, Optional.empty()).block().getAllChanges())
             .hasSameElementsAs(change1.getCreated());
     }
 
     @Test
     default void getChangesShouldNotReturnMoreThanMaxChanges() {
         MailboxChangeRepository repository = mailboxChangeRepository();
+        State.Factory stateFactory = stateFactory();
+        State referenceState = stateFactory.generate();
 
-        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(STATE_0).date(DATE.minusHours(2)).created(ImmutableList.of(TestId.of(1))).build();
-        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE.minusHours(1)).created(ImmutableList.of(TestId.of(2), TestId.of(3))).build();
-        MailboxChange change2 = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE).created(ImmutableList.of(TestId.of(4), TestId.of(5))).build();
-        repository.save(oldState).block();
-        repository.save(change1).block();
-        repository.save(change2).block();
+        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(referenceState).date(DATE.minusHours(2)).created(ImmutableList.of(TestId.of(1))).build();
+        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE.minusHours(1)).created(ImmutableList.of(TestId.of(2), TestId.of(3))).build();
+        MailboxChange change2 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE).created(ImmutableList.of(TestId.of(4), TestId.of(5))).build();
+        repository.save(oldState);
+        repository.save(change1);
+        repository.save(change2);
 
-        assertThat(repository.getSinceState(ACCOUNT_ID, STATE_0, Optional.of(Limit.of(3))).block().getAllChanges())
+        assertThat(repository.getSinceState(ACCOUNT_ID, referenceState, Optional.of(Limit.of(3))).block().getAllChanges())
             .hasSameElementsAs(change1.getCreated());
     }
 
     @Test
     default void getChangesShouldReturnEmptyWhenNumberOfChangesExceedMaxChanges() {
         MailboxChangeRepository repository = mailboxChangeRepository();
+        State.Factory stateFactory = stateFactory();
+        State referenceState = stateFactory.generate();
 
-        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(STATE_0).date(DATE.minusHours(2)).created(ImmutableList.of(TestId.of(1))).build();
-        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE.minusHours(1)).created(ImmutableList.of(TestId.of(2), TestId.of(3))).build();
-        repository.save(oldState).block();
-        repository.save(change1).block();
+        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(referenceState).date(DATE.minusHours(2)).created(ImmutableList.of(TestId.of(1))).build();
+        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE.minusHours(1)).created(ImmutableList.of(TestId.of(2), TestId.of(3))).build();
+        repository.save(oldState);
+        repository.save(change1);
 
-        assertThat(repository.getSinceState(ACCOUNT_ID, STATE_0, Optional.of(Limit.of(1))).block().getAllChanges())
+        assertThat(repository.getSinceState(ACCOUNT_ID, referenceState, Optional.of(Limit.of(1))).block().getAllChanges())
             .isEmpty();
     }
 
     @Test
     default void getChangesShouldReturnNewState() {
         MailboxChangeRepository repository = mailboxChangeRepository();
+        State.Factory stateFactory = stateFactory();
+        State referenceState = stateFactory.generate();
 
-        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(STATE_0).date(DATE.minusHours(2)).created(ImmutableList.of(TestId.of(1))).build();
-        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE.minusHours(1)).created(ImmutableList.of(TestId.of(2), TestId.of(3))).build();
-        MailboxChange change2 = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE).updated(ImmutableList.of(TestId.of(2), TestId.of(3))).build();
-        repository.save(oldState).block();
-        repository.save(change1).block();
-        repository.save(change2).block();
+        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(referenceState).date(DATE.minusHours(2)).created(ImmutableList.of(TestId.of(1))).build();
+        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE.minusHours(1)).created(ImmutableList.of(TestId.of(2), TestId.of(3))).build();
+        MailboxChange change2 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE).updated(ImmutableList.of(TestId.of(2), TestId.of(3))).build();
+        repository.save(oldState);
+        repository.save(change1);
+        repository.save(change2);
 
-        assertThat(repository.getSinceState(ACCOUNT_ID, STATE_0, Optional.empty()).block().getNewState())
+        assertThat(repository.getSinceState(ACCOUNT_ID, referenceState, Optional.empty()).block().getNewState())
             .isEqualTo(change2.getState());
     }
 
     @Test
     default void hasMoreChangesShouldBeTrueWhenMoreChanges() {
         MailboxChangeRepository repository = mailboxChangeRepository();
+        State.Factory stateFactory = stateFactory();
+        State referenceState = stateFactory.generate();
 
-        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(STATE_0).date(DATE.minusHours(2)).created(ImmutableList.of(TestId.of(1))).build();
-        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE.minusHours(1)).created(ImmutableList.of(TestId.of(2), TestId.of(3))).build();
-        MailboxChange change2 = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE).updated(ImmutableList.of(TestId.of(2), TestId.of(3))).build();
-        repository.save(oldState).block();
-        repository.save(change1).block();
-        repository.save(change2).block();
+        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(referenceState).date(DATE.minusHours(2)).created(ImmutableList.of(TestId.of(1))).build();
+        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE.minusHours(1)).created(ImmutableList.of(TestId.of(2), TestId.of(3))).build();
+        MailboxChange change2 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE).updated(ImmutableList.of(TestId.of(2), TestId.of(3))).build();
+        repository.save(oldState);
+        repository.save(change1);
+        repository.save(change2);
 
-        assertThat(repository.getSinceState(ACCOUNT_ID, STATE_0, Optional.of(Limit.of(1))).block().hasMoreChanges())
+        assertThat(repository.getSinceState(ACCOUNT_ID, referenceState, Optional.of(Limit.of(1))).block().hasMoreChanges())
             .isTrue();
     }
 
     @Test
     default void hasMoreChangesShouldBeFalseWhenNoMoreChanges() {
         MailboxChangeRepository repository = mailboxChangeRepository();
+        State.Factory stateFactory = stateFactory();
+        State referenceState = stateFactory.generate();
 
-        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(STATE_0).date(DATE.minusHours(2)).created(ImmutableList.of(TestId.of(1))).build();
-        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE.minusHours(1)).created(ImmutableList.of(TestId.of(2), TestId.of(3))).build();
-        MailboxChange change2 = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE).updated(ImmutableList.of(TestId.of(2), TestId.of(3))).build();
-        repository.save(oldState).block();
-        repository.save(change1).block();
-        repository.save(change2).block();
+        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(referenceState).date(DATE.minusHours(2)).created(ImmutableList.of(TestId.of(1))).build();
+        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE.minusHours(1)).created(ImmutableList.of(TestId.of(2), TestId.of(3))).build();
+        MailboxChange change2 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE).updated(ImmutableList.of(TestId.of(2), TestId.of(3))).build();
+        repository.save(oldState);
+        repository.save(change1);
+        repository.save(change2);
 
-        assertThat(repository.getSinceState(ACCOUNT_ID, STATE_0, Optional.of(Limit.of(4))).block().hasMoreChanges())
+        assertThat(repository.getSinceState(ACCOUNT_ID, referenceState, Optional.of(Limit.of(4))).block().hasMoreChanges())
             .isFalse();
     }
 
     @Test
     default void changesShouldBeStoredInTheirRespectiveType() {
         MailboxChangeRepository repository = mailboxChangeRepository();
+        State.Factory stateFactory = stateFactory();
+        State referenceState = stateFactory.generate();
 
-        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(STATE_0).date(DATE.minusHours(3)).created(ImmutableList.of(TestId.of(1))).build();
-        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE.minusHours(2)).created(ImmutableList.of(TestId.of(2), TestId.of(3), TestId.of(4), TestId.of(5))).build();
+        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(referenceState).date(DATE.minusHours(3)).created(ImmutableList.of(TestId.of(1))).build();
+        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE.minusHours(2)).created(ImmutableList.of(TestId.of(2), TestId.of(3), TestId.of(4), TestId.of(5))).build();
         MailboxChange change2 = MailboxChange.builder()
             .accountId(ACCOUNT_ID)
-            .state(State.of(UUID.randomUUID()))
+            .state(stateFactory.generate())
             .date(DATE.minusHours(1))
             .created(ImmutableList.of(TestId.of(6), TestId.of(7)))
             .updated(ImmutableList.of(TestId.of(2), TestId.of(3)))
             .destroyed(ImmutableList.of(TestId.of(4))).build();
         MailboxChange change3 = MailboxChange.builder()
             .accountId(ACCOUNT_ID)
-            .state(State.of(UUID.randomUUID()))
+            .state(stateFactory.generate())
             .date(DATE)
             .created(ImmutableList.of(TestId.of(8)))
             .updated(ImmutableList.of(TestId.of(6), TestId.of(7)))
@@ -301,7 +330,7 @@ public interface MailboxChangeRepositoryContract {
         repository.save(change2).block();
         repository.save(change3).block();
 
-        MailboxChanges mailboxChanges = repository.getSinceState(ACCOUNT_ID, STATE_0, Optional.of(Limit.of(20))).block();
+        MailboxChanges mailboxChanges = repository.getSinceState(ACCOUNT_ID, referenceState, Optional.of(Limit.of(20))).block();
 
         SoftAssertions.assertSoftly(softly -> {
             softly.assertThat(mailboxChanges.getCreated()).containsExactlyInAnyOrder(TestId.of(2), TestId.of(3), TestId.of(4), TestId.of(5), TestId.of(6), TestId.of(7), TestId.of(8));
@@ -313,12 +342,14 @@ public interface MailboxChangeRepositoryContract {
     @Test
     default void getChangesShouldIgnoreDuplicatedValues() {
         MailboxChangeRepository repository = mailboxChangeRepository();
+        State.Factory stateFactory = stateFactory();
+        State referenceState = stateFactory.generate();
 
-        MailboxChange oldState = MailboxChange.created(ACCOUNT_ID, STATE_0, DATE.minusHours(2), ImmutableList.of(TestId.of(1))).build();
-        MailboxChange change1 = MailboxChange.updated(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE.minusHours(1), ImmutableList.of(TestId.of(1), TestId.of(2))).build();
+        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(referenceState).date(DATE.minusHours(2)).created(ImmutableList.of(TestId.of(1))).build();
+        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE.minusHours(1)).updated(ImmutableList.of(TestId.of(1), TestId.of(2))).build();
         MailboxChange change2 = MailboxChange.builder()
             .accountId(ACCOUNT_ID)
-            .state(State.of(UUID.randomUUID()))
+            .state(stateFactory.generate())
             .date(DATE)
             .created(ImmutableList.of(TestId.of(3)))
             .updated(ImmutableList.of(TestId.of(1), TestId.of(2)))
@@ -328,7 +359,7 @@ public interface MailboxChangeRepositoryContract {
         repository.save(change1).block();
         repository.save(change2).block();
 
-        MailboxChanges mailboxChanges = repository.getSinceState(ACCOUNT_ID, STATE_0, Optional.of(Limit.of(3))).block();
+        MailboxChanges mailboxChanges = repository.getSinceState(ACCOUNT_ID, referenceState, Optional.of(Limit.of(3))).block();
         SoftAssertions.assertSoftly(softly -> {
             softly.assertThat(mailboxChanges.getUpdated()).containsExactly(TestId.of(1), TestId.of(2));
             softly.assertThat(mailboxChanges.getCreated()).containsExactly(TestId.of(3));
@@ -338,11 +369,13 @@ public interface MailboxChangeRepositoryContract {
     @Test
     default void getChangesShouldReturnDelegatedChanges() {
         MailboxChangeRepository repository = mailboxChangeRepository();
+        State.Factory stateFactory = stateFactory();
+        State referenceState = stateFactory.generate();
 
-        MailboxChange oldState = MailboxChange.created(ACCOUNT_ID, STATE_0, DATE.minusHours(2), ImmutableList.of(TestId.of(1))).build();
+        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(referenceState).date(DATE.minusHours(2)).created(ImmutableList.of(TestId.of(1))).build();
         MailboxChange change1 = MailboxChange.builder()
             .accountId(ACCOUNT_ID)
-            .state(State.of(UUID.randomUUID()))
+            .state(stateFactory.generate())
             .date(DATE.minusHours(1))
             .updated(ImmutableList.of(TestId.of(1)))
             .delegated()
@@ -351,7 +384,7 @@ public interface MailboxChangeRepositoryContract {
         repository.save(oldState);
         repository.save(change1);
 
-        assertThat(repository.getSinceStateWithDelegation(ACCOUNT_ID, STATE_0, Optional.empty()).block().getUpdated())
+        assertThat(repository.getSinceStateWithDelegation(ACCOUNT_ID, referenceState, Optional.empty()).block().getUpdated())
             .containsExactly(TestId.of(1));
 
     }
@@ -359,21 +392,25 @@ public interface MailboxChangeRepositoryContract {
     @Test
     default void getChangesShouldFailWhenInvalidMaxChanges() {
         MailboxChangeRepository repository = mailboxChangeRepository();
+        State.Factory stateFactory = stateFactory();
+        State referenceState = stateFactory.generate();
 
-        MailboxChange currentState = MailboxChange.builder().accountId(ACCOUNT_ID).state(STATE_0).date(DATE).created(ImmutableList.of(TestId.of(1))).build();
-        MailboxChange change = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE).created(ImmutableList.of(TestId.of(2))).build();
-        repository.save(currentState).block();
-        repository.save(change).block();
+        MailboxChange currentState = MailboxChange.builder().accountId(ACCOUNT_ID).state(referenceState).date(DATE).created(ImmutableList.of(TestId.of(1))).build();
+        MailboxChange change = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE).created(ImmutableList.of(TestId.of(2))).build();
+        repository.save(currentState);
+        repository.save(change);
 
-        assertThatThrownBy(() -> repository.getSinceState(ACCOUNT_ID, STATE_0, Optional.of(Limit.of(-1))))
+        assertThatThrownBy(() -> repository.getSinceState(ACCOUNT_ID, referenceState, Optional.of(Limit.of(-1))))
             .isInstanceOf(IllegalArgumentException.class);
     }
 
     @Test
     default void getChangesShouldFailWhenSinceStateNotFound() {
         MailboxChangeRepository repository = mailboxChangeRepository();
+        State.Factory stateFactory = stateFactory();
+        State referenceState = stateFactory.generate();
 
-        assertThatThrownBy(() -> repository.getSinceState(ACCOUNT_ID, STATE_0, Optional.empty()).block())
+        assertThatThrownBy(() -> repository.getSinceState(ACCOUNT_ID, referenceState, Optional.empty()).block())
             .isInstanceOf(ChangeNotFoundException.class);
     }
 }
diff --git a/server/data/data-jmap/src/test/java/org/apache/james/jmap/memory/change/MemoryMailboxChangeRepositoryTest.java b/server/data/data-jmap/src/test/java/org/apache/james/jmap/memory/change/MemoryMailboxChangeRepositoryTest.java
index 9079e65..6c3b74f 100644
--- a/server/data/data-jmap/src/test/java/org/apache/james/jmap/memory/change/MemoryMailboxChangeRepositoryTest.java
+++ b/server/data/data-jmap/src/test/java/org/apache/james/jmap/memory/change/MemoryMailboxChangeRepositoryTest.java
@@ -19,20 +19,28 @@
 
 package org.apache.james.jmap.memory.change;
 
+import org.apache.james.jmap.api.change.MailboxChange;
 import org.apache.james.jmap.api.change.MailboxChangeRepository;
 import org.apache.james.jmap.api.change.MailboxChangeRepositoryContract;
 import org.junit.jupiter.api.BeforeEach;
 
 public class MemoryMailboxChangeRepositoryTest implements MailboxChangeRepositoryContract {
     MailboxChangeRepository mailboxChangeRepository;
+    MailboxChange.State.Factory stateFactory;
 
     @BeforeEach
     void setup() {
         mailboxChangeRepository = new MemoryMailboxChangeRepository();
+        stateFactory = new MailboxChange.State.DefaultFactory();
     }
 
     @Override
     public MailboxChangeRepository mailboxChangeRepository() {
         return mailboxChangeRepository;
     }
+
+    @Override
+    public MailboxChange.State.Factory stateFactory() {
+        return stateFactory;
+    }
 }
diff --git a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxChangesMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxChangesMethodContract.scala
index 4467a54..c230dd5 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxChangesMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxChangesMethodContract.scala
@@ -63,6 +63,8 @@ case class TestId(value: Long) extends MailboxId {
 
 trait MailboxChangesMethodContract {
 
+  def stateFactory: State.Factory
+
   @BeforeEach
   def setUp(server: GuiceJamesServer): Unit = {
     server.getProbe(classOf[DataProbeImpl])
@@ -506,12 +508,12 @@ trait MailboxChangesMethodContract {
           .setBody(request)
           .build, new ResponseSpecBuilder().build)
         .post
-        .`then`
-          .statusCode(SC_OK)
-          .contentType(JSON)
-          .extract
-          .body
-          .asString
+      .`then`
+        .statusCode(SC_OK)
+        .contentType(JSON)
+        .extract
+        .body
+        .asString
 
       assertThatJson(response)
         .whenIgnoringPaths("methodResponses[0][1].newState")
@@ -570,12 +572,12 @@ trait MailboxChangesMethodContract {
           .setBody(request)
           .build, new ResponseSpecBuilder().build)
         .post
-        .`then`
-          .statusCode(SC_OK)
-          .contentType(JSON)
-          .extract
-          .body
-          .asString
+      .`then`
+        .statusCode(SC_OK)
+        .contentType(JSON)
+        .extract
+        .body
+        .asString
 
       assertThatJson(response)
         .whenIgnoringPaths("methodResponses[0][1].newState")
@@ -641,12 +643,12 @@ trait MailboxChangesMethodContract {
           .setBody(request)
           .build, new ResponseSpecBuilder().build)
         .post
-        .`then`
-          .statusCode(SC_OK)
-          .contentType(JSON)
-          .extract
-          .body
-          .asString
+      .`then`
+        .statusCode(SC_OK)
+        .contentType(JSON)
+        .extract
+        .body
+        .asString
 
       assertThatJson(response)
         .whenIgnoringPaths("methodResponses[0][1].newState")
@@ -711,12 +713,12 @@ trait MailboxChangesMethodContract {
           .setBody(request)
           .build, new ResponseSpecBuilder().build)
         .post
-        .`then`
-          .statusCode(SC_OK)
-          .contentType(JSON)
-          .extract
-          .body
-          .asString
+      .`then`
+        .statusCode(SC_OK)
+        .contentType(JSON)
+        .extract
+        .body
+        .asString
 
       assertThatJson(response)
         .whenIgnoringPaths("methodResponses[0][1].newState")
@@ -782,12 +784,12 @@ trait MailboxChangesMethodContract {
           .setBody(request)
           .build, new ResponseSpecBuilder().build)
         .post
-        .`then`
-          .statusCode(SC_OK)
-          .contentType(JSON)
-          .extract
-          .body
-          .asString
+      .`then`
+        .statusCode(SC_OK)
+        .contentType(JSON)
+        .extract
+        .body
+        .asString
 
       assertThatJson(response)
         .whenIgnoringPaths("methodResponses[0][1].newState")
@@ -853,12 +855,12 @@ trait MailboxChangesMethodContract {
           .setBody(request)
           .build, new ResponseSpecBuilder().build)
         .post
-        .`then`
-          .statusCode(SC_OK)
-          .contentType(JSON)
-          .extract
-          .body
-          .asString
+      .`then`
+        .statusCode(SC_OK)
+        .contentType(JSON)
+        .extract
+        .body
+        .asString
 
       assertThatJson(response)
         .whenIgnoringPaths("methodResponses[0][1].newState")
@@ -918,12 +920,12 @@ trait MailboxChangesMethodContract {
           .setBody(request)
           .build, new ResponseSpecBuilder().build)
         .post
-        .`then`
-          .statusCode(SC_OK)
-          .contentType(JSON)
-          .extract
-          .body
-          .asString
+      .`then`
+        .statusCode(SC_OK)
+        .contentType(JSON)
+        .extract
+        .body
+        .asString
 
       assertThatJson(response)
         .whenIgnoringPaths("methodResponses[0][1].newState")
@@ -1545,9 +1547,10 @@ trait MailboxChangesMethodContract {
   }
 
   private def storeReferenceState(server: GuiceJamesServer, username: Username): State = {
-    val state: State = State.of(UUID.randomUUID())
+    val state: State = stateFactory.generate()
     val jmapGuiceProbe: JmapGuiceProbe = server.getProbe(classOf[JmapGuiceProbe])
-    jmapGuiceProbe.saveMailboxChange(MailboxChange.updated(AccountId.fromUsername(username), state, ZonedDateTime.now(), List(TestId.of(0)).asJava).build)
+
+    jmapGuiceProbe.saveMailboxChange(MailboxChange.builder.accountId(AccountId.fromUsername(username)).state(state).date(ZonedDateTime.now()).updated(List(TestId.of(0)).asJava).build)
 
     state
   }
diff --git a/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryMailboxChangesMethodTest.java b/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryMailboxChangesMethodTest.java
index eadfa66..93104a9 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryMailboxChangesMethodTest.java
+++ b/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryMailboxChangesMethodTest.java
@@ -24,6 +24,7 @@ import static org.apache.james.MemoryJamesServerMain.IN_MEMORY_SERVER_AGGREGATE_
 import org.apache.james.GuiceJamesServer;
 import org.apache.james.JamesServerBuilder;
 import org.apache.james.JamesServerExtension;
+import org.apache.james.jmap.api.change.MailboxChange;
 import org.apache.james.jmap.rfc8621.contract.MailboxChangesMethodContract;
 import org.apache.james.modules.TestJMAPServerModule;
 import org.junit.jupiter.api.extension.RegisterExtension;
@@ -35,4 +36,9 @@ public class MemoryMailboxChangesMethodTest implements MailboxChangesMethodContr
             .combineWith(IN_MEMORY_SERVER_AGGREGATE_MODULE)
             .overrideWith(new TestJMAPServerModule()))
         .build();
+
+    @Override
+    public MailboxChange.State.Factory stateFactory() {
+        return new MailboxChange.State.DefaultFactory();
+    }
 }
\ No newline at end of file
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/change/MailboxChangeListener.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/change/MailboxChangeListener.scala
index b577eb8..aba030f 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/change/MailboxChangeListener.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/change/MailboxChangeListener.scala
@@ -24,6 +24,7 @@ import org.apache.james.jmap.api.change.{MailboxChange, MailboxChangeRepository}
 import org.apache.james.mailbox.MailboxManager
 import org.apache.james.mailbox.events.MailboxListener.{MailboxEvent, ReactiveGroupMailboxListener}
 import org.apache.james.mailbox.events.{Event, Group}
+import org.apache.james.util.ReactorUtils.DEFAULT_CONCURRENCY
 import org.reactivestreams.Publisher
 import reactor.core.scala.publisher.{SFlux, SMono}
 
@@ -32,16 +33,14 @@ import scala.jdk.CollectionConverters._
 case class MailboxChangeListenerGroup() extends Group {}
 
 case class MailboxChangeListener @Inject() (mailboxChangeRepository: MailboxChangeRepository,
-                                            mailboxManager: MailboxManager) extends ReactiveGroupMailboxListener {
-
-  override def reactiveEvent(event: Event): Publisher[Void] = {
-    MailboxChange.fromEvent(event, mailboxManager)
-      .map(changes => SFlux.fromIterable(changes.asScala)
-        .map(change => mailboxChangeRepository.save(change))
-        .`then`()
-        .`then`(SMono.empty[Void]).asJava)
-      .orElse(SMono.empty[Void].asJava)
-  }
+                                            mailboxManager: MailboxManager,
+                                            mailboxChangeFactory: MailboxChange.Factory) extends ReactiveGroupMailboxListener {
+
+  override def reactiveEvent(event: Event): Publisher[Void] =
+    SFlux.fromIterable(mailboxChangeFactory.fromEvent(event).asScala)
+      .flatMap(change => mailboxChangeRepository.save(change), DEFAULT_CONCURRENCY)
+      .`then`()
+      .`then`(SMono.empty[Void]).asJava
 
   override def getDefaultGroup: Group = MailboxChangeListenerGroup()
 
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxChangesMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxChangesMethod.scala
index 5bbca81..c60e038 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxChangesMethod.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxChangesMethod.scala
@@ -46,12 +46,14 @@ class MailboxChangesMethod @Inject()(mailboxSerializer: MailboxSerializer,
   override val requiredCapabilities: Set[CapabilityIdentifier] = Set(JMAP_MAIL)
 
   override def doProcess(capabilities: Set[CapabilityIdentifier], invocation: InvocationWithContext, mailboxSession: MailboxSession, request: MailboxChangesRequest): SMono[InvocationWithContext] =
-    SMono.fromPublisher(
+    SMono.fromPublisher({
+      val accountId: JavaAccountId = JavaAccountId.fromUsername(mailboxSession.getUser)
+      val state: JavaState = JavaState.of(request.sinceState.value)
       if (capabilities.contains(CapabilityIdentifier.JAMES_SHARES)) {
-        mailboxChangeRepository.getSinceStateWithDelegation(JavaAccountId.fromUsername(mailboxSession.getUser), JavaState.of(request.sinceState.value), request.maxChanged.toJava)
+        mailboxChangeRepository.getSinceStateWithDelegation(accountId, state, request.maxChanged.toJava)
       } else {
-        mailboxChangeRepository.getSinceState(JavaAccountId.fromUsername(mailboxSession.getUser), JavaState.of(request.sinceState.value), request.maxChanged.toJava)
-      })
+        mailboxChangeRepository.getSinceState(accountId, state, request.maxChanged.toJava)
+      }})
       .map(mailboxChanges => MailboxChangesResponse(
         accountId = request.accountId,
         oldState = request.sinceState,
diff --git a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/change/MailboxChangeListenerTest.scala b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/change/MailboxChangeListenerTest.scala
index aecbfc7..a207f72 100644
--- a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/change/MailboxChangeListenerTest.scala
+++ b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/change/MailboxChangeListenerTest.scala
@@ -19,8 +19,7 @@
 
 package org.apache.james.jmap.change
 
-import java.time.ZonedDateTime
-import java.util.UUID
+import java.time.{Clock, ZonedDateTime}
 
 import javax.mail.Flags
 import org.apache.james.jmap.api.change.MailboxChange.State
@@ -50,7 +49,10 @@ class MailboxChangeListenerTest {
 
   var repository: MailboxChangeRepository = _
   var mailboxManager: MailboxManager = _
+  var mailboxChangeFactory: MailboxChange.Factory = _
+  var stateFactory: State.Factory = _
   var listener: MailboxChangeListener = _
+  var clock: Clock = _
 
   @BeforeEach
   def setUp: Unit = {
@@ -61,16 +63,19 @@ class MailboxChangeListenerTest {
       .defaultAnnotationLimits.defaultMessageParser.scanningSearchIndex.noPreDeletionHooks.storeQuotaManager
       .build
 
+    clock = Clock.systemUTC()
     mailboxManager = resources.getMailboxManager
+    stateFactory = new State.DefaultFactory
+    mailboxChangeFactory = new MailboxChange.Factory(clock, mailboxManager, stateFactory)
     repository = new MemoryMailboxChangeRepository()
-    listener = MailboxChangeListener(repository, mailboxManager)
+    listener = MailboxChangeListener(repository, mailboxManager, mailboxChangeFactory)
     resources.getEventBus.register(listener)
   }
 
   @Test
   def createMailboxShouldStoreCreatedEvent(): Unit = {
-    val state = State.of(UUID.randomUUID)
-    repository.save(MailboxChange.created(ACCOUNT_ID, state, ZonedDateTime.now, List[MailboxId](TestId.of(0)).asJava).build).block()
+    val state = stateFactory.generate()
+    repository.save(MailboxChange.builder().accountId(ACCOUNT_ID).state(state).date(ZonedDateTime.now).created(List[MailboxId](TestId.of(0)).asJava).build).block()
 
     val mailboxSession = MailboxSessionUtil.create(BOB)
     val inboxId: MailboxId = mailboxManager.createMailbox(MailboxPath.inbox(BOB), mailboxSession).get
@@ -86,8 +91,8 @@ class MailboxChangeListenerTest {
     val newPath = MailboxPath.forUser(BOB, "another")
     val inboxId: MailboxId = mailboxManager.createMailbox(path, mailboxSession).get
 
-    val state = State.of(UUID.randomUUID)
-    repository.save(MailboxChange.created(ACCOUNT_ID, state, ZonedDateTime.now, List[MailboxId](TestId.of(0)).asJava).build).block()
+    val state = stateFactory.generate()
+    repository.save(MailboxChange.builder().accountId(ACCOUNT_ID).state(state).date(ZonedDateTime.now).created(List[MailboxId](TestId.of(0)).asJava).build).block()
 
     mailboxManager.renameMailbox(path, newPath, mailboxSession)
 
@@ -97,8 +102,8 @@ class MailboxChangeListenerTest {
 
   @Test
   def updateMailboxACLShouldStoreUpdatedEvent(): Unit = {
-    val state = State.of(UUID.randomUUID)
-    repository.save(MailboxChange.created(ACCOUNT_ID, state, ZonedDateTime.now, List[MailboxId](TestId.of(0)).asJava).build).block()
+    val state = stateFactory.generate()
+    repository.save(MailboxChange.builder().accountId(ACCOUNT_ID).state(state).date(ZonedDateTime.now).created(List[MailboxId](TestId.of(0)).asJava).build).block()
 
     val mailboxSession = MailboxSessionUtil.create(BOB)
     val path = MailboxPath.inbox(BOB)
@@ -116,8 +121,8 @@ class MailboxChangeListenerTest {
     val path = MailboxPath.forUser(BOB, "test")
     val inboxId: MailboxId = mailboxManager.createMailbox(path, mailboxSession).get
 
-    val state = State.of(UUID.randomUUID)
-    repository.save(MailboxChange.created(ACCOUNT_ID, state, ZonedDateTime.now, List[MailboxId](TestId.of(0)).asJava).build).block()
+    val state = stateFactory.generate()
+    repository.save(MailboxChange.builder().accountId(ACCOUNT_ID).state(state).date(ZonedDateTime.now).created(List[MailboxId](TestId.of(0)).asJava).build).block()
 
     mailboxManager.applyRightsCommand(path, MailboxACL.command().forUser(ALICE).rights(MailboxACL.Right.Read).asAddition(), mailboxSession)
 
@@ -137,8 +142,8 @@ class MailboxChangeListenerTest {
     val messageManager: MessageManager = mailboxManager.getMailbox(inboxId, mailboxSession)
     messageManager.appendMessage(AppendCommand.builder().build("header: value\r\n\r\nbody"), mailboxSession)
 
-    val state = State.of(UUID.randomUUID)
-    repository.save(MailboxChange.created(ACCOUNT_ID, state, ZonedDateTime.now, List[MailboxId](TestId.of(0)).asJava).build).block()
+    val state = stateFactory.generate()
+    repository.save(MailboxChange.builder().accountId(ACCOUNT_ID).state(state).date(ZonedDateTime.now).created(List[MailboxId](TestId.of(0)).asJava).build).block()
 
     messageManager.setFlags(new Flags(Flags.Flag.SEEN), FlagsUpdateMode.ADD, MessageRange.all(), mailboxSession)
 
@@ -156,8 +161,8 @@ class MailboxChangeListenerTest {
       .withFlags(new Flags(Flags.Flag.SEEN))
       .build("header: value\r\n\r\nbody"), mailboxSession)
 
-    val state = State.of(UUID.randomUUID)
-    repository.save(MailboxChange.created(ACCOUNT_ID, state, ZonedDateTime.now, List[MailboxId](TestId.of(0)).asJava).build).block()
+    val state = stateFactory.generate()
+    repository.save(MailboxChange.builder().accountId(ACCOUNT_ID).state(state).date(ZonedDateTime.now).created(List[MailboxId](TestId.of(0)).asJava).build).block()
 
     messageManager.setFlags(new Flags(Flags.Flag.SEEN), FlagsUpdateMode.REMOVE, MessageRange.all(), mailboxSession)
 
@@ -173,8 +178,8 @@ class MailboxChangeListenerTest {
     val messageManager: MessageManager = mailboxManager.getMailbox(inboxId, mailboxSession)
     messageManager.appendMessage(AppendCommand.builder().build("header: value\r\n\r\nbody"), mailboxSession)
 
-    val state = State.of(UUID.randomUUID)
-    repository.save(MailboxChange.created(ACCOUNT_ID, state, ZonedDateTime.now, List[MailboxId](TestId.of(0)).asJava).build).block()
+    val state = stateFactory.generate()
+    repository.save(MailboxChange.builder().accountId(ACCOUNT_ID).state(state).date(ZonedDateTime.now).created(List[MailboxId](TestId.of(0)).asJava).build).block()
 
     messageManager.setFlags(new Flags(Flags.Flag.ANSWERED), FlagsUpdateMode.ADD, MessageRange.all(), mailboxSession)
 
@@ -192,8 +197,8 @@ class MailboxChangeListenerTest {
       .withFlags(new Flags(Flags.Flag.ANSWERED))
       .build("header: value\r\n\r\nbody"), mailboxSession)
 
-    val state = State.of(UUID.randomUUID)
-    repository.save(MailboxChange.created(ACCOUNT_ID, state, ZonedDateTime.now, List[MailboxId](TestId.of(0)).asJava).build).block()
+    val state = stateFactory.generate()
+    repository.save(MailboxChange.builder().accountId(ACCOUNT_ID).state(state).date(ZonedDateTime.now).created(List[MailboxId](TestId.of(0)).asJava).build).block()
 
     messageManager.setFlags(new Flags(Flags.Flag.DELETED), FlagsUpdateMode.REPLACE, MessageRange.all(), mailboxSession)
 
@@ -209,8 +214,8 @@ class MailboxChangeListenerTest {
     val messageManager: MessageManager = mailboxManager.getMailbox(inboxId, mailboxSession)
     val appendResult: AppendResult = messageManager.appendMessage(AppendCommand.builder().build("header: value\r\n\r\nbody"), mailboxSession)
 
-    val state = State.of(UUID.randomUUID)
-    repository.save(MailboxChange.created(ACCOUNT_ID, state, ZonedDateTime.now, List[MailboxId](TestId.of(0)).asJava).build).block()
+    val state = stateFactory.generate()
+    repository.save(MailboxChange.builder().accountId(ACCOUNT_ID).state(state).date(ZonedDateTime.now).created(List[MailboxId](TestId.of(0)).asJava).build).block()
     messageManager.delete(List(appendResult.getId.getUid).asJava, mailboxSession)
 
     assertThat(repository.getSinceState(ACCOUNT_ID, state, None.toJava).block().getUpdated)
@@ -223,8 +228,8 @@ class MailboxChangeListenerTest {
     val path = MailboxPath.forUser(BOB, "test")
     val inboxId: MailboxId = mailboxManager.createMailbox(path, mailboxSession).get
 
-    val state = State.of(UUID.randomUUID)
-    repository.save(MailboxChange.created(ACCOUNT_ID, state, ZonedDateTime.now, List[MailboxId](TestId.of(0)).asJava).build).block()
+    val state = stateFactory.generate()
+    repository.save(MailboxChange.builder().accountId(ACCOUNT_ID).state(state).date(ZonedDateTime.now).created(List[MailboxId](TestId.of(0)).asJava).build).block()
 
     mailboxManager.deleteMailbox(inboxId, mailboxSession)
 


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


[james-project] 14/17: JAMES-3481 Mailbox/change handle delegated mailbox

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 833710a5056cfc980452fee7ebc0c92a1d16451b
Author: LanKhuat <dl...@linagora.com>
AuthorDate: Tue Dec 15 18:14:05 2020 +0700

    JAMES-3481 Mailbox/change handle delegated mailbox
---
 .../james/jmap/api/change/MailboxChange.java       | 175 ++++-
 .../jmap/api/change/MailboxChangeRepository.java   |   2 +-
 .../change/MailboxChangeRepositoryContract.java    | 136 ++--
 .../contract/MailboxChangesMethodContract.scala    | 718 +++++++++++++++++++--
 .../james/jmap/change/MailboxChangeListener.scala  |  22 +-
 .../jmap/change/MailboxChangeListenerTest.scala    |  26 +-
 6 files changed, 964 insertions(+), 115 deletions(-)

diff --git a/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/MailboxChange.java b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/MailboxChange.java
index 52461e6..5613db9 100644
--- a/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/MailboxChange.java
+++ b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/MailboxChange.java
@@ -25,10 +25,14 @@ import java.util.List;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.UUID;
+import java.util.stream.Stream;
 
 import javax.mail.Flags;
 
+import org.apache.james.core.Username;
 import org.apache.james.jmap.api.model.AccountId;
+import org.apache.james.mailbox.MailboxManager;
+import org.apache.james.mailbox.MailboxSession;
 import org.apache.james.mailbox.events.Event;
 import org.apache.james.mailbox.events.MailboxListener.Added;
 import org.apache.james.mailbox.events.MailboxListener.Expunged;
@@ -37,8 +41,12 @@ import org.apache.james.mailbox.events.MailboxListener.MailboxACLUpdated;
 import org.apache.james.mailbox.events.MailboxListener.MailboxAdded;
 import org.apache.james.mailbox.events.MailboxListener.MailboxDeletion;
 import org.apache.james.mailbox.events.MailboxListener.MailboxRenamed;
+import org.apache.james.mailbox.exception.MailboxException;
+import org.apache.james.mailbox.model.MailboxACL;
 import org.apache.james.mailbox.model.MailboxId;
+import org.apache.james.mailbox.model.MailboxPath;
 
+import com.github.steveash.guavate.Guavate;
 import com.google.common.collect.ImmutableList;
 
 public class MailboxChange {
@@ -93,31 +101,139 @@ public class MailboxChange {
         }
     }
 
-    public static MailboxChange of(AccountId accountId, State state,  ZonedDateTime date, List<MailboxId> created, List<MailboxId> updated, List<MailboxId> destroyed) {
-        return new MailboxChange(accountId, state, date, created, updated, destroyed);
+    @FunctionalInterface
+    interface RequiredAccountId {
+        RequiredState accountId(AccountId accountId);
     }
 
-    public static Optional<MailboxChange> fromEvent(Event event) {
+    @FunctionalInterface
+    interface RequiredState {
+        RequiredDate state(State state);
+    }
+
+    @FunctionalInterface
+    interface RequiredDate {
+        Builder date(ZonedDateTime date);
+    }
+
+    public static class Builder {
+        private final AccountId accountId;
+        private final State state;
+        private final ZonedDateTime date;
+        private boolean delegated;
+        private Optional<List<MailboxId>> created;
+        private Optional<List<MailboxId>> updated;
+        private Optional<List<MailboxId>> destroyed;
+
+        private Builder(AccountId accountId, State state, ZonedDateTime date) {
+            this.accountId = accountId;
+            this.state = state;
+            this.date = date;
+            this.created = Optional.empty();
+            this.updated = Optional.empty();
+            this.destroyed = Optional.empty();
+        }
+
+        public Builder delegated() {
+            this.delegated = true;
+            return this;
+        }
+
+        public Builder created(List<MailboxId> created) {
+            this.created = Optional.of(created);
+            return this;
+        }
+
+        public Builder updated(List<MailboxId> updated) {
+            this.updated = Optional.of(updated);
+            return this;
+        }
+
+        public Builder destroyed(List<MailboxId> destroyed) {
+            this.destroyed = Optional.of(destroyed);
+            return this;
+        }
+
+        public MailboxChange build() {
+            return new MailboxChange(accountId, state, date, delegated, created.orElse(ImmutableList.of()), updated.orElse(ImmutableList.of()), destroyed.orElse(ImmutableList.of()));
+        }
+    }
+
+    public static RequiredAccountId builder() {
+        return accountId -> state -> date -> new Builder(accountId, state, date);
+    }
+
+    public static Builder created(AccountId accountId, State state, ZonedDateTime date, List<MailboxId> created) {
+        return MailboxChange.builder()
+            .accountId(accountId)
+            .state(state)
+            .date(date)
+            .created(created);
+    }
+
+    public static Builder updated(AccountId accountId, State state, ZonedDateTime date, List<MailboxId> updated) {
+        return MailboxChange.builder()
+            .accountId(accountId)
+            .state(state)
+            .date(date)
+            .updated(updated);
+    }
+
+    public static Builder destroyed(AccountId accountId, State state, ZonedDateTime date, List<MailboxId> destroyed) {
+        return MailboxChange.builder()
+            .accountId(accountId)
+            .state(state)
+            .date(date)
+            .destroyed(destroyed);
+    }
+
+    public static Optional<List<MailboxChange>> fromEvent(Event event, MailboxManager mailboxManager) {
         ZonedDateTime now = ZonedDateTime.now(Clock.systemUTC());
         if (event instanceof MailboxAdded) {
             MailboxAdded mailboxAdded = (MailboxAdded) event;
-            return Optional.of(MailboxChange.of(AccountId.fromUsername(mailboxAdded.getUsername()), State.of(UUID.randomUUID()), now, ImmutableList.of(mailboxAdded.getMailboxId()), ImmutableList.of(), ImmutableList.of()));
+            return Optional.of(ImmutableList.of(MailboxChange.created(AccountId.fromUsername(mailboxAdded.getUsername()), State.of(UUID.randomUUID()), now, ImmutableList.of(mailboxAdded.getMailboxId())).build()));
         }
         if (event instanceof MailboxRenamed) {
             MailboxRenamed mailboxRenamed = (MailboxRenamed) event;
-            return Optional.of(MailboxChange.of(AccountId.fromUsername(mailboxRenamed.getUsername()), State.of(UUID.randomUUID()), now, ImmutableList.of(), ImmutableList.of(mailboxRenamed.getMailboxId()), ImmutableList.of()));
+
+            MailboxChange ownerChange = MailboxChange.updated(AccountId.fromUsername(mailboxRenamed.getUsername()), State.of(UUID.randomUUID()), now, ImmutableList.of(mailboxRenamed.getMailboxId())).build();
+            Stream<MailboxChange> shareeChanges = getSharees(mailboxRenamed.getNewPath(), mailboxRenamed.getUsername(), mailboxManager)
+                .map(name -> MailboxChange.updated(AccountId.fromString(name), State.of(UUID.randomUUID()), now, ImmutableList.of(mailboxRenamed.getMailboxId()))
+                    .delegated()
+                    .build());
+
+            List<MailboxChange> blah = Stream.concat(Stream.of(ownerChange), shareeChanges).collect(Guavate.toImmutableList());
+            return Optional.of(blah);
         }
         if (event instanceof MailboxACLUpdated) {
             MailboxACLUpdated mailboxACLUpdated = (MailboxACLUpdated) event;
-            return Optional.of(MailboxChange.of(AccountId.fromUsername(mailboxACLUpdated.getUsername()), State.of(UUID.randomUUID()), now, ImmutableList.of(), ImmutableList.of(mailboxACLUpdated.getMailboxId()), ImmutableList.of()));
+
+            MailboxChange ownerChange = MailboxChange.updated(AccountId.fromUsername(mailboxACLUpdated.getUsername()), State.of(UUID.randomUUID()), now, ImmutableList.of(mailboxACLUpdated.getMailboxId())).build();
+            Stream<MailboxChange> shareeChanges = getSharees(mailboxACLUpdated.getMailboxPath(), mailboxACLUpdated.getUsername(), mailboxManager)
+                .map(name -> MailboxChange.updated(AccountId.fromString(name), State.of(UUID.randomUUID()), now, ImmutableList.of(mailboxACLUpdated.getMailboxId()))
+                    .delegated()
+                    .build());
+
+            return Optional.of(
+                Stream.concat(Stream.of(ownerChange), shareeChanges)
+                    .collect(Guavate.toImmutableList()));
         }
         if (event instanceof MailboxDeletion) {
             MailboxDeletion mailboxDeletion = (MailboxDeletion) event;
-            return Optional.of(MailboxChange.of(AccountId.fromUsername(mailboxDeletion.getUsername()), State.of(UUID.randomUUID()), now, ImmutableList.of(), ImmutableList.of(), ImmutableList.of(mailboxDeletion.getMailboxId())));
+            return Optional.of(ImmutableList.of(MailboxChange.destroyed(AccountId.fromUsername(mailboxDeletion.getUsername()), State.of(UUID.randomUUID()), now, ImmutableList.of(mailboxDeletion.getMailboxId())).build()));
         }
         if (event instanceof Added) {
             Added messageAdded = (Added) event;
-            return Optional.of(MailboxChange.of(AccountId.fromUsername(messageAdded.getUsername()), State.of(UUID.randomUUID()), now, ImmutableList.of(), ImmutableList.of(messageAdded.getMailboxId()), ImmutableList.of()));
+
+            MailboxChange ownerChange = MailboxChange.updated(AccountId.fromUsername(messageAdded.getUsername()), State.of(UUID.randomUUID()), now, ImmutableList.of(messageAdded.getMailboxId())).build();
+            Stream<MailboxChange> shareeChanges = getSharees(messageAdded.getMailboxPath(), messageAdded.getUsername(), mailboxManager)
+                .map(name -> MailboxChange.updated(AccountId.fromString(name), State.of(UUID.randomUUID()), now, ImmutableList.of(messageAdded.getMailboxId()))
+                    .delegated()
+                    .build());
+
+            return Optional.of(
+                Stream.concat(Stream.of(ownerChange), shareeChanges)
+                    .collect(Guavate.toImmutableList()));
         }
         if (event instanceof FlagsUpdated) {
             FlagsUpdated messageFlagUpdated = (FlagsUpdated) event;
@@ -125,28 +241,61 @@ public class MailboxChange {
                 .stream()
                 .anyMatch(flags -> flags.isChanged(Flags.Flag.SEEN));
             if (isSeenChanged) {
-                return Optional.of(MailboxChange.of(AccountId.fromUsername(messageFlagUpdated.getUsername()), State.of(UUID.randomUUID()), now, ImmutableList.of(), ImmutableList.of(messageFlagUpdated.getMailboxId()), ImmutableList.of()));
+
+                MailboxChange ownerChange = MailboxChange.updated(AccountId.fromUsername(messageFlagUpdated.getUsername()), State.of(UUID.randomUUID()), now, ImmutableList.of(messageFlagUpdated.getMailboxId())).build();
+                Stream<MailboxChange> shareeChanges = getSharees(messageFlagUpdated.getMailboxPath(), messageFlagUpdated.getUsername(), mailboxManager)
+                    .map(name -> MailboxChange.updated(AccountId.fromString(name), State.of(UUID.randomUUID()), now, ImmutableList.of(messageFlagUpdated.getMailboxId()))
+                        .delegated()
+                        .build());
+
+                return Optional.of(
+                    Stream.concat(Stream.of(ownerChange), shareeChanges)
+                        .collect(Guavate.toImmutableList()));
             }
         }
         if (event instanceof Expunged) {
             Expunged expunged = (Expunged) event;
-            return Optional.of(MailboxChange.of(AccountId.fromUsername(expunged.getUsername()), State.of(UUID.randomUUID()), now, ImmutableList.of(), ImmutableList.of(expunged.getMailboxId()), ImmutableList.of()));
+
+            MailboxChange ownerChange = MailboxChange.updated(AccountId.fromUsername(expunged.getUsername()), State.of(UUID.randomUUID()), now, ImmutableList.of(expunged.getMailboxId())).build();
+            Stream<MailboxChange> shareeChanges = getSharees(expunged.getMailboxPath(), expunged.getUsername(), mailboxManager)
+                .map(name -> MailboxChange.updated(AccountId.fromString(name), State.of(UUID.randomUUID()), now, ImmutableList.of(expunged.getMailboxId()))
+                    .delegated()
+                    .build());
+
+            return Optional.of(
+                Stream.concat(Stream.of(ownerChange), shareeChanges)
+                    .collect(Guavate.toImmutableList()));
         }
 
         return Optional.empty();
     }
 
+    private static Stream<String> getSharees(MailboxPath path, Username username, MailboxManager mailboxManager) {
+        MailboxSession mailboxSession = mailboxManager.createSystemSession(username);
+        try {
+            MailboxACL mailboxACL = mailboxManager.listRights(path, mailboxSession);
+            return mailboxACL.getEntries().keySet()
+                .stream()
+                .filter(rfc4314Rights -> !rfc4314Rights.isNegative())
+                .map(MailboxACL.EntryKey::getName);
+        } catch (MailboxException e) {
+            return Stream.of();
+        }
+    }
+
     private final AccountId accountId;
     private final State state;
     private final ZonedDateTime date;
+    private final boolean delegated;
     private final List<MailboxId> created;
     private final List<MailboxId> updated;
     private final List<MailboxId> destroyed;
 
-    private MailboxChange(AccountId accountId, State state, ZonedDateTime date, List<MailboxId> created, List<MailboxId> updated, List<MailboxId> destroyed) {
+    private MailboxChange(AccountId accountId, State state, ZonedDateTime date, boolean delegated, List<MailboxId> created, List<MailboxId> updated, List<MailboxId> destroyed) {
         this.accountId = accountId;
         this.state = state;
         this.date = date;
+        this.delegated = delegated;
         this.created = created;
         this.updated = updated;
         this.destroyed = destroyed;
@@ -164,6 +313,10 @@ public class MailboxChange {
         return date;
     }
 
+    public boolean isDelegated() {
+        return delegated;
+    }
+
     public List<MailboxId> getCreated() {
         return created;
     }
diff --git a/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/MailboxChangeRepository.java b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/MailboxChangeRepository.java
index ccc0a36..006316b 100644
--- a/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/MailboxChangeRepository.java
+++ b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/MailboxChangeRepository.java
@@ -31,7 +31,7 @@ public interface MailboxChangeRepository {
 
     Mono<Void> save(MailboxChange change);
 
-    Mono<MailboxChanges> getSinceState(AccountId accountId, State state, Optional<Limit> maxIdsToReturn);
+    Mono<MailboxChanges> getSinceState(AccountId accountId, State state, Optional<Limit> maxChanges);
 
     Mono<State> getLatestState(AccountId accountId);
 }
diff --git a/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/change/MailboxChangeRepositoryContract.java b/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/change/MailboxChangeRepositoryContract.java
index 9cb83475..26c04ba 100644
--- a/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/change/MailboxChangeRepositoryContract.java
+++ b/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/change/MailboxChangeRepositoryContract.java
@@ -49,7 +49,7 @@ public interface MailboxChangeRepositoryContract {
     default void saveChangeShouldSuccess() {
         MailboxChangeRepository repository = mailboxChangeRepository();
 
-        MailboxChange change = MailboxChange.of(ACCOUNT_ID, STATE_0, DATE, ImmutableList.of(TestId.of(1)), ImmutableList.of(), ImmutableList.of());
+        MailboxChange change = MailboxChange.created(ACCOUNT_ID, STATE_0, DATE, ImmutableList.of(TestId.of(1))).build();
 
         assertThatCode(() -> repository.save(change).block())
             .doesNotThrowAnyException();
@@ -67,9 +67,9 @@ public interface MailboxChangeRepositoryContract {
     default void getLatestStateShouldReturnLastPersistedState() {
         MailboxChangeRepository repository = mailboxChangeRepository();
 
-        MailboxChange change1 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE.minusHours(2), ImmutableList.of(TestId.of(2)), ImmutableList.of(), ImmutableList.of());
-        MailboxChange change2 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE.minusHours(1), ImmutableList.of(TestId.of(3)), ImmutableList.of(), ImmutableList.of());
-        MailboxChange change3 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE, ImmutableList.of(TestId.of(4)), ImmutableList.of(), ImmutableList.of());
+        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE.minusHours(2)).created(ImmutableList.of(TestId.of(2))).build();
+        MailboxChange change2 = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE.minusHours(1)).created(ImmutableList.of(TestId.of(3))).build();
+        MailboxChange change3 = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE).created(ImmutableList.of(TestId.of(4))).build();
         repository.save(change1).block();
         repository.save(change2).block();
         repository.save(change3).block();
@@ -82,7 +82,7 @@ public interface MailboxChangeRepositoryContract {
     default void saveChangeShouldFailWhenNoAccountId() {
         MailboxChangeRepository repository = mailboxChangeRepository();
 
-        MailboxChange change = MailboxChange.of(null, STATE_0, DATE, ImmutableList.of(TestId.of(1)), ImmutableList.of(), ImmutableList.of());
+        MailboxChange change = MailboxChange.created(null, STATE_0, DATE, ImmutableList.of(TestId.of(1))).build();
 
         assertThatThrownBy(() -> repository.save(change).block())
             .isInstanceOf(NullPointerException.class);
@@ -92,7 +92,7 @@ public interface MailboxChangeRepositoryContract {
     default void saveChangeShouldFailWhenNoState() {
         MailboxChangeRepository repository = mailboxChangeRepository();
 
-        MailboxChange change = MailboxChange.of(ACCOUNT_ID, null, DATE, ImmutableList.of(TestId.of(1)), ImmutableList.of(), ImmutableList.of());
+        MailboxChange change = MailboxChange.created(ACCOUNT_ID, null, DATE, ImmutableList.of(TestId.of(1))).build();
 
         assertThatThrownBy(() -> repository.save(change).block())
             .isInstanceOf(NullPointerException.class);
@@ -102,8 +102,8 @@ public interface MailboxChangeRepositoryContract {
     default void getChangesShouldSuccess() {
         MailboxChangeRepository repository = mailboxChangeRepository();
 
-        MailboxChange oldState = MailboxChange.of(ACCOUNT_ID, STATE_0, DATE.minusHours(1), ImmutableList.of(TestId.of(1)), ImmutableList.of(), ImmutableList.of());
-        MailboxChange change = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE, ImmutableList.of(), ImmutableList.of(TestId.of(1)), ImmutableList.of());
+        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(STATE_0).date(DATE.minusHours(1)).created(ImmutableList.of(TestId.of(1))).build();
+        MailboxChange change = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE).updated(ImmutableList.of(TestId.of(1))).build();
         repository.save(oldState).block();
         repository.save(change).block();
 
@@ -115,7 +115,7 @@ public interface MailboxChangeRepositoryContract {
     default void getChangesShouldReturnEmptyWhenNoNewerState() {
         MailboxChangeRepository repository = mailboxChangeRepository();
 
-        MailboxChange oldState = MailboxChange.of(ACCOUNT_ID, STATE_0, DATE, ImmutableList.of(TestId.of(1)), ImmutableList.of(), ImmutableList.of());
+        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(STATE_0).date(DATE).created(ImmutableList.of(TestId.of(1))).build();
         repository.save(oldState).block();
 
         assertThat(repository.getSinceState(ACCOUNT_ID, STATE_0, Optional.empty()).block().getAllChanges())
@@ -126,7 +126,7 @@ public interface MailboxChangeRepositoryContract {
     default void getChangesShouldReturnCurrentStateWhenNoNewerState() {
         MailboxChangeRepository repository = mailboxChangeRepository();
 
-        MailboxChange oldState = MailboxChange.of(ACCOUNT_ID, STATE_0, DATE, ImmutableList.of(TestId.of(1)), ImmutableList.of(), ImmutableList.of());
+        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(STATE_0).date(DATE).created(ImmutableList.of(TestId.of(1))).build();
         repository.save(oldState).block();
 
         assertThat(repository.getSinceState(ACCOUNT_ID, STATE_0, Optional.empty()).block().getNewState())
@@ -137,10 +137,10 @@ public interface MailboxChangeRepositoryContract {
     default void getChangesShouldLimitChanges() {
         MailboxChangeRepository repository = mailboxChangeRepository();
 
-        MailboxChange oldState = MailboxChange.of(ACCOUNT_ID, STATE_0, DATE.minusHours(3), ImmutableList.of(TestId.of(1)), ImmutableList.of(), ImmutableList.of());
-        MailboxChange change1 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE.minusHours(2), ImmutableList.of(TestId.of(2)), ImmutableList.of(), ImmutableList.of());
-        MailboxChange change2 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE.minusHours(1), ImmutableList.of(TestId.of(3)), ImmutableList.of(), ImmutableList.of());
-        MailboxChange change3 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE, ImmutableList.of(TestId.of(4)), ImmutableList.of(), ImmutableList.of());
+        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(STATE_0).date(DATE.minusHours(3)).created(ImmutableList.of(TestId.of(1))).build();
+        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE.minusHours(2)).created(ImmutableList.of(TestId.of(2))).build();
+        MailboxChange change2 = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE.minusHours(1)).created(ImmutableList.of(TestId.of(3))).build();
+        MailboxChange change3 = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE).created(ImmutableList.of(TestId.of(4))).build();
         repository.save(oldState).block();
         repository.save(change1).block();
         repository.save(change2).block();
@@ -154,10 +154,10 @@ public interface MailboxChangeRepositoryContract {
     default void getChangesShouldReturnAllFromInitial() {
         MailboxChangeRepository repository = mailboxChangeRepository();
 
-        MailboxChange oldState = MailboxChange.of(ACCOUNT_ID, STATE_0, DATE.minusHours(3), ImmutableList.of(TestId.of(1)), ImmutableList.of(), ImmutableList.of());
-        MailboxChange change1 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE.minusHours(2), ImmutableList.of(TestId.of(2)), ImmutableList.of(), ImmutableList.of());
-        MailboxChange change2 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE.minusHours(1), ImmutableList.of(TestId.of(3)), ImmutableList.of(), ImmutableList.of());
-        MailboxChange change3 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE, ImmutableList.of(TestId.of(4)), ImmutableList.of(), ImmutableList.of());
+        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(STATE_0).date(DATE.minusHours(3)).created(ImmutableList.of(TestId.of(1))).build();
+        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE.minusHours(2)).created(ImmutableList.of(TestId.of(2))).build();
+        MailboxChange change2 = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE.minusHours(1)).created(ImmutableList.of(TestId.of(3))).build();
+        MailboxChange change3 = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE).created(ImmutableList.of(TestId.of(4))).build();
         repository.save(oldState).block();
         repository.save(change1).block();
         repository.save(change2).block();
@@ -171,11 +171,11 @@ public interface MailboxChangeRepositoryContract {
     default void getChangesFromInitialShouldReturnNewState() {
         MailboxChangeRepository repository = mailboxChangeRepository();
 
-        MailboxChange oldState = MailboxChange.of(ACCOUNT_ID, STATE_0, DATE.minusHours(3), ImmutableList.of(TestId.of(1)), ImmutableList.of(), ImmutableList.of());
-        MailboxChange change1 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE.minusHours(2), ImmutableList.of(TestId.of(2)), ImmutableList.of(), ImmutableList.of());
+        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(STATE_0).date(DATE.minusHours(3)).created(ImmutableList.of(TestId.of(1))).build();
+        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE.minusHours(2)).created(ImmutableList.of(TestId.of(2))).build();
         State state2 = State.of(UUID.randomUUID());
-        MailboxChange change2 = MailboxChange.of(ACCOUNT_ID, state2, DATE.minusHours(1), ImmutableList.of(TestId.of(3)), ImmutableList.of(), ImmutableList.of());
-        MailboxChange change3 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE, ImmutableList.of(TestId.of(4)), ImmutableList.of(), ImmutableList.of());
+        MailboxChange change2 = MailboxChange.builder().accountId(ACCOUNT_ID).state(state2).date(DATE.minusHours(1)).created(ImmutableList.of(TestId.of(3))).build();
+        MailboxChange change3 = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE).created(ImmutableList.of(TestId.of(4))).build();
         repository.save(oldState).block();
         repository.save(change1).block();
         repository.save(change2).block();
@@ -190,9 +190,9 @@ public interface MailboxChangeRepositoryContract {
     default void getChangesShouldLimitChangesWhenMaxChangesOmitted() {
         MailboxChangeRepository repository = mailboxChangeRepository();
 
-        MailboxChange oldState = MailboxChange.of(ACCOUNT_ID, STATE_0, DATE.minusHours(2), ImmutableList.of(TestId.of(1)), ImmutableList.of(), ImmutableList.of());
-        MailboxChange change1 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE.minusHours(1), ImmutableList.of(TestId.of(2), TestId.of(3), TestId.of(4), TestId.of(5), TestId.of(6)), ImmutableList.of(), ImmutableList.of());
-        MailboxChange change2 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE, ImmutableList.of(TestId.of(7)), ImmutableList.of(), ImmutableList.of());
+        MailboxChange oldState = MailboxChange.created(ACCOUNT_ID, STATE_0, DATE.minusHours(2), ImmutableList.of(TestId.of(1))).build();
+        MailboxChange change1 = MailboxChange.created(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE.minusHours(1), ImmutableList.of(TestId.of(2), TestId.of(3), TestId.of(4), TestId.of(5), TestId.of(6))).build();
+        MailboxChange change2 = MailboxChange.created(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE, ImmutableList.of(TestId.of(7))).build();
 
         repository.save(oldState).block();
         repository.save(change1).block();
@@ -206,9 +206,9 @@ public interface MailboxChangeRepositoryContract {
     default void getChangesShouldNotReturnMoreThanMaxChanges() {
         MailboxChangeRepository repository = mailboxChangeRepository();
 
-        MailboxChange oldState = MailboxChange.of(ACCOUNT_ID, STATE_0, DATE.minusHours(2), ImmutableList.of(TestId.of(1)), ImmutableList.of(), ImmutableList.of());
-        MailboxChange change1 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE.minusHours(1), ImmutableList.of(TestId.of(2), TestId.of(3)), ImmutableList.of(), ImmutableList.of());
-        MailboxChange change2 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE, ImmutableList.of(TestId.of(4), TestId.of(5)), ImmutableList.of(), ImmutableList.of());
+        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(STATE_0).date(DATE.minusHours(2)).created(ImmutableList.of(TestId.of(1))).build();
+        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE.minusHours(1)).created(ImmutableList.of(TestId.of(2), TestId.of(3))).build();
+        MailboxChange change2 = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE).created(ImmutableList.of(TestId.of(4), TestId.of(5))).build();
         repository.save(oldState).block();
         repository.save(change1).block();
         repository.save(change2).block();
@@ -221,8 +221,8 @@ public interface MailboxChangeRepositoryContract {
     default void getChangesShouldReturnEmptyWhenNumberOfChangesExceedMaxChanges() {
         MailboxChangeRepository repository = mailboxChangeRepository();
 
-        MailboxChange oldState = MailboxChange.of(ACCOUNT_ID, STATE_0, DATE.minusHours(2), ImmutableList.of(TestId.of(1)), ImmutableList.of(), ImmutableList.of());
-        MailboxChange change1 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE.minusHours(1), ImmutableList.of(TestId.of(2), TestId.of(3)), ImmutableList.of(), ImmutableList.of());
+        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(STATE_0).date(DATE.minusHours(2)).created(ImmutableList.of(TestId.of(1))).build();
+        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE.minusHours(1)).created(ImmutableList.of(TestId.of(2), TestId.of(3))).build();
         repository.save(oldState).block();
         repository.save(change1).block();
 
@@ -234,9 +234,9 @@ public interface MailboxChangeRepositoryContract {
     default void getChangesShouldReturnNewState() {
         MailboxChangeRepository repository = mailboxChangeRepository();
 
-        MailboxChange oldState = MailboxChange.of(ACCOUNT_ID, STATE_0, DATE.minusHours(2), ImmutableList.of(TestId.of(1)), ImmutableList.of(), ImmutableList.of());
-        MailboxChange change1 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE.minusHours(1), ImmutableList.of(TestId.of(2), TestId.of(3)), ImmutableList.of(), ImmutableList.of());
-        MailboxChange change2 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE, ImmutableList.of(), ImmutableList.of(TestId.of(2), TestId.of(3)), ImmutableList.of());
+        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(STATE_0).date(DATE.minusHours(2)).created(ImmutableList.of(TestId.of(1))).build();
+        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE.minusHours(1)).created(ImmutableList.of(TestId.of(2), TestId.of(3))).build();
+        MailboxChange change2 = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE).updated(ImmutableList.of(TestId.of(2), TestId.of(3))).build();
         repository.save(oldState).block();
         repository.save(change1).block();
         repository.save(change2).block();
@@ -249,9 +249,9 @@ public interface MailboxChangeRepositoryContract {
     default void hasMoreChangesShouldBeTrueWhenMoreChanges() {
         MailboxChangeRepository repository = mailboxChangeRepository();
 
-        MailboxChange oldState = MailboxChange.of(ACCOUNT_ID, STATE_0, DATE.minusHours(2), ImmutableList.of(TestId.of(1)), ImmutableList.of(), ImmutableList.of());
-        MailboxChange change1 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE.minusHours(1), ImmutableList.of(TestId.of(2), TestId.of(3)), ImmutableList.of(), ImmutableList.of());
-        MailboxChange change2 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE, ImmutableList.of(), ImmutableList.of(TestId.of(2), TestId.of(3)), ImmutableList.of());
+        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(STATE_0).date(DATE.minusHours(2)).created(ImmutableList.of(TestId.of(1))).build();
+        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE.minusHours(1)).created(ImmutableList.of(TestId.of(2), TestId.of(3))).build();
+        MailboxChange change2 = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE).updated(ImmutableList.of(TestId.of(2), TestId.of(3))).build();
         repository.save(oldState).block();
         repository.save(change1).block();
         repository.save(change2).block();
@@ -264,9 +264,9 @@ public interface MailboxChangeRepositoryContract {
     default void hasMoreChangesShouldBeFalseWhenNoMoreChanges() {
         MailboxChangeRepository repository = mailboxChangeRepository();
 
-        MailboxChange oldState = MailboxChange.of(ACCOUNT_ID, STATE_0, DATE.minusHours(2), ImmutableList.of(TestId.of(1)), ImmutableList.of(), ImmutableList.of());
-        MailboxChange change1 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE.minusHours(1), ImmutableList.of(TestId.of(2), TestId.of(3)), ImmutableList.of(), ImmutableList.of());
-        MailboxChange change2 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE, ImmutableList.of(), ImmutableList.of(TestId.of(2), TestId.of(3)), ImmutableList.of());
+        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(STATE_0).date(DATE.minusHours(2)).created(ImmutableList.of(TestId.of(1))).build();
+        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE.minusHours(1)).created(ImmutableList.of(TestId.of(2), TestId.of(3))).build();
+        MailboxChange change2 = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE).updated(ImmutableList.of(TestId.of(2), TestId.of(3))).build();
         repository.save(oldState).block();
         repository.save(change1).block();
         repository.save(change2).block();
@@ -279,10 +279,23 @@ public interface MailboxChangeRepositoryContract {
     default void changesShouldBeStoredInTheirRespectiveType() {
         MailboxChangeRepository repository = mailboxChangeRepository();
 
-        MailboxChange oldState = MailboxChange.of(ACCOUNT_ID, STATE_0, DATE.minusHours(3), ImmutableList.of(TestId.of(1)), ImmutableList.of(), ImmutableList.of());
-        MailboxChange change1 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE.minusHours(2), ImmutableList.of(TestId.of(2), TestId.of(3), TestId.of(4), TestId.of(5)), ImmutableList.of(), ImmutableList.of());
-        MailboxChange change2 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE.minusHours(1), ImmutableList.of(TestId.of(6), TestId.of(7)), ImmutableList.of(TestId.of(2), TestId.of(3)), ImmutableList.of(TestId.of(4)));
-        MailboxChange change3 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE, ImmutableList.of(TestId.of(8)), ImmutableList.of(TestId.of(6), TestId.of(7)), ImmutableList.of(TestId.of(5)));
+        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(STATE_0).date(DATE.minusHours(3)).created(ImmutableList.of(TestId.of(1))).build();
+        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE.minusHours(2)).created(ImmutableList.of(TestId.of(2), TestId.of(3), TestId.of(4), TestId.of(5))).build();
+        MailboxChange change2 = MailboxChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE.minusHours(1))
+            .created(ImmutableList.of(TestId.of(6), TestId.of(7)))
+            .updated(ImmutableList.of(TestId.of(2), TestId.of(3)))
+            .destroyed(ImmutableList.of(TestId.of(4))).build();
+        MailboxChange change3 = MailboxChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE)
+            .created(ImmutableList.of(TestId.of(8)))
+            .updated(ImmutableList.of(TestId.of(6), TestId.of(7)))
+            .destroyed(ImmutableList.of(TestId.of(5))).build();
+
         repository.save(oldState).block();
         repository.save(change1).block();
         repository.save(change2).block();
@@ -301,9 +314,15 @@ public interface MailboxChangeRepositoryContract {
     default void getChangesShouldIgnoreDuplicatedValues() {
         MailboxChangeRepository repository = mailboxChangeRepository();
 
-        MailboxChange oldState = MailboxChange.of(ACCOUNT_ID, STATE_0, DATE.minusHours(2), ImmutableList.of(TestId.of(1)), ImmutableList.of(), ImmutableList.of());
-        MailboxChange change1 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE.minusHours(1), ImmutableList.of(), ImmutableList.of(TestId.of(1), TestId.of(2)), ImmutableList.of());
-        MailboxChange change2 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE, ImmutableList.of(TestId.of(3)), ImmutableList.of(TestId.of(1), TestId.of(2)), ImmutableList.of());
+        MailboxChange oldState = MailboxChange.created(ACCOUNT_ID, STATE_0, DATE.minusHours(2), ImmutableList.of(TestId.of(1))).build();
+        MailboxChange change1 = MailboxChange.updated(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE.minusHours(1), ImmutableList.of(TestId.of(1), TestId.of(2))).build();
+        MailboxChange change2 = MailboxChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE)
+            .created(ImmutableList.of(TestId.of(3)))
+            .updated(ImmutableList.of(TestId.of(1), TestId.of(2)))
+            .build();
 
         repository.save(oldState).block();
         repository.save(change1).block();
@@ -317,11 +336,32 @@ public interface MailboxChangeRepositoryContract {
     }
 
     @Test
+    default void getChangesShouldReturnDelegatedChanges() {
+        MailboxChangeRepository repository = mailboxChangeRepository();
+
+        MailboxChange oldState = MailboxChange.created(ACCOUNT_ID, STATE_0, DATE.minusHours(2), ImmutableList.of(TestId.of(1))).build();
+        MailboxChange change1 = MailboxChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE.minusHours(1))
+            .updated(ImmutableList.of(TestId.of(1)))
+            .delegated()
+            .build();
+
+        repository.save(oldState);
+        repository.save(change1);
+
+        assertThat(repository.getSinceState(ACCOUNT_ID, STATE_0, Optional.empty()).block().getUpdated())
+            .containsExactly(TestId.of(1));
+
+    }
+
+    @Test
     default void getChangesShouldFailWhenInvalidMaxChanges() {
         MailboxChangeRepository repository = mailboxChangeRepository();
 
-        MailboxChange currentState = MailboxChange.of(ACCOUNT_ID, STATE_0, DATE, ImmutableList.of(TestId.of(1)), ImmutableList.of(), ImmutableList.of());
-        MailboxChange change = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE, ImmutableList.of(TestId.of(2)), ImmutableList.of(), ImmutableList.of());
+        MailboxChange currentState = MailboxChange.builder().accountId(ACCOUNT_ID).state(STATE_0).date(DATE).created(ImmutableList.of(TestId.of(1))).build();
+        MailboxChange change = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE).created(ImmutableList.of(TestId.of(2))).build();
         repository.save(currentState).block();
         repository.save(change).block();
 
diff --git a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxChangesMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxChangesMethodContract.scala
index e7de455..e07eef1 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxChangesMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxChangesMethodContract.scala
@@ -27,11 +27,13 @@ import io.netty.handler.codec.http.HttpHeaderNames.ACCEPT
 import io.restassured.RestAssured.{`given`, requestSpecification}
 import io.restassured.builder.ResponseSpecBuilder
 import io.restassured.http.ContentType.JSON
+import javax.mail.Flags
 import net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson
 import net.javacrumbs.jsonunit.core.Option.IGNORING_ARRAY_ORDER
 import net.javacrumbs.jsonunit.core.internal.Options
 import org.apache.http.HttpStatus.SC_OK
 import org.apache.james.GuiceJamesServer
+import org.apache.james.core.Username
 import org.apache.james.jmap.api.change.MailboxChange
 import org.apache.james.jmap.api.change.MailboxChange.State
 import org.apache.james.jmap.api.model.AccountId
@@ -41,12 +43,12 @@ import org.apache.james.jmap.http.UserCredential
 import org.apache.james.jmap.rfc8621.contract.Fixture.{ACCEPT_RFC8621_VERSION_HEADER, ANDRE, ANDRE_ACCOUNT_ID, ANDRE_PASSWORD, BOB, BOB_PASSWORD, DOMAIN, authScheme, baseRequestSpecBuilder}
 import org.apache.james.mailbox.MessageManager.AppendCommand
 import org.apache.james.mailbox.model.MailboxACL.Right
-import org.apache.james.mailbox.model.{MailboxACL, MailboxId, MailboxPath}
+import org.apache.james.mailbox.model.{MailboxACL, MailboxId, MailboxPath, MessageId}
 import org.apache.james.mime4j.dom.Message
 import org.apache.james.modules.{ACLProbeImpl, MailboxProbeImpl}
 import org.apache.james.utils.DataProbeImpl
 import org.assertj.core.api.Assertions.assertThat
-import org.junit.jupiter.api.{BeforeEach, Disabled, Test}
+import org.junit.jupiter.api.{BeforeEach, Disabled, Nested, Test}
 import play.api.libs.json.{JsString, Json}
 
 import scala.jdk.CollectionConverters._
@@ -80,7 +82,7 @@ trait MailboxChangesMethodContract {
     val mailboxProbe: MailboxProbeImpl = server.getProbe(classOf[MailboxProbeImpl])
     provisionSystemMailboxes(server)
 
-    val oldState: State = storeReferenceState(server)
+    val oldState: State = storeReferenceState(server, BOB)
 
     val mailboxId1: String = mailboxProbe
       .createMailbox(MailboxPath.forUser(BOB, "mailbox1"))
@@ -148,7 +150,7 @@ trait MailboxChangesMethodContract {
       .createMailbox(path)
       .serialize
 
-    val oldState: State = storeReferenceState(server)
+    val oldState: State = storeReferenceState(server, BOB)
 
     renameMailbox(mailboxId, "mailbox11")
 
@@ -207,7 +209,7 @@ trait MailboxChangesMethodContract {
       .createMailbox(path)
       .serialize
 
-    val oldState: State = storeReferenceState(server)
+    val oldState: State = storeReferenceState(server, BOB)
 
     val message: Message = Message.Builder
       .of
@@ -261,28 +263,26 @@ trait MailboxChangesMethodContract {
   }
 
   @Test
-  @Disabled("Not implemented yet")
-  def mailboxChangesShouldReturnUpdatedChangesWhenAppendMessageToDelegatedMailbox(server: GuiceJamesServer): Unit = {
+  def mailboxChangesShouldReturnUpdatedChangesWhenAddSeenFlag(server: GuiceJamesServer): Unit = {
     val mailboxProbe: MailboxProbeImpl = server.getProbe(classOf[MailboxProbeImpl])
 
     provisionSystemMailboxes(server)
 
-    val oldState: State = storeReferenceState(server)
-
     val path = MailboxPath.forUser(BOB, "mailbox1")
     val mailboxId: String = mailboxProbe
       .createMailbox(path)
       .serialize
 
-    server.getProbe(classOf[ACLProbeImpl])
-      .replaceRights(path, ANDRE.asString, new MailboxACL.Rfc4314Rights(Right.Lookup, Right.Read))
-
     val message: Message = Message.Builder
       .of
       .setSubject("test")
       .setBody("testmail", StandardCharsets.UTF_8)
       .build
-    mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message))
+    val messageId: MessageId = mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message)).getMessageId
+
+    val oldState: State = storeReferenceState(server, BOB)
+
+    markEmailAsSeen(messageId)
 
     val request =
       s"""{
@@ -290,25 +290,23 @@ trait MailboxChangesMethodContract {
          |  "methodCalls": [[
          |    "Mailbox/changes",
          |    {
-         |      "accountId": "$ANDRE_ACCOUNT_ID",
+         |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
          |      "sinceState": "${oldState.getValue}"
          |    },
          |    "c1"]]
          |}""".stripMargin
 
-    val response = `given`(
-      baseRequestSpecBuilder(server)
-        .setAuth(authScheme(UserCredential(ANDRE, ANDRE_PASSWORD)))
-        .addHeader(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
-        .setBody(request)
-        .build, new ResponseSpecBuilder().build)
+    val response = `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
       .post
-      .`then`
-        .statusCode(SC_OK)
-        .contentType(JSON)
-        .extract
-        .body
-        .asString
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
 
     assertThatJson(response)
       .whenIgnoringPaths("methodResponses[0][1].newState")
@@ -318,7 +316,7 @@ trait MailboxChangesMethodContract {
            |    "sessionState": "${SESSION_STATE.value}",
            |    "methodResponses": [
            |      [ "Mailbox/changes", {
-           |        "accountId": "$ANDRE_ACCOUNT_ID",
+           |        "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
            |        "oldState": "${oldState.getValue}",
            |        "hasMoreChanges": false,
            |        "updatedProperties": [],
@@ -331,6 +329,553 @@ trait MailboxChangesMethodContract {
   }
 
   @Test
+  def mailboxChangesShouldReturnUpdatedChangesWhenRemoveSeenFlag(server: GuiceJamesServer): Unit = {
+    val mailboxProbe: MailboxProbeImpl = server.getProbe(classOf[MailboxProbeImpl])
+
+    provisionSystemMailboxes(server)
+
+    val path = MailboxPath.forUser(BOB, "mailbox1")
+    val mailboxId: String = mailboxProbe
+      .createMailbox(path)
+      .serialize
+
+    server.getProbe(classOf[ACLProbeImpl])
+      .replaceRights(path, ANDRE.asString, new MailboxACL.Rfc4314Rights(Right.Lookup, Right.Read))
+
+    val messageId: MessageId = mailboxProbe.appendMessage(BOB.asString(), path,
+      AppendCommand.builder()
+        .withFlags(new Flags(Flags.Flag.SEEN))
+        .build("header: value\r\n\r\nbody"))
+      .getMessageId
+
+    val oldState: State = storeReferenceState(server, BOB)
+
+    markEmailAsNotSeen(messageId)
+
+    val request =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [[
+         |    "Mailbox/changes",
+         |    {
+         |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |      "sinceState": "${oldState.getValue}"
+         |    },
+         |    "c1"]]
+         |}""".stripMargin
+
+    val response = `given`
+        .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+        .body(request)
+      .when
+        .post
+      .`then`
+        .statusCode(SC_OK)
+        .contentType(JSON)
+        .extract
+        .body
+        .asString
+
+    assertThatJson(response)
+      .whenIgnoringPaths("methodResponses[0][1].newState")
+      .withOptions(new Options(IGNORING_ARRAY_ORDER))
+      .isEqualTo(
+        s"""{
+           |  "sessionState": "${SESSION_STATE.value}",
+           |  "methodResponses": [
+           |    ["Mailbox/changes", {
+           |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |      "oldState": "${oldState.getValue}",
+           |      "hasMoreChanges": false,
+           |      "updatedProperties": [],
+           |      "created": [],
+           |      "updated": ["$mailboxId"],
+           |      "destroyed": []
+           |    }, "c1"]
+           |  ]
+           |}""".stripMargin)
+  }
+
+  @Test
+  def mailboxChangesShouldReturnUpdatedChangesWhenDestroyEmail(server: GuiceJamesServer): Unit = {
+    val mailboxProbe: MailboxProbeImpl = server.getProbe(classOf[MailboxProbeImpl])
+
+    provisionSystemMailboxes(server)
+
+    val path = MailboxPath.forUser(BOB, "mailbox1")
+    val mailboxId: String = mailboxProbe
+      .createMailbox(path)
+      .serialize
+
+    val message: Message = Message.Builder
+      .of
+      .setSubject("test")
+      .setBody("testmail", StandardCharsets.UTF_8)
+      .build
+    val messageId: MessageId = mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message)).getMessageId
+
+    val oldState: State = storeReferenceState(server, BOB)
+
+    destroyEmail(messageId)
+
+    val request =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [[
+         |    "Mailbox/changes",
+         |    {
+         |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |      "sinceState": "${oldState.getValue}"
+         |    },
+         |    "c1"]]
+         |}""".stripMargin
+
+    val response = `given`
+        .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+        .body(request)
+      .when
+        .post
+      .`then`
+        .statusCode(SC_OK)
+        .contentType(JSON)
+        .extract
+        .body
+        .asString
+
+    assertThatJson(response)
+      .whenIgnoringPaths("methodResponses[0][1].newState")
+      .withOptions(new Options(IGNORING_ARRAY_ORDER))
+      .isEqualTo(
+        s"""{
+           |  "sessionState": "${SESSION_STATE.value}",
+           |  "methodResponses": [
+           |    ["Mailbox/changes", {
+           |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |      "oldState": "${oldState.getValue}",
+           |      "hasMoreChanges": false,
+           |      "updatedProperties": [],
+           |      "created": [],
+           |      "updated": ["$mailboxId"],
+           |      "destroyed": []
+           |    }, "c1"]
+           |  ]
+           |}""".stripMargin)
+  }
+
+  @Nested
+  class MailboxDelegationTest {
+    @Test
+    def mailboxChangesShouldReturnUpdatedChangesWhenAppendMessageToMailbox(server: GuiceJamesServer): Unit = {
+      val mailboxProbe: MailboxProbeImpl = server.getProbe(classOf[MailboxProbeImpl])
+
+      provisionSystemMailboxes(server)
+
+      val path = MailboxPath.forUser(BOB, "mailbox1")
+      val mailboxId: String = mailboxProbe
+        .createMailbox(path)
+        .serialize
+
+      server.getProbe(classOf[ACLProbeImpl])
+        .replaceRights(path, ANDRE.asString, new MailboxACL.Rfc4314Rights(Right.Lookup, Right.Read))
+
+      val oldState: State = storeReferenceState(server, ANDRE)
+
+      val message: Message = Message.Builder
+        .of
+        .setSubject("test")
+        .setBody("testmail", StandardCharsets.UTF_8)
+        .build
+      mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message))
+
+      val request =
+        s"""{
+           |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+           |  "methodCalls": [[
+           |    "Mailbox/changes",
+           |    {
+           |      "accountId": "$ANDRE_ACCOUNT_ID",
+           |      "sinceState": "${oldState.getValue}"
+           |    },
+           |    "c1"]]
+           |}""".stripMargin
+
+      val response = `given`(
+        baseRequestSpecBuilder(server)
+          .setAuth(authScheme(UserCredential(ANDRE, ANDRE_PASSWORD)))
+          .addHeader(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+          .setBody(request)
+          .build, new ResponseSpecBuilder().build)
+        .post
+        .`then`
+          .statusCode(SC_OK)
+          .contentType(JSON)
+          .extract
+          .body
+          .asString
+
+      assertThatJson(response)
+        .whenIgnoringPaths("methodResponses[0][1].newState")
+        .withOptions(new Options(IGNORING_ARRAY_ORDER))
+        .isEqualTo(
+          s"""{
+             |    "sessionState": "${SESSION_STATE.value}",
+             |    "methodResponses": [
+             |      [ "Mailbox/changes", {
+             |        "accountId": "$ANDRE_ACCOUNT_ID",
+             |        "oldState": "${oldState.getValue}",
+             |        "hasMoreChanges": false,
+             |        "updatedProperties": [],
+             |        "created": [],
+             |        "updated": ["$mailboxId"],
+             |        "destroyed": []
+             |      }, "c1"]
+             |    ]
+             |}""".stripMargin)
+    }
+
+    @Test
+    def mailboxChangesShouldReturnUpdatedChangesWhenRenameMailbox(server: GuiceJamesServer): Unit = {
+      val mailboxProbe: MailboxProbeImpl = server.getProbe(classOf[MailboxProbeImpl])
+
+      provisionSystemMailboxes(server)
+
+      val path = MailboxPath.forUser(BOB, "mailbox1")
+      val mailboxId: String = mailboxProbe
+        .createMailbox(path)
+        .serialize
+
+      server.getProbe(classOf[ACLProbeImpl])
+        .replaceRights(path, ANDRE.asString, new MailboxACL.Rfc4314Rights(Right.Lookup, Right.Read))
+
+      val oldState: State = storeReferenceState(server, ANDRE)
+
+      renameMailbox(mailboxId, "mailbox11")
+
+      val request =
+        s"""{
+           |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+           |  "methodCalls": [[
+           |    "Mailbox/changes",
+           |    {
+           |      "accountId": "$ANDRE_ACCOUNT_ID",
+           |      "sinceState": "${oldState.getValue}"
+           |    },
+           |    "c1"]]
+           |}""".stripMargin
+
+      val response = `given`(
+        baseRequestSpecBuilder(server)
+          .setAuth(authScheme(UserCredential(ANDRE, ANDRE_PASSWORD)))
+          .addHeader(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+          .setBody(request)
+          .build, new ResponseSpecBuilder().build)
+        .post
+        .`then`
+          .statusCode(SC_OK)
+          .contentType(JSON)
+          .extract
+          .body
+          .asString
+
+      assertThatJson(response)
+        .whenIgnoringPaths("methodResponses[0][1].newState")
+        .withOptions(new Options(IGNORING_ARRAY_ORDER))
+        .isEqualTo(
+          s"""{
+             |    "sessionState": "${SESSION_STATE.value}",
+             |    "methodResponses": [
+             |      [ "Mailbox/changes", {
+             |        "accountId": "$ANDRE_ACCOUNT_ID",
+             |        "oldState": "${oldState.getValue}",
+             |        "hasMoreChanges": false,
+             |        "updatedProperties": [],
+             |        "created": [],
+             |        "updated": ["$mailboxId"],
+             |        "destroyed": []
+             |      }, "c1"]
+             |    ]
+             |}""".stripMargin)
+    }
+
+    @Test
+    def mailboxChangesShouldReturnUpdatedChangesWhenAddSeenFlag(server: GuiceJamesServer): Unit = {
+      val mailboxProbe: MailboxProbeImpl = server.getProbe(classOf[MailboxProbeImpl])
+
+      provisionSystemMailboxes(server)
+
+      val path = MailboxPath.forUser(BOB, "mailbox1")
+      val mailboxId: String = mailboxProbe
+        .createMailbox(path)
+        .serialize
+
+      server.getProbe(classOf[ACLProbeImpl])
+        .replaceRights(path, ANDRE.asString, new MailboxACL.Rfc4314Rights(Right.Lookup, Right.Read))
+
+      val message: Message = Message.Builder
+        .of
+        .setSubject("test")
+        .setBody("testmail", StandardCharsets.UTF_8)
+        .build
+      val messageId: MessageId = mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message)).getMessageId
+
+      val oldState: State = storeReferenceState(server, ANDRE)
+
+      markEmailAsSeen(messageId)
+
+      val request =
+        s"""{
+           |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+           |  "methodCalls": [[
+           |    "Mailbox/changes",
+           |    {
+           |      "accountId": "$ANDRE_ACCOUNT_ID",
+           |      "sinceState": "${oldState.getValue}"
+           |    },
+           |    "c1"]]
+           |}""".stripMargin
+
+      val response = `given`(
+        baseRequestSpecBuilder(server)
+          .setAuth(authScheme(UserCredential(ANDRE, ANDRE_PASSWORD)))
+          .addHeader(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+          .setBody(request)
+          .build, new ResponseSpecBuilder().build)
+        .post
+        .`then`
+          .statusCode(SC_OK)
+          .contentType(JSON)
+          .extract
+          .body
+          .asString
+
+      assertThatJson(response)
+        .whenIgnoringPaths("methodResponses[0][1].newState")
+        .withOptions(new Options(IGNORING_ARRAY_ORDER))
+        .isEqualTo(
+          s"""{
+             |    "sessionState": "${SESSION_STATE.value}",
+             |    "methodResponses": [
+             |      [ "Mailbox/changes", {
+             |        "accountId": "$ANDRE_ACCOUNT_ID",
+             |        "oldState": "${oldState.getValue}",
+             |        "hasMoreChanges": false,
+             |        "updatedProperties": [],
+             |        "created": [],
+             |        "updated": ["$mailboxId"],
+             |        "destroyed": []
+             |      }, "c1"]
+             |    ]
+             |}""".stripMargin)
+    }
+
+    @Test
+    def mailboxChangesShouldReturnUpdatedChangesWhenRemoveSeenFlag(server: GuiceJamesServer): Unit = {
+      val mailboxProbe: MailboxProbeImpl = server.getProbe(classOf[MailboxProbeImpl])
+
+      provisionSystemMailboxes(server)
+
+      val path = MailboxPath.forUser(BOB, "mailbox1")
+      val mailboxId: String = mailboxProbe
+        .createMailbox(path)
+        .serialize
+
+      server.getProbe(classOf[ACLProbeImpl])
+        .replaceRights(path, ANDRE.asString, new MailboxACL.Rfc4314Rights(Right.Lookup, Right.Read))
+
+      val messageId: MessageId = mailboxProbe.appendMessage(BOB.asString(), path,
+        AppendCommand.builder()
+          .withFlags(new Flags(Flags.Flag.SEEN))
+          .build("header: value\r\n\r\nbody"))
+        .getMessageId
+
+      val oldState: State = storeReferenceState(server, ANDRE)
+
+      markEmailAsNotSeen(messageId)
+
+      val request =
+        s"""{
+           |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+           |  "methodCalls": [[
+           |    "Mailbox/changes",
+           |    {
+           |      "accountId": "$ANDRE_ACCOUNT_ID",
+           |      "sinceState": "${oldState.getValue}"
+           |    },
+           |    "c1"]]
+           |}""".stripMargin
+
+      val response = `given`(
+        baseRequestSpecBuilder(server)
+          .setAuth(authScheme(UserCredential(ANDRE, ANDRE_PASSWORD)))
+          .addHeader(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+          .setBody(request)
+          .build, new ResponseSpecBuilder().build)
+        .post
+        .`then`
+          .statusCode(SC_OK)
+          .contentType(JSON)
+          .extract
+          .body
+          .asString
+
+      assertThatJson(response)
+        .whenIgnoringPaths("methodResponses[0][1].newState")
+        .withOptions(new Options(IGNORING_ARRAY_ORDER))
+        .isEqualTo(
+          s"""{
+             |    "sessionState": "${SESSION_STATE.value}",
+             |    "methodResponses": [
+             |      [ "Mailbox/changes", {
+             |        "accountId": "$ANDRE_ACCOUNT_ID",
+             |        "oldState": "${oldState.getValue}",
+             |        "hasMoreChanges": false,
+             |        "updatedProperties": [],
+             |        "created": [],
+             |        "updated": ["$mailboxId"],
+             |        "destroyed": []
+             |      }, "c1"]
+             |    ]
+             |}""".stripMargin)
+    }
+
+    @Test
+    def mailboxChangesShouldReturnUpdatedChangesWhenDestroyEmail(server: GuiceJamesServer): Unit = {
+      val mailboxProbe: MailboxProbeImpl = server.getProbe(classOf[MailboxProbeImpl])
+
+      provisionSystemMailboxes(server)
+
+      val path = MailboxPath.forUser(BOB, "mailbox1")
+      val mailboxId: String = mailboxProbe
+        .createMailbox(path)
+        .serialize
+
+      server.getProbe(classOf[ACLProbeImpl])
+        .replaceRights(path, ANDRE.asString, new MailboxACL.Rfc4314Rights(Right.Lookup, Right.Read))
+
+      val message: Message = Message.Builder
+        .of
+        .setSubject("test")
+        .setBody("testmail", StandardCharsets.UTF_8)
+        .build
+      val messageId: MessageId = mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message)).getMessageId
+
+      val oldState: State = storeReferenceState(server, ANDRE)
+
+      destroyEmail(messageId)
+
+      val request =
+        s"""{
+           |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+           |  "methodCalls": [[
+           |    "Mailbox/changes",
+           |    {
+           |      "accountId": "$ANDRE_ACCOUNT_ID",
+           |      "sinceState": "${oldState.getValue}"
+           |    },
+           |    "c1"]]
+           |}""".stripMargin
+
+      val response = `given`(
+        baseRequestSpecBuilder(server)
+          .setAuth(authScheme(UserCredential(ANDRE, ANDRE_PASSWORD)))
+          .addHeader(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+          .setBody(request)
+          .build, new ResponseSpecBuilder().build)
+        .post
+        .`then`
+          .statusCode(SC_OK)
+          .contentType(JSON)
+          .extract
+          .body
+          .asString
+
+      assertThatJson(response)
+        .whenIgnoringPaths("methodResponses[0][1].newState")
+        .withOptions(new Options(IGNORING_ARRAY_ORDER))
+        .isEqualTo(
+          s"""{
+             |    "sessionState": "${SESSION_STATE.value}",
+             |    "methodResponses": [
+             |      [ "Mailbox/changes", {
+             |        "accountId": "$ANDRE_ACCOUNT_ID",
+             |        "oldState": "${oldState.getValue}",
+             |        "hasMoreChanges": false,
+             |        "updatedProperties": [],
+             |        "created": [],
+             |        "updated": ["$mailboxId"],
+             |        "destroyed": []
+             |      }, "c1"]
+             |    ]
+             |}""".stripMargin)
+    }
+
+    @Test
+    @Disabled("Not implemented yet")
+    def mailboxChangesShouldReturnUpdatedChangesWhenDestroyDelegatedMailbox(server: GuiceJamesServer): Unit = {
+      val mailboxProbe: MailboxProbeImpl = server.getProbe(classOf[MailboxProbeImpl])
+
+      provisionSystemMailboxes(server)
+
+      val path = MailboxPath.forUser(BOB, "mailbox1")
+      val mailboxId: String = mailboxProbe
+        .createMailbox(path)
+        .serialize
+
+      server.getProbe(classOf[ACLProbeImpl])
+        .replaceRights(path, ANDRE.asString, new MailboxACL.Rfc4314Rights(Right.Lookup, Right.Read))
+
+      val oldState: State = storeReferenceState(server, ANDRE)
+
+      destroyMailbox(mailboxId)
+
+      val request =
+        s"""{
+           |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+           |  "methodCalls": [[
+           |    "Mailbox/changes",
+           |    {
+           |      "accountId": "$ANDRE_ACCOUNT_ID",
+           |      "sinceState": "${oldState.getValue}"
+           |    },
+           |    "c1"]]
+           |}""".stripMargin
+
+      val response = `given`(
+        baseRequestSpecBuilder(server)
+          .setAuth(authScheme(UserCredential(ANDRE, ANDRE_PASSWORD)))
+          .addHeader(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+          .setBody(request)
+          .build, new ResponseSpecBuilder().build)
+        .post
+        .`then`
+          .statusCode(SC_OK)
+          .contentType(JSON)
+          .extract
+          .body
+          .asString
+
+      assertThatJson(response)
+        .whenIgnoringPaths("methodResponses[0][1].newState")
+        .withOptions(new Options(IGNORING_ARRAY_ORDER))
+        .isEqualTo(
+          s"""{
+             |    "sessionState": "${SESSION_STATE.value}",
+             |    "methodResponses": [
+             |      [ "Mailbox/changes", {
+             |        "accountId": "$ANDRE_ACCOUNT_ID",
+             |        "oldState": "${oldState.getValue}",
+             |        "hasMoreChanges": false,
+             |        "updatedProperties": [],
+             |        "created": [],
+             |        "updated": [],
+             |        "destroyed": ["$mailboxId"]
+             |      }, "c1"]
+             |    ]
+             |}""".stripMargin)
+    }
+  }
+
+  @Test
   def mailboxChangesShouldReturnDestroyedChanges(server: GuiceJamesServer): Unit = {
     val mailboxProbe: MailboxProbeImpl = server.getProbe(classOf[MailboxProbeImpl])
 
@@ -341,7 +886,7 @@ trait MailboxChangesMethodContract {
       .createMailbox(path)
       .serialize
 
-    val oldState: State = storeReferenceState(server)
+    val oldState: State = storeReferenceState(server, BOB)
 
     mailboxProbe
       .deleteMailbox(path.getNamespace, BOB.asString(), path.getName)
@@ -396,7 +941,7 @@ trait MailboxChangesMethodContract {
 
     provisionSystemMailboxes(server)
 
-    val oldState: State = storeReferenceState(server)
+    val oldState: State = storeReferenceState(server, BOB)
 
     val path1 = MailboxPath.forUser(BOB, "mailbox1")
     val mailboxId1: String = mailboxProbe
@@ -468,7 +1013,7 @@ trait MailboxChangesMethodContract {
     val mailboxProbe: MailboxProbeImpl = server.getProbe(classOf[MailboxProbeImpl])
     provisionSystemMailboxes(server)
 
-    val oldState: State = storeReferenceState(server)
+    val oldState: State = storeReferenceState(server, BOB)
 
     val mailboxId1: String = mailboxProbe
       .createMailbox(MailboxPath.forUser(BOB, "mailbox1"))
@@ -540,7 +1085,7 @@ trait MailboxChangesMethodContract {
 
   @Test
   def mailboxChangesShouldFailWhenAccountIdNotFound(server: GuiceJamesServer): Unit = {
-    val oldState: State = storeReferenceState(server)
+    val oldState: State = storeReferenceState(server, BOB)
 
     val request =
       s"""{
@@ -627,7 +1172,7 @@ trait MailboxChangesMethodContract {
   def mailboxChangesShouldReturnNoChangesWhenNoNewerState(server: GuiceJamesServer): Unit = {
     provisionSystemMailboxes(server)
 
-    val oldState: State = storeReferenceState(server)
+    val oldState: State = storeReferenceState(server, BOB)
 
     val request =
       s"""{
@@ -678,7 +1223,7 @@ trait MailboxChangesMethodContract {
     val mailboxProbe: MailboxProbeImpl = server.getProbe(classOf[MailboxProbeImpl])
     provisionSystemMailboxes(server)
 
-    val oldState: State = storeReferenceState(server)
+    val oldState: State = storeReferenceState(server, BOB)
     mailboxProbe.createMailbox(MailboxPath.forUser(BOB, "mailbox1"))
     mailboxProbe.createMailbox(MailboxPath.forUser(BOB, "mailbox2"))
 
@@ -720,7 +1265,7 @@ trait MailboxChangesMethodContract {
     val mailboxProbe: MailboxProbeImpl = server.getProbe(classOf[MailboxProbeImpl])
     provisionSystemMailboxes(server)
 
-    val oldState: State = storeReferenceState(server)
+    val oldState: State = storeReferenceState(server, BOB)
     mailboxProbe.createMailbox(MailboxPath.forUser(BOB, "mailbox1"))
     mailboxProbe.createMailbox(MailboxPath.forUser(BOB, "mailbox2"))
 
@@ -827,10 +1372,111 @@ trait MailboxChangesMethodContract {
       .contentType(JSON)
   }
 
-  private def storeReferenceState(server: GuiceJamesServer): State = {
+  private def destroyMailbox(mailboxId: String): Unit = {
+    val request =
+      s"""
+         |{
+         |  "using": [ "urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail" ],
+         |  "methodCalls": [[
+         |    "Mailbox/set", {
+         |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |      "destroy": ["$mailboxId"]
+         |    }, "c1"]
+         |  ]
+         |}
+         |""".stripMargin
+
+    `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .log().ifValidationFails()
+      .statusCode(SC_OK)
+      .contentType(JSON)
+  }
+
+  private def markEmailAsSeen(messageId: MessageId): Unit = {
+    val request = String.format(
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [
+         |    ["Email/set", {
+         |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |      "update": {
+         |        "${messageId.serialize}": {
+         |          "keywords": {
+         |             "$$seen": true
+         |          }
+         |        }
+         |      }
+         |    }, "c1"]]
+         |}""".stripMargin)
+
+    `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .log().ifValidationFails()
+      .statusCode(SC_OK)
+      .contentType(JSON)
+  }
+
+  private def markEmailAsNotSeen(messageId: MessageId): Unit = {
+    val request = String.format(
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [
+         |    ["Email/set", {
+         |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |      "update": {
+         |        "${messageId.serialize}": {
+         |          "keywords/$$seen": null
+         |        }
+         |      }
+         |    }, "c1"]]
+         |}""".stripMargin)
+
+    `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .log().ifValidationFails()
+      .statusCode(SC_OK)
+      .contentType(JSON)
+  }
+
+  private def destroyEmail(messageId: MessageId): Unit = {
+    val request = String.format(
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [
+         |    ["Email/set", {
+         |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |      "destroy": ["${messageId.serialize}"]
+         |    }, "c1"]]
+         |}""".stripMargin)
+
+    `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .log().ifValidationFails()
+      .statusCode(SC_OK)
+      .contentType(JSON)
+  }
+
+  private def storeReferenceState(server: GuiceJamesServer, username: Username): State = {
     val state: State = State.of(UUID.randomUUID())
     val jmapGuiceProbe: JmapGuiceProbe = server.getProbe(classOf[JmapGuiceProbe])
-    jmapGuiceProbe.saveMailboxChange(MailboxChange.of(AccountId.fromUsername(BOB), state, ZonedDateTime.now(), List().asJava, List(TestId.of(0)).asJava, List().asJava))
+    jmapGuiceProbe.saveMailboxChange(MailboxChange.updated(AccountId.fromUsername(username), state, ZonedDateTime.now(), List(TestId.of(0)).asJava).build)
 
     state
   }
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/change/MailboxChangeListener.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/change/MailboxChangeListener.scala
index c251fb5..b577eb8 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/change/MailboxChangeListener.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/change/MailboxChangeListener.scala
@@ -21,21 +21,29 @@ package org.apache.james.jmap.change
 
 import javax.inject.Inject
 import org.apache.james.jmap.api.change.{MailboxChange, MailboxChangeRepository}
-import org.apache.james.mailbox.events.MailboxListener.ReactiveGroupMailboxListener
+import org.apache.james.mailbox.MailboxManager
+import org.apache.james.mailbox.events.MailboxListener.{MailboxEvent, ReactiveGroupMailboxListener}
 import org.apache.james.mailbox.events.{Event, Group}
 import org.reactivestreams.Publisher
-import reactor.core.scala.publisher.SMono
+import reactor.core.scala.publisher.{SFlux, SMono}
+
+import scala.jdk.CollectionConverters._
 
 case class MailboxChangeListenerGroup() extends Group {}
 
-case class MailboxChangeListener @Inject() (mailboxChangeRepository: MailboxChangeRepository) extends ReactiveGroupMailboxListener {
+case class MailboxChangeListener @Inject() (mailboxChangeRepository: MailboxChangeRepository,
+                                            mailboxManager: MailboxManager) extends ReactiveGroupMailboxListener {
 
-  override def reactiveEvent(event: Event): Publisher[Void] =
-    MailboxChange.fromEvent(event)
-      .map(mailboxChangeRepository.save(_))
+  override def reactiveEvent(event: Event): Publisher[Void] = {
+    MailboxChange.fromEvent(event, mailboxManager)
+      .map(changes => SFlux.fromIterable(changes.asScala)
+        .map(change => mailboxChangeRepository.save(change))
+        .`then`()
+        .`then`(SMono.empty[Void]).asJava)
       .orElse(SMono.empty[Void].asJava)
+  }
 
   override def getDefaultGroup: Group = MailboxChangeListenerGroup()
 
-  override def isHandling(event: Event): Boolean = MailboxChange.fromEvent(event).isPresent
+  override def isHandling(event: Event): Boolean = event.isInstanceOf[MailboxEvent]
 }
diff --git a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/change/MailboxChangeListenerTest.scala b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/change/MailboxChangeListenerTest.scala
index 4c7acb8..aecbfc7 100644
--- a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/change/MailboxChangeListenerTest.scala
+++ b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/change/MailboxChangeListenerTest.scala
@@ -63,14 +63,14 @@ class MailboxChangeListenerTest {
 
     mailboxManager = resources.getMailboxManager
     repository = new MemoryMailboxChangeRepository()
-    listener = MailboxChangeListener(repository)
+    listener = MailboxChangeListener(repository, mailboxManager)
     resources.getEventBus.register(listener)
   }
 
   @Test
   def createMailboxShouldStoreCreatedEvent(): Unit = {
     val state = State.of(UUID.randomUUID)
-    repository.save(MailboxChange.of(ACCOUNT_ID, state, ZonedDateTime.now, List[MailboxId](TestId.of(0)).asJava, List().asJava, List().asJava)).block()
+    repository.save(MailboxChange.created(ACCOUNT_ID, state, ZonedDateTime.now, List[MailboxId](TestId.of(0)).asJava).build).block()
 
     val mailboxSession = MailboxSessionUtil.create(BOB)
     val inboxId: MailboxId = mailboxManager.createMailbox(MailboxPath.inbox(BOB), mailboxSession).get
@@ -87,7 +87,7 @@ class MailboxChangeListenerTest {
     val inboxId: MailboxId = mailboxManager.createMailbox(path, mailboxSession).get
 
     val state = State.of(UUID.randomUUID)
-    repository.save(MailboxChange.of(ACCOUNT_ID, state, ZonedDateTime.now, List[MailboxId](TestId.of(0)).asJava, List().asJava, List().asJava)).block()
+    repository.save(MailboxChange.created(ACCOUNT_ID, state, ZonedDateTime.now, List[MailboxId](TestId.of(0)).asJava).build).block()
 
     mailboxManager.renameMailbox(path, newPath, mailboxSession)
 
@@ -98,13 +98,13 @@ class MailboxChangeListenerTest {
   @Test
   def updateMailboxACLShouldStoreUpdatedEvent(): Unit = {
     val state = State.of(UUID.randomUUID)
-    repository.save(MailboxChange.of(ACCOUNT_ID, state, ZonedDateTime.now, List[MailboxId](TestId.of(0)).asJava, List().asJava, List().asJava)).block()
+    repository.save(MailboxChange.created(ACCOUNT_ID, state, ZonedDateTime.now, List[MailboxId](TestId.of(0)).asJava).build).block()
 
     val mailboxSession = MailboxSessionUtil.create(BOB)
     val path = MailboxPath.inbox(BOB)
     val inboxId: MailboxId = mailboxManager.createMailbox(MailboxPath.inbox(BOB), mailboxSession).get
 
-    mailboxManager.applyRightsCommand(path, MailboxACL.command().forUser(ALICE).rights(MailboxACL.Right.Read).asAddition(), mailboxSession);
+    mailboxManager.applyRightsCommand(path, MailboxACL.command().forUser(ALICE).rights(MailboxACL.Right.Read).asAddition(), mailboxSession)
 
     assertThat(repository.getSinceState(ACCOUNT_ID, state, None.toJava).block().getUpdated)
       .containsExactly(inboxId)
@@ -117,7 +117,9 @@ class MailboxChangeListenerTest {
     val inboxId: MailboxId = mailboxManager.createMailbox(path, mailboxSession).get
 
     val state = State.of(UUID.randomUUID)
-    repository.save(MailboxChange.of(ACCOUNT_ID, state, ZonedDateTime.now, List[MailboxId](TestId.of(0)).asJava, List().asJava, List().asJava)).block()
+    repository.save(MailboxChange.created(ACCOUNT_ID, state, ZonedDateTime.now, List[MailboxId](TestId.of(0)).asJava).build).block()
+
+    mailboxManager.applyRightsCommand(path, MailboxACL.command().forUser(ALICE).rights(MailboxACL.Right.Read).asAddition(), mailboxSession)
 
     mailboxManager
       .getMailbox(inboxId, mailboxSession)
@@ -136,7 +138,7 @@ class MailboxChangeListenerTest {
     messageManager.appendMessage(AppendCommand.builder().build("header: value\r\n\r\nbody"), mailboxSession)
 
     val state = State.of(UUID.randomUUID)
-    repository.save(MailboxChange.of(ACCOUNT_ID, state, ZonedDateTime.now, List[MailboxId](TestId.of(0)).asJava, List().asJava, List().asJava)).block()
+    repository.save(MailboxChange.created(ACCOUNT_ID, state, ZonedDateTime.now, List[MailboxId](TestId.of(0)).asJava).build).block()
 
     messageManager.setFlags(new Flags(Flags.Flag.SEEN), FlagsUpdateMode.ADD, MessageRange.all(), mailboxSession)
 
@@ -155,7 +157,7 @@ class MailboxChangeListenerTest {
       .build("header: value\r\n\r\nbody"), mailboxSession)
 
     val state = State.of(UUID.randomUUID)
-    repository.save(MailboxChange.of(ACCOUNT_ID, state, ZonedDateTime.now, List[MailboxId](TestId.of(0)).asJava, List().asJava, List().asJava)).block()
+    repository.save(MailboxChange.created(ACCOUNT_ID, state, ZonedDateTime.now, List[MailboxId](TestId.of(0)).asJava).build).block()
 
     messageManager.setFlags(new Flags(Flags.Flag.SEEN), FlagsUpdateMode.REMOVE, MessageRange.all(), mailboxSession)
 
@@ -172,7 +174,7 @@ class MailboxChangeListenerTest {
     messageManager.appendMessage(AppendCommand.builder().build("header: value\r\n\r\nbody"), mailboxSession)
 
     val state = State.of(UUID.randomUUID)
-    repository.save(MailboxChange.of(ACCOUNT_ID, state, ZonedDateTime.now, List[MailboxId](TestId.of(0)).asJava, List().asJava, List().asJava)).block()
+    repository.save(MailboxChange.created(ACCOUNT_ID, state, ZonedDateTime.now, List[MailboxId](TestId.of(0)).asJava).build).block()
 
     messageManager.setFlags(new Flags(Flags.Flag.ANSWERED), FlagsUpdateMode.ADD, MessageRange.all(), mailboxSession)
 
@@ -191,7 +193,7 @@ class MailboxChangeListenerTest {
       .build("header: value\r\n\r\nbody"), mailboxSession)
 
     val state = State.of(UUID.randomUUID)
-    repository.save(MailboxChange.of(ACCOUNT_ID, state, ZonedDateTime.now, List[MailboxId](TestId.of(0)).asJava, List().asJava, List().asJava)).block()
+    repository.save(MailboxChange.created(ACCOUNT_ID, state, ZonedDateTime.now, List[MailboxId](TestId.of(0)).asJava).build).block()
 
     messageManager.setFlags(new Flags(Flags.Flag.DELETED), FlagsUpdateMode.REPLACE, MessageRange.all(), mailboxSession)
 
@@ -208,7 +210,7 @@ class MailboxChangeListenerTest {
     val appendResult: AppendResult = messageManager.appendMessage(AppendCommand.builder().build("header: value\r\n\r\nbody"), mailboxSession)
 
     val state = State.of(UUID.randomUUID)
-    repository.save(MailboxChange.of(ACCOUNT_ID, state, ZonedDateTime.now, List[MailboxId](TestId.of(0)).asJava, List().asJava, List().asJava)).block()
+    repository.save(MailboxChange.created(ACCOUNT_ID, state, ZonedDateTime.now, List[MailboxId](TestId.of(0)).asJava).build).block()
     messageManager.delete(List(appendResult.getId.getUid).asJava, mailboxSession)
 
     assertThat(repository.getSinceState(ACCOUNT_ID, state, None.toJava).block().getUpdated)
@@ -222,7 +224,7 @@ class MailboxChangeListenerTest {
     val inboxId: MailboxId = mailboxManager.createMailbox(path, mailboxSession).get
 
     val state = State.of(UUID.randomUUID)
-    repository.save(MailboxChange.of(ACCOUNT_ID, state, ZonedDateTime.now, List[MailboxId](TestId.of(0)).asJava, List().asJava, List().asJava)).block()
+    repository.save(MailboxChange.created(ACCOUNT_ID, state, ZonedDateTime.now, List[MailboxId](TestId.of(0)).asJava).build).block()
 
     mailboxManager.deleteMailbox(inboxId, mailboxSession)
 


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


[james-project] 01/17: JAMES-3431 [REFACTORING] Rely on Bouncer DELIVERY_ERROR attribute name

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 bb3e426e767cfcc1e2da8cf0a645f5f6e2e5f1bf
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Wed Dec 16 16:40:55 2020 +0700

    JAMES-3431 [REFACTORING] Rely on Bouncer DELIVERY_ERROR attribute name
    
    This avoids divergences
---
 .../src/main/java/org/apache/james/transport/mailets/DSNBounce.java   | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/DSNBounce.java b/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/DSNBounce.java
index 11b4a06..211eb67 100755
--- a/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/DSNBounce.java
+++ b/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/DSNBounce.java
@@ -19,6 +19,8 @@
 
 package org.apache.james.transport.mailets;
 
+import static org.apache.james.transport.mailets.remote.delivery.Bouncer.DELIVERY_ERROR;
+
 import java.net.InetAddress;
 import java.net.UnknownHostException;
 import java.time.LocalDateTime;
@@ -59,7 +61,6 @@ import org.apache.james.transport.util.ReplyToUtils;
 import org.apache.james.transport.util.SenderUtils;
 import org.apache.james.transport.util.SpecialAddressesUtils;
 import org.apache.james.transport.util.TosUtils;
-import org.apache.mailet.AttributeName;
 import org.apache.mailet.AttributeUtils;
 import org.apache.mailet.Mail;
 import org.apache.mailet.base.DateFormats;
@@ -122,7 +123,6 @@ public class DSNBounce extends GenericMailet implements RedirectNotify {
     private static final Pattern DIAG_PATTERN = Patterns.compilePatternUncheckedException("^\\d{3}\\s.*$");
     private static final String MACHINE_PATTERN = "[machine]";
     private static final String LINE_BREAK = "\n";
-    private static final AttributeName DELIVERY_ERROR = AttributeName.of("delivery-error");
 
     private final DNSService dns;
     private final DateTimeFormatter dateFormatter;


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