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 2022/09/16 01:10:17 UTC

[james-project] branch master updated: JAMES-2656 - Add initial JPAMailRepository implementation (#1176)

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


The following commit(s) were added to refs/heads/master by this push:
     new 066e2c98e7 JAMES-2656 - Add initial JPAMailRepository implementation (#1176)
066e2c98e7 is described below

commit 066e2c98e71e24a6ae4fc7d621b231e73a04c83b
Author: amichair <am...@amichais.net>
AuthorDate: Fri Sep 16 04:10:12 2022 +0300

    JAMES-2656 - Add initial JPAMailRepository implementation (#1176)
---
 .../src/main/resources/META-INF/persistence.xml    |   3 +-
 .../src/main/resources/META-INF/persistence.xml    |   3 +-
 .../java/org/apache/james/JPAJamesServerTest.java  |   5 +-
 .../src/main/resources/META-INF/persistence.xml    |   3 +-
 .../modules/data/JPAMailRepositoryModule.java      |   6 +-
 .../mailrepository/file/FileMailRepository.java    |  13 -
 .../mailrepository/jdbc/JDBCMailRepository.java    | 101 ++----
 server/data/data-jpa/pom.xml                       |  15 +-
 .../mailrepository/jpa/JPAMailRepository.java      | 379 +++++++++++++++++++++
 .../jpa/JPAMailRepositoryUrlStore.java             |   1 +
 .../mailrepository/jpa/MimeMessageJPASource.java}  |  45 +--
 .../james/mailrepository/jpa/model/JPAMail.java    | 246 +++++++++++++
 .../mailrepository/jpa/{ => model}/JPAUrl.java     |   2 +-
 .../src/main/resources/META-INF/persistence.xml    |   3 +-
 .../james/jpa/healthcheck/JPAHealthCheckTest.java  |   2 +-
 .../mailrepository/jpa/JPAMailRepositoryTest.java  |  70 ++++
 .../jpa/JPAMailRepositoryUrlStoreExtension.java    |   1 +
 .../mailrepository/MailRepositoryContract.java     |   2 +
 18 files changed, 777 insertions(+), 123 deletions(-)

diff --git a/server/apps/jpa-app/src/main/resources/META-INF/persistence.xml b/server/apps/jpa-app/src/main/resources/META-INF/persistence.xml
index e847af4a4b..3c26a90ca2 100644
--- a/server/apps/jpa-app/src/main/resources/META-INF/persistence.xml
+++ b/server/apps/jpa-app/src/main/resources/META-INF/persistence.xml
@@ -32,7 +32,8 @@
         <class>org.apache.james.mailbox.jpa.user.model.JPASubscription</class>
 
         <class>org.apache.james.domainlist.jpa.model.JPADomain</class>
-        <class>org.apache.james.mailrepository.jpa.JPAUrl</class>
+        <class>org.apache.james.mailrepository.jpa.model.JPAUrl</class>
+        <class>org.apache.james.mailrepository.jpa.model.JPAMail</class>
         <class>org.apache.james.user.jpa.model.JPAUser</class>
         <class>org.apache.james.rrt.jpa.model.JPARecipientRewrite</class>
         <class>org.apache.james.sieve.jpa.model.JPASieveQuota</class>
diff --git a/server/apps/jpa-smtp-app/src/main/resources/META-INF/persistence.xml b/server/apps/jpa-smtp-app/src/main/resources/META-INF/persistence.xml
index 1dd8538d5b..07931a5f31 100644
--- a/server/apps/jpa-smtp-app/src/main/resources/META-INF/persistence.xml
+++ b/server/apps/jpa-smtp-app/src/main/resources/META-INF/persistence.xml
@@ -25,7 +25,8 @@
 
     <persistence-unit name="Global" transaction-type="RESOURCE_LOCAL">
         <class>org.apache.james.domainlist.jpa.model.JPADomain</class>
-        <class>org.apache.james.mailrepository.jpa.JPAUrl</class>
+        <class>org.apache.james.mailrepository.jpa.model.JPAUrl</class>
+        <class>org.apache.james.mailrepository.jpa.model.JPAMail</class>
         <class>org.apache.james.user.jpa.model.JPAUser</class>
         <class>org.apache.james.rrt.jpa.model.JPARecipientRewrite</class>
         <properties>
diff --git a/server/apps/jpa-smtp-app/src/test/java/org/apache/james/JPAJamesServerTest.java b/server/apps/jpa-smtp-app/src/test/java/org/apache/james/JPAJamesServerTest.java
index cdbeaae97d..e580d5e1cf 100644
--- a/server/apps/jpa-smtp-app/src/test/java/org/apache/james/JPAJamesServerTest.java
+++ b/server/apps/jpa-smtp-app/src/test/java/org/apache/james/JPAJamesServerTest.java
@@ -32,7 +32,8 @@ import javax.persistence.EntityManagerFactory;
 
 import org.apache.james.backends.jpa.JpaTestCluster;
 import org.apache.james.domainlist.jpa.model.JPADomain;
-import org.apache.james.mailrepository.jpa.JPAUrl;
+import org.apache.james.mailrepository.jpa.model.JPAUrl;
+import org.apache.james.mailrepository.jpa.model.JPAMail;
 import org.apache.james.modules.protocols.SmtpGuiceProbe;
 import org.apache.james.rrt.jpa.model.JPARecipientRewrite;
 import org.apache.james.user.jpa.model.JPAUser;
@@ -66,7 +67,7 @@ public class JPAJamesServerTest {
                 .overrideWith(
                         new TestJPAConfigurationModule(),
                         (binder) -> binder.bind(EntityManagerFactory.class)
-                            .toInstance(JpaTestCluster.create(JPAUser.class, JPADomain.class, JPARecipientRewrite.class, JPAUrl.class)
+                            .toInstance(JpaTestCluster.create(JPAUser.class, JPADomain.class, JPARecipientRewrite.class, JPAUrl.class, JPAMail.class)
                                     .getEntityManagerFactory()));
     }
     
diff --git a/server/apps/spring-app/src/main/resources/META-INF/persistence.xml b/server/apps/spring-app/src/main/resources/META-INF/persistence.xml
index 405b5b519b..69659a9eae 100644
--- a/server/apps/spring-app/src/main/resources/META-INF/persistence.xml
+++ b/server/apps/spring-app/src/main/resources/META-INF/persistence.xml
@@ -33,7 +33,8 @@
         <class>org.apache.james.mailbox.jpa.mail.model.JPAMailboxAnnotation</class>
         <class>org.apache.james.mailbox.jpa.user.model.JPASubscription</class>
         <class>org.apache.james.domainlist.jpa.model.JPADomain</class>
-        <class>org.apache.james.mailrepository.jpa.JPAUrl</class>
+        <class>org.apache.james.mailrepository.jpa.model.JPAUrl</class>
+        <class>org.apache.james.mailrepository.jpa.model.JPAMail</class>
         <class>org.apache.james.user.jpa.model.JPAUser</class>
         <class>org.apache.james.rrt.jpa.model.JPARecipientRewrite</class>
         <class>org.apache.james.sieve.jpa.model.JPASieveQuota</class>
diff --git a/server/container/guice/jpa-common/src/main/java/org/apache/james/modules/data/JPAMailRepositoryModule.java b/server/container/guice/jpa-common/src/main/java/org/apache/james/modules/data/JPAMailRepositoryModule.java
index f17764fb9e..99ac66dce7 100644
--- a/server/container/guice/jpa-common/src/main/java/org/apache/james/modules/data/JPAMailRepositoryModule.java
+++ b/server/container/guice/jpa-common/src/main/java/org/apache/james/modules/data/JPAMailRepositoryModule.java
@@ -22,7 +22,7 @@ package org.apache.james.modules.data;
 import org.apache.commons.configuration2.BaseHierarchicalConfiguration;
 import org.apache.james.mailrepository.api.MailRepositoryUrlStore;
 import org.apache.james.mailrepository.api.Protocol;
-import org.apache.james.mailrepository.file.FileMailRepository;
+import org.apache.james.mailrepository.jpa.JPAMailRepository;
 import org.apache.james.mailrepository.jpa.JPAMailRepositoryUrlStore;
 import org.apache.james.mailrepository.memory.MailRepositoryStoreConfiguration;
 
@@ -40,8 +40,8 @@ public class JPAMailRepositoryModule extends AbstractModule {
 
         bind(MailRepositoryStoreConfiguration.Item.class)
             .toProvider(() -> new MailRepositoryStoreConfiguration.Item(
-                ImmutableList.of(new Protocol("file")),
-                FileMailRepository.class.getName(),
+                ImmutableList.of(new Protocol("jpa")),
+                JPAMailRepository.class.getName(),
                 new BaseHierarchicalConfiguration()));
     }
 }
diff --git a/server/data/data-file/src/main/java/org/apache/james/mailrepository/file/FileMailRepository.java b/server/data/data-file/src/main/java/org/apache/james/mailrepository/file/FileMailRepository.java
index a76ecb8ea0..78f9bf13a3 100644
--- a/server/data/data-file/src/main/java/org/apache/james/mailrepository/file/FileMailRepository.java
+++ b/server/data/data-file/src/main/java/org/apache/james/mailrepository/file/FileMailRepository.java
@@ -54,20 +54,7 @@ import com.github.fge.lambdas.Throwing;
 import com.google.common.collect.Iterators;
 
 /**
- * <p>
  * Implementation of a MailRepository on a FileSystem.
- * </p>
- * <p>
- * Requires a configuration element in the .conf.xml file of the form:
- * <p/>
- * <pre>
- *  &lt;repository destinationURL="file://path-to-root-dir-for-repository"
- *              type="MAIL"
- *              model="SYNCHRONOUS"/&gt;
- * </pre>
- * <p/>
- * Requires a logger called MailRepository.
- * </p>
  */
 public class FileMailRepository implements MailRepository, Configurable, Initializable {
     private static final Logger LOGGER = LoggerFactory.getLogger(FileMailRepository.class);
diff --git a/server/data/data-jdbc/src/main/java/org/apache/james/mailrepository/jdbc/JDBCMailRepository.java b/server/data/data-jdbc/src/main/java/org/apache/james/mailrepository/jdbc/JDBCMailRepository.java
index 4dd12ff1f8..8c7c017124 100644
--- a/server/data/data-jdbc/src/main/java/org/apache/james/mailrepository/jdbc/JDBCMailRepository.java
+++ b/server/data/data-jdbc/src/main/java/org/apache/james/mailrepository/jdbc/JDBCMailRepository.java
@@ -40,6 +40,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 import java.util.StringTokenizer;
+import java.util.stream.Collectors;
 
 import javax.annotation.PostConstruct;
 import javax.inject.Inject;
@@ -57,6 +58,7 @@ import org.apache.james.lifecycle.api.Configurable;
 import org.apache.james.mailrepository.api.Initializable;
 import org.apache.james.mailrepository.api.MailKey;
 import org.apache.james.mailrepository.api.MailRepository;
+import org.apache.james.mailrepository.api.MailRepositoryUrl;
 import org.apache.james.repository.file.FilePersistentStreamRepository;
 import org.apache.james.server.core.MailImpl;
 import org.apache.james.server.core.MimeMessageWrapper;
@@ -74,27 +76,6 @@ import com.google.common.collect.ImmutableMap;
 
 /**
  * Implementation of a MailRepository on a database.
- * 
- * <p>
- * Requires a configuration element in the .conf.xml file of the form:
- * 
- * <pre>
- *  &lt;repository destinationURL="db://&lt;datasource&gt;/&lt;table_name&gt;/&lt;repository_name&gt;"
- *              type="MAIL"
- *              model="SYNCHRONOUS"/&gt;
- *  &lt;/repository&gt;
- * </pre>
- * 
- * </p>
- * <p>
- * destinationURL specifies..(Serge??) <br>
- * Type can be SPOOL or MAIL <br>
- * Model is currently not used and may be dropped
- * </p>
- * 
- * <p>
- * Requires a logger called MailRepository.
- * </p>
  */
 public class JDBCMailRepository implements MailRepository, Configurable, Initializable {
     private static final Logger LOGGER = LoggerFactory.getLogger(JDBCMailRepository.class);
@@ -165,55 +146,27 @@ public class JDBCMailRepository implements MailRepository, Configurable, Initial
     public void configure(HierarchicalConfiguration<ImmutableNode> configuration) throws ConfigurationException {
         LOGGER.debug("{}.configure()", getClass().getName());
         destination = configuration.getString("[@destinationURL]");
-
-        // normalize the destination, to simplify processing.
-        if (!destination.endsWith("/")) {
-            destination += "/";
-        }
-        // Parse the DestinationURL for the name of the datasource,
-        // the table to use, and the (optional) repository Key.
-        // Split on "/", starting after "db://"
-        List<String> urlParams = new ArrayList<>();
-        int start = 5;
-        if (destination.startsWith("dbfile")) {
-            // this is dbfile:// instead of db://
-            start += 4;
+        MailRepositoryUrl url = MailRepositoryUrl.from(destination); // also validates url
+        // parse the destinationURL into the name of the datasource,
+        // the table to use, and the (optional) repository key
+        String[] parts = url.getPath().asString().split("/", 3);
+        if (parts.length == 0) {
+            throw new ConfigurationException(
+                "Malformed destinationURL - Must be of the format 'db://<data-source>[/<table>[/<repositoryName>]]'.  Was passed " + destination);
         }
-        int end = destination.indexOf('/', start);
-        while (end > -1) {
-            urlParams.add(destination.substring(start, end));
-            start = end + 1;
-            end = destination.indexOf('/', start);
-        }
-
-        // Build SqlParameters and get datasource name from URL parameters
-        if (urlParams.size() == 0) {
-            String exceptionBuffer = "Malformed destinationURL - Must be of the format '" + "db://<data-source>[/<table>[/<repositoryName>]]'.  Was passed " + configuration.getString("[@destinationURL]");
-            throw new ConfigurationException(exceptionBuffer);
+        datasourceName = parts[0];
+        if (parts.length > 1) {
+            tableName = parts[1];
         }
-        if (urlParams.size() >= 1) {
-            datasourceName = urlParams.get(0);
-        }
-        if (urlParams.size() >= 2) {
-            tableName = urlParams.get(1);
-        }
-        if (urlParams.size() >= 3) {
-            repositoryName = "";
-            for (int i = 2; i < urlParams.size(); i++) {
-                if (i >= 3) {
-                    repositoryName += '/';
-                }
-                repositoryName += urlParams.get(i);
-            }
+        if (parts.length > 2) {
+            repositoryName = parts[2];
         }
 
-        LOGGER.debug("Parsed URL: table = '{}', repositoryName = '{}'", tableName, repositoryName);
+        LOGGER.debug("Parsed URL: datasource = '{}', table = '{}', repositoryName = '{}'", datasource, tableName, repositoryName);
 
         inMemorySizeLimit = configuration.getInt("inMemorySizeLimit", 409600000);
-
         filestore = configuration.getString("filestore", null);
         sqlFileName = configuration.getString("sqlFile");
-
     }
 
     /**
@@ -427,14 +380,10 @@ public class JDBCMailRepository implements MailRepository, Configurable, Initial
             } else {
                 insertMessage.setString(5, mc.getMaybeSender().get().toString());
             }
-            StringBuilder recipients = new StringBuilder();
-            for (Iterator<MailAddress> i = mc.getRecipients().iterator(); i.hasNext();) {
-                recipients.append(i.next().toString());
-                if (i.hasNext()) {
-                    recipients.append("\r\n");
-                }
-            }
-            insertMessage.setString(6, recipients.toString());
+            String recipients = mc.getRecipients().stream()
+                .map(MailAddress::toString)
+                .collect(Collectors.joining("\r\n"));
+            insertMessage.setString(6, recipients);
             insertMessage.setString(7, mc.getRemoteHost());
             insertMessage.setString(8, mc.getRemoteAddr());
             if (mc.getPerRecipientSpecificHeaders().getHeadersByRecipient().isEmpty()) {
@@ -515,14 +464,10 @@ public class JDBCMailRepository implements MailRepository, Configurable, Initial
             } else {
                 updateMessage.setString(3, mc.getMaybeSender().get().toString());
             }
-            StringBuilder recipients = new StringBuilder();
-            for (Iterator<MailAddress> i = mc.getRecipients().iterator(); i.hasNext();) {
-                recipients.append(i.next().toString());
-                if (i.hasNext()) {
-                    recipients.append("\r\n");
-                }
-            }
-            updateMessage.setString(4, recipients.toString());
+            String recipients = mc.getRecipients().stream()
+                .map(MailAddress::toString)
+                .collect(Collectors.joining("\r\n"));
+            updateMessage.setString(4, recipients);
             updateMessage.setString(5, mc.getRemoteHost());
             updateMessage.setString(6, mc.getRemoteAddr());
             updateMessage.setTimestamp(7, new java.sql.Timestamp(mc.getLastUpdated().getTime()));
diff --git a/server/data/data-jpa/pom.xml b/server/data/data-jpa/pom.xml
index 7a4e913b1a..61bd23ca00 100644
--- a/server/data/data-jpa/pom.xml
+++ b/server/data/data-jpa/pom.xml
@@ -43,6 +43,10 @@
             <type>test-jar</type>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
+            <artifactId>james-server-core</artifactId>
+        </dependency>
         <dependency>
             <groupId>${james.groupId}</groupId>
             <artifactId>james-server-data-api</artifactId>
@@ -86,6 +90,11 @@
             <type>test-jar</type>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
+            <artifactId>james-server-testing</artifactId>
+            <scope>test</scope>
+        </dependency>
         <dependency>
             <groupId>${james.groupId}</groupId>
             <artifactId>testing-base</artifactId>
@@ -151,7 +160,8 @@
                         org/apache/james/user/jpa/model/JPAUser.class,
                         org/apache/james/rrt/jpa/model/JPARecipientRewrite.class,
                         org/apache/james/domainlist/jpa/model/JPADomain.class,
-                        org/apache/james/mailrepository/jpa/JPAUrl.class</includes>
+                        org/apache/james/mailrepository/jpa/model/JPAUrl.class,
+                        org/apache/james/mailrepository/jpa/model/JPAMail.class</includes>
                     <addDefaultConstructor>true</addDefaultConstructor>
                     <enforcePropertyRestrictions>true</enforcePropertyRestrictions>
                     <toolProperties>
@@ -166,7 +176,8 @@
                                 org.apache.james.user.jpa.model.JPAUser;
                                 org.apache.james.rrt.jpa.model.JPARecipientRewrite;
                                 org.apache.james.domainlist.jpa.model.JPADomain;
-                                org.apache.james.mailrepository.jpa.JPAUrl)</value>
+                                org.apache.james.mailrepository.jpa.model.JPAUrl;
+                                org.apache.james.mailrepository.jpa.model.JPAMail)</value>
                         </property>
                     </toolProperties>
                 </configuration>
diff --git a/server/data/data-jpa/src/main/java/org/apache/james/mailrepository/jpa/JPAMailRepository.java b/server/data/data-jpa/src/main/java/org/apache/james/mailrepository/jpa/JPAMailRepository.java
new file mode 100644
index 0000000000..702b661e6f
--- /dev/null
+++ b/server/data/data-jpa/src/main/java/org/apache/james/mailrepository/jpa/JPAMailRepository.java
@@ -0,0 +1,379 @@
+/****************************************************************
+ * 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.mailrepository.jpa;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.sql.Timestamp;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.StringTokenizer;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import javax.annotation.PostConstruct;
+import javax.inject.Inject;
+import javax.mail.MessagingException;
+import javax.mail.internet.AddressException;
+import javax.persistence.EntityManager;
+import javax.persistence.EntityManagerFactory;
+import javax.persistence.EntityTransaction;
+import javax.persistence.NoResultException;
+
+import org.apache.commons.configuration2.HierarchicalConfiguration;
+import org.apache.commons.configuration2.ex.ConfigurationException;
+import org.apache.commons.configuration2.tree.ImmutableNode;
+import org.apache.james.backends.jpa.EntityManagerUtils;
+import org.apache.james.core.MailAddress;
+import org.apache.james.lifecycle.api.Configurable;
+import org.apache.james.mailrepository.api.Initializable;
+import org.apache.james.mailrepository.api.MailKey;
+import org.apache.james.mailrepository.api.MailRepository;
+import org.apache.james.mailrepository.api.MailRepositoryUrl;
+import org.apache.james.mailrepository.jpa.model.JPAMail;
+import org.apache.james.server.core.MailImpl;
+import org.apache.james.server.core.MimeMessageWrapper;
+import org.apache.james.util.streams.Iterators;
+import org.apache.mailet.Attribute;
+import org.apache.mailet.AttributeName;
+import org.apache.mailet.AttributeValue;
+import org.apache.mailet.Mail;
+import org.apache.mailet.PerRecipientHeaders;
+import org.apache.mailet.PerRecipientHeaders.Header;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+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.collect.ImmutableList;
+
+/**
+ * Implementation of a MailRepository on a database via JPA.
+ */
+public class JPAMailRepository implements MailRepository, Configurable, Initializable {
+    private static final Logger LOGGER = LoggerFactory.getLogger(JPAMailRepository.class);
+    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
+    private String repositoryName;
+
+    private final EntityManagerFactory entityManagerFactory;
+
+    @Inject
+    public JPAMailRepository(EntityManagerFactory entityManagerFactory) {
+        this.entityManagerFactory = entityManagerFactory;
+    }
+
+    public String getRepositoryName() {
+        return repositoryName;
+    }
+
+    // note: caller must close the returned EntityManager when done using it
+    protected EntityManager entityManager() {
+        return entityManagerFactory.createEntityManager();
+    }
+
+    @Override
+    public void configure(HierarchicalConfiguration<ImmutableNode> configuration) throws ConfigurationException {
+        LOGGER.debug("{}.configure()", getClass().getName());
+        String destination = configuration.getString("[@destinationURL]");
+        MailRepositoryUrl url = MailRepositoryUrl.from(destination); // also validates url and standardizes slashes
+        repositoryName = url.getPath().asString();
+        if (repositoryName.isEmpty()) {
+            throw new ConfigurationException(
+                "Malformed destinationURL - Must be of the format 'jpa://<repositoryName>'.  Was passed " + url);
+        }
+        LOGGER.debug("Parsed URL: repositoryName = '{}'", repositoryName);
+    }
+
+    /**
+     * Initialises the JPA repository.
+     *
+     * @throws Exception if an error occurs
+     */
+    @Override
+    @PostConstruct
+    public void init() throws Exception {
+        LOGGER.debug("{}.initialize()", getClass().getName());
+        list();
+    }
+
+    @Override
+    public MailKey store(Mail mail) throws MessagingException {
+        MailKey key = MailKey.forMail(mail);
+        EntityManager entityManager = entityManager();
+        try {
+            JPAMail jpaMail = new JPAMail();
+            jpaMail.setRepositoryName(repositoryName);
+            jpaMail.setMessageName(mail.getName());
+            jpaMail.setMessageState(mail.getState());
+            jpaMail.setErrorMessage(mail.getErrorMessage());
+            if (!mail.getMaybeSender().isNullSender()) {
+                jpaMail.setSender(mail.getMaybeSender().get().toString());
+            }
+            String recipients = mail.getRecipients().stream()
+                .map(MailAddress::toString)
+                .collect(Collectors.joining("\r\n"));
+            jpaMail.setRecipients(recipients);
+            jpaMail.setRemoteHost(mail.getRemoteHost());
+            jpaMail.setRemoteAddr(mail.getRemoteAddr());
+            jpaMail.setPerRecipientHeaders(serializePerRecipientHeaders(mail.getPerRecipientSpecificHeaders()));
+            jpaMail.setLastUpdated(new Timestamp(mail.getLastUpdated().getTime()));
+            jpaMail.setMessageBody(getBody(mail));
+            jpaMail.setMessageAttributes(serializeAttributes(mail.attributes()));
+            EntityTransaction transaction = entityManager.getTransaction();
+            transaction.begin();
+            jpaMail = entityManager.merge(jpaMail);
+            transaction.commit();
+            return key;
+        } catch (MessagingException e) {
+            LOGGER.error("Exception caught while storing mail {}", key, e);
+            throw e;
+        } catch (Exception e) {
+            LOGGER.error("Exception caught while storing mail {}", key, e);
+            throw new MessagingException("Exception caught while storing mail " + key, e);
+        } finally {
+            EntityManagerUtils.safelyClose(entityManager);
+        }
+    }
+
+    private byte[] getBody(Mail mail) throws MessagingException, IOException {
+        ByteArrayOutputStream out = new ByteArrayOutputStream((int)mail.getMessageSize());
+        if (mail instanceof MimeMessageWrapper) {
+            // we need to force the loading of the message from the
+            // stream as we want to override the old message
+            ((MimeMessageWrapper) mail).loadMessage();
+            ((MimeMessageWrapper) mail).writeTo(out, out, null, true);
+        } else {
+            mail.getMessage().writeTo(out);
+        }
+        return out.toByteArray();
+    }
+
+    private String serializeAttributes(Stream<Attribute> attributes) {
+        Map<String, JsonNode> map = attributes.collect(Collectors.toMap(
+            attribute -> attribute.getName().asString(),
+            attribute -> attribute.getValue().toJson()));
+
+        return new ObjectNode(JsonNodeFactory.instance, map).toString();
+    }
+
+    private List<Attribute> deserializeAttributes(String data) {
+        try {
+            JsonNode jsonNode = OBJECT_MAPPER.readTree(data);
+            if (jsonNode instanceof ObjectNode) {
+                ObjectNode objectNode = (ObjectNode) jsonNode;
+
+                return Iterators.toStream(objectNode.fields())
+                    .map(entry -> new Attribute(AttributeName.of(entry.getKey()), AttributeValue.fromJson(entry.getValue())))
+                    .collect(ImmutableList.toImmutableList());
+            }
+            throw new IllegalArgumentException("JSON object corresponding to mail attibutes must be a JSON object");
+        } catch (JsonProcessingException e) {
+            throw new IllegalArgumentException("Mail attributes is not a valid JSON object", e);
+        }
+    }
+
+    private String serializePerRecipientHeaders(PerRecipientHeaders perRecipientHeaders) {
+        if (perRecipientHeaders == null) {
+            return null;
+        }
+        Map<MailAddress, Collection<Header>> map = perRecipientHeaders.getHeadersByRecipient().asMap();
+        if (map.isEmpty()) {
+            return null;
+        }
+        ObjectNode node = JsonNodeFactory.instance.objectNode();
+        for (Map.Entry<MailAddress, Collection<Header>> entry : map.entrySet()) {
+            String recipient = entry.getKey().asString();
+            ObjectNode headers = node.putObject(recipient);
+            entry.getValue().forEach(header -> headers.put(header.getName(), header.getValue()));
+        }
+        return node.toString();
+    }
+
+    private PerRecipientHeaders deserializePerRecipientHeaders(String data) {
+        if (data == null || data.isEmpty()) {
+            return null;
+        }
+        PerRecipientHeaders perRecipientHeaders = new PerRecipientHeaders();
+        try {
+            JsonNode node = OBJECT_MAPPER.readTree(data);
+            if (node instanceof ObjectNode) {
+                ObjectNode objectNode = (ObjectNode) node;
+                Iterators.toStream(objectNode.fields()).forEach(
+                    entry -> addPerRecipientHeaders(perRecipientHeaders, entry.getKey(), entry.getValue()));
+                return perRecipientHeaders;
+            }
+            throw new IllegalArgumentException("JSON object corresponding to recipient headers must be a JSON object");
+        } catch (JsonProcessingException e) {
+            throw new IllegalArgumentException("per recipient headers is not a valid JSON object", e);
+        }
+    }
+
+    private void addPerRecipientHeaders(PerRecipientHeaders perRecipientHeaders, String recipient, JsonNode headers) {
+        try {
+            MailAddress address = new MailAddress(recipient);
+            Iterators.toStream(headers.fields()).forEach(
+                entry -> {
+                    String name = entry.getKey();
+                    String value = entry.getValue().textValue();
+                    Header header = Header.builder().name(name).value(value).build();
+                    perRecipientHeaders.addHeaderForRecipient(header, address);
+                });
+        } catch (AddressException ae) {
+            throw new IllegalArgumentException("invalid recipient address", ae);
+        }
+    }
+
+    @Override
+    public Mail retrieve(MailKey key) throws MessagingException {
+        EntityManager entityManager = entityManager();
+        try {
+            JPAMail jpaMail = entityManager.createNamedQuery("findMailMessage", JPAMail.class)
+                .setParameter("repositoryName", repositoryName)
+                .setParameter("messageName", key.asString())
+                .getSingleResult();
+
+            MailImpl.Builder mail = MailImpl.builder().name(key.asString());
+            if (jpaMail.getMessageAttributes() != null) {
+                mail.addAttributes(deserializeAttributes(jpaMail.getMessageAttributes()));
+            }
+            mail.state(jpaMail.getMessageState());
+            mail.errorMessage(jpaMail.getErrorMessage());
+            String sender = jpaMail.getSender();
+            if (sender == null) {
+                mail.sender((MailAddress)null);
+            } else {
+                mail.sender(new MailAddress(sender));
+            }
+            StringTokenizer st = new StringTokenizer(jpaMail.getRecipients(), "\r\n", false);
+            while (st.hasMoreTokens()) {
+                mail.addRecipient(st.nextToken());
+            }
+            mail.remoteHost(jpaMail.getRemoteHost());
+            mail.remoteAddr(jpaMail.getRemoteAddr());
+            PerRecipientHeaders perRecipientHeaders = deserializePerRecipientHeaders(jpaMail.getPerRecipientHeaders());
+            if (perRecipientHeaders != null) {
+                mail.addAllHeadersForRecipients(perRecipientHeaders);
+            }
+            mail.lastUpdated(jpaMail.getLastUpdated());
+
+            MimeMessageJPASource source = new MimeMessageJPASource(this, key.asString(), jpaMail.getMessageBody());
+            MimeMessageWrapper message = new MimeMessageWrapper(source);
+            mail.mimeMessage(message);
+            return mail.build();
+        } catch (NoResultException nre) {
+            LOGGER.debug("Did not find mail {} in repository {}", key, repositoryName);
+            return null;
+        } catch (Exception e) {
+            throw new MessagingException("Exception while retrieving mail: " + e.getMessage(), e);
+        } finally {
+            EntityManagerUtils.safelyClose(entityManager);
+        }
+    }
+
+    @Override
+    public long size() throws MessagingException {
+        EntityManager entityManager = entityManager();
+        try {
+            return entityManager.createNamedQuery("countMailMessages", long.class)
+                .setParameter("repositoryName", repositoryName)
+                .getSingleResult();
+        } catch (Exception me) {
+            throw new MessagingException("Exception while listing messages: " + me.getMessage(), me);
+        } finally {
+            EntityManagerUtils.safelyClose(entityManager);
+        }
+    }
+
+    @Override
+    public Iterator<MailKey> list() throws MessagingException {
+        EntityManager entityManager = entityManager();
+        try {
+            return entityManager.createNamedQuery("listMailMessages", String.class)
+                .setParameter("repositoryName", repositoryName)
+                .getResultStream()
+                .map(MailKey::new)
+                .iterator();
+        } catch (Exception me) {
+            throw new MessagingException("Exception while listing messages: " + me.getMessage(), me);
+        } finally {
+            EntityManagerUtils.safelyClose(entityManager);
+        }
+    }
+
+    @Override
+    public void remove(MailKey key) throws MessagingException {
+        remove(Collections.singleton(key));
+    }
+
+    @Override
+    public void remove(Collection<MailKey> keys) throws MessagingException {
+        Collection<String> messageNames = keys.stream().map(MailKey::asString).collect(Collectors.toList());
+        EntityManager entityManager = entityManager();
+        EntityTransaction transaction = entityManager.getTransaction();
+        transaction.begin();
+        try {
+            entityManager.createNamedQuery("deleteMailMessages")
+                .setParameter("repositoryName", repositoryName)
+                .setParameter("messageNames", messageNames)
+                .executeUpdate();
+            transaction.commit();
+        } catch (Exception e) {
+            throw new MessagingException("Exception while removing message(s): " + e.getMessage(), e);
+        } finally {
+            EntityManagerUtils.safelyClose(entityManager);
+        }
+    }
+
+    @Override
+    public void removeAll() throws MessagingException {
+        EntityManager entityManager = entityManager();
+        EntityTransaction transaction = entityManager.getTransaction();
+        transaction.begin();
+        try {
+            entityManager.createNamedQuery("deleteAllMailMessages")
+                .setParameter("repositoryName", repositoryName)
+                .executeUpdate();
+            transaction.commit();
+        } catch (Exception e) {
+            throw new MessagingException("Exception while removing message(s): " + e.getMessage(), e);
+        } finally {
+            EntityManagerUtils.safelyClose(entityManager);
+        }
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        return obj instanceof JPAMailRepository
+            && Objects.equals(repositoryName, ((JPAMailRepository)obj).repositoryName);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(repositoryName);
+    }
+}
diff --git a/server/data/data-jpa/src/main/java/org/apache/james/mailrepository/jpa/JPAMailRepositoryUrlStore.java b/server/data/data-jpa/src/main/java/org/apache/james/mailrepository/jpa/JPAMailRepositoryUrlStore.java
index ca1b8d29c1..1f448a9eca 100644
--- a/server/data/data-jpa/src/main/java/org/apache/james/mailrepository/jpa/JPAMailRepositoryUrlStore.java
+++ b/server/data/data-jpa/src/main/java/org/apache/james/mailrepository/jpa/JPAMailRepositoryUrlStore.java
@@ -27,6 +27,7 @@ import javax.persistence.EntityManagerFactory;
 import org.apache.james.backends.jpa.TransactionRunner;
 import org.apache.james.mailrepository.api.MailRepositoryUrl;
 import org.apache.james.mailrepository.api.MailRepositoryUrlStore;
+import org.apache.james.mailrepository.jpa.model.JPAUrl;
 
 public class JPAMailRepositoryUrlStore implements MailRepositoryUrlStore {
     private final TransactionRunner transactionRunner;
diff --git a/server/container/guice/jpa-common/src/main/java/org/apache/james/modules/data/JPAMailRepositoryModule.java b/server/data/data-jpa/src/main/java/org/apache/james/mailrepository/jpa/MimeMessageJPASource.java
similarity index 52%
copy from server/container/guice/jpa-common/src/main/java/org/apache/james/modules/data/JPAMailRepositoryModule.java
copy to server/data/data-jpa/src/main/java/org/apache/james/mailrepository/jpa/MimeMessageJPASource.java
index f17764fb9e..f5445c279c 100644
--- a/server/container/guice/jpa-common/src/main/java/org/apache/james/modules/data/JPAMailRepositoryModule.java
+++ b/server/data/data-jpa/src/main/java/org/apache/james/mailrepository/jpa/MimeMessageJPASource.java
@@ -17,31 +17,38 @@
  * under the License.                                           *
  ****************************************************************/
 
-package org.apache.james.modules.data;
+package org.apache.james.mailrepository.jpa;
 
-import org.apache.commons.configuration2.BaseHierarchicalConfiguration;
-import org.apache.james.mailrepository.api.MailRepositoryUrlStore;
-import org.apache.james.mailrepository.api.Protocol;
-import org.apache.james.mailrepository.file.FileMailRepository;
-import org.apache.james.mailrepository.jpa.JPAMailRepositoryUrlStore;
-import org.apache.james.mailrepository.memory.MailRepositoryStoreConfiguration;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
 
-import com.google.common.collect.ImmutableList;
-import com.google.inject.AbstractModule;
-import com.google.inject.Scopes;
+import org.apache.james.server.core.MimeMessageSource;
 
-public class JPAMailRepositoryModule extends AbstractModule {
+public class MimeMessageJPASource implements MimeMessageSource {
+
+    private final JPAMailRepository jpaMailRepository;
+    private final String key;
+    private final byte[] body;
+
+    public MimeMessageJPASource(JPAMailRepository jpaMailRepository, String key, byte[] body) {
+        this.jpaMailRepository = jpaMailRepository;
+        this.key = key;
+        this.body = body;
+    }
 
     @Override
-    protected void configure() {
-        bind(JPAMailRepositoryUrlStore.class).in(Scopes.SINGLETON);
+    public String getSourceId() {
+        return jpaMailRepository.getRepositoryName() + "/" + key;
+    }
 
-        bind(MailRepositoryUrlStore.class).to(JPAMailRepositoryUrlStore.class);
+    @Override
+    public InputStream getInputStream() throws IOException {
+        return new ByteArrayInputStream(body);
+    }
 
-        bind(MailRepositoryStoreConfiguration.Item.class)
-            .toProvider(() -> new MailRepositoryStoreConfiguration.Item(
-                ImmutableList.of(new Protocol("file")),
-                FileMailRepository.class.getName(),
-                new BaseHierarchicalConfiguration()));
+    @Override
+    public long getMessageSize() throws IOException {
+        return body.length;
     }
 }
diff --git a/server/data/data-jpa/src/main/java/org/apache/james/mailrepository/jpa/model/JPAMail.java b/server/data/data-jpa/src/main/java/org/apache/james/mailrepository/jpa/model/JPAMail.java
new file mode 100644
index 0000000000..187241dfcb
--- /dev/null
+++ b/server/data/data-jpa/src/main/java/org/apache/james/mailrepository/jpa/model/JPAMail.java
@@ -0,0 +1,246 @@
+/****************************************************************
+ * 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.mailrepository.jpa.model;
+
+import java.io.Serializable;
+import java.sql.Timestamp;
+import java.util.Objects;
+
+import javax.persistence.Basic;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.FetchType;
+import javax.persistence.Id;
+import javax.persistence.IdClass;
+import javax.persistence.Index;
+import javax.persistence.Lob;
+import javax.persistence.NamedQueries;
+import javax.persistence.NamedQuery;
+import javax.persistence.Table;
+
+@Entity(name = "JamesMailStore")
+@IdClass(JPAMail.JPAMailId.class)
+@Table(name = "JAMES_MAIL_STORE", indexes = {
+   @Index(name = "REPOSITORY_NAME_MESSAGE_NAME_INDEX", columnList = "REPOSITORY_NAME, MESSAGE_NAME")
+})
+@NamedQueries({
+    @NamedQuery(name = "listMailMessages",
+        query = "SELECT mail.messageName FROM JamesMailStore mail WHERE mail.repositoryName = :repositoryName"),
+    @NamedQuery(name = "countMailMessages",
+        query = "SELECT COUNT(mail) FROM JamesMailStore mail WHERE mail.repositoryName = :repositoryName"),
+    @NamedQuery(name = "deleteMailMessages",
+        query = "DELETE FROM JamesMailStore mail WHERE mail.repositoryName = :repositoryName AND mail.messageName IN (:messageNames)"),
+    @NamedQuery(name = "deleteAllMailMessages",
+        query = "DELETE FROM JamesMailStore mail WHERE mail.repositoryName = :repositoryName"),
+    @NamedQuery(name = "findMailMessage",
+        query = "SELECT mail FROM JamesMailStore mail WHERE mail.repositoryName = :repositoryName AND mail.messageName = :messageName")
+})
+public class JPAMail {
+
+    static class JPAMailId implements Serializable {
+        public JPAMailId() {
+        }
+
+        String repositoryName;
+        String messageName;
+
+        public boolean equals(Object obj) {
+            return obj instanceof JPAMailId
+                && Objects.equals(messageName, ((JPAMailId) obj).messageName)
+                && Objects.equals(repositoryName, ((JPAMailId) obj).repositoryName);
+        }
+
+        public int hashCode() {
+            return Objects.hash(messageName, repositoryName);
+        }
+    }
+
+    @Id
+    @Basic(optional = false)
+    @Column(name = "REPOSITORY_NAME", nullable = false, length = 255)
+    private String repositoryName;
+
+    @Id
+    @Basic(optional = false)
+    @Column(name = "MESSAGE_NAME", nullable = false, length = 200)
+    private String messageName;
+
+    @Basic(optional = false)
+    @Column(name = "MESSAGE_STATE", nullable = false, length = 30)
+    private String messageState;
+
+    @Basic(optional = true)
+    @Column(name = "ERROR_MESSAGE", nullable = true, length = 200)
+    private String errorMessage;
+
+    @Basic(optional = true)
+    @Column(name = "SENDER", nullable = true, length = 255)
+    private String sender;
+
+    @Basic(optional = false)
+    @Column(name = "RECIPIENTS", nullable = false)
+    private String recipients; // CRLF delimited
+
+    @Basic(optional = false)
+    @Column(name = "REMOTE_HOST", nullable = false, length = 255)
+    private String remoteHost;
+
+    @Basic(optional = false)
+    @Column(name = "REMOTE_ADDR", nullable = false, length = 20)
+    private String remoteAddr;
+
+    @Basic(optional = false)
+    @Column(name = "LAST_UPDATED", nullable = false)
+    private Timestamp lastUpdated;
+
+    @Basic(optional = true)
+    @Column(name = "PER_RECIPIENT_HEADERS", nullable = true, length = 10485760)
+    @Lob
+    private String perRecipientHeaders;
+
+    @Basic(optional = false, fetch = FetchType.LAZY)
+    @Column(name = "MESSAGE_BODY", nullable = false, length = 1048576000)
+    @Lob
+    private byte[] messageBody; // TODO: support streaming body where possible (see e.g. JPAStreamingMailboxMessage)
+
+    @Basic(optional = true)
+    @Column(name = "MESSAGE_ATTRIBUTES", nullable = true, length = 10485760)
+    @Lob
+    private String messageAttributes;
+
+    public JPAMail() {
+    }
+
+    public String getRepositoryName() {
+        return repositoryName;
+    }
+
+    public void setRepositoryName(String repositoryName) {
+        this.repositoryName = repositoryName;
+    }
+
+    public String getMessageName() {
+        return messageName;
+    }
+
+    public void setMessageName(String messageName) {
+        this.messageName = messageName;
+    }
+
+    public String getMessageState() {
+        return messageState;
+    }
+
+    public void setMessageState(String messageState) {
+        this.messageState = messageState;
+    }
+
+    public String getErrorMessage() {
+        return errorMessage;
+    }
+
+    public void setErrorMessage(String errorMessage) {
+        this.errorMessage = errorMessage;
+    }
+
+    public String getSender() {
+        return sender;
+    }
+
+    public void setSender(String sender) {
+        this.sender = sender;
+    }
+
+    public String getRecipients() {
+        return recipients;
+    }
+
+    public void setRecipients(String recipients) {
+        this.recipients = recipients;
+    }
+
+    public String getRemoteHost() {
+        return remoteHost;
+    }
+
+    public void setRemoteHost(String remoteHost) {
+        this.remoteHost = remoteHost;
+    }
+
+    public String getRemoteAddr() {
+        return remoteAddr;
+    }
+
+    public void setRemoteAddr(String remoteAddr) {
+        this.remoteAddr = remoteAddr;
+    }
+
+    public Timestamp getLastUpdated() {
+        return lastUpdated;
+    }
+
+    public void setLastUpdated(Timestamp lastUpdated) {
+        this.lastUpdated = lastUpdated;
+    }
+
+    public String getPerRecipientHeaders() {
+        return perRecipientHeaders;
+    }
+
+    public void setPerRecipientHeaders(String perRecipientHeaders) {
+        this.perRecipientHeaders = perRecipientHeaders;
+    }
+
+    public byte[] getMessageBody() {
+        return messageBody;
+    }
+
+    public void setMessageBody(byte[] messageBody) {
+        this.messageBody = messageBody;
+    }
+
+    public String getMessageAttributes() {
+        return messageAttributes;
+    }
+
+    public void setMessageAttributes(String messageAttributes) {
+        this.messageAttributes = messageAttributes;
+    }
+
+    @Override
+    public String toString() {
+        return "JPAMail ( "
+            + "repositoryName = " + repositoryName
+            + ", messageName = " + messageName
+            + " )";
+    }
+
+    @Override
+    public final boolean equals(Object obj) {
+        return obj instanceof JPAMail
+            && Objects.equals(this.repositoryName, ((JPAMail)obj).repositoryName)
+            && Objects.equals(this.messageName, ((JPAMail)obj).messageName);
+    }
+
+    @Override
+    public final int hashCode() {
+        return Objects.hash(repositoryName, messageName);
+    }
+}
diff --git a/server/data/data-jpa/src/main/java/org/apache/james/mailrepository/jpa/JPAUrl.java b/server/data/data-jpa/src/main/java/org/apache/james/mailrepository/jpa/model/JPAUrl.java
similarity index 97%
rename from server/data/data-jpa/src/main/java/org/apache/james/mailrepository/jpa/JPAUrl.java
rename to server/data/data-jpa/src/main/java/org/apache/james/mailrepository/jpa/model/JPAUrl.java
index 8de1eb86b1..9f8e74c69c 100644
--- a/server/data/data-jpa/src/main/java/org/apache/james/mailrepository/jpa/JPAUrl.java
+++ b/server/data/data-jpa/src/main/java/org/apache/james/mailrepository/jpa/model/JPAUrl.java
@@ -17,7 +17,7 @@
  * under the License.                                           *
  ****************************************************************/
 
-package org.apache.james.mailrepository.jpa;
+package org.apache.james.mailrepository.jpa.model;
 
 import javax.persistence.Column;
 import javax.persistence.Entity;
diff --git a/server/data/data-jpa/src/main/resources/META-INF/persistence.xml b/server/data/data-jpa/src/main/resources/META-INF/persistence.xml
index 1927b87f6e..6224adb74f 100644
--- a/server/data/data-jpa/src/main/resources/META-INF/persistence.xml
+++ b/server/data/data-jpa/src/main/resources/META-INF/persistence.xml
@@ -29,7 +29,8 @@
         <class>org.apache.james.domainlist.jpa.model.JPADomain</class>
         <class>org.apache.james.user.jpa.model.JPAUser</class>
         <class>org.apache.james.rrt.jpa.model.JPARecipientRewrite</class>
-        <class>org.apache.james.mailrepository.jpa.JPAUrl</class>
+        <class>org.apache.james.mailrepository.jpa.model.JPAUrl</class>
+        <class>org.apache.james.mailrepository.jpa.model.JPAMail</class>
         <class>org.apache.james.sieve.jpa.model.JPASieveQuota</class>
         <class>org.apache.james.sieve.jpa.model.JPASieveScript</class>
         <exclude-unlisted-classes>true</exclude-unlisted-classes>
diff --git a/server/data/data-jpa/src/test/java/org/apache/james/jpa/healthcheck/JPAHealthCheckTest.java b/server/data/data-jpa/src/test/java/org/apache/james/jpa/healthcheck/JPAHealthCheckTest.java
index 16f880bfc9..20ed1bbaa2 100644
--- a/server/data/data-jpa/src/test/java/org/apache/james/jpa/healthcheck/JPAHealthCheckTest.java
+++ b/server/data/data-jpa/src/test/java/org/apache/james/jpa/healthcheck/JPAHealthCheckTest.java
@@ -24,7 +24,7 @@ import static org.assertj.core.api.Assertions.fail;
 import org.apache.james.backends.jpa.JpaTestCluster;
 import org.apache.james.core.healthcheck.Result;
 import org.apache.james.core.healthcheck.ResultStatus;
-import org.apache.james.mailrepository.jpa.JPAUrl;
+import org.apache.james.mailrepository.jpa.model.JPAUrl;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
diff --git a/server/data/data-jpa/src/test/java/org/apache/james/mailrepository/jpa/JPAMailRepositoryTest.java b/server/data/data-jpa/src/test/java/org/apache/james/mailrepository/jpa/JPAMailRepositoryTest.java
new file mode 100644
index 0000000000..3c41aa53ab
--- /dev/null
+++ b/server/data/data-jpa/src/test/java/org/apache/james/mailrepository/jpa/JPAMailRepositoryTest.java
@@ -0,0 +1,70 @@
+/****************************************************************
+ * 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.mailrepository.jpa;
+
+import org.apache.commons.configuration2.BaseHierarchicalConfiguration;
+import org.apache.james.backends.jpa.JpaTestCluster;
+import org.apache.james.mailrepository.MailRepositoryContract;
+import org.apache.james.mailrepository.api.MailRepository;
+import org.apache.james.mailrepository.api.MailRepositoryPath;
+import org.apache.james.mailrepository.api.MailRepositoryUrl;
+import org.apache.james.mailrepository.api.Protocol;
+import org.apache.james.mailrepository.jpa.model.JPAMail;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Disabled;
+
+public class JPAMailRepositoryTest implements MailRepositoryContract {
+
+    final JpaTestCluster JPA_TEST_CLUSTER = JpaTestCluster.create(JPAMail.class);
+
+    private JPAMailRepository mailRepository;
+
+    @BeforeEach
+    void setUp() throws Exception {
+        mailRepository = retrieveRepository(MailRepositoryPath.from("testrepo"));
+    }
+
+    @AfterEach
+    void tearDown() {
+        JPA_TEST_CLUSTER.clear("JAMES_MAIL_STORE");
+    }
+
+    @Override
+    public MailRepository retrieveRepository() {
+        return mailRepository;
+    }
+
+    @Override
+    public JPAMailRepository retrieveRepository(MailRepositoryPath url) throws Exception {
+        BaseHierarchicalConfiguration conf = new BaseHierarchicalConfiguration();
+        conf.addProperty("[@destinationURL]", MailRepositoryUrl.fromPathAndProtocol(new Protocol("jpa"), url).asString());
+        JPAMailRepository mailRepository = new JPAMailRepository(JPA_TEST_CLUSTER.getEntityManagerFactory());
+        mailRepository.configure(conf);
+        mailRepository.init();
+        return mailRepository;
+    }
+
+    @Override
+    @Disabled("JAMES-3431 No support for Attribute collection Java serialization yet")
+    public void shouldPreserveDsnParameters() throws Exception {
+        MailRepositoryContract.super.shouldPreserveDsnParameters();
+    }
+}
diff --git a/server/data/data-jpa/src/test/java/org/apache/james/mailrepository/jpa/JPAMailRepositoryUrlStoreExtension.java b/server/data/data-jpa/src/test/java/org/apache/james/mailrepository/jpa/JPAMailRepositoryUrlStoreExtension.java
index 3eabf52c87..c8af2008d1 100644
--- a/server/data/data-jpa/src/test/java/org/apache/james/mailrepository/jpa/JPAMailRepositoryUrlStoreExtension.java
+++ b/server/data/data-jpa/src/test/java/org/apache/james/mailrepository/jpa/JPAMailRepositoryUrlStoreExtension.java
@@ -21,6 +21,7 @@ package org.apache.james.mailrepository.jpa;
 
 import org.apache.james.backends.jpa.JpaTestCluster;
 import org.apache.james.mailrepository.api.MailRepositoryUrlStore;
+import org.apache.james.mailrepository.jpa.model.JPAUrl;
 import org.junit.jupiter.api.extension.AfterEachCallback;
 import org.junit.jupiter.api.extension.ExtensionContext;
 import org.junit.jupiter.api.extension.ParameterContext;
diff --git a/server/mailrepository/mailrepository-api/src/test/java/org/apache/james/mailrepository/MailRepositoryContract.java b/server/mailrepository/mailrepository-api/src/test/java/org/apache/james/mailrepository/MailRepositoryContract.java
index cab80c8189..3eb4f4ba94 100644
--- a/server/mailrepository/mailrepository-api/src/test/java/org/apache/james/mailrepository/MailRepositoryContract.java
+++ b/server/mailrepository/mailrepository-api/src/test/java/org/apache/james/mailrepository/MailRepositoryContract.java
@@ -163,6 +163,8 @@ public interface MailRepositoryContract {
             .name(MAIL_1.asString())
             .sender(MailAddress.nullSender())
             .recipient(MailAddressFixture.RECIPIENT1)
+            .lastUpdated(new Date())
+            .state(Mail.DEFAULT)
             .mimeMessage(MimeMessageBuilder.mimeMessageBuilder()
                 .setSubject("test")
                 .setText("String body")


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