diff --git a/tmail-backend/tmail-third-party/openpaas/pom.xml b/tmail-backend/tmail-third-party/openpaas/pom.xml
new file mode 100644
index 0000000000..effa8f2995
--- /dev/null
+++ b/tmail-backend/tmail-third-party/openpaas/pom.xml
@@ -0,0 +1,73 @@
+
+
+ 4.0.0
+
+ com.linagora.tmail
+ tmail-third-party
+ 1.0.0-SNAPSHOT
+
+
+ tmail-openpaas
+ Twake Mail :: Third Party :: OpenPaaS
+ OpenPaaS integration for Twake Mail
+
+
+
+ ${james.groupId}
+ james-core
+
+
+ ${james.groupId}
+ james-server-testing
+
+
+ ${james.groupId}
+ testing-base
+ test
+
+
+ com.fasterxml.jackson.core
+ jackson-annotations
+
+
+ com.fasterxml.jackson.core
+ jackson-core
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+
+
+ io.projectreactor
+ reactor-core
+
+
+ io.projectreactor.netty
+ reactor-netty
+
+
+ org.mock-server
+ mockserver-netty
+ test
+
+
+
+
+
+
+
+ net.alchim31.maven
+ scala-maven-plugin
+
+
+ io.github.evis
+ scalafix-maven-plugin_2.13
+
+ ${project.parent.parent.parent.basedir}/.scalafix.conf
+
+
+
+
+
\ No newline at end of file
diff --git a/tmail-backend/tmail-third-party/openpaas/src/main/java/com/linagora/tmail/HttpUtils.java b/tmail-backend/tmail-third-party/openpaas/src/main/java/com/linagora/tmail/HttpUtils.java
new file mode 100644
index 0000000000..7d6af56948
--- /dev/null
+++ b/tmail-backend/tmail-third-party/openpaas/src/main/java/com/linagora/tmail/HttpUtils.java
@@ -0,0 +1,20 @@
+package com.linagora.tmail;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+
+public class HttpUtils {
+
+ /**
+ * Returns the AUTHORIZATION header value in order to implement basic authentication.
+ *
+ * For more information see RFC2617#section-2.
+ * */
+ public static String createBasicAuthenticationToken(String user, String password) {
+ String userPassword = user + ":" + password;
+ byte[] base64UserPassword = Base64
+ .getEncoder()
+ .encode(userPassword.getBytes(StandardCharsets.UTF_8));
+ return "Basic " + new String(base64UserPassword, StandardCharsets.UTF_8);
+ }
+}
diff --git a/tmail-backend/tmail-third-party/openpaas/src/main/java/com/linagora/tmail/OpenPaasConfiguration.java b/tmail-backend/tmail-third-party/openpaas/src/main/java/com/linagora/tmail/OpenPaasConfiguration.java
new file mode 100644
index 0000000000..65565a1f73
--- /dev/null
+++ b/tmail-backend/tmail-third-party/openpaas/src/main/java/com/linagora/tmail/OpenPaasConfiguration.java
@@ -0,0 +1,6 @@
+package com.linagora.tmail;
+
+import java.net.URL;
+
+public record OpenPaasConfiguration(URL restClientUrl, String restClientUser, String restClientPassword) {
+}
diff --git a/tmail-backend/tmail-third-party/openpaas/src/main/java/com/linagora/tmail/api/OpenPaasRestClient.java b/tmail-backend/tmail-third-party/openpaas/src/main/java/com/linagora/tmail/api/OpenPaasRestClient.java
new file mode 100644
index 0000000000..02f70a7e61
--- /dev/null
+++ b/tmail-backend/tmail-third-party/openpaas/src/main/java/com/linagora/tmail/api/OpenPaasRestClient.java
@@ -0,0 +1,86 @@
+package com.linagora.tmail.api;
+
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+
+import jakarta.mail.internet.AddressException;
+
+import org.apache.james.core.MailAddress;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.linagora.tmail.HttpUtils;
+import com.linagora.tmail.OpenPaasConfiguration;
+
+import reactor.core.publisher.Mono;
+import reactor.netty.ByteBufMono;
+import reactor.netty.http.client.HttpClient;
+import reactor.netty.http.client.HttpClientResponse;
+
+public class OpenPaasRestClient {
+ private static final Logger LOGGER = LoggerFactory.getLogger(OpenPaasRestClient.class);
+
+ private static final Duration RESPONSE_TIMEOUT = Duration.ofSeconds(10);
+ private static final String AUTHORIZATION_HEADER = "Authorization";
+ private final HttpClient client;
+ private final ObjectMapper deserializer = new ObjectMapper();
+
+ public OpenPaasRestClient(OpenPaasConfiguration openPaasConfiguration) {
+ URL apiUrl = openPaasConfiguration.restClientUrl();
+ String user = openPaasConfiguration.restClientUser();
+ String password = openPaasConfiguration.restClientPassword();
+ this.client = HttpClient.create()
+ .baseUrl(apiUrl.toString())
+ .headers(headers -> headers.add(AUTHORIZATION_HEADER, HttpUtils.createBasicAuthenticationToken(user, password)))
+ .responseTimeout(RESPONSE_TIMEOUT);
+ }
+
+ public Mono retrieveMailAddress(String openPaasUserId) {
+ return client.get()
+ .uri(String.format("/users/%s", openPaasUserId))
+ .responseSingle((statusCode, data) -> handleUserResponse(openPaasUserId, statusCode, data))
+ .map(OpenPaasUserResponse::preferredEmail)
+ .map(this::parseMailAddress)
+ .onErrorResume(e -> Mono.error(new OpenPaasRestClientException("Failed to retrieve user mail using OpenPaas id", e)));
+ }
+
+ private Mono handleUserResponse(String openPaasUserId, HttpClientResponse httpClientResponse, ByteBufMono dataBuf) {
+ int statusCode = httpClientResponse.status().code();
+
+ return switch (statusCode) {
+ case 200 -> dataBuf.asByteArray()
+ .map(this::parseUserResponse)
+ .onErrorResume(e -> Mono.error(new OpenPaasRestClientException("Bad user response body format", e)));
+ case 404 -> {
+ LOGGER.warn("Unable to retrieve mail address as OpenPaas user with id {} not found", openPaasUserId);
+ yield Mono.empty();
+ }
+ default -> dataBuf.asString(StandardCharsets.UTF_8)
+ .switchIfEmpty(Mono.just(""))
+ .flatMap(errorResponse -> Mono.error(new OpenPaasRestClientException(
+ String.format("""
+ Error when getting OpenPaas user response.\s
+ Response Status = %s,
+ Response Body = %s""", statusCode, errorResponse))));
+ };
+ }
+
+ private OpenPaasUserResponse parseUserResponse(byte[] responseAsBytes) {
+ try {
+ return deserializer.readValue(new String(responseAsBytes, StandardCharsets.UTF_8), OpenPaasUserResponse.class);
+ } catch (JsonProcessingException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private MailAddress parseMailAddress(String email) {
+ try {
+ return new MailAddress(email);
+ } catch (AddressException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/tmail-backend/tmail-third-party/openpaas/src/main/java/com/linagora/tmail/api/OpenPaasRestClientException.java b/tmail-backend/tmail-third-party/openpaas/src/main/java/com/linagora/tmail/api/OpenPaasRestClientException.java
new file mode 100644
index 0000000000..24823210dc
--- /dev/null
+++ b/tmail-backend/tmail-third-party/openpaas/src/main/java/com/linagora/tmail/api/OpenPaasRestClientException.java
@@ -0,0 +1,12 @@
+package com.linagora.tmail.api;
+
+public class OpenPaasRestClientException extends RuntimeException {
+
+ public OpenPaasRestClientException(String message) {
+ super(message);
+ }
+
+ public OpenPaasRestClientException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/tmail-backend/tmail-third-party/openpaas/src/main/java/com/linagora/tmail/api/OpenPaasUserResponse.java b/tmail-backend/tmail-third-party/openpaas/src/main/java/com/linagora/tmail/api/OpenPaasUserResponse.java
new file mode 100644
index 0000000000..4756406a19
--- /dev/null
+++ b/tmail-backend/tmail-third-party/openpaas/src/main/java/com/linagora/tmail/api/OpenPaasUserResponse.java
@@ -0,0 +1,16 @@
+package com.linagora.tmail.api;
+
+import java.util.List;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+public record OpenPaasUserResponse(@JsonProperty("id") String id,
+ @JsonProperty("firstname") String firstname,
+ @JsonProperty("lastname") String lastname,
+ @JsonProperty("preferredEmail") String preferredEmail,
+ @JsonProperty("emails") List emails,
+ @JsonProperty("main_phone") String mainPhone,
+ @JsonProperty("displayName") String displayName) {
+}
diff --git a/tmail-backend/tmail-third-party/openpaas/src/test/scala/com/linagora/tmail/api/OpenPaasRestClientTest.java b/tmail-backend/tmail-third-party/openpaas/src/test/scala/com/linagora/tmail/api/OpenPaasRestClientTest.java
new file mode 100644
index 0000000000..68f0a91748
--- /dev/null
+++ b/tmail-backend/tmail-third-party/openpaas/src/test/scala/com/linagora/tmail/api/OpenPaasRestClientTest.java
@@ -0,0 +1,56 @@
+package com.linagora.tmail.api;
+
+import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
+import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
+
+import jakarta.mail.internet.AddressException;
+
+import org.apache.james.core.MailAddress;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import com.linagora.tmail.OpenPaasConfiguration;
+
+public class OpenPaasRestClientTest {
+ public static final String BAD_USER_ID = "BAD_ID";
+ @RegisterExtension
+ static OpenPaasServerExtension openPaasServerExtension = new OpenPaasServerExtension();
+
+ OpenPaasRestClient restClient;
+
+ @BeforeEach
+ void setup() {
+ OpenPaasConfiguration openPaasConfig = new OpenPaasConfiguration(
+ openPaasServerExtension.getBaseUrl(),
+ OpenPaasServerExtension.GOOD_USER(),
+ OpenPaasServerExtension.GOOD_PASSWORD());
+
+ restClient = new OpenPaasRestClient(openPaasConfig);
+ }
+
+ @Test
+ void shouldReturnUserMailAddressWhenUserIdAndAuthenticationTokenIsCorrect()
+ throws AddressException {
+ assertThat(restClient.retrieveMailAddress(OpenPaasServerExtension.ALICE_USER_ID()).block())
+ .isEqualTo(new MailAddress(OpenPaasServerExtension.ALICE_EMAIL()));
+ }
+
+ @Test
+ void shouldReturnEmptyMonoWhenUserWithIdNotFound() {
+ assertThat(restClient.retrieveMailAddress(BAD_USER_ID).blockOptional()).isEmpty();
+ }
+
+ @Test
+ void shouldThrowExceptionOnErrorStatusCode() {
+ OpenPaasConfiguration openPaasConfig = new OpenPaasConfiguration(
+ openPaasServerExtension.getBaseUrl(),
+ OpenPaasServerExtension.BAD_USER(),
+ OpenPaasServerExtension.BAD_PASSWORD());
+
+ restClient = new OpenPaasRestClient(openPaasConfig);
+
+ assertThatThrownBy(() -> restClient.retrieveMailAddress(OpenPaasServerExtension.ALICE_USER_ID()).block())
+ .isInstanceOf(OpenPaasRestClientException.class);
+ }
+}
diff --git a/tmail-backend/tmail-third-party/openpaas/src/test/scala/com/linagora/tmail/api/OpenPaasServerExtension.scala b/tmail-backend/tmail-third-party/openpaas/src/test/scala/com/linagora/tmail/api/OpenPaasServerExtension.scala
new file mode 100644
index 0000000000..e6157e3015
--- /dev/null
+++ b/tmail-backend/tmail-third-party/openpaas/src/test/scala/com/linagora/tmail/api/OpenPaasServerExtension.scala
@@ -0,0 +1,107 @@
+package com.linagora.tmail.api
+
+import java.net.{URI, URL}
+
+import com.linagora.tmail.HttpUtils
+import com.linagora.tmail.api.OpenPaasServerExtension.{ALICE_EMAIL, ALICE_USER_ID, BAD_AUTHENTICATION_TOKEN, GOOD_AUTHENTICATION_TOKEN, LOGGER}
+import org.junit.jupiter.api.extension._
+import org.mockserver.configuration.ConfigurationProperties
+import org.mockserver.integration.ClientAndServer
+import org.mockserver.integration.ClientAndServer.startClientAndServer
+import org.mockserver.model.HttpRequest.request
+import org.mockserver.model.HttpResponse.response
+import org.mockserver.model.NottableString.string
+import org.slf4j.{Logger, LoggerFactory}
+
+
+object OpenPaasServerExtension {
+ val ALICE_USER_ID: String = "abc0a663bdaffe0026290xyz"
+ val ALICE_EMAIL: String = "adoe@linagora.com"
+ val GOOD_USER = "admin"
+ val GOOD_PASSWORD = "admin"
+ val BAD_USER = "BAD_USER"
+ val BAD_PASSWORD = "BAD_PASSWORD"
+ val GOOD_AUTHENTICATION_TOKEN: String = HttpUtils.createBasicAuthenticationToken(GOOD_USER, GOOD_PASSWORD)
+ val BAD_AUTHENTICATION_TOKEN: String = HttpUtils.createBasicAuthenticationToken(BAD_USER, BAD_PASSWORD)
+ private val LOGGER: Logger = LoggerFactory.getLogger(OpenPaasServerExtension.getClass)
+}
+
+class OpenPaasServerExtension extends BeforeEachCallback with AfterEachCallback with ParameterResolver{
+ var mockServer: ClientAndServer = _
+
+ override def beforeEach(context: ExtensionContext): Unit = {
+ mockServer = startClientAndServer(0)
+ ConfigurationProperties.logLevel("DEBUG")
+
+ mockServer.when(
+ request.withPath(s"/users/$ALICE_USER_ID")
+ .withMethod("GET")
+ .withHeader(string("Authorization"), string(BAD_AUTHENTICATION_TOKEN)))
+ .respond(response.withStatusCode(401))
+
+ mockServer.when(
+ request.withPath(s"/users/$ALICE_USER_ID")
+ .withMethod("GET")
+ .withHeader(string("Authorization"), string(GOOD_AUTHENTICATION_TOKEN)))
+ .respond(response.withStatusCode(200)
+ .withBody(s"""{
+ | "_id": "$ALICE_USER_ID",
+ | "firstname": "Alice",
+ | "lastname": "DOE",
+ | "preferredEmail": "$ALICE_EMAIL",
+ | "emails": [
+ | "$ALICE_EMAIL"
+ | ],
+ | "domains": [
+ | {
+ | "joined_at": "2020-09-03T08:16:35.682Z",
+ | "domain_id": "$ALICE_USER_ID"
+ | }
+ | ],
+ | "states": [],
+ | "avatars": [
+ | "$ALICE_USER_ID"
+ | ],
+ | "main_phone": "01111111111",
+ | "accounts": [
+ | {
+ | "timestamps": {
+ | "creation": "2020-09-03T08:16:35.682Z"
+ | },
+ | "hosted": true,
+ | "emails": [
+ | "adoe@linagora.com"
+ | ],
+ | "preferredEmailIndex": 0,
+ | "type": "email"
+ | }
+ | ],
+ | "login": {
+ | "failures": [],
+ | "success": "2024-10-04T12:59:44.469Z"
+ | },
+ | "id": "$ALICE_USER_ID",
+ | "displayName": "Alice DOE",
+ | "objectType": "user",
+ | "followers": 0,
+ | "followings": 0
+ |}""".stripMargin))
+ }
+
+ override def afterEach(context: ExtensionContext): Unit = {
+ if (mockServer == null) {
+ LOGGER.warn("Mock server is null")
+ } else {
+ mockServer.close()
+ }
+ }
+
+ override def supportsParameter(parameterContext: ParameterContext, extensionContext: ExtensionContext): Boolean =
+ parameterContext.getParameter.getType eq classOf[ClientAndServer]
+
+ override def resolveParameter(parameterContext: ParameterContext, extensionContext: ExtensionContext): AnyRef =
+ mockServer
+
+ def getBaseUrl: URL = new URI(s"http://localhost:${mockServer.getLocalPort}").toURL
+
+}
diff --git a/tmail-backend/tmail-third-party/pom.xml b/tmail-backend/tmail-third-party/pom.xml
index cfe189b026..ba8c99b381 100644
--- a/tmail-backend/tmail-third-party/pom.xml
+++ b/tmail-backend/tmail-third-party/pom.xml
@@ -18,5 +18,6 @@
open-ai
+ openpaas
\ No newline at end of file