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

[james-project] 08/08: MAILBOX-381 DeletedMessage <-> Mail conversion

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 945b91e3d891174dabac262104371a44e6114e1b
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Mon Feb 25 16:29:11 2019 +0700

    MAILBOX-381 DeletedMessage <-> Mail conversion
---
 .../java/org/apache/mailet/AttributeValue.java     |   4 +-
 .../java/org/apache/james/vault/MailConverter.java | 159 ++++++++++++++
 .../org/apache/james/vault/SerializableDate.java   |  63 ++++++
 .../org/apache/james/vault/SerializableUser.java   |  62 ++++++
 .../org/apache/james/vault/MailConverterTest.java  | 237 +++++++++++++++++++++
 .../apache/james/vault/SerializableDateTest.java   |  52 +++++
 .../apache/james/vault/SerializableUserTest.java   |  52 +++++
 7 files changed, 626 insertions(+), 3 deletions(-)

diff --git a/mailet/api/src/main/java/org/apache/mailet/AttributeValue.java b/mailet/api/src/main/java/org/apache/mailet/AttributeValue.java
index 79c097a..dd496f6 100644
--- a/mailet/api/src/main/java/org/apache/mailet/AttributeValue.java
+++ b/mailet/api/src/main/java/org/apache/mailet/AttributeValue.java
@@ -35,7 +35,6 @@ import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.node.JsonNodeFactory;
 import com.fasterxml.jackson.databind.node.ObjectNode;
-import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Preconditions;
 
@@ -174,8 +173,7 @@ public class AttributeValue<T> {
         }
     }
 
-    @VisibleForTesting
-    static AttributeValue<?> fromJson(JsonNode input) {
+    public static AttributeValue<?> fromJson(JsonNode input) {
         return Optional.ofNullable(input)
                 .filter(ObjectNode.class::isInstance)
                 .map(ObjectNode.class::cast)
diff --git a/server/mailrepository/deleted-messages-vault-repository/src/main/java/org/apache/james/vault/MailConverter.java b/server/mailrepository/deleted-messages-vault-repository/src/main/java/org/apache/james/vault/MailConverter.java
new file mode 100644
index 0000000..1f1a68b
--- /dev/null
+++ b/server/mailrepository/deleted-messages-vault-repository/src/main/java/org/apache/james/vault/MailConverter.java
@@ -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.vault;
+
+import java.io.InputStream;
+import java.time.ZonedDateTime;
+import java.util.List;
+import java.util.Optional;
+import java.util.Properties;
+
+import javax.mail.MessagingException;
+import javax.mail.Session;
+import javax.mail.internet.MimeMessage;
+
+import org.apache.james.core.User;
+import org.apache.james.mailbox.model.MailboxId;
+import org.apache.james.mailbox.model.MessageId;
+import org.apache.james.server.core.MailImpl;
+import org.apache.mailet.AttributeName;
+import org.apache.mailet.AttributeUtils;
+import org.apache.mailet.AttributeValue;
+import org.apache.mailet.Mail;
+
+import com.github.steveash.guavate.Guavate;
+import com.google.common.collect.ImmutableList;
+
+class MailConverter {
+    static final AttributeName ORIGIN_MAILBOXES_ATTRIBUTE_NAME = AttributeName.of("originMailboxes");
+    static final AttributeName HAS_ATTACHMENT_ATTRIBUTE_NAME = AttributeName.of("hasAttachment");
+    static final AttributeName OWNER_ATTRIBUTE_NAME = AttributeName.of("owner");
+    static final AttributeName DELIVERY_DATE_ATTRIBUTE_NAME = AttributeName.of("deliveryDate");
+    static final AttributeName DELETION_DATE_ATTRIBUTE_NAME = AttributeName.of("deletionDate");
+    static final AttributeName SUBJECT_ATTRIBUTE_VALUE = AttributeName.of("subject");
+
+    private final MailboxId.Factory mailboxIdFactory;
+    private final MessageId.Factory messageIdFactory;
+
+    MailConverter(MailboxId.Factory mailboxIdFactory, MessageId.Factory messageIdFactory) {
+        this.mailboxIdFactory = mailboxIdFactory;
+        this.messageIdFactory = messageIdFactory;
+    }
+
+    MailImpl toMail(DeletedMessage deletedMessage, InputStream inputStream) throws MessagingException {
+        return MailImpl.builder()
+            .name(deletedMessage.getMessageId().serialize())
+            .sender(deletedMessage.getSender())
+            .addRecipients(deletedMessage.getRecipients())
+            .mimeMessage(new MimeMessage(Session.getDefaultInstance(new Properties()), inputStream))
+            .addAttribute(OWNER_ATTRIBUTE_NAME.withValue(SerializableUser.toAttributeValue(deletedMessage.getOwner())))
+            .addAttribute(HAS_ATTACHMENT_ATTRIBUTE_NAME.withValue(AttributeValue.of(deletedMessage.hasAttachment())))
+            .addAttribute(DELIVERY_DATE_ATTRIBUTE_NAME.withValue(SerializableDate.toAttributeValue(deletedMessage.getDeliveryDate())))
+            .addAttribute(DELETION_DATE_ATTRIBUTE_NAME.withValue(SerializableDate.toAttributeValue(deletedMessage.getDeletionDate())))
+            .addAttribute(ORIGIN_MAILBOXES_ATTRIBUTE_NAME.withValue(AttributeValue.of(serializedMailboxIds(deletedMessage))))
+            .addAttribute(SUBJECT_ATTRIBUTE_VALUE.withValue(subjectAttributeValue(deletedMessage)))
+            .build();
+    }
+
+    DeletedMessage fromMail(Mail mail) {
+        return DeletedMessage.builder()
+            .messageId(messageIdFactory.fromString(mail.getName()))
+            .originMailboxes(retrieveMailboxIds(mail))
+            .user(retrieveOwner(mail))
+            .deliveryDate(retrieveDate(mail, DELIVERY_DATE_ATTRIBUTE_NAME))
+            .deletionDate(retrieveDate(mail, DELETION_DATE_ATTRIBUTE_NAME))
+            .sender(mail.getMaybeSender())
+            .recipients(mail.getRecipients())
+            .hasAttachment(retrieveHasAttachment(mail))
+            .subject(retrieveSubject(mail))
+            .build();
+    }
+
+    private ImmutableList<AttributeValue<?>> serializedMailboxIds(DeletedMessage deletedMessage) {
+        return deletedMessage.getOriginMailboxes().stream()
+            .map(MailboxId::serialize)
+            .map(AttributeValue::of)
+            .collect(Guavate.toImmutableList());
+    }
+
+    private AttributeValue<Optional<AttributeValue<String>>> subjectAttributeValue(DeletedMessage deletedMessage) {
+        return AttributeValue.of(deletedMessage.getSubject().map(AttributeValue::of));
+    }
+
+    private Optional<String> retrieveSubject(Mail mail) {
+        return AttributeUtils.getValueAndCastFromMail(mail, SUBJECT_ATTRIBUTE_VALUE, Optional.class)
+                .map(this::retrieveSubject)
+                .orElseThrow(() -> new IllegalArgumentException("mail should have a 'subject' attribute being of type 'Optional<String>"));
+    }
+
+    private Optional<String> retrieveSubject(Optional<?> maybeSubject) {
+        return maybeSubject.map(this::castSubjectToString);
+    }
+
+    private String castSubjectToString(Object object) {
+        Optional<String> optional = Optional.of(object)
+            .filter(obj -> obj instanceof AttributeValue)
+            .map(AttributeValue.class::cast)
+            .flatMap(attributeValue -> attributeValue.valueAs(String.class));
+
+        return optional
+            .orElseThrow(() -> new IllegalArgumentException("mail should have a 'subject' attribute being of type 'Optional<String>"));
+    }
+
+    private boolean retrieveHasAttachment(Mail mail) {
+        return AttributeUtils.getValueAndCastFromMail(mail, HAS_ATTACHMENT_ATTRIBUTE_NAME, Boolean.class)
+            .orElseThrow(() -> new IllegalArgumentException("mail should have a 'hasAttachment' attribute of type boolean"));
+    }
+
+    private ZonedDateTime retrieveDate(Mail mail, AttributeName attributeName) {
+        return AttributeUtils.getValueAndCastFromMail(mail, attributeName, SerializableDate.class)
+            .map(SerializableDate::getValue)
+            .orElseThrow(() -> new IllegalArgumentException("'mail' should have a '" + attributeName.asString() + "' attribute of type SerializableDate"));
+    }
+
+    private User retrieveOwner(Mail mail) {
+        return AttributeUtils.getValueAndCastFromMail(mail, OWNER_ATTRIBUTE_NAME, SerializableUser.class)
+            .map(SerializableUser::getValue)
+            .orElseThrow(() -> new IllegalArgumentException("Supplied email is missing the 'owner' attribute of type SerializableUser"));
+    }
+
+    private List<MailboxId> retrieveMailboxIds(Mail mail) {
+        return AttributeUtils.getValueAndCastFromMail(mail, ORIGIN_MAILBOXES_ATTRIBUTE_NAME, List.class)
+            .map(this::retrieveMailboxIds)
+            .orElseThrow(() -> new IllegalArgumentException("Supplied email is missing the 'originMailboxes' attribute of type List<String>"));
+    }
+
+    private List<MailboxId> retrieveMailboxIds(List<?> list) {
+        return list.stream()
+            .map(this::retrieveMailboxId)
+            .collect(Guavate.toImmutableList());
+    }
+
+    private MailboxId retrieveMailboxId(Object object) {
+        Optional<String> serializedMailboxId = Optional.of(object)
+            .filter(obj -> obj instanceof AttributeValue)
+            .map(AttributeValue.class::cast)
+            .flatMap(attributeValue -> attributeValue.valueAs(String.class));
+
+        return serializedMailboxId
+            .map(mailboxIdFactory::fromString)
+            .orElseThrow(() -> new IllegalArgumentException("Found a non String element in originMailboxes attribute"));
+    }
+}
diff --git a/server/mailrepository/deleted-messages-vault-repository/src/main/java/org/apache/james/vault/SerializableDate.java b/server/mailrepository/deleted-messages-vault-repository/src/main/java/org/apache/james/vault/SerializableDate.java
new file mode 100644
index 0000000..f804667
--- /dev/null
+++ b/server/mailrepository/deleted-messages-vault-repository/src/main/java/org/apache/james/vault/SerializableDate.java
@@ -0,0 +1,63 @@
+/****************************************************************
+ * 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.vault;
+
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Optional;
+
+import org.apache.mailet.ArbitrarySerializable;
+import org.apache.mailet.AttributeValue;
+
+import com.google.common.base.Preconditions;
+
+public class SerializableDate implements ArbitrarySerializable<SerializableDate> {
+    public static class Factory implements ArbitrarySerializable.Deserializer<SerializableDate> {
+        @Override
+        public Optional<SerializableDate> deserialize(Serializable<SerializableDate> serializable) {
+            return Optional.of(serializable.getValue().value())
+                .filter(String.class::isInstance)
+                .map(String.class::cast)
+                .map(ZonedDateTime::parse)
+                .map(SerializableDate::new);
+        }
+    }
+
+    static AttributeValue<SerializableDate> toAttributeValue(ZonedDateTime date) {
+        return AttributeValue.of(new SerializableDate(date));
+    }
+
+    private final ZonedDateTime value;
+
+    SerializableDate(ZonedDateTime value) {
+        Preconditions.checkNotNull(value);
+
+        this.value = value;
+    }
+
+    @Override
+    public Serializable<SerializableDate> serialize() {
+        return new Serializable<>(AttributeValue.of(value.format(DateTimeFormatter.ISO_ZONED_DATE_TIME)), SerializableDate.Factory.class);
+    }
+
+    public ZonedDateTime getValue() {
+        return value;
+    }
+}
diff --git a/server/mailrepository/deleted-messages-vault-repository/src/main/java/org/apache/james/vault/SerializableUser.java b/server/mailrepository/deleted-messages-vault-repository/src/main/java/org/apache/james/vault/SerializableUser.java
new file mode 100644
index 0000000..6327bba
--- /dev/null
+++ b/server/mailrepository/deleted-messages-vault-repository/src/main/java/org/apache/james/vault/SerializableUser.java
@@ -0,0 +1,62 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.vault;
+
+import java.util.Optional;
+
+import org.apache.james.core.User;
+import org.apache.mailet.ArbitrarySerializable;
+import org.apache.mailet.AttributeValue;
+
+import com.google.common.base.Preconditions;
+
+public class SerializableUser implements ArbitrarySerializable<SerializableUser> {
+    public static class Factory implements ArbitrarySerializable.Deserializer<SerializableUser> {
+        @Override
+        public Optional<SerializableUser> deserialize(Serializable<SerializableUser> serializable) {
+            return Optional.of(serializable.getValue().value())
+                .filter(String.class::isInstance)
+                .map(String.class::cast)
+                .map(User::fromUsername)
+                .map(SerializableUser::new);
+        }
+    }
+
+    static AttributeValue<SerializableUser> toAttributeValue(User user) {
+        return AttributeValue.of(new SerializableUser(user));
+    }
+
+    private final User value;
+
+    SerializableUser(User value) {
+        Preconditions.checkNotNull(value);
+
+        this.value = value;
+    }
+
+    @Override
+    public Serializable<SerializableUser> serialize() {
+        return new Serializable<>(AttributeValue.of(value.asString()), Factory.class);
+    }
+
+    public User getValue() {
+        return value;
+    }
+}
diff --git a/server/mailrepository/deleted-messages-vault-repository/src/test/java/org/apache/james/vault/MailConverterTest.java b/server/mailrepository/deleted-messages-vault-repository/src/test/java/org/apache/james/vault/MailConverterTest.java
new file mode 100644
index 0000000..6644cd5
--- /dev/null
+++ b/server/mailrepository/deleted-messages-vault-repository/src/test/java/org/apache/james/vault/MailConverterTest.java
@@ -0,0 +1,237 @@
+/****************************************************************
+ * 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.vault;
+
+import static org.apache.james.vault.DeletedMessageFixture.CONTENT;
+import static org.apache.james.vault.DeletedMessageFixture.DELETED_MESSAGE;
+import static org.apache.james.vault.DeletedMessageFixture.DELETED_MESSAGE_WITH_SUBJECT;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.io.ByteArrayInputStream;
+
+import org.apache.james.core.MailAddress;
+import org.apache.james.core.MaybeSender;
+import org.apache.james.mailbox.inmemory.InMemoryId;
+import org.apache.james.mailbox.inmemory.InMemoryMessageId;
+import org.apache.james.server.core.MailImpl;
+import org.apache.james.util.MimeMessageUtil;
+import org.apache.mailet.AttributeValue;
+import org.apache.mailet.Mail;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import com.google.common.collect.ImmutableList;
+
+class MailConverterTest {
+
+    private MailConverter mailConverter;
+
+    @BeforeEach
+    void setUp() {
+        mailConverter = new MailConverter(new InMemoryId.Factory(), new InMemoryMessageId.Factory());
+    }
+
+    @Test
+    void convertBackAndForthShouldPreserveDeletedMessageWithSubjectEquality() throws Exception {
+        assertThat(mailConverter.fromMail(mailConverter.toMail(DELETED_MESSAGE_WITH_SUBJECT, new ByteArrayInputStream(CONTENT))))
+            .isEqualTo(DELETED_MESSAGE_WITH_SUBJECT);
+    }
+
+    @Test
+    void convertBackAndForthShouldPreserveDeletedMessageWithoutSubjectEquality() throws Exception {
+        assertThat(mailConverter.fromMail(mailConverter.toMail(DELETED_MESSAGE, new ByteArrayInputStream(CONTENT))))
+            .isEqualTo(DELETED_MESSAGE);
+    }
+
+    @Test
+    void fromMailShouldReturnDeletedMessageWhenNoRecipient() throws Exception {
+        Mail mail = mailConverter.toMail(DELETED_MESSAGE, new ByteArrayInputStream(CONTENT));
+        mail.setRecipients(ImmutableList.of());
+
+        DeletedMessage deletedMessage = mailConverter.fromMail(mail);
+
+        assertThat(deletedMessage.getRecipients())
+            .isEmpty();
+    }
+
+    @Test
+    void fromMailShouldReturnDeletedMessageWhenNoSender() throws Exception {
+        MailImpl mail = mailConverter.toMail(DELETED_MESSAGE, new ByteArrayInputStream(CONTENT));
+        mail.setSender(MailAddress.nullSender());
+
+        DeletedMessage deletedMessage = mailConverter.fromMail(mail);
+
+        assertThat(deletedMessage.getSender())
+            .isEqualTo(MaybeSender.nullSender());
+    }
+
+    @Test
+    void toMailShouldGenerateAMailWithTheRightContent() throws Exception {
+        Mail deletedMessageAsMail = mailConverter.toMail(DELETED_MESSAGE_WITH_SUBJECT, new ByteArrayInputStream(CONTENT));
+
+        deletedMessageAsMail.setAttribute(MailConverter.OWNER_ATTRIBUTE_NAME.withValue(AttributeValue.of("notASerializedUser")));
+
+        assertThat(new ByteArrayInputStream(MimeMessageUtil.asBytes(deletedMessageAsMail.getMessage())))
+            .hasSameContentAs(new ByteArrayInputStream(CONTENT));
+    }
+
+    @Test
+    void fromMailShouldThrowWhenNameIsNotAMessageId() throws Exception {
+        Mail deletedMessageAsMail = mailConverter.toMail(DELETED_MESSAGE_WITH_SUBJECT, new ByteArrayInputStream(CONTENT));
+
+        deletedMessageAsMail.setName("notAMessageId");
+
+        assertThatThrownBy(() -> mailConverter.fromMail(deletedMessageAsMail))
+            .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @Test
+    void fromMailShouldThrowWhenNoOriginMailboxes() throws Exception {
+        Mail deletedMessageAsMail = mailConverter.toMail(DELETED_MESSAGE_WITH_SUBJECT, new ByteArrayInputStream(CONTENT));
+
+        deletedMessageAsMail.removeAttribute(MailConverter.ORIGIN_MAILBOXES_ATTRIBUTE_NAME);
+
+        assertThatThrownBy(() -> mailConverter.fromMail(deletedMessageAsMail))
+            .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @Test
+    void fromMailShouldThrowWhenOriginMailboxesIsNotAList() throws Exception {
+        Mail deletedMessageAsMail = mailConverter.toMail(DELETED_MESSAGE_WITH_SUBJECT, new ByteArrayInputStream(CONTENT));
+
+        deletedMessageAsMail.setAttribute(MailConverter.ORIGIN_MAILBOXES_ATTRIBUTE_NAME.withValue(AttributeValue.of("notAList")));
+
+        assertThatThrownBy(() -> mailConverter.fromMail(deletedMessageAsMail))
+            .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @Test
+    void fromMailShouldThrowWhenOriginMailboxesContainsNonStringElements() throws Exception {
+        Mail deletedMessageAsMail = mailConverter.toMail(DELETED_MESSAGE_WITH_SUBJECT, new ByteArrayInputStream(CONTENT));
+
+        deletedMessageAsMail.setAttribute(MailConverter.ORIGIN_MAILBOXES_ATTRIBUTE_NAME.withValue(
+            AttributeValue.of(ImmutableList.of(AttributeValue.of(42)))));
+
+        assertThatThrownBy(() -> mailConverter.fromMail(deletedMessageAsMail))
+            .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @Test
+    void fromMailShouldThrowWhenOriginMailboxesContainsBadFormatStringElements() throws Exception {
+        Mail deletedMessageAsMail = mailConverter.toMail(DELETED_MESSAGE_WITH_SUBJECT, new ByteArrayInputStream(CONTENT));
+
+        deletedMessageAsMail.setAttribute(MailConverter.ORIGIN_MAILBOXES_ATTRIBUTE_NAME.withValue(
+            AttributeValue.of(ImmutableList.of(AttributeValue.of("badFormat")))));
+
+        assertThatThrownBy(() -> mailConverter.fromMail(deletedMessageAsMail))
+            .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @Test
+    void fromMailShouldThrowWhenNoHasAttachment() throws Exception {
+        Mail deletedMessageAsMail = mailConverter.toMail(DELETED_MESSAGE_WITH_SUBJECT, new ByteArrayInputStream(CONTENT));
+
+        deletedMessageAsMail.removeAttribute(MailConverter.HAS_ATTACHMENT_ATTRIBUTE_NAME);
+
+        assertThatThrownBy(() -> mailConverter.fromMail(deletedMessageAsMail))
+            .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @Test
+    void fromMailShouldThrowWhenHasAttachmentIsNotABoolean() throws Exception {
+        Mail deletedMessageAsMail = mailConverter.toMail(DELETED_MESSAGE_WITH_SUBJECT, new ByteArrayInputStream(CONTENT));
+
+        deletedMessageAsMail.setAttribute(MailConverter.HAS_ATTACHMENT_ATTRIBUTE_NAME.withValue(AttributeValue.of("notABoolean")));
+
+        assertThatThrownBy(() -> mailConverter.fromMail(deletedMessageAsMail))
+            .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @Test
+    void fromMailShouldThrowWhenNoOwner() throws Exception {
+        Mail deletedMessageAsMail = mailConverter.toMail(DELETED_MESSAGE_WITH_SUBJECT, new ByteArrayInputStream(CONTENT));
+
+        deletedMessageAsMail.removeAttribute(MailConverter.OWNER_ATTRIBUTE_NAME);
+
+        assertThatThrownBy(() -> mailConverter.fromMail(deletedMessageAsMail))
+            .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @Test
+    void fromMailShouldThrowWhenOwnerIsNotASerializedUser() throws Exception {
+        Mail deletedMessageAsMail = mailConverter.toMail(DELETED_MESSAGE_WITH_SUBJECT, new ByteArrayInputStream(CONTENT));
+
+        deletedMessageAsMail.setAttribute(MailConverter.OWNER_ATTRIBUTE_NAME.withValue(AttributeValue.of("notASerializedUser")));
+
+        assertThatThrownBy(() -> mailConverter.fromMail(deletedMessageAsMail))
+            .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @Test
+    void fromMailShouldThrowWhenNoDeliveryDate() throws Exception {
+        Mail deletedMessageAsMail = mailConverter.toMail(DELETED_MESSAGE_WITH_SUBJECT, new ByteArrayInputStream(CONTENT));
+
+        deletedMessageAsMail.removeAttribute(MailConverter.DELIVERY_DATE_ATTRIBUTE_NAME);
+
+        assertThatThrownBy(() -> mailConverter.fromMail(deletedMessageAsMail))
+            .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @Test
+    void fromMailShouldThrowWhenDeliveryDateIsNotASerializedDate() throws Exception {
+        Mail deletedMessageAsMail = mailConverter.toMail(DELETED_MESSAGE_WITH_SUBJECT, new ByteArrayInputStream(CONTENT));
+
+        deletedMessageAsMail.setAttribute(MailConverter.DELIVERY_DATE_ATTRIBUTE_NAME.withValue(AttributeValue.of("notADate")));
+
+        assertThatThrownBy(() -> mailConverter.fromMail(deletedMessageAsMail))
+            .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @Test
+    void fromMailShouldThrowWhenNoDeletionDate() throws Exception {
+        Mail deletedMessageAsMail = mailConverter.toMail(DELETED_MESSAGE_WITH_SUBJECT, new ByteArrayInputStream(CONTENT));
+
+        deletedMessageAsMail.removeAttribute(MailConverter.DELETION_DATE_ATTRIBUTE_NAME);
+
+        assertThatThrownBy(() -> mailConverter.fromMail(deletedMessageAsMail))
+            .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @Test
+    void fromMailShouldThrowWhenDeletionDateIsNotASerializedDate() throws Exception {
+        Mail deletedMessageAsMail = mailConverter.toMail(DELETED_MESSAGE_WITH_SUBJECT, new ByteArrayInputStream(CONTENT));
+
+        deletedMessageAsMail.setAttribute(MailConverter.DELETION_DATE_ATTRIBUTE_NAME.withValue(AttributeValue.of("notADate")));
+
+        assertThatThrownBy(() -> mailConverter.fromMail(deletedMessageAsMail))
+            .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @Test
+    void fromMailShouldThrowWhenSubjectIsNotAString() throws Exception {
+        Mail deletedMessageAsMail = mailConverter.toMail(DELETED_MESSAGE_WITH_SUBJECT, new ByteArrayInputStream(CONTENT));
+
+        deletedMessageAsMail.setAttribute(MailConverter.SUBJECT_ATTRIBUTE_VALUE.withValue(AttributeValue.of(42)));
+
+        assertThatThrownBy(() -> mailConverter.fromMail(deletedMessageAsMail))
+            .isInstanceOf(IllegalArgumentException.class);
+    }
+}
\ No newline at end of file
diff --git a/server/mailrepository/deleted-messages-vault-repository/src/test/java/org/apache/james/vault/SerializableDateTest.java b/server/mailrepository/deleted-messages-vault-repository/src/test/java/org/apache/james/vault/SerializableDateTest.java
new file mode 100644
index 0000000..4efd8bf
--- /dev/null
+++ b/server/mailrepository/deleted-messages-vault-repository/src/test/java/org/apache/james/vault/SerializableDateTest.java
@@ -0,0 +1,52 @@
+/****************************************************************
+ * 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.vault;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.time.ZonedDateTime;
+
+import org.apache.mailet.AttributeValue;
+import org.junit.jupiter.api.Test;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+class SerializableDateTest {
+    private static final ZonedDateTime DATE = ZonedDateTime.parse("2014-10-30T14:12:00Z");
+
+    @Test
+    void serializeBackAndForthShouldPreserveDate() {
+        JsonNode serializableDate = AttributeValue.of(new SerializableDate(DATE)).toJson();
+
+        ZonedDateTime deserializedDate = AttributeValue.fromJson(serializableDate)
+            .valueAs(SerializableDate.class)
+            .get()
+            .getValue();
+
+        assertThat(deserializedDate).isEqualTo(DATE);
+    }
+
+    @Test
+    void constructorShouldThrowOnNull() {
+        assertThatThrownBy(() -> new SerializableDate(null))
+            .isInstanceOf(NullPointerException.class);
+    }
+}
\ No newline at end of file
diff --git a/server/mailrepository/deleted-messages-vault-repository/src/test/java/org/apache/james/vault/SerializableUserTest.java b/server/mailrepository/deleted-messages-vault-repository/src/test/java/org/apache/james/vault/SerializableUserTest.java
new file mode 100644
index 0000000..0d11909
--- /dev/null
+++ b/server/mailrepository/deleted-messages-vault-repository/src/test/java/org/apache/james/vault/SerializableUserTest.java
@@ -0,0 +1,52 @@
+/****************************************************************
+ * 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.vault;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import org.apache.james.core.User;
+import org.apache.mailet.AttributeValue;
+import org.junit.jupiter.api.Test;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+class SerializableUserTest {
+    private static final User USER = User.fromUsername("bob@apache.org");
+
+    @Test
+    void serializeBackAndForthShouldPreserveUser() {
+        JsonNode serializableUser = AttributeValue.of(new SerializableUser(USER)).toJson();
+
+        User user = AttributeValue.fromJson(serializableUser)
+            .valueAs(SerializableUser.class)
+            .get()
+            .getValue();
+
+        assertThat(user).isEqualTo(USER);
+    }
+
+    @Test
+    void constructorShouldThrowOnNull() {
+        assertThatThrownBy(() -> new SerializableUser(null))
+            .isInstanceOf(NullPointerException.class);
+    }
+
+}
\ No newline at end of file


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