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