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

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

commit f85ea6d91e981cbd1419a5e7bf22e12d498fc8f6
Author: Jean Helou <j...@xn--gml-cma.com>
AuthorDate: Mon Mar 17 12:04:18 2025 +0100

    [JAMES-4119] Core data migration tool from JPA to Postgres
---
 mpt/impl/smtp/jpa-pulsar/pom.xml                   |   7 +
 pom.xml                                            |   5 +
 server/apps/migration/core-data-jpa-to-pg/pom.xml  | 267 +++++++++++
 .../sample-configuration/blob.properties           |  66 +++
 .../sample-configuration/james-database.properties |  45 ++
 .../sample-configuration/logback.xml               |  39 ++
 .../sample-configuration/postgres.properties       |  51 +++
 .../org/apache/james/JpaToPgCoreDataMigration.java | 504 +++++++++++++++++++++
 .../org/apache/james/MigrationConfiguration.java   | 124 +++++
 .../src/main/resources/META-INF/persistence.xml    |  44 ++
 .../src/main/scripts/james-migration               |   7 +
 .../apache/james/JpaToPgCoreDataMigrationTest.java | 354 +++++++++++++++
 .../java/org/apache/james/MariaDBExtension.java    |  80 ++++
 .../modules/blobstore/BlobStoreModulesChooser.java |   2 +-
 .../modules/data/PostgresQuotaGuiceModule.java     |   2 +-
 .../apache/james/droplists/jpa/JPADropList.java    |  17 +
 .../droplists/jpa/model/JPADropListEntry.java      |   2 +
 .../apache/james/sieve/jpa/JPASieveRepository.java |  32 +-
 .../james/sieve/jpa/model/JPASieveQuota.java       |   6 +
 .../james/sieve/jpa/model/JPASieveScript.java      |   1 +
 .../org/apache/james/user/jpa/model/JPAUser.java   |  15 +-
 server/pom.xml                                     |   1 +
 22 files changed, 1665 insertions(+), 6 deletions(-)

diff --git a/mpt/impl/smtp/jpa-pulsar/pom.xml b/mpt/impl/smtp/jpa-pulsar/pom.xml
index e2a9dff099..6104541893 100644
--- a/mpt/impl/smtp/jpa-pulsar/pom.xml
+++ b/mpt/impl/smtp/jpa-pulsar/pom.xml
@@ -31,6 +31,12 @@
     <name>Apache James :: MPT :: SMTP :: JPA - Pulsar</name>
 
     <dependencies>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
+            <artifactId>apache-james-backends-postgres</artifactId>
+            <type>test-jar</type>
+            <scope>test</scope>
+        </dependency>
         <dependency>
             <groupId>${james.groupId}</groupId>
             <artifactId>apache-james-backends-pulsar</artifactId>
@@ -41,6 +47,7 @@
             <type>test-jar</type>
             <scope>test</scope>
         </dependency>
+
         <dependency>
             <groupId>${james.groupId}</groupId>
             <artifactId>apache-james-mpt-smtp-core</artifactId>
diff --git a/pom.xml b/pom.xml
index 867339af2a..0946424db3 100644
--- a/pom.xml
+++ b/pom.xml
@@ -3012,6 +3012,11 @@
                 <artifactId>junit-jupiter</artifactId>
                 <version>${testcontainers.version}</version>
             </dependency>
+            <dependency>
+                <groupId>org.testcontainers</groupId>
+                <artifactId>mariadb</artifactId>
+                <version>${testcontainers.version}</version>
+            </dependency>
             <dependency>
                 <groupId>org.testcontainers</groupId>
                 <artifactId>postgresql</artifactId>
diff --git a/server/apps/migration/core-data-jpa-to-pg/pom.xml 
b/server/apps/migration/core-data-jpa-to-pg/pom.xml
new file mode 100644
index 0000000000..43ba423b75
--- /dev/null
+++ b/server/apps/migration/core-data-jpa-to-pg/pom.xml
@@ -0,0 +1,267 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+    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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/maven-v4_0_0.xsd";>
+
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.apache.james</groupId>
+        <artifactId>james-server</artifactId>
+        <version>3.9.0-SNAPSHOT</version>
+        <relativePath>../../../pom.xml</relativePath>
+    </parent>
+
+    <artifactId>migration-core-data-jpa-to-pg</artifactId>
+    <packaging>jar</packaging>
+
+    <name>Apache James :: Server :: Binaries :: core data migration tool from 
JPA to PG</name>
+    <description>
+        A tool to help migrating servers from the JPA implementation
+        (regardless of backing database) to the new recommended Postgres
+        implementation. This tool only migrates Core DATA supported by JPA:
+        - Domains
+        - Users
+        - RRT
+        - Mail Repository urls
+        - DropLists
+        - Sieve (quota and scripts)
+    </description>
+
+    <dependencyManagement>
+        <dependencies>
+            <dependency>
+                <groupId>${james.groupId}</groupId>
+                <artifactId>james-server-guice</artifactId>
+                <version>${project.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
+
+    <dependencies>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
+            <artifactId>apache-james-backends-postgres</artifactId>
+            <type>test-jar</type>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
+            <artifactId>james-server-data-jpa</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
+            <artifactId>james-server-guice-common</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
+            <artifactId>james-server-guice-common</artifactId>
+            <type>test-jar</type>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
+            <artifactId>james-server-guice-sieve-postgres</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
+            <artifactId>james-server-jpa-common-guice</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
+            <artifactId>james-server-postgres-common-guice</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
+            <artifactId>james-server-testing</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
+            <artifactId>james-server-util</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
+            <artifactId>mailrepository-blob</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
+            <artifactId>testing-base</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>ch.qos.logback</groupId>
+            <artifactId>logback-classic</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>io.rest-assured</groupId>
+            <artifactId>rest-assured</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.james</groupId>
+            <artifactId>james-server-guice-memory</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.awaitility</groupId>
+            <artifactId>awaitility</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.mariadb.jdbc</groupId>
+            <artifactId>mariadb-java-client</artifactId>
+            <version>2.7.2</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.postgresql</groupId>
+            <artifactId>postgresql</artifactId>
+            <version>42.6.1</version>
+        </dependency>
+        <dependency>
+            <groupId>org.testcontainers</groupId>
+            <artifactId>mariadb</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.testcontainers</groupId>
+            <artifactId>postgresql</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.testcontainers</groupId>
+            <artifactId>testcontainers</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-dependency-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>copy-dependencies</id>
+                        <goals>
+                            <goal>copy-dependencies</goal>
+                        </goals>
+                        <phase>package</phase>
+                        <configuration>
+                            <includeScope>compile</includeScope>
+                            <includeScope>runtime</includeScope>
+                            
<outputDirectory>${project.build.directory}/${project.artifactId}.lib</outputDirectory>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-jar-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>default-jar</id>
+                        <goals>
+                            <goal>jar</goal>
+                        </goals>
+                        <configuration>
+                            <finalName>${project.artifactId}</finalName>
+                            <archive>
+                                <manifest>
+                                    <addClasspath>true</addClasspath>
+                                    
<classpathPrefix>${project.artifactId}.lib/</classpathPrefix>
+                                    
<mainClass>org.apache.james.JpaToPgCoreDataMigration</mainClass>
+                                    
<useUniqueVersions>false</useUniqueVersions>
+                                </manifest>
+                            </archive>
+                        </configuration>
+                    </execution>
+                    <execution>
+                        <id>test-jar</id>
+                        <goals>
+                            <goal>test-jar</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+
+            <plugin>
+                <groupId>com.google.cloud.tools</groupId>
+                <artifactId>jib-maven-plugin</artifactId>
+                <configuration>
+                    <from>
+                        <image>eclipse-temurin:21-jre-jammy</image>
+                    </from>
+                    <to>
+                        <image>apache/james</image>
+                        <tags>
+                            <tag>migration/core-data-jpa-to-pg</tag>
+                        </tags>
+                    </to>
+                    <container>
+                        
<mainClass>org.apache.james.JpaToPgCoreDataMigration</mainClass>
+                        
<extraClasspath>/root/extensions-jars/*</extraClasspath>
+                        <appRoot>/root</appRoot>
+                        <jvmFlags>
+                            
<jvmFlag>-Dlogback.configurationFile=/root/conf/logback.xml</jvmFlag>
+                            <jvmFlag>-Dworking.directory=/root/</jvmFlag>
+                            <!-- Prevents Logjam (CVE-2015-4000) -->
+                            
<jvmFlag>-Djdk.tls.ephemeralDHKeySize=2048</jvmFlag>
+                            
<jvmFlag>-Dextra.props=/root/conf/jvm.properties</jvmFlag>
+                        </jvmFlags>
+                        <creationTime>USE_CURRENT_TIMESTAMP</creationTime>
+                        <volumes>
+                            <volume>/logs</volume>
+                            <volume>/root/conf</volume>
+                        </volumes>
+                    </container>
+                    <extraDirectories>
+                        <paths>
+                            <path>
+                                <from>sample-configuration</from>
+                                <into>/root/conf</into>
+                            </path>
+                            <path>
+                                <from>src/main/scripts</from>
+                                <into>/usr/bin</into>
+                            </path>
+                        </paths>
+                        <permissions>
+                            <permission>
+                                <file>/usr/bin/james-migration</file>
+                                <mode>755</mode>
+                                <!-- Read/write/execute for owner, 
read/execute for group/other -->
+                            </permission>
+                        </permissions>
+                    </extraDirectories>
+                </configuration>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>buildTar</goal>
+                        </goals>
+                        <phase>package</phase>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+</project>
diff --git 
a/server/apps/migration/core-data-jpa-to-pg/sample-configuration/blob.properties
 
b/server/apps/migration/core-data-jpa-to-pg/sample-configuration/blob.properties
new file mode 100644
index 0000000000..3a01ce1e91
--- /dev/null
+++ 
b/server/apps/migration/core-data-jpa-to-pg/sample-configuration/blob.properties
@@ -0,0 +1,66 @@
+# ============================================= BlobStore Implementation 
==================================
+# Read https://james.apache.org/server/config-blobstore.html for further 
details
+
+# Choose your BlobStore implementation
+# Mandatory, allowed values are: file, s3, postgres.
+implementation=postgres
+
+# ========================================= Deduplication 
========================================
+# If you choose to enable deduplication, the mails with the same content will 
be stored only once.
+# Warning: Once this feature is enabled, there is no turning back as turning 
it off will lead to the deletion of all
+# the mails sharing the same content once one is deleted.
+# Mandatory, Allowed values are: true, false
+deduplication.enable=true
+
+# deduplication.family needs to be incremented every time the 
deduplication.generation.duration is changed
+# Positive integer, defaults to 1
+# deduplication.gc.generation.family=1
+
+# Duration of generation.
+# Deduplication only takes place within a singe generation.
+# Only items two generation old can be garbage collected. (This prevent 
concurrent insertions issues and
+# accounts for a clock skew).
+# deduplication.family needs to be incremented everytime this parameter is 
changed.
+# Duration. Default unit: days. Defaults to 30 days.
+# deduplication.gc.generation.duration=30days
+
+# ========================================= Encryption 
========================================
+# If you choose to enable encryption, the blob content will be encrypted 
before storing them in the BlobStore.
+# Warning: Once this feature is enabled, there is no turning back as turning 
it off will lead to all content being
+# encrypted. This comes at a performance impact but presents you from leaking 
data if, for instance the third party
+# offering you a S3 service is compromised.
+# Optional, Allowed values are: true, false, defaults to false
+encryption.aes.enable=false
+
+# Mandatory (if AES encryption is enabled) salt and password. Salt needs to be 
an hexadecimal encoded string
+#encryption.aes.password=xxx
+#encryption.aes.salt=73616c7479
+# Optional, defaults to PBKDF2WithHmacSHA512
+#encryption.aes.private.key.algorithm=PBKDF2WithHmacSHA512
+
+# ============================================ Blobs Exporting 
==============================================
+# Read https://james.apache.org/server/config-blob-export.html for further 
details
+
+# Choosing blob exporting mechanism, allowed mechanism are: localFile, linshare
+# LinShare is a file sharing service, will be explained in the below section
+# Optional, default is localFile
+blob.export.implementation=localFile
+
+# ======================================= Local File Blobs Exporting 
========================================
+# Optional, directory to store exported blob, directory path follows James 
file system format
+# default is file://var/blobExporting
+blob.export.localFile.directory=file://var/blobExporting
+
+# ======================================= LinShare File Blobs Exporting 
========================================
+# LinShare is a sharing service where you can use james, connects to an 
existing LinShare server and shares files to
+# other mail addresses as long as those addresses available in LinShare. For 
example you can deploy James and LinShare
+# sharing the same LDAP repository
+# Mandatory if you choose LinShare, url to connect to LinShare service
+# blob.export.linshare.url=http://linshare:8080
+
+# ======================================= LinShare Configuration 
BasicAuthentication ===================================
+# Authentication is mandatory if you choose LinShare, TechnicalAccount is need 
to connect to LinShare specific service.
+# For Example: It will be formalized to 'Authorization: Basic {Credential of 
UUID/password}'
+
+# blob.export.linshare.technical.account.uuid=Technical_Account_UUID
+# blob.export.linshare.technical.account.password=password
diff --git 
a/server/apps/migration/core-data-jpa-to-pg/sample-configuration/james-database.properties
 
b/server/apps/migration/core-data-jpa-to-pg/sample-configuration/james-database.properties
new file mode 100644
index 0000000000..fbdcb18ca6
--- /dev/null
+++ 
b/server/apps/migration/core-data-jpa-to-pg/sample-configuration/james-database.properties
@@ -0,0 +1,45 @@
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing,
+#  software distributed under the License is distributed on an
+#  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+#  KIND, either express or implied.  See the License for the
+#  specific language governing permissions and limitations
+#  under the License.
+
+#  This template file can be used as example for James Server configuration
+#  DO NOT USE IT AS SUCH AND ADAPT IT TO YOUR NEEDS
+
+# Read 
https://james.apache.org/server/config-system.html#james-database.properties 
for further details
+
+# Use derby as default
+database.driverClassName=org.apache.derby.jdbc.EmbeddedDriver
+database.url=jdbc:derby:../var/store/derby;create=true
+#database.url=${env:DATABASE_URL}
+database.username=app
+#database.username=${env:DATABASE_USERNAME}
+database.password=app
+#database.password=${env:DATABASE_PASSWORD}
+
+# Use streaming for Blobs
+# This is only supported on a limited set of databases atm. You should check 
if its supported by your DB before enable
+# it. 
+# 
+# See:
+# 
http://openjpa.apache.org/builds/latest/docs/manual/ref_guide_mapping_jpa.html  
#7.11.  LOB Streaming 
+# 
+#openjpa.streaming=false
+
+# Validate the data source before using it
+# datasource.testOnBorrow=true
+# datasource.validationQueryTimeoutSec=2
+# This is different per database. See 
https://stackoverflow.com/questions/10684244/dbcp-validationquery-for-different-databases#10684260
+# datasource.validationQuery=select 1
diff --git 
a/server/apps/migration/core-data-jpa-to-pg/sample-configuration/logback.xml 
b/server/apps/migration/core-data-jpa-to-pg/sample-configuration/logback.xml
new file mode 100644
index 0000000000..85c261041b
--- /dev/null
+++ b/server/apps/migration/core-data-jpa-to-pg/sample-configuration/logback.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration>
+
+        <contextListener 
class="ch.qos.logback.classic.jul.LevelChangePropagator">
+                <resetJUL>true</resetJUL>
+        </contextListener>
+
+        <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
+                <encoder>
+                        <pattern>%d{HH:mm:ss.SSS} %highlight([%-5level]) 
%logger{15} - %msg%n%rEx</pattern>
+                        <immediateFlush>false</immediateFlush>
+                </encoder>
+        </appender>
+
+        <appender name="LOG_FILE" 
class="ch.qos.logback.core.rolling.RollingFileAppender">
+                <file>/logs/james.log</file>
+                <rollingPolicy 
class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
+                    
<fileNamePattern>/logs/james.%i.log.tar.gz</fileNamePattern>
+                    <minIndex>1</minIndex>
+                    <maxIndex>3</maxIndex>
+                </rollingPolicy>
+
+                <triggeringPolicy 
class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
+                    <maxFileSize>100MB</maxFileSize>
+                </triggeringPolicy>
+
+                <encoder>
+                        <pattern>%d{HH:mm:ss.SSS} [%-5level] %logger{15} - 
%msg%n%rEx</pattern>
+                        <immediateFlush>false</immediateFlush>
+                </encoder>
+        </appender>
+        <root level="WARN">
+                <appender-ref ref="CONSOLE" />
+                <appender-ref ref="LOG_FILE" />
+        </root>
+
+        <logger name="org.apache.james" level="INFO" />
+
+</configuration>
diff --git 
a/server/apps/migration/core-data-jpa-to-pg/sample-configuration/postgres.properties
 
b/server/apps/migration/core-data-jpa-to-pg/sample-configuration/postgres.properties
new file mode 100644
index 0000000000..99f4eba218
--- /dev/null
+++ 
b/server/apps/migration/core-data-jpa-to-pg/sample-configuration/postgres.properties
@@ -0,0 +1,51 @@
+# String. Optional, default to 'postgres'. Database name.
+database.name=james
+#database.name=${env:POSTGRESQL_DATABASE_NAME}
+
+# String. Optional, default to 'public'. Database schema.
+database.schema=public
+#database.schema=${env:POSTGRESQL_DATABASE_SCHEMA}
+
+# String. Optional, default to 'localhost'. Database host.
+database.host=postgres
+#database.host=${env:POSTGRESQL_HOST}
+
+# Integer. Optional, default to 5432. Database port.
+database.port=5432
+#database.port=${env:POSTGRESQL_PORT}
+
+# String. Required. Database username.
+database.username=james
+#database.username=${env:POSTGRESQL_USERNAME}
+
+# String. Required. Database password of the user.
+database.password=secret1
+#database.password=${env:POSTGRESQL_PASSWORD}
+
+# Boolean. Optional, default to false. Whether to enable row level security.
+row.level.security.enabled=false
+
+# String. It is required when row.level.security.enabled is true. Database 
username with the permission of bypassing RLS.
+#database.by-pass-rls.username=bypassrlsjames
+
+# String. It is required when row.level.security.enabled is true. Database 
password of by-pass-rls user.
+#database.by-pass-rls.password=secret1
+
+# Integer. Optional, default to 10. Database connection pool initial size.
+pool.initial.size=10
+
+# Integer. Optional, default to 15. Database connection pool max size.
+pool.max.size=15
+
+# Integer. Optional, default to 5. rls-bypass database connection pool initial 
size.
+by-pass-rls.pool.initial.size=5
+
+# Integer. Optional, default to 10. rls-bypass database connection pool max 
size.
+by-pass-rls.pool.max.size=10
+
+# String. Optional, defaults to allow. SSLMode required to connect to the 
Postgresql db server.
+# Check 
https://www.postgresql.org/docs/current/libpq-ssl.html#LIBPQ-SSL-PROTECTION for 
a list of supported SSLModes.
+ssl.mode=allow
+
+## Duration. Optional, defaults to 10 second. jOOQ reactive timeout when 
executing Postgres query. This setting prevent jooq reactive bug from causing 
hanging issue.
+#jooq.reactive.timeout=10second
\ No newline at end of file
diff --git 
a/server/apps/migration/core-data-jpa-to-pg/src/main/java/org/apache/james/JpaToPgCoreDataMigration.java
 
b/server/apps/migration/core-data-jpa-to-pg/src/main/java/org/apache/james/JpaToPgCoreDataMigration.java
new file mode 100644
index 0000000000..8590affead
--- /dev/null
+++ 
b/server/apps/migration/core-data-jpa-to-pg/src/main/java/org/apache/james/JpaToPgCoreDataMigration.java
@@ -0,0 +1,504 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james;
+
+import static 
org.apache.james.modules.blobstore.BlobStoreModulesChooser.chooseBlobStoreDAOModule;
+import static 
org.apache.james.modules.blobstore.BlobStoreModulesChooser.chooseEncryptionModule;
+import static 
org.apache.james.modules.blobstore.BlobStoreModulesChooser.chooseStoragePolicyModule;
+
+import java.util.List;
+import java.util.Set;
+import java.util.UUID;
+
+import jakarta.inject.Inject;
+import jakarta.persistence.EntityManagerFactory;
+
+import org.apache.james.backends.postgres.PostgresModule;
+import org.apache.james.core.Username;
+import org.apache.james.domainlist.api.DomainListException;
+import org.apache.james.domainlist.jpa.JPADomainList;
+import org.apache.james.domainlist.lib.DomainListConfiguration;
+import org.apache.james.domainlist.postgres.PostgresDomainList;
+import org.apache.james.droplists.jpa.JPADropList;
+import org.apache.james.droplists.postgres.PostgresDropList;
+import org.apache.james.filesystem.api.FileSystem;
+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.api.MailRepositoryUrlStore;
+import org.apache.james.mailrepository.jpa.JPAMailRepositoryFactory;
+import org.apache.james.mailrepository.jpa.JPAMailRepositoryUrlStore;
+import 
org.apache.james.mailrepository.postgres.PostgresMailRepositoryContentDAO;
+import org.apache.james.mailrepository.postgres.PostgresMailRepositoryUrlStore;
+import org.apache.james.modules.ClockModule;
+import org.apache.james.modules.blobstore.BlobStoreCacheModulesChooser;
+import org.apache.james.modules.blobstore.BlobStoreConfiguration;
+import org.apache.james.modules.data.JPAEntityManagerModule;
+import org.apache.james.modules.data.PostgresCommonModule;
+import org.apache.james.modules.data.PostgresDomainListModule;
+import org.apache.james.modules.data.PostgresDropListsModule;
+import org.apache.james.modules.data.PostgresQuotaGuiceModule;
+import org.apache.james.modules.data.PostgresRecipientRewriteTableModule;
+import org.apache.james.modules.data.PostgresUsersRepositoryModule;
+import org.apache.james.modules.data.SievePostgresRepositoryModules;
+import org.apache.james.modules.server.DNSServiceModule;
+import org.apache.james.modules.server.DropWizardMetricsModule;
+import org.apache.james.rrt.api.RecipientRewriteTableException;
+import org.apache.james.rrt.jpa.JPARecipientRewriteTable;
+import org.apache.james.rrt.postgres.PostgresRecipientRewriteTable;
+import org.apache.james.server.core.configuration.Configuration;
+import org.apache.james.server.core.configuration.ConfigurationProvider;
+import org.apache.james.server.core.configuration.FileConfigurationProvider;
+import org.apache.james.server.core.filesystem.FileSystemImpl;
+import org.apache.james.sieve.jpa.JPASieveRepository;
+import org.apache.james.sieve.jpa.model.JPASieveScript;
+import org.apache.james.sieve.postgres.PostgresSieveRepository;
+import org.apache.james.sieverepository.api.ScriptContent;
+import org.apache.james.sieverepository.api.ScriptName;
+import org.apache.james.user.api.AlreadyExistInUsersRepositoryException;
+import org.apache.james.user.api.UsersRepositoryException;
+import org.apache.james.user.jpa.JPAUsersDAO;
+import org.apache.james.user.jpa.model.JPAUser;
+import org.apache.james.user.lib.model.Algorithm;
+import org.apache.james.user.lib.model.DefaultUser;
+import org.apache.james.user.postgres.PostgresUsersDAO;
+import org.apache.james.user.postgres.PostgresUsersRepositoryConfiguration;
+import org.apache.james.util.LoggingLevel;
+import org.apache.james.utils.InitializationOperation;
+import org.apache.james.utils.InitializationOperations;
+import org.apache.james.utils.InitilizationOperationBuilder;
+import org.apache.james.utils.PropertiesProvider;
+import org.apache.mailet.Mail;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.github.fge.lambdas.Throwing;
+import com.google.common.collect.ImmutableList;
+import com.google.inject.AbstractModule;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.Module;
+import com.google.inject.Provides;
+import com.google.inject.Scopes;
+import com.google.inject.Singleton;
+import com.google.inject.multibindings.Multibinder;
+import com.google.inject.multibindings.ProvidesIntoSet;
+import com.google.inject.util.Modules;
+
+public class JpaToPgCoreDataMigration {
+    private static final Logger LOGGER = 
LoggerFactory.getLogger(JpaToPgCoreDataMigration.class);
+
+    private static final Module JPA_MODULES = Modules.combine(
+            new JPAEntityManagerModule(),
+            new UnboundJPAMigrationModule()
+    );
+    private static final Module POSTGRESQL_MODULES = Modules.combine(
+            new PostgresCommonModule(),
+            new PostgresDomainListModule(),
+            new PostgresRecipientRewriteTableModule(),
+            new PostgresUsersRepositoryModule(),
+            new org.apache.james.modules.data.PostgresMailRepositoryModule(),
+            new PostgresDropListsModule(),
+            new PostgresQuotaGuiceModule(),
+            new SievePostgresRepositoryModules()
+    );
+
+    static final Module MIGRATION_MODULES = Modules.combine(
+            new DNSServiceModule(),
+            new DropWizardMetricsModule(),
+            new ClockModule(),
+            new CoreEntityValidatorsModule(),
+            PostgresUsersRepositoryModule.USER_CONFIGURATION_MODULE,
+            JPA_MODULES,
+            POSTGRESQL_MODULES
+    );
+
+    private final Injector injector;
+
+    public static void main(String[] args) {
+        MigrationConfiguration configuration = MigrationConfiguration.builder()
+                .useWorkingDirectoryEnvProperty()
+                .build();
+        LOGGER.info("Loading configuration {}", configuration.toString());
+        var blobstoreModule  = 
Modules.combine(chooseBlobStoreModules(configuration));
+        var module = Modules.combine(
+                new MigrationModule(configuration),
+                MIGRATION_MODULES,
+                blobstoreModule
+        );
+
+
+        JpaToPgCoreDataMigration migration = new 
JpaToPgCoreDataMigration(module);
+        migration.start();
+    }
+
+    static List<Module> chooseBlobStoreModules(MigrationConfiguration 
configuration) {
+        ImmutableList.Builder<Module> builder = ImmutableList.<Module>builder()
+                .addAll(chooseModules(configuration.blobStoreConfiguration()))
+                .add(new BlobStoreCacheModulesChooser.CacheDisabledModule());
+
+
+        return builder.build();
+    }
+
+    public static List<Module> chooseModules(BlobStoreConfiguration 
choosingConfiguration) {
+        return ImmutableList.<Module>builder()
+                
.add(chooseEncryptionModule(choosingConfiguration.getCryptoConfig()))
+                
.add(chooseBlobStoreDAOModule(choosingConfiguration.getImplementation()))
+                
.addAll(chooseStoragePolicyModule(choosingConfiguration.storageStrategy()))
+                .add(binder -> 
binder.bind(BlobStoreConfiguration.class).toInstance(choosingConfiguration))
+                .build();
+    }
+
+    static class DomainMigration {
+        private static final Logger LOGGER = 
LoggerFactory.getLogger(DomainMigration.class);
+
+        private final JPADomainList jpaDomainList;
+        private final PostgresDomainList pgDomainList;
+
+        @Inject
+        DomainMigration(
+                JPADomainList jpaDomainList,
+                PostgresDomainList pgDomainList
+        ) {
+            this.jpaDomainList = jpaDomainList;
+            this.pgDomainList = pgDomainList;
+        }
+
+        void doMigration() {
+            LOGGER.info("Start domains migration");
+            try {
+                jpaDomainList.getDomains().forEach(domain -> {
+                            try {
+                                pgDomainList.addDomain(domain);
+                            } catch (DomainListException e) {
+                                if (!e.getMessage().contains("already 
exists.")) {
+                                    LOGGER.warn("Failed to migrate domain {}", 
domain, e);
+                                }
+                            }
+                        }
+                );
+
+            } catch (DomainListException e) {
+                throw new RuntimeException("Unable to migrate domains, 
aborting processing", e);
+            }
+            LOGGER.info("Domains migration completed");
+        }
+    }
+
+    static class UsersMigration {
+        private static final Logger LOGGER = 
LoggerFactory.getLogger(UsersMigration.class);
+        private final Algorithm algorithm;
+        private final JPAUsersDAO jpaUsersDAO;
+        private final PostgresUsersDAO postgresUsersDAO;
+
+
+        @Inject
+        UsersMigration(
+                JPAUsersDAO jpaUsersDAO,
+                PostgresUsersDAO postgresUsersDAO,
+                PostgresUsersRepositoryConfiguration 
postgresUsersRepositoryConfiguration
+        ) {
+            this.algorithm = 
postgresUsersRepositoryConfiguration.getPreferredAlgorithm();
+            this.jpaUsersDAO = jpaUsersDAO;
+            this.postgresUsersDAO = postgresUsersDAO;
+        }
+
+        void doMigration() {
+            LOGGER.info("Start users migration");
+            try {
+                jpaUsersDAO.list().forEachRemaining(username -> {
+                            try {
+                                
jpaUsersDAO.getUserByName(username).ifPresent(user -> {
+                                    var jpaUser = (JPAUser) user;
+                                    var pgUser = new DefaultUser(
+                                            username,
+                                            jpaUser.getPasswordHash(),
+                                            jpaUser.getAlgorithm(),
+                                            algorithm
+                                    );
+                                    try {
+                                        postgresUsersDAO.addUser(username, 
UUID.randomUUID().toString());
+                                    } catch (RuntimeException e) {
+                                        if (!(e.getCause() instanceof 
AlreadyExistInUsersRepositoryException)) {
+                                            throw e;
+                                        }
+                                        // Idempotent behavior, if the user 
already exists, we
+                                        // only update it.
+                                    }
+                                    Throwing.runnable(() ->
+                                            postgresUsersDAO.updateUser(pgUser)
+                                    ).sneakyThrow().run();
+
+                                });
+                            } catch (UsersRepositoryException e) {
+                                LOGGER.warn("Failed to migrate user {}", 
username, e);
+                            }
+                        }
+                );
+            } catch (UsersRepositoryException e) {
+                throw new RuntimeException("Failed to retrieve users for 
migration", e);
+            }
+            LOGGER.info("Users migration complete");
+        }
+    }
+
+    static class RRTMigration {
+        private static final Logger LOGGER = 
LoggerFactory.getLogger(RRTMigration.class);
+
+        private final JPARecipientRewriteTable jpaRRTRepository;
+        private final PostgresRecipientRewriteTable pgRRTRepository;
+
+        @Inject
+        RRTMigration(
+                JPARecipientRewriteTable jpaRRTRepository,
+                PostgresRecipientRewriteTable pgRRTRepository
+        ) {
+            this.jpaRRTRepository = jpaRRTRepository;
+            this.pgRRTRepository = pgRRTRepository;
+        }
+
+        void doMigration() {
+            LOGGER.info("Start RRT migration");
+            try {
+                jpaRRTRepository.getAllMappings().forEach((mappingSource, 
mappings) ->
+                        mappings.forEach(mapping ->
+                                pgRRTRepository.addMapping(mappingSource, 
mapping)
+                        )
+                );
+            } catch (RecipientRewriteTableException e) {
+                throw new RuntimeException(e);
+            }
+            LOGGER.info("RRT migration complete");
+        }
+    }
+
+    static class MailRepositoryMigration {
+        private static final Logger LOGGER = 
LoggerFactory.getLogger(MailRepositoryMigration.class);
+
+        private final JPAMailRepositoryUrlStore jpaMailRepositoryUrlStore;
+        private final JPAMailRepositoryFactory jpaMailRepositoryFactory;
+        private final PostgresMailRepositoryUrlStore pgMailRepositoryUrlStore;
+        private final PostgresMailRepositoryContentDAO 
postgresMailRepositoryContentDAO;
+
+        @Inject
+        MailRepositoryMigration(
+                JPAMailRepositoryUrlStore jpaMailRepositoryUrlStore,
+                JPAMailRepositoryFactory jpaMailRepositoryFactory,
+                PostgresMailRepositoryUrlStore pgMailRepositoryUrlStore,
+                PostgresMailRepositoryContentDAO 
postgresMailRepositoryContentDAO
+        ) {
+            this.jpaMailRepositoryUrlStore = jpaMailRepositoryUrlStore;
+            this.jpaMailRepositoryFactory = jpaMailRepositoryFactory;
+            this.pgMailRepositoryUrlStore = pgMailRepositoryUrlStore;
+            this.postgresMailRepositoryContentDAO = 
postgresMailRepositoryContentDAO;
+        }
+
+        void doMigration() {
+            LOGGER.info("Start mail repository urls migration");
+            jpaMailRepositoryUrlStore.listDistinct().forEach(
+                    Throwing.consumer((MailRepositoryUrl url) -> {
+                        pgMailRepositoryUrlStore.add(url);
+                        MailRepository mailRepository = 
jpaMailRepositoryFactory.create(url);
+                        mailRepository.list().forEachRemaining(
+                                Throwing.consumer((MailKey mailKey) -> {
+                                    Mail jpaMail = 
mailRepository.retrieve(mailKey);
+                                    
postgresMailRepositoryContentDAO.store(jpaMail, url);
+                                })
+                        );
+                    }).sneakyThrow()
+            );
+            LOGGER.info("Mail repository urls migration complete");
+        }
+    }
+
+    static class DropListMigration {
+        private static final Logger LOGGER =
+                LoggerFactory.getLogger(MailRepositoryMigration.class);
+
+        private final JPADropList jpaDropList;
+        private final PostgresDropList pgDropList;
+
+        @Inject
+        DropListMigration(
+                JPADropList jpaDropList,
+                PostgresDropList pgDropList
+        ) {
+            this.jpaDropList = jpaDropList;
+            this.pgDropList = pgDropList;
+        }
+
+        void doMigration() {
+            LOGGER.info("Start drop list migration");
+            jpaDropList.listAll().flatMap(pgDropList::add).count().block();
+            LOGGER.info("Mail drop list migration complete");
+        }
+    }
+
+    static class SieveMigration {
+        private static final Logger LOGGER =
+                LoggerFactory.getLogger(MailRepositoryMigration.class);
+
+        private final JPASieveRepository jpaSieveRepository;
+        private final PostgresSieveRepository pgSieveRepository;
+
+        @Inject
+        SieveMigration(
+                JPASieveRepository jpaSieveRepository,
+                PostgresSieveRepository pgSieveRepository
+        ) {
+            this.jpaSieveRepository = jpaSieveRepository;
+            this.pgSieveRepository = pgSieveRepository;
+        }
+
+        void doMigration() {
+            LOGGER.info("Start Sieve quotas migration");
+            jpaSieveRepository.listAllSieveQuotas().forEach(quota ->
+                    pgSieveRepository.setQuota(quota.getUsername(), 
quota.toQuotaSize())
+            );
+            LOGGER.info("Mail Sieve quotas migration complete");
+            LOGGER.info("Start Sieve scripts migration");
+            jpaSieveRepository.listAllSieveScripts().forEach(
+                    Throwing.consumer((JPASieveScript sieveScript) -> 
pgSieveRepository.putScript(
+                            Username.of(sieveScript.getUsername()),
+                            new ScriptName(sieveScript.getScriptName()),
+                            new ScriptContent(sieveScript.getScriptContent())
+                    )).sneakyThrow()
+            );
+            LOGGER.info("Mail Sieve scripts migration complete");
+        }
+    }
+
+    static class MigrationModule extends AbstractModule {
+        private final Configuration configuration;
+        private final FileSystemImpl fileSystem;
+
+        @Inject
+        public MigrationModule(Configuration configuration) {
+            this.configuration = configuration;
+            this.fileSystem = new FileSystemImpl(configuration.directories());
+        }
+
+        @Override
+        protected void configure() {
+            bind(FileSystem.class).toInstance(fileSystem);
+            bind(Configuration.class).toInstance(configuration);
+
+            bind(ConfigurationProvider.class).toInstance(new 
FileConfigurationProvider(fileSystem, configuration));
+            bind(DomainMigration.class).in(Scopes.SINGLETON);
+            bind(UsersMigration.class).in(Scopes.SINGLETON);
+            bind(RRTMigration.class).in(Scopes.SINGLETON);
+            bind(MailRepositoryMigration.class).in(Scopes.SINGLETON);
+        }
+
+        @Provides
+        @jakarta.inject.Singleton
+        public Configuration.ConfigurationPath configurationPath() {
+            return configuration.configurationPath();
+        }
+
+        @Provides
+        @jakarta.inject.Singleton
+        public PropertiesProvider providePropertiesProvider(FileSystem 
fileSystem, Configuration.ConfigurationPath configurationPrefix) {
+            return new PropertiesProvider(fileSystem, configurationPrefix);
+        }
+
+        @Provides
+        @Singleton
+        DomainListConfiguration domainListConfiguration() {
+            return DomainListConfiguration.DEFAULT;
+        }
+    }
+
+    static class UnboundJPAMigrationModule extends AbstractModule {
+        @Override
+        protected void configure() {
+            bind(JPADomainList.class).in(Scopes.SINGLETON);
+            bind(JPARecipientRewriteTable.class).in(Scopes.SINGLETON);
+            bind(JPAMailRepositoryUrlStore.class).in(Scopes.SINGLETON);
+            bind(JPAUsersDAO.class).in(Scopes.SINGLETON);
+        }
+
+        @ProvidesIntoSet
+        InitializationOperation configureDomainList(DomainListConfiguration 
configuration, JPADomainList jpaDomainList) {
+            return InitilizationOperationBuilder
+                    .forClass(JPADomainList.class)
+                    .init(() -> jpaDomainList.configure(configuration));
+        }
+
+        @ProvidesIntoSet
+        InitializationOperation configureJpaUsers(
+                EntityManagerFactory entityManagerFactory,
+                ConfigurationProvider configurationProvider,
+                JPAUsersDAO usersDAO
+        ) {
+            return InitilizationOperationBuilder
+                    .forClass(JPAUsersDAO.class)
+                    .init(() -> {
+                        
usersDAO.configure(configurationProvider.getConfiguration("usersrepository", 
LoggingLevel.INFO));
+                        usersDAO.setEntityManagerFactory(entityManagerFactory);
+                        usersDAO.init();
+                    });
+        }
+    }
+
+    static class CoreEntityValidatorsModule extends AbstractModule {
+        @Override
+        protected void configure() {
+            Multibinder.newSetBinder(binder(), 
UserEntityValidator.class).addBinding().to(DefaultUserEntityValidator.class);
+            Multibinder.newSetBinder(binder(), 
UserEntityValidator.class).addBinding().to(RecipientRewriteTableUserEntityValidator.class);
+        }
+
+        @Provides
+        @Singleton
+        UserEntityValidator userEntityValidator(Set<UserEntityValidator> 
validatorSet) {
+            return new AggregateUserEntityValidator(validatorSet);
+        }
+    }
+
+    static class PostgresMailRepositoryModule extends AbstractModule {
+        @Override
+        protected void configure() {
+            bind(PostgresMailRepositoryUrlStore.class).in(Scopes.SINGLETON);
+            
bind(MailRepositoryUrlStore.class).to(PostgresMailRepositoryUrlStore.class);
+            Multibinder.newSetBinder(binder(), PostgresModule.class)
+                    
.addBinding().toInstance(org.apache.james.mailrepository.postgres.PostgresMailRepositoryModule.MODULE);
+        }
+    }
+
+    Injector getInjector() {
+        return injector;
+    }
+
+    public JpaToPgCoreDataMigration(Module module) {
+        this.injector = Guice.createInjector(module);
+        injector.getInstance(InitializationOperations.class).initModules();
+    }
+
+    void start() {
+        injector.getInstance(DomainMigration.class).doMigration();
+        injector.getInstance(UsersMigration.class).doMigration();
+        injector.getInstance(RRTMigration.class).doMigration();
+        injector.getInstance(MailRepositoryMigration.class).doMigration();
+        injector.getInstance(DropListMigration.class).doMigration();
+        injector.getInstance(SieveMigration.class).doMigration();
+    }
+}
+
diff --git 
a/server/apps/migration/core-data-jpa-to-pg/src/main/java/org/apache/james/MigrationConfiguration.java
 
b/server/apps/migration/core-data-jpa-to-pg/src/main/java/org/apache/james/MigrationConfiguration.java
new file mode 100644
index 0000000000..00f108cdfa
--- /dev/null
+++ 
b/server/apps/migration/core-data-jpa-to-pg/src/main/java/org/apache/james/MigrationConfiguration.java
@@ -0,0 +1,124 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james;
+
+import java.io.File;
+import java.util.Optional;
+
+import org.apache.james.filesystem.api.FileSystem;
+import org.apache.james.filesystem.api.JamesDirectoriesProvider;
+import org.apache.james.modules.blobstore.BlobStoreConfiguration;
+import org.apache.james.server.core.JamesServerResourceLoader;
+import org.apache.james.server.core.MissingArgumentException;
+import org.apache.james.server.core.configuration.Configuration;
+import org.apache.james.server.core.filesystem.FileSystemImpl;
+import org.apache.james.utils.PropertiesProvider;
+
+import com.github.fge.lambdas.Throwing;
+import com.google.common.base.MoreObjects;
+
+public record MigrationConfiguration(
+        ConfigurationPath configurationPath,
+        JamesDirectoriesProvider directories,
+        BlobStoreConfiguration blobStoreConfiguration
+) implements Configuration {
+    private static final BlobStoreConfiguration.BlobStoreImplName 
DEFAULT_BLOB_STORE = BlobStoreConfiguration.BlobStoreImplName.POSTGRES;
+
+    public static class Builder {
+        private Optional<String> rootDirectory;
+        private Optional<ConfigurationPath> configurationPath;
+        private Optional<BlobStoreConfiguration> blobStoreConfiguration;
+
+        private Builder() {
+            rootDirectory = Optional.empty();
+            configurationPath = Optional.empty();
+        }
+
+        public Builder workingDirectory(String path) {
+            rootDirectory = Optional.of(path);
+            return this;
+        }
+
+        public Builder workingDirectory(File file) {
+            rootDirectory = Optional.of(file.getAbsolutePath());
+            return this;
+        }
+
+        public Builder useWorkingDirectoryEnvProperty() {
+            rootDirectory = 
Optional.ofNullable(System.getProperty(WORKING_DIRECTORY));
+            if (rootDirectory.isEmpty()) {
+                throw new MissingArgumentException("Server needs a 
working.directory env entry");
+            }
+            return this;
+        }
+
+        public Builder configurationPath(ConfigurationPath path) {
+            configurationPath = Optional.of(path);
+            return this;
+        }
+
+        public Builder configurationFromClasspath() {
+            configurationPath = Optional.of(new 
ConfigurationPath(FileSystem.CLASSPATH_PROTOCOL));
+            return this;
+        }
+
+
+        public MigrationConfiguration build() {
+            ConfigurationPath configurationPath = 
this.configurationPath.orElse(new 
ConfigurationPath(FileSystem.FILE_PROTOCOL_AND_CONF));
+            JamesServerResourceLoader directories = new 
JamesServerResourceLoader(rootDirectory
+                    .orElseThrow(() -> new MissingArgumentException("Server 
needs a working.directory env entry")));
+            FileSystemImpl fileSystem = new FileSystemImpl(directories);
+            PropertiesProvider propertiesProvider = new 
PropertiesProvider(fileSystem, configurationPath);
+            BlobStoreConfiguration blobStoreConfiguration = 
this.blobStoreConfiguration.orElseGet(
+                    Throwing.supplier(
+                            () -> 
BlobStoreConfiguration.parse(propertiesProvider, DEFAULT_BLOB_STORE)
+                    )
+            );
+            return new MigrationConfiguration(configurationPath, directories, 
blobStoreConfiguration);
+        }
+
+        public Builder blobStore(BlobStoreConfiguration 
blobStoreConfiguration) {
+            this.blobStoreConfiguration = Optional.of(blobStoreConfiguration);
+            return this;
+        }
+    }
+
+    static MigrationConfiguration.Builder builder() {
+        return new Builder();
+    }
+
+    @Override
+    public ConfigurationPath configurationPath() {
+        return configurationPath;
+    }
+
+    @Override
+    public JamesDirectoriesProvider directories() {
+        return directories;
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(this)
+                .add("configurationPath", configurationPath)
+                .add("directories", directories)
+                .toString();
+    }
+}
diff --git 
a/server/apps/migration/core-data-jpa-to-pg/src/main/resources/META-INF/persistence.xml
 
b/server/apps/migration/core-data-jpa-to-pg/src/main/resources/META-INF/persistence.xml
new file mode 100644
index 0000000000..068e05e04b
--- /dev/null
+++ 
b/server/apps/migration/core-data-jpa-to-pg/src/main/resources/META-INF/persistence.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+
+<persistence xmlns="http://java.sun.com/xml/ns/persistence";
+             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+             xsi:schemaLocation="http://java.sun.com/xml/ns/persistence 
http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd";
+             version="2.0">
+
+    <persistence-unit name="Global" transaction-type="RESOURCE_LOCAL">
+        <class>org.apache.james.domainlist.jpa.model.JPADomain</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.droplists.jpa.model.JPADropListEntry</class>
+        <class>org.apache.james.sieve.jpa.model.JPASieveScript</class>
+        <class>org.apache.james.sieve.jpa.model.JPASieveQuota</class>
+        <properties>
+            <property name="openjpa.jdbc.SynchronizeMappings" 
value="buildSchema(ForeignKeys=true)"/>
+            <property name="openjpa.jdbc.MappingDefaults" 
value="ForeignKeyDeleteAction=cascade, JoinForeignKeyDeleteAction=cascade"/>
+            <property name="openjpa.jdbc.SchemaFactory" 
value="native(ForeignKeys=true)"/>
+            <property name="openjpa.jdbc.QuerySQLCache" value="false"/>
+        </properties>
+
+    </persistence-unit>
+
+</persistence>
diff --git 
a/server/apps/migration/core-data-jpa-to-pg/src/main/scripts/james-migration 
b/server/apps/migration/core-data-jpa-to-pg/src/main/scripts/james-migration
new file mode 100644
index 0000000000..b4bebfb9cc
--- /dev/null
+++ b/server/apps/migration/core-data-jpa-to-pg/src/main/scripts/james-migration
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+
+unset JAVA_TOOL_OPTIONS
+java -cp /root/resources:/root/classes:/root/libs/* \
+-Dworking.directory=/root/conf \
+-Dlogback.configurationFile=/root/conf/logback.xml \
+org.apache.james.JpaToPgCoreDataMigration "$@"
\ No newline at end of file
diff --git 
a/server/apps/migration/core-data-jpa-to-pg/src/test/java/org/apache/james/JpaToPgCoreDataMigrationTest.java
 
b/server/apps/migration/core-data-jpa-to-pg/src/test/java/org/apache/james/JpaToPgCoreDataMigrationTest.java
new file mode 100644
index 0000000000..6993f9481b
--- /dev/null
+++ 
b/server/apps/migration/core-data-jpa-to-pg/src/test/java/org/apache/james/JpaToPgCoreDataMigrationTest.java
@@ -0,0 +1,354 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james;
+
+import static java.util.UUID.randomUUID;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.SoftAssertions.assertSoftly;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.time.temporal.ChronoUnit;
+import java.util.Date;
+import java.util.List;
+import java.util.UUID;
+
+import jakarta.mail.MessagingException;
+import jakarta.mail.internet.AddressException;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.RandomUtils;
+import org.apache.james.backends.postgres.PostgresExtension;
+import org.apache.james.core.Domain;
+import org.apache.james.core.Username;
+import org.apache.james.core.builder.MimeMessageBuilder;
+import org.apache.james.core.quota.QuotaSizeLimit;
+import org.apache.james.domainlist.api.DomainListException;
+import org.apache.james.domainlist.jpa.JPADomainList;
+import org.apache.james.domainlist.postgres.PostgresDomainList;
+import org.apache.james.droplists.api.DropListEntry;
+import org.apache.james.droplists.api.OwnerScope;
+import org.apache.james.droplists.jpa.JPADropList;
+import org.apache.james.droplists.postgres.PostgresDropList;
+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.JPAMailRepositoryFactory;
+import org.apache.james.mailrepository.jpa.JPAMailRepositoryUrlStore;
+import org.apache.james.mailrepository.postgres.PostgresMailRepositoryFactory;
+import org.apache.james.mailrepository.postgres.PostgresMailRepositoryUrlStore;
+import org.apache.james.modules.blobstore.BlobStoreConfiguration;
+import org.apache.james.rrt.api.RecipientRewriteTable;
+import org.apache.james.rrt.api.RecipientRewriteTableException;
+import org.apache.james.rrt.jpa.JPARecipientRewriteTable;
+import org.apache.james.rrt.lib.Mapping;
+import org.apache.james.rrt.lib.MappingSource;
+import org.apache.james.rrt.postgres.PostgresRecipientRewriteTable;
+import org.apache.james.server.core.MailImpl;
+import org.apache.james.sieve.jpa.JPASieveRepository;
+import org.apache.james.sieve.postgres.PostgresSieveRepository;
+import org.apache.james.sieverepository.api.ScriptContent;
+import org.apache.james.sieverepository.api.ScriptName;
+import org.apache.james.sieverepository.api.exception.QuotaExceededException;
+import org.apache.james.sieverepository.api.exception.QuotaNotFoundException;
+import org.apache.james.sieverepository.api.exception.ScriptNotFoundException;
+import org.apache.james.sieverepository.api.exception.StorageException;
+import org.apache.james.user.api.UsersRepositoryException;
+import org.apache.james.user.jpa.JPAUsersDAO;
+import org.apache.james.user.postgres.PostgresUsersDAO;
+import org.apache.mailet.Attribute;
+import org.apache.mailet.Mail;
+import org.jetbrains.annotations.NotNull;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.junit.rules.TemporaryFolder;
+
+import com.github.fge.lambdas.Throwing;
+import com.google.inject.Injector;
+import com.google.inject.util.Modules;
+
+class JpaToPgCoreDataMigrationTest {
+
+    @RegisterExtension
+    static MariaDBExtension mariaDBExtension = new MariaDBExtension();
+
+    @RegisterExtension
+    static PostgresExtension postgresExtension = PostgresExtension.empty();
+
+    @RegisterExtension
+    static TemporaryFolderRegistrableExtension folderRegistrableExtension = 
new TemporaryFolderRegistrableExtension();
+
+    record User(Username username, String password) {
+        static User create(Domain domain) {
+            return new User(
+                    Username.of(randomUUID() + "@" + domain.asString()),
+                    randomUUID().toString()
+            );
+        }
+    }
+
+    record RecipientRewrite(Username username, Mapping mapping) {
+        static RecipientRewrite create(Domain domain) {
+            return new RecipientRewrite(
+                    Username.of(randomUUID() + "@" + domain.asString()),
+                    Mapping.forward(randomUUID() + "@email.example")
+            );
+        }
+    }
+
+    record MailInRepository(MailRepositoryUrl url, Mail mail) {
+        static MailInRepository create(Domain domain, MailRepositoryUrl url) 
throws MessagingException {
+            return new MailInRepository(
+                    url,
+                    MailImpl.builder()
+                            .name(UUID.randomUUID().toString())
+                            .sender(randomUUID() + "@" + domain.asString())
+                            .addRecipient(randomUUID() + "@" + 
domain.asString())
+                            .addRecipient(randomUUID() + "@" + 
domain.asString())
+                            .addAttribute(Attribute.convertToAttribute("attr" 
+ randomUUID(), "value" + randomUUID()))
+                            .mimeMessage(MimeMessageBuilder
+                                    .mimeMessageBuilder()
+                                    .addHeader(UUID.randomUUID().toString(), 
UUID.randomUUID().toString())
+                                    .setSubject("test")
+                                    .setText("test" + UUID.randomUUID())
+                                    .build())
+                            .state(Mail.DEFAULT)
+                            .lastUpdated(new Date())
+                            .build()
+            );
+        }
+    }
+
+    record SieveEntry(Username username, QuotaSizeLimit quota, ScriptName 
scriptName, ScriptContent scriptContent) {
+        static SieveEntry create(Domain domain) {
+            return new SieveEntry(
+                    Username.of(randomUUID() + "@" + domain.asString()),
+                    QuotaSizeLimit.size(RandomUtils.nextLong()),
+                    new ScriptName(UUID.randomUUID().toString()),
+                    new ScriptContent(UUID.randomUUID().toString())
+            );
+        }
+    }
+
+    private static @NotNull Domain someDomain() {
+        return Domain.of(randomUUID() + ".example");
+    }
+
+    private JpaToPgCoreDataMigration dataMigration;
+    private Injector injector;
+
+    @BeforeEach
+    void setUp() throws IOException {
+        dataMigration = createDataMigration();
+        injector = dataMigration.getInjector();
+    }
+
+    @Test
+    void migrate_data() throws Exception {
+        // Given
+        var domain = givenJpaDomain();
+        var user = givenJpaUser(domain);
+        var recipientRewrite = givenJpaRecipientRewrite(domain);
+        var mailRepositoryUrl = givenJpaMailRepositoryUrl();
+        var mail = givenJpaMailInRepository(domain, mailRepositoryUrl);
+        var dropListEntry = givenJpaDropList();
+        var sieve = givenJpaSieveEntry(domain);
+
+        // When
+        dataMigration.start();
+
+        // Then
+        verifyPgDomain(domain);
+        verifyPgUser(user);
+        verifyPgRecipientRewrite(recipientRewrite);
+        verifyPgRepositoryUrl(mailRepositoryUrl);
+        verifyPgMailInRepository(mail);
+        verifyPgDropList(dropListEntry);
+        verifyPgSieveEntry(sieve);
+    }
+
+    private Domain givenJpaDomain() throws DomainListException {
+        var domain = Domain.of(randomUUID() + ".apache.example");
+        var jpaDomainList = injector.getInstance(JPADomainList.class);
+        jpaDomainList.addDomain(domain);
+        return domain;
+    }
+
+    private void verifyPgDomain(Domain domain) throws DomainListException {
+        var domainList = injector.getInstance(PostgresDomainList.class);
+        assertThat(domainList.containsDomain(domain)).isTrue();
+    }
+
+    private User givenJpaUser(Domain domain) throws UsersRepositoryException {
+        var user = User.create(domain);
+        var usersDAO = injector.getInstance(JPAUsersDAO.class);
+        usersDAO.addUser(user.username, user.password);
+        return user;
+    }
+
+    private void verifyPgUser(User user) {
+        var usersDAO = injector.getInstance(PostgresUsersDAO.class);
+        var pgUser = usersDAO.getUserByName(user.username).orElseThrow();
+        assertThat(pgUser.verifyPassword(user.password)).isTrue();
+    }
+
+
+    private RecipientRewrite givenJpaRecipientRewrite(Domain domain)
+            throws RecipientRewriteTableException {
+        var recipientRewrite = RecipientRewrite.create(domain);
+        var recipientRewriteTable = 
injector.getInstance(JPARecipientRewriteTable.class);
+        recipientRewriteTable.addMapping(
+                MappingSource.fromUser(recipientRewrite.username),
+                recipientRewrite.mapping
+        );
+        return recipientRewrite;
+    }
+
+    private void verifyPgRecipientRewrite(RecipientRewrite recipientRewrite)
+            throws RecipientRewriteTable.ErrorMappingException, 
RecipientRewriteTableException {
+        var rewriteTable = 
injector.getInstance(PostgresRecipientRewriteTable.class);
+        var resolvedMappings = rewriteTable.getResolvedMappings(
+                recipientRewrite.username.getLocalPart(),
+                recipientRewrite.username.getDomainPart().orElseThrow()
+        );
+        assertThat(resolvedMappings).contains(recipientRewrite.mapping);
+    }
+
+    private MailRepositoryUrl givenJpaMailRepositoryUrl() {
+        var url = MailRepositoryUrl.from("blob://var/mail/" + randomUUID());
+        var urlStore = injector.getInstance(JPAMailRepositoryUrlStore.class);
+        urlStore.add(url);
+        return url;
+    }
+
+    private void verifyPgRepositoryUrl(MailRepositoryUrl url) {
+        var urlStore = 
injector.getInstance(PostgresMailRepositoryUrlStore.class);
+        assertThat(urlStore.contains(url)).isTrue();
+    }
+
+    private MailInRepository givenJpaMailInRepository(Domain domain, 
MailRepositoryUrl url) throws MessagingException {
+        MailInRepository mailInRepository = MailInRepository.create(domain, 
url);
+        var mail = mailInRepository.mail;
+        var mailRepositoryFactory = 
injector.getInstance(JPAMailRepositoryFactory.class);
+        MailRepository mailRepository = mailRepositoryFactory.create(url);
+        mailRepository.store(mail);
+        return mailInRepository;
+    }
+
+    private void checkMailEquality(Mail actual, Mail expected) {
+        assertSoftly(Throwing.consumer(softly -> {
+            
softly.assertThat(actual.getMessage().getContent()).isEqualTo(expected.getMessage().getContent());
+            
softly.assertThat(actual.getMessageSize()).isEqualTo(expected.getMessageSize());
+            softly.assertThat(actual.getName()).isEqualTo(expected.getName());
+            
softly.assertThat(actual.getState()).isEqualTo(expected.getState());
+            
softly.assertThat(actual.attributes()).containsAll(expected.attributes().toList());
+            
softly.assertThat(actual.getErrorMessage()).isEqualTo(expected.getErrorMessage());
+            
softly.assertThat(actual.getRemoteHost()).isEqualTo(expected.getRemoteHost());
+            
softly.assertThat(actual.getRemoteAddr()).isEqualTo(expected.getRemoteAddr());
+            // JPA implementation trucates the date to seconds losing 
precision in the process
+            
softly.assertThat(actual.getLastUpdated().toInstant()).isEqualTo(expected.getLastUpdated().toInstant().truncatedTo(ChronoUnit.SECONDS));
+            
softly.assertThat(actual.getPerRecipientSpecificHeaders()).isEqualTo(expected.getPerRecipientSpecificHeaders());
+        }));
+    }
+
+
+    private void verifyPgMailInRepository(MailInRepository mailInRepository) 
throws MessagingException {
+        var mailRepositoryFactory = 
injector.getInstance(PostgresMailRepositoryFactory.class);
+        var mailRepository = 
mailRepositoryFactory.create(mailInRepository.url);
+
+        Mail retrieved = 
mailRepository.retrieve(MailKey.forMail(mailInRepository.mail));
+        assertThat(retrieved).satisfies(actual -> checkMailEquality(actual, 
mailInRepository.mail));
+    }
+
+    private DropListEntry givenJpaDropList() throws AddressException {
+        Domain aDomain = someDomain();
+        Domain anotherDomain = someDomain();
+        DropListEntry entry = DropListEntry.builder()
+                .denyDomain(aDomain)
+                
.denyAddress(User.create(anotherDomain).username.asMailAddress())
+                .forAll()
+                .build();
+        var dropList = injector.getInstance(JPADropList.class);
+        dropList.add(entry).block();
+        return entry;
+    }
+
+    private void verifyPgDropList(DropListEntry entry) {
+        var dropList = injector.getInstance(PostgresDropList.class);
+        List<DropListEntry> entries =
+                dropList.list(OwnerScope.GLOBAL, "").collectList().block();
+        assertThat(entries).contains(entry);
+    }
+
+    private SieveEntry givenJpaSieveEntry(Domain domain)
+            throws StorageException, QuotaExceededException {
+        var sieveEntry = SieveEntry.create(domain);
+        var sieveRepository = injector.getInstance(JPASieveRepository.class);
+        sieveRepository.setQuota(sieveEntry.username, sieveEntry.quota);
+        sieveRepository.putScript(
+                sieveEntry.username,
+                sieveEntry.scriptName,
+                sieveEntry.scriptContent
+        );
+        return sieveEntry;
+    }
+
+    private void verifyPgSieveEntry(SieveEntry sieveEntry)
+            throws QuotaNotFoundException, ScriptNotFoundException {
+        var sieveRepository = 
injector.getInstance(PostgresSieveRepository.class);
+        var quota = sieveRepository.getQuota(sieveEntry.username);
+        var scripts = sieveRepository.listScripts(sieveEntry.username);
+        var script = sieveRepository.getScript(sieveEntry.username, 
sieveEntry.scriptName);
+
+        assertThat(quota).isEqualTo(sieveEntry.quota);
+        assertThat(scripts).hasSize(1);
+        assertThat(script).hasSameContentAs(
+                IOUtils.toInputStream(sieveEntry.scriptContent.getValue(), 
StandardCharsets.UTF_8)
+        );
+    }
+
+    private @NotNull JpaToPgCoreDataMigration createDataMigration() throws 
IOException {
+        TemporaryFolder tmpDir = 
folderRegistrableExtension.getTemporaryFolder();
+        MigrationConfiguration configuration = MigrationConfiguration.builder()
+                .workingDirectory(tmpDir.newFolder())
+                .configurationFromClasspath()
+                .blobStore(BlobStoreConfiguration.builder()
+                        .postgres()
+                        .disableCache()
+                        .passthrough()
+                        .noCryptoConfig())
+                .build();
+
+        var blobstoreModule = 
Modules.combine(JpaToPgCoreDataMigration.chooseBlobStoreModules(configuration));
+        var module = Modules.override(
+                Modules.combine(
+                        JpaToPgCoreDataMigration.MIGRATION_MODULES,
+                        blobstoreModule
+                )
+        ).with(
+                new JpaToPgCoreDataMigration.MigrationModule(configuration),
+                mariaDBExtension.getModule(),
+                postgresExtension.getModule()
+        );
+        return new JpaToPgCoreDataMigration(module);
+    }
+
+}
\ No newline at end of file
diff --git 
a/server/apps/migration/core-data-jpa-to-pg/src/test/java/org/apache/james/MariaDBExtension.java
 
b/server/apps/migration/core-data-jpa-to-pg/src/test/java/org/apache/james/MariaDBExtension.java
new file mode 100644
index 0000000000..7c739f6459
--- /dev/null
+++ 
b/server/apps/migration/core-data-jpa-to-pg/src/test/java/org/apache/james/MariaDBExtension.java
@@ -0,0 +1,80 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james;
+
+import java.util.UUID;
+
+import org.apache.james.backends.jpa.JPAConfiguration;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testcontainers.containers.MariaDBContainer;
+import org.testcontainers.containers.output.OutputFrame;
+
+import com.google.inject.Module;
+import com.google.inject.util.Modules;
+
+public class MariaDBExtension implements GuiceModuleTestExtension {
+
+    private static final Logger logger = 
LoggerFactory.getLogger(MariaDBExtension.class);
+
+    private static void displayDockerLog(OutputFrame outputFrame) {
+        logger.info(outputFrame.getUtf8String().trim());
+    }
+
+    private final MariaDBContainer container;
+    private JPAConfiguration jpaConfiguration;
+
+    public MariaDBExtension() {
+        container = new MariaDBContainer<>("mariadb:10.6")
+                .withLogConsumer(MariaDBExtension::displayDockerLog)
+                .withReuse(true);
+    }
+
+    @Override
+    public void beforeAll(ExtensionContext extensionContext) throws Exception {
+        container.start();
+    }
+
+    @Override
+    public void afterAll(ExtensionContext extensionContext) throws Exception {
+        container.stop();
+    }
+
+    @Override
+    public void beforeEach(ExtensionContext extensionContext) throws Exception 
{
+        var dbName = UUID.randomUUID().toString().replace('-', '_');
+        String script = String.format("create database %s; grant all on %s.* 
to %s", dbName, dbName, container.getUsername());
+        container.execInContainer("mariadb", "-u", "root", "--password=" + 
container.getPassword(), "--execute", script);
+        container.withDatabaseName(dbName);
+        jpaConfiguration = JPAConfiguration.builder()
+                .driverName(container.getDriverClassName())
+                .driverURL(container.getJdbcUrl())
+                .username(container.getUsername())
+                .password(container.getPassword())
+                .build();
+    }
+
+    @Override
+    public Module getModule() {
+        return Modules.combine(binder -> binder.bind(JPAConfiguration.class)
+                .toInstance(jpaConfiguration));
+    }
+}
diff --git 
a/server/container/guice/distributed/src/main/java/org/apache/james/modules/blobstore/BlobStoreModulesChooser.java
 
b/server/container/guice/distributed/src/main/java/org/apache/james/modules/blobstore/BlobStoreModulesChooser.java
index 41f1388a26..a0c467bdb9 100644
--- 
a/server/container/guice/distributed/src/main/java/org/apache/james/modules/blobstore/BlobStoreModulesChooser.java
+++ 
b/server/container/guice/distributed/src/main/java/org/apache/james/modules/blobstore/BlobStoreModulesChooser.java
@@ -180,7 +180,7 @@ public class BlobStoreModulesChooser {
         return encryptionModule.orElse(new NoEncryptionModule());
     }
 
-    private static List<Module> chooseStoragePolicyModule(StorageStrategy 
storageStrategy) {
+    public static List<Module> chooseStoragePolicyModule(StorageStrategy 
storageStrategy) {
         switch (storageStrategy) {
             case DEDUPLICATION:
                 Module deduplicationBlobModule = binder -> 
binder.bind(BlobStore.class)
diff --git 
a/server/container/guice/postgres-common/src/main/java/org/apache/james/modules/data/PostgresQuotaGuiceModule.java
 
b/server/container/guice/postgres-common/src/main/java/org/apache/james/modules/data/PostgresQuotaGuiceModule.java
index 1df8435867..e7f0f1da11 100644
--- 
a/server/container/guice/postgres-common/src/main/java/org/apache/james/modules/data/PostgresQuotaGuiceModule.java
+++ 
b/server/container/guice/postgres-common/src/main/java/org/apache/james/modules/data/PostgresQuotaGuiceModule.java
@@ -36,7 +36,7 @@ public class PostgresQuotaGuiceModule extends AbstractModule {
     @Override
     public void configure() {
         bind(PostgresQuotaCurrentValueDAO.class).in(Scopes.SINGLETON);
-//        bind(PostgresQuotaLimitDAO.class).in(Scopes.SINGLETON);
+        bind(PostgresQuotaLimitDAO.class).in(Scopes.SINGLETON);
 
         Multibinder<PostgresModule> postgresDataDefinitions = 
Multibinder.newSetBinder(binder(), PostgresModule.class);
         
postgresDataDefinitions.addBinding().toInstance(org.apache.james.backends.postgres.quota.PostgresQuotaModule.MODULE);
diff --git 
a/server/data/data-jpa/src/main/java/org/apache/james/droplists/jpa/JPADropList.java
 
b/server/data/data-jpa/src/main/java/org/apache/james/droplists/jpa/JPADropList.java
index 88d87cf03c..2ff086dd9b 100644
--- 
a/server/data/data-jpa/src/main/java/org/apache/james/droplists/jpa/JPADropList.java
+++ 
b/server/data/data-jpa/src/main/java/org/apache/james/droplists/jpa/JPADropList.java
@@ -88,6 +88,16 @@ public class JPADropList implements DropList {
             .doFinally(any -> EntityManagerUtils.safelyClose(entityManager));
     }
 
+    // This operation is not exposed in the interface on purpose.
+    // It only makes sense in the context of a migration tool and
+    // should not be used in normal operations.
+    public Flux<DropListEntry> listAll() {
+        EntityManager entityManager = 
entityManagerFactory.createEntityManager();
+        return Flux.fromStream(() -> getAllDropListEntries(entityManager)
+                        .map(JPADropListEntry::toDropListEntry))
+                .doFinally(any -> 
EntityManagerUtils.safelyClose(entityManager));
+    }
+
     @Override
     public Mono<Status> query(OwnerScope ownerScope, String owner, MailAddress 
sender) {
         Preconditions.checkArgument(ownerScope != null);
@@ -107,6 +117,13 @@ public class JPADropList implements DropList {
             .getResultStream();
     }
 
+    @SuppressWarnings("unchecked")
+    private Stream<JPADropListEntry> getAllDropListEntries(EntityManager 
entityManager) {
+        return entityManager
+                .createNamedQuery("listAllDropListEntries")
+                .getResultStream();
+    }
+
     private DropList.Status queryDropList(EntityManager entityManager, 
OwnerScope ownerScope, String owner, MailAddress sender) {
         String specifiedOwner = ownerScope.equals(OwnerScope.GLOBAL) ? "" : 
owner;
         return entityManager.createNamedQuery("queryDropListEntry")
diff --git 
a/server/data/data-jpa/src/main/java/org/apache/james/droplists/jpa/model/JPADropListEntry.java
 
b/server/data/data-jpa/src/main/java/org/apache/james/droplists/jpa/model/JPADropListEntry.java
index dded720f49..0a4780d084 100644
--- 
a/server/data/data-jpa/src/main/java/org/apache/james/droplists/jpa/model/JPADropListEntry.java
+++ 
b/server/data/data-jpa/src/main/java/org/apache/james/droplists/jpa/model/JPADropListEntry.java
@@ -45,6 +45,8 @@ import com.google.common.base.MoreObjects;
 @Table(name = "JAMES_DROP_LIST")
 @NamedQuery(name = "listDropListEntries",
     query = "SELECT j FROM JamesDropList j WHERE j.ownerScope = :ownerScope 
AND j.owner = :owner")
+@NamedQuery(name = "listAllDropListEntries",
+        query = "SELECT j FROM JamesDropList j")
 @NamedQuery(name = "queryDropListEntry",
     query = "SELECT j FROM JamesDropList j WHERE j.ownerScope = :ownerScope 
AND j.owner = :owner AND j.deniedEntity IN :deniedEntity")
 @NamedQuery(name = "removeDropListEntry",
diff --git 
a/server/data/data-jpa/src/main/java/org/apache/james/sieve/jpa/JPASieveRepository.java
 
b/server/data/data-jpa/src/main/java/org/apache/james/sieve/jpa/JPASieveRepository.java
index 172e5efc23..b068715cb8 100644
--- 
a/server/data/data-jpa/src/main/java/org/apache/james/sieve/jpa/JPASieveRepository.java
+++ 
b/server/data/data-jpa/src/main/java/org/apache/james/sieve/jpa/JPASieveRepository.java
@@ -26,6 +26,7 @@ import java.util.List;
 import java.util.Optional;
 import java.util.function.Consumer;
 import java.util.function.Function;
+import java.util.stream.Stream;
 
 import jakarta.inject.Inject;
 import jakarta.persistence.EntityManager;
@@ -66,10 +67,12 @@ public class JPASieveRepository implements SieveRepository {
     private static final String DEFAULT_SIEVE_QUOTA_USERNAME = "default.quota";
 
     private final TransactionRunner transactionRunner;
+    private final EntityManagerFactory entityManagerFactory;
 
     @Inject
     public JPASieveRepository(EntityManagerFactory entityManagerFactory) {
         this.transactionRunner = new TransactionRunner(entityManagerFactory);
+        this.entityManagerFactory = entityManagerFactory;
     }
 
     @Override
@@ -87,9 +90,9 @@ public class JPASieveRepository implements SieveRepository {
 
     private QuotaSizeLimit limitToUser(Username username) throws 
StorageException {
         return findQuotaForUser(username.asString())
-            .or(Throwing.supplier(() -> 
findQuotaForUser(DEFAULT_SIEVE_QUOTA_USERNAME)).sneakyThrow())
-            .map(JPASieveQuota::toQuotaSize)
-            .orElse(QuotaSizeLimit.unlimited());
+                .or(Throwing.supplier(() -> 
findQuotaForUser(DEFAULT_SIEVE_QUOTA_USERNAME)).sneakyThrow())
+                .map(JPASieveQuota::toQuotaSize)
+                .orElse(QuotaSizeLimit.unlimited());
     }
 
     private boolean overQuotaAfterModification(long usedSpace, long size, 
QuotaSizeLimit quota) {
@@ -128,6 +131,29 @@ public class JPASieveRepository implements SieveRepository 
{
         return Mono.fromCallable(() -> 
listScripts(username)).flatMapMany(Flux::fromIterable);
     }
 
+
+    // This operation is not exposed in the interface on purpose.
+    // It only makes sense in the context of a migration tool and
+    // should not be used in normal operations.
+    @SuppressWarnings("unchecked")
+    public Stream<JPASieveScript> listAllSieveScripts() {
+        EntityManager entityManager = 
entityManagerFactory.createEntityManager();
+        return entityManager
+                .createNamedQuery("listAllSieveScripts")
+                .getResultStream();
+    }
+
+    // This operation is not exposed in the interface on purpose.
+    // It only makes sense in the context of a migration tool and
+    // should not be used in normal operations.
+    @SuppressWarnings("unchecked")
+    public Stream<JPASieveQuota> listAllSieveQuotas() {
+        EntityManager entityManager = 
entityManagerFactory.createEntityManager();
+        return entityManager
+                .createNamedQuery("listAllSieveQuotas")
+                .getResultStream();
+    }
+
     private List<JPASieveScript> findAllSieveScriptsForUser(Username username) 
throws StorageException {
         return transactionRunner.runAndRetrieveResult(entityManager -> {
             List<JPASieveScript> sieveScripts = 
entityManager.createNamedQuery("findAllByUsername", JPASieveScript.class)
diff --git 
a/server/data/data-jpa/src/main/java/org/apache/james/sieve/jpa/model/JPASieveQuota.java
 
b/server/data/data-jpa/src/main/java/org/apache/james/sieve/jpa/model/JPASieveQuota.java
index 3e33d04907..07ebee209b 100644
--- 
a/server/data/data-jpa/src/main/java/org/apache/james/sieve/jpa/model/JPASieveQuota.java
+++ 
b/server/data/data-jpa/src/main/java/org/apache/james/sieve/jpa/model/JPASieveQuota.java
@@ -27,12 +27,14 @@ import jakarta.persistence.Id;
 import jakarta.persistence.NamedQuery;
 import jakarta.persistence.Table;
 
+import org.apache.james.core.Username;
 import org.apache.james.core.quota.QuotaSizeLimit;
 
 import com.google.common.base.MoreObjects;
 
 @Entity(name = "JamesSieveQuota")
 @Table(name = "JAMES_SIEVE_QUOTA")
+@NamedQuery(name = "listAllSieveQuotas", query = "SELECT sieveQuota FROM 
JamesSieveQuota sieveQuota")
 @NamedQuery(name = "findByUsername", query = "SELECT sieveQuota FROM 
JamesSieveQuota sieveQuota WHERE sieveQuota.username=:username")
 public class JPASieveQuota {
 
@@ -91,4 +93,8 @@ public class JPASieveQuota {
                 .add("size", size)
                 .toString();
     }
+
+    public Username getUsername() {
+        return Username.of(username);
+    }
 }
diff --git 
a/server/data/data-jpa/src/main/java/org/apache/james/sieve/jpa/model/JPASieveScript.java
 
b/server/data/data-jpa/src/main/java/org/apache/james/sieve/jpa/model/JPASieveScript.java
index 3df5b9e4c0..394513e016 100644
--- 
a/server/data/data-jpa/src/main/java/org/apache/james/sieve/jpa/model/JPASieveScript.java
+++ 
b/server/data/data-jpa/src/main/java/org/apache/james/sieve/jpa/model/JPASieveScript.java
@@ -39,6 +39,7 @@ import com.google.common.base.Preconditions;
 
 @Entity(name = "JamesSieveScript")
 @Table(name = "JAMES_SIEVE_SCRIPT")
+@NamedQuery(name = "listAllSieveScripts", query = "SELECT sieveScript FROM 
JamesSieveScript sieveScript")
 @NamedQuery(name = "findAllByUsername", query = "SELECT sieveScript FROM 
JamesSieveScript sieveScript WHERE sieveScript.username=:username")
 @NamedQuery(name = "findActiveByUsername", query = "SELECT sieveScript FROM 
JamesSieveScript sieveScript WHERE sieveScript.username=:username AND 
sieveScript.isActive=true")
 @NamedQuery(name = "findSieveScript", query = "SELECT sieveScript FROM 
JamesSieveScript sieveScript WHERE sieveScript.username=:username AND 
sieveScript.scriptName=:scriptName")
diff --git 
a/server/data/data-jpa/src/main/java/org/apache/james/user/jpa/model/JPAUser.java
 
b/server/data/data-jpa/src/main/java/org/apache/james/user/jpa/model/JPAUser.java
index 9a7f049678..76ae23fc91 100644
--- 
a/server/data/data-jpa/src/main/java/org/apache/james/user/jpa/model/JPAUser.java
+++ 
b/server/data/data-jpa/src/main/java/org/apache/james/user/jpa/model/JPAUser.java
@@ -57,7 +57,7 @@ public class JPAUser implements User {
      */
     @VisibleForTesting
     static String hashPassword(String password, String nullableSalt, String 
nullableAlgorithm) {
-        Algorithm algorithm = 
Algorithm.of(Optional.ofNullable(nullableAlgorithm).orElse("SHA-512"));
+        Algorithm algorithm = buildAlgorithm(nullableAlgorithm);
         if (algorithm.isPBKDF2()) {
             return algorithm.digest(password, nullableSalt);
         }
@@ -68,6 +68,19 @@ public class JPAUser implements User {
         return chooseHashFunction(algorithm.getName()).apply(credentials);
     }
 
+    private static Algorithm buildAlgorithm(String nullableAlgorithm) {
+        return 
Algorithm.of(Optional.ofNullable(nullableAlgorithm).orElse("SHA-512"));
+    }
+
+    public String getPasswordHash() {
+        return password;
+    }
+
+    public Algorithm getAlgorithm() {
+        return buildAlgorithm(alg);
+    }
+
+
     interface PasswordHashFunction extends Function<String, String> {}
 
     private static PasswordHashFunction chooseHashFunction(String algorithm) {
diff --git a/server/pom.xml b/server/pom.xml
index 81d7cf120f..76f5442d25 100644
--- a/server/pom.xml
+++ b/server/pom.xml
@@ -46,6 +46,7 @@
         <module>apps/jpa-app</module>
         <module>apps/jpa-smtp-app</module>
         <module>apps/memory-app</module>
+        <module>apps/migration/core-data-jpa-to-pg</module>
         <module>apps/postgres-app</module>
         <module>apps/scaling-pulsar-smtp</module>
         <module>apps/spring-app</module>


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


Reply via email to