Fix warning deprecate new URL in java 21 (#2193)

diff --git a/backends-common/opensearch/src/test/java/org/apache/james/backends/opensearch/DockerOpenSearch.java b/backends-common/opensearch/src/test/java/org/apache/james/backends/opensearch/DockerOpenSearch.java
index ad94fb6..f742b32 100644
--- a/backends-common/opensearch/src/test/java/org/apache/james/backends/opensearch/DockerOpenSearch.java
+++ b/backends-common/opensearch/src/test/java/org/apache/james/backends/opensearch/DockerOpenSearch.java
@@ -24,6 +24,8 @@
 import static org.apache.james.backends.opensearch.DockerOpenSearch.Fixture.OS_MEMORY;
 
 import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
 import java.net.URL;
 import java.security.cert.X509Certificate;
 import java.time.Duration;
@@ -276,8 +278,8 @@
         @Override
         public URL getUrl() {
             try {
-                return new URL("https://" + getIp() + ":" + getHttpPort());
-            } catch (MalformedURLException e) {
+                return new URI("https://" + getIp() + ":" + getHttpPort()).toURL();
+            } catch (MalformedURLException | URISyntaxException e) {
                 throw new RuntimeException(e);
             }
         }
@@ -321,8 +323,8 @@
 
     default URL getUrl() {
         try {
-            return new URL("http://" + getIp() + ":" + getHttpPort());
-        } catch (MalformedURLException e) {
+            return new URI("http://" + getIp() + ":" + getHttpPort()).toURL();
+        } catch (MalformedURLException | URISyntaxException e) {
             throw new RuntimeException(e);
         }
     }
diff --git a/mailet/api/src/main/java/org/apache/mailet/Serializer.java b/mailet/api/src/main/java/org/apache/mailet/Serializer.java
index cd13974..0736dbe 100644
--- a/mailet/api/src/main/java/org/apache/mailet/Serializer.java
+++ b/mailet/api/src/main/java/org/apache/mailet/Serializer.java
@@ -23,6 +23,8 @@
 
 import java.io.Serializable;
 import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
 import java.net.URL;
 import java.time.ZonedDateTime;
 import java.util.Base64;
@@ -484,8 +486,8 @@
         public Optional<URL> deserialize(JsonNode json) {
             return STRING_SERIALIZER.deserialize(json).flatMap(url -> {
                 try {
-                    return Optional.of(new URL(url));
-                } catch (MalformedURLException e) {
+                    return Optional.of(new URI(url).toURL());
+                } catch (MalformedURLException | URISyntaxException e) {
                     return Optional.empty();
                 }
             });
diff --git a/mailet/api/src/test/java/org/apache/mailet/AttributeValueTest.java b/mailet/api/src/test/java/org/apache/mailet/AttributeValueTest.java
index 384b5a5..9b8778a 100644
--- a/mailet/api/src/test/java/org/apache/mailet/AttributeValueTest.java
+++ b/mailet/api/src/test/java/org/apache/mailet/AttributeValueTest.java
@@ -24,6 +24,8 @@
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
 import java.net.URL;
 import java.time.ZonedDateTime;
 import java.util.List;
@@ -396,8 +398,8 @@
     @Nested
     class UrlSerialization {
         @Test
-        void urlShouldBeSerializedAndBack() throws MalformedURLException {
-            AttributeValue<URL> expected = AttributeValue.of(new URL("https://james.apache.org/"));
+        void urlShouldBeSerializedAndBack() throws MalformedURLException, URISyntaxException {
+            AttributeValue<URL> expected = AttributeValue.of(new URI("https://james.apache.org/").toURL());
 
             JsonNode json = expected.toJson().get();
             AttributeValue<?> actual = AttributeValue.fromJson(json);
@@ -413,7 +415,7 @@
 
         @Test
         void fromJsonStringShouldReturnUrlAttributeValueWhenUrl() throws Exception {
-            AttributeValue<URL> expected = AttributeValue.of(new URL("https://james.apache.org/"));
+            AttributeValue<URL> expected = AttributeValue.of(new URI("https://james.apache.org/").toURL());
 
             AttributeValue<?> actual = AttributeValue.fromJsonString("{\"serializer\":\"UrlSerializer\",\"value\": \"https://james.apache.org/\"}");
 
diff --git a/mailet/standard/src/main/java/org/apache/james/transport/mailets/HeadersToHTTP.java b/mailet/standard/src/main/java/org/apache/james/transport/mailets/HeadersToHTTP.java
index 0e2de06..d66a3ca 100644
--- a/mailet/standard/src/main/java/org/apache/james/transport/mailets/HeadersToHTTP.java
+++ b/mailet/standard/src/main/java/org/apache/james/transport/mailets/HeadersToHTTP.java
@@ -21,7 +21,8 @@
 import java.io.IOException;
 import java.io.UnsupportedEncodingException;
 import java.net.MalformedURLException;
-import java.net.URL;
+import java.net.URI;
+import java.net.URISyntaxException;
 import java.util.Arrays;
 import java.util.HashSet;
 
@@ -82,8 +83,8 @@
             try {
                 // targetUrl = targetUrl + ( targetUrl.contains("?") ? "&" :
                 // "?") + parameterKey + "=" + parameterValue;
-                url = new URL(targetUrl).toExternalForm();
-            } catch (MalformedURLException e) {
+                url = new URI(targetUrl).toURL().toExternalForm();
+            } catch (MalformedURLException | URISyntaxException e) {
                 throw new MessagingException(
                         "Unable to contruct URL object from url");
             }
diff --git a/mailet/standard/src/main/java/org/apache/james/transport/mailets/SerialiseToHTTP.java b/mailet/standard/src/main/java/org/apache/james/transport/mailets/SerialiseToHTTP.java
index b901468..3e4deec 100644
--- a/mailet/standard/src/main/java/org/apache/james/transport/mailets/SerialiseToHTTP.java
+++ b/mailet/standard/src/main/java/org/apache/james/transport/mailets/SerialiseToHTTP.java
@@ -21,7 +21,8 @@
 
 import java.io.IOException;
 import java.net.MalformedURLException;
-import java.net.URL;
+import java.net.URI;
+import java.net.URISyntaxException;
 
 import jakarta.mail.MessagingException;
 import jakarta.mail.internet.MimeMessage;
@@ -90,8 +91,8 @@
             try {
                 // targetUrl = targetUrl + ( targetUrl.contains("?") ? "&" :
                 // "?") + parameterKey + "=" + parameterValue;
-                url = new URL(targetUrl).toExternalForm();
-            } catch (MalformedURLException e) {
+                url = new URI(targetUrl).toURL().toExternalForm();
+            } catch (MalformedURLException | URISyntaxException e) {
                 throw new MessagingException(
                         "Unable to contruct URL object from url");
             }
diff --git a/protocols/api/src/main/java/org/apache/james/protocols/api/OidcSASLConfiguration.java b/protocols/api/src/main/java/org/apache/james/protocols/api/OidcSASLConfiguration.java
index 232ca8a..92f0053 100644
--- a/protocols/api/src/main/java/org/apache/james/protocols/api/OidcSASLConfiguration.java
+++ b/protocols/api/src/main/java/org/apache/james/protocols/api/OidcSASLConfiguration.java
@@ -20,6 +20,8 @@
 package org.apache.james.protocols.api;
 
 import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
 import java.net.URL;
 import java.util.Optional;
 
@@ -31,7 +33,7 @@
 
 public class OidcSASLConfiguration {
 
-    public static OidcSASLConfiguration parse(HierarchicalConfiguration<ImmutableNode> configuration) throws MalformedURLException {
+    public static OidcSASLConfiguration parse(HierarchicalConfiguration<ImmutableNode> configuration) throws MalformedURLException, URISyntaxException {
         String jwksURL = configuration.getString("jwksURL", null);
         String claim = configuration.getString("claim", null);
         String oidcConfigurationURL = configuration.getString("oidcConfigurationURL", null);
@@ -45,9 +47,9 @@
         String introspectionUrl = configuration.getString("introspection.url", null);
         String userInfoUrl = configuration.getString("userinfo.url", null);
 
-        return new OidcSASLConfiguration(new URL(jwksURL), claim, new URL(oidcConfigurationURL), scope, Optional.ofNullable(introspectionUrl)
-            .map(Throwing.function(URL::new)), Optional.ofNullable(configuration.getString("introspection.auth", null)),
-            Optional.ofNullable(userInfoUrl).map(Throwing.function(URL::new)));
+        return new OidcSASLConfiguration(new URI(jwksURL).toURL(), claim, new URI(oidcConfigurationURL).toURL(), scope, Optional.ofNullable(introspectionUrl)
+            .map(Throwing.function(value -> new URI(value).toURL())), Optional.ofNullable(configuration.getString("introspection.auth", null)),
+            Optional.ofNullable(userInfoUrl).map(Throwing.function(value -> new URI(value).toURL())));
     }
 
     private final URL jwksURL;
diff --git a/server/data/data-jmap/src/main/scala/org/apache/james/jmap/api/model/PushSubscription.scala b/server/data/data-jmap/src/main/scala/org/apache/james/jmap/api/model/PushSubscription.scala
index 3dfeac1..011345d 100644
--- a/server/data/data-jmap/src/main/scala/org/apache/james/jmap/api/model/PushSubscription.scala
+++ b/server/data/data-jmap/src/main/scala/org/apache/james/jmap/api/model/PushSubscription.scala
@@ -19,7 +19,7 @@
 
 package org.apache.james.jmap.api.model
 
-import java.net.URL
+import java.net.{URI, URL}
 import java.security.interfaces.ECPublicKey
 import java.time.ZonedDateTime
 import java.time.format.DateTimeFormatter
@@ -56,7 +56,7 @@
 case class VerificationCode(value: String) extends AnyVal
 
 object PushSubscriptionServerURL {
-  def from(value: String): Try[PushSubscriptionServerURL] = Try(PushSubscriptionServerURL(new URL(value)))
+  def from(value: String): Try[PushSubscriptionServerURL] = Try(PushSubscriptionServerURL(new URI(value).toURL))
 }
 
 case class PushSubscriptionServerURL(value: URL)
diff --git a/server/data/data-jmap/src/test/scala/org/apache/james/jmap/api/pushsubscription/PushDeleteUserDataTaskStepTest.scala b/server/data/data-jmap/src/test/scala/org/apache/james/jmap/api/pushsubscription/PushDeleteUserDataTaskStepTest.scala
index e369bec..c039ca8 100644
--- a/server/data/data-jmap/src/test/scala/org/apache/james/jmap/api/pushsubscription/PushDeleteUserDataTaskStepTest.scala
+++ b/server/data/data-jmap/src/test/scala/org/apache/james/jmap/api/pushsubscription/PushDeleteUserDataTaskStepTest.scala
@@ -19,7 +19,7 @@
 
 package org.apache.james.jmap.api.pushsubscription
 
-import java.net.URL
+import java.net.{URI, URL}
 import java.time.Clock
 
 import org.apache.james.jmap.api.identity.CustomIdentityDAOContract.bob
@@ -51,7 +51,7 @@
   def shouldDeleteUserData(): Unit = {
     val validRequest = PushSubscriptionCreationRequest(
       deviceClientId = DeviceClientId("1"),
-      url = PushSubscriptionServerURL(new URL("https://example.com/push")),
+      url = PushSubscriptionServerURL(new URI("https://example.com/push").toURL()),
       types = Seq(CustomTypeName1))
     SMono.fromPublisher(pushSubscriptionRepository.save(ALICE, validRequest)).block().id
 
diff --git a/server/data/data-jmap/src/test/scala/org/apache/james/jmap/api/pushsubscription/PushSubscriptionRepositoryContract.scala b/server/data/data-jmap/src/test/scala/org/apache/james/jmap/api/pushsubscription/PushSubscriptionRepositoryContract.scala
index c499dfe..78a013f 100644
--- a/server/data/data-jmap/src/test/scala/org/apache/james/jmap/api/pushsubscription/PushSubscriptionRepositoryContract.scala
+++ b/server/data/data-jmap/src/test/scala/org/apache/james/jmap/api/pushsubscription/PushSubscriptionRepositoryContract.scala
@@ -19,7 +19,7 @@
 
 package org.apache.james.jmap.api.pushsubscription
 
-import java.net.URL
+import java.net.{URI, URL}
 import java.time.{Clock, Instant, ZoneId, ZonedDateTime}
 
 import org.apache.james.core.Username
@@ -79,7 +79,7 @@
   def validSubscriptionShouldBeSavedSuccessfully(): Unit = {
     val validRequest = PushSubscriptionCreationRequest(
       deviceClientId = DeviceClientId("1"),
-      url = PushSubscriptionServerURL(new URL("https://example.com/push")),
+      url = PushSubscriptionServerURL(new URI("https://example.com/push").toURL()),
       types = Seq(CustomTypeName1))
     val pushSubscriptionId = SMono.fromPublisher(testee.save(ALICE, validRequest)).block().id
     val singleRecordSaved = SFlux.fromPublisher(testee.get(ALICE, Set(pushSubscriptionId).asJava)).count().block()
@@ -91,7 +91,7 @@
   def newSavedSubscriptionShouldNotBeValidated(): Unit = {
     val validRequest = PushSubscriptionCreationRequest(
       deviceClientId = DeviceClientId("1"),
-      url = PushSubscriptionServerURL(new URL("https://example.com/push")),
+      url = PushSubscriptionServerURL(new URI("https://example.com/push").toURL()),
       types = Seq(CustomTypeName1))
     val pushSubscriptionId = SMono.fromPublisher(testee.save(ALICE, validRequest)).block().id
     val newSavedSubscription = SFlux.fromPublisher(testee.get(ALICE, Set(pushSubscriptionId).asJava)).blockFirst().get
@@ -103,7 +103,7 @@
   def subscriptionWithExpireBiggerThanMaxExpireShouldBeSetToMaxExpire(): Unit = {
     val request = PushSubscriptionCreationRequest(
       deviceClientId = DeviceClientId("1"),
-      url = PushSubscriptionServerURL(new URL("https://example.com/push")),
+      url = PushSubscriptionServerURL(new URI("https://example.com/push").toURL()),
       expires = Some(PushSubscriptionExpiredTime(VALID_EXPIRE.plusDays(8))),
       types = Seq(CustomTypeName1))
     val pushSubscriptionId = SMono.fromPublisher(testee.save(ALICE, request)).block().id
@@ -116,7 +116,7 @@
   def subscriptionWithInvalidExpireTimeShouldThrowException(): Unit = {
     val invalidRequest = PushSubscriptionCreationRequest(
       deviceClientId = DeviceClientId("1"),
-      url = PushSubscriptionServerURL(new URL("https://example.com/push")),
+      url = PushSubscriptionServerURL(new URI("https://example.com/push").toURL()),
       expires = Some(PushSubscriptionExpiredTime(INVALID_EXPIRE)),
       types = Seq(CustomTypeName1))
 
@@ -128,13 +128,13 @@
   def subscriptionWithDuplicatedDeviceClientIdShouldThrowException(): Unit = {
     val firstRequest = PushSubscriptionCreationRequest(
       deviceClientId = DeviceClientId("1"),
-      url = PushSubscriptionServerURL(new URL("https://example.com/push")),
+      url = PushSubscriptionServerURL(new URI("https://example.com/push").toURL()),
       types = Seq(CustomTypeName1))
     SMono.fromPublisher(testee.save(ALICE, firstRequest)).block()
 
     val secondRequestWithDuplicatedDeviceClientId = PushSubscriptionCreationRequest(
       deviceClientId = DeviceClientId("1"),
-      url = PushSubscriptionServerURL(new URL("https://example.com/push")),
+      url = PushSubscriptionServerURL(new URI("https://example.com/push").toURL()),
       types = Seq(CustomTypeName1))
 
     assertThatThrownBy(() => SMono.fromPublisher(testee.save(ALICE, secondRequestWithDuplicatedDeviceClientId)).block())
@@ -145,7 +145,7 @@
   def updateWithOutdatedExpiresShouldThrowException(): Unit = {
     val validRequest = PushSubscriptionCreationRequest(
       deviceClientId = DeviceClientId("1"),
-      url = PushSubscriptionServerURL(new URL("https://example.com/push")),
+      url = PushSubscriptionServerURL(new URI("https://example.com/push").toURL()),
       types = Seq(CustomTypeName1))
     val pushSubscriptionId = SMono.fromPublisher(testee.save(ALICE, validRequest)).block().id
 
@@ -157,7 +157,7 @@
   def updateWithExpiresBiggerThanMaxExpiresShouldBeSetToMaxExpires(): Unit = {
     val validRequest = PushSubscriptionCreationRequest(
       deviceClientId = DeviceClientId("1"),
-      url = PushSubscriptionServerURL(new URL("https://example.com/push")),
+      url = PushSubscriptionServerURL(new URI("https://example.com/push").toURL()),
       types = Seq(CustomTypeName1))
     val pushSubscriptionId = SMono.fromPublisher(testee.save(ALICE, validRequest)).block().id
     SMono.fromPublisher(testee.updateExpireTime(ALICE, pushSubscriptionId, MAX_EXPIRE.plusDays(1))).block()
@@ -178,7 +178,7 @@
   def updateWithValidExpiresShouldSucceed(): Unit = {
     val validRequest = PushSubscriptionCreationRequest(
       deviceClientId = DeviceClientId("1"),
-      url = PushSubscriptionServerURL(new URL("https://example.com/push")),
+      url = PushSubscriptionServerURL(new URI("https://example.com/push").toURL()),
       types = Seq(CustomTypeName1))
     val pushSubscriptionId = SMono.fromPublisher(testee.save(ALICE, validRequest)).block().id
     SMono.fromPublisher(testee.updateExpireTime(ALICE, pushSubscriptionId, VALID_EXPIRE)).block()
@@ -191,7 +191,7 @@
   def updateWithExpiresBiggerThanMaxExpiresShouldReturnServerFixedExpires(): Unit = {
     val validRequest = PushSubscriptionCreationRequest(
       deviceClientId = DeviceClientId("1"),
-      url = PushSubscriptionServerURL(new URL("https://example.com/push")),
+      url = PushSubscriptionServerURL(new URI("https://example.com/push").toURL()),
       types = Seq(CustomTypeName1))
     val pushSubscriptionId = SMono.fromPublisher(testee.save(ALICE, validRequest)).block().id
     val fixedExpires = SMono.fromPublisher(testee.updateExpireTime(ALICE, pushSubscriptionId, MAX_EXPIRE.plusDays(1))).block()
@@ -203,7 +203,7 @@
   def updateWithValidTypesShouldSucceed(): Unit = {
     val validRequest = PushSubscriptionCreationRequest(
       deviceClientId = DeviceClientId("1"),
-      url = PushSubscriptionServerURL(new URL("https://example.com/push")),
+      url = PushSubscriptionServerURL(new URI("https://example.com/push").toURL()),
       types = Seq(CustomTypeName1))
     val pushSubscriptionId = SMono.fromPublisher(testee.save(ALICE, validRequest)).block().id
 
@@ -235,7 +235,7 @@
   def revokeStoredSubscriptionShouldSucceed(): Unit = {
     val validRequest = PushSubscriptionCreationRequest(
       deviceClientId = DeviceClientId("1"),
-      url = PushSubscriptionServerURL(new URL("https://example.com/push")),
+      url = PushSubscriptionServerURL(new URI("https://example.com/push").toURL()),
       types = Seq(CustomTypeName1))
     val pushSubscriptionId = SMono.fromPublisher(testee.save(ALICE, validRequest)).block().id
     val singleRecordSaved = SFlux.fromPublisher(testee.get(ALICE, Set(pushSubscriptionId).asJava)).count().block()
@@ -251,7 +251,7 @@
   def deleteStoredSubscriptionShouldSucceed(): Unit = {
     val validRequest = PushSubscriptionCreationRequest(
       deviceClientId = DeviceClientId("1"),
-      url = PushSubscriptionServerURL(new URL("https://example.com/push")),
+      url = PushSubscriptionServerURL(new URI("https://example.com/push").toURL()),
       types = Seq(CustomTypeName1))
     val pushSubscriptionId = SMono.fromPublisher(testee.save(ALICE, validRequest)).block().id
     val singleRecordSaved = SFlux.fromPublisher(testee.get(ALICE, Set(pushSubscriptionId).asJava)).count().block()
@@ -283,12 +283,12 @@
     val deviceClientId2 = DeviceClientId("2")
     val validRequest1 = PushSubscriptionCreationRequest(
       deviceClientId = deviceClientId1,
-      url = PushSubscriptionServerURL(new URL("https://example.com/push")),
+      url = PushSubscriptionServerURL(new URI("https://example.com/push").toURL()),
       expires = Option(PushSubscriptionExpiredTime(VALID_EXPIRE)),
       types = Seq(CustomTypeName1))
     val validRequest2 = PushSubscriptionCreationRequest(
       deviceClientId = deviceClientId2,
-      url = PushSubscriptionServerURL(new URL("https://example.com/push")),
+      url = PushSubscriptionServerURL(new URI("https://example.com/push").toURL()),
       expires = Option(PushSubscriptionExpiredTime(VALID_EXPIRE)),
       types = Seq(CustomTypeName2))
     val pushSubscriptionId1 = SMono.fromPublisher(testee.save(ALICE, validRequest1)).block().id
@@ -304,7 +304,7 @@
     val deviceClientId1 = DeviceClientId("1")
     val validRequest1 = PushSubscriptionCreationRequest(
       deviceClientId = deviceClientId1,
-      url = PushSubscriptionServerURL(new URL("https://example.com/push")),
+      url = PushSubscriptionServerURL(new URI("https://example.com/push").toURL()),
       expires = Option(PushSubscriptionExpiredTime(VALID_EXPIRE)),
       types = Seq(CustomTypeName1))
     val pushSubscriptionId1 = SMono.fromPublisher(testee.save(ALICE, validRequest1)).block().id
@@ -319,7 +319,7 @@
   def getSubscriptionShouldReturnExpiredSubscriptions(): Unit = {
     val validRequest1 = PushSubscriptionCreationRequest(
       deviceClientId = DeviceClientId("1"),
-      url = PushSubscriptionServerURL(new URL("https://example.com/push")),
+      url = PushSubscriptionServerURL(new URI("https://example.com/push").toURL()),
       expires = Option(PushSubscriptionExpiredTime(VALID_EXPIRE.plusDays(1))),
       types = Seq(CustomTypeName1))
     val pushSubscriptionId = SMono.fromPublisher(testee.save(ALICE, validRequest1)).block().id
@@ -337,11 +337,11 @@
     val deviceClientId2 = DeviceClientId("2")
     val validRequest1 = PushSubscriptionCreationRequest(
       deviceClientId = deviceClientId1,
-      url = PushSubscriptionServerURL(new URL("https://example.com/push")),
+      url = PushSubscriptionServerURL(new URI("https://example.com/push").toURL()),
       types = Seq(CustomTypeName1))
     val validRequest2 = PushSubscriptionCreationRequest(
       deviceClientId = deviceClientId2,
-      url = PushSubscriptionServerURL(new URL("https://example.com/push")),
+      url = PushSubscriptionServerURL(new URI("https://example.com/push").toURL()),
       types = Seq(CustomTypeName2))
     val pushSubscriptionId1: PushSubscriptionId = SMono.fromPublisher(testee.save(ALICE, validRequest1)).block().id
     val pushSubscriptionId2: PushSubscriptionId = SMono.fromPublisher(testee.save(ALICE, validRequest2)).block().id
@@ -358,7 +358,7 @@
   def listSubscriptionShouldReturnExpiredSubscriptions(): Unit = {
     val validRequest1 = PushSubscriptionCreationRequest(
       deviceClientId = DeviceClientId("1"),
-      url = PushSubscriptionServerURL(new URL("https://example.com/push")),
+      url = PushSubscriptionServerURL(new URI("https://example.com/push").toURL()),
       expires = Option(PushSubscriptionExpiredTime(VALID_EXPIRE.plusDays(1))),
       types = Seq(CustomTypeName1))
     val pushSubscriptionId = SMono.fromPublisher(testee.save(ALICE, validRequest1)).block().id
@@ -374,7 +374,7 @@
   def validateVerificationCodeShouldSucceed(): Unit = {
     val validRequest = PushSubscriptionCreationRequest(
       deviceClientId = DeviceClientId("1"),
-      url = PushSubscriptionServerURL(new URL("https://example.com/push")),
+      url = PushSubscriptionServerURL(new URI("https://example.com/push").toURL()),
       types = Seq(CustomTypeName1))
     val pushSubscriptionId = SMono.fromPublisher(testee.save(ALICE, validRequest)).block().id
     SMono.fromPublisher(testee.validateVerificationCode(ALICE, pushSubscriptionId)).block()
@@ -396,7 +396,7 @@
     val fullKeyPair = Some(PushSubscriptionKeys(p256dh = "p256h", auth = "auth"))
     val validRequest = PushSubscriptionCreationRequest(
       deviceClientId = DeviceClientId("1"),
-      url = PushSubscriptionServerURL(new URL("https://example.com/push")),
+      url = PushSubscriptionServerURL(new URI("https://example.com/push").toURL()),
       types = Seq(CustomTypeName1),
       keys = fullKeyPair)
 
@@ -412,7 +412,7 @@
     val emptyKeyPair = None
     val validRequest = PushSubscriptionCreationRequest(
       deviceClientId = DeviceClientId("1"),
-      url = PushSubscriptionServerURL(new URL("https://example.com/push")),
+      url = PushSubscriptionServerURL(new URI("https://example.com/push").toURL()),
       types = Seq(CustomTypeName1),
       keys = emptyKeyPair)
     val pushSubscriptionId1 = SMono.fromPublisher(testee.save(ALICE, validRequest)).block().id
@@ -428,7 +428,7 @@
 
     val validRequest = PushSubscriptionCreationRequest(
       deviceClientId = DeviceClientId("1"),
-      url = PushSubscriptionServerURL(new URL("https://example.com/push")),
+      url = PushSubscriptionServerURL(new URI("https://example.com/push").toURL()),
       types = Seq(CustomTypeName1),
       keys = emptyP256hKey)
 
@@ -442,7 +442,7 @@
 
     val validRequest = PushSubscriptionCreationRequest(
       deviceClientId = DeviceClientId("1"),
-      url = PushSubscriptionServerURL(new URL("https://example.com/push")),
+      url = PushSubscriptionServerURL(new URI("https://example.com/push").toURL),
       types = Seq(CustomTypeName1),
       keys = emptyAuthKey)
 
diff --git a/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/managesieve/ManageSieveMailet.java b/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/managesieve/ManageSieveMailet.java
index cb2f4af..691cda7 100644
--- a/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/managesieve/ManageSieveMailet.java
+++ b/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/managesieve/ManageSieveMailet.java
@@ -23,6 +23,8 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
 import java.net.URL;
 import java.nio.charset.StandardCharsets;
 import java.util.Scanner;
@@ -172,8 +174,8 @@
 
     private void setHelpURL(String helpURL) throws MessagingException {
         try {
-            this.helpURL = new URL(helpURL);
-        } catch (MalformedURLException ex) {
+            this.helpURL = new URI(helpURL).toURL();
+        } catch (MalformedURLException | URISyntaxException ex) {
             throw new MessagingException("Invalid helpURL", ex);
         }
     }
diff --git a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/PushServerExtension.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/PushServerExtension.scala
index 8cd6a50..71769c2 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/PushServerExtension.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/PushServerExtension.scala
@@ -19,7 +19,7 @@
 
 package org.apache.james.jmap.rfc8621.contract
 
-import java.net.URL
+import java.net.{URI, URL}
 import java.time.Clock
 import java.util.UUID
 
@@ -48,7 +48,7 @@
     MockPushServer.appendSpec(mockServer)
   }
 
-  def getBaseUrl: URL = new URL(s"http://127.0.0.1:${mockServer.getLocalPort}")
+  def getBaseUrl: URL = new URI(s"http://127.0.0.1:${mockServer.getLocalPort}").toURL
 }
 
 object MockPushServer {
diff --git a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/PushSubscriptionSetMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/PushSubscriptionSetMethodContract.scala
index 39714e0..58a091a 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/PushSubscriptionSetMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/PushSubscriptionSetMethodContract.scala
@@ -19,7 +19,7 @@
 
 package org.apache.james.jmap.rfc8621.contract
 
-import java.net.URL
+import java.net.URI
 import java.security.KeyPair
 import java.security.interfaces.ECPublicKey
 import java.time.ZonedDateTime
@@ -199,7 +199,7 @@
     val probe = server.getProbe(classOf[PushSubscriptionProbe])
     val pushSubscription = probe
       .createPushSubscription(username = BOB,
-        url = PushSubscriptionServerURL(new URL("https://example.com/push/?device=X8980fc&client=12c6d086")),
+        url = PushSubscriptionServerURL(new URI("https://example.com/push/?device=X8980fc&client=12c6d086").toURL),
         deviceId = DeviceClientId("12c6d086"),
         types = Seq(MailboxTypeName))
 
@@ -258,7 +258,7 @@
     val probe = server.getProbe(classOf[PushSubscriptionProbe])
     val pushSubscription = probe
       .createPushSubscription(username = BOB,
-        url = PushSubscriptionServerURL(new URL("https://example.com/push/?device=X8980fc&client=12c6d086")),
+        url = PushSubscriptionServerURL(new URI("https://example.com/push/?device=X8980fc&client=12c6d086").toURL()),
         deviceId = DeviceClientId("12c6d086"),
         types = Seq(MailboxTypeName))
 
@@ -318,7 +318,7 @@
     val probe = server.getProbe(classOf[PushSubscriptionProbe])
     val pushSubscription = probe
       .createPushSubscription(username = BOB,
-        url = PushSubscriptionServerURL(new URL("https://example.com/push/?device=X8980fc&client=12c6d086")),
+        url = PushSubscriptionServerURL(new URI("https://example.com/push/?device=X8980fc&client=12c6d086").toURL()),
         deviceId = DeviceClientId("12c6d086"),
         types = Seq(MailboxTypeName))
 
@@ -378,7 +378,7 @@
     val probe = server.getProbe(classOf[PushSubscriptionProbe])
     val pushSubscription = probe
       .createPushSubscription(username = BOB,
-        url = PushSubscriptionServerURL(new URL("https://example.com/push/?device=X8980fc&client=12c6d086")),
+        url = PushSubscriptionServerURL(new URI("https://example.com/push/?device=X8980fc&client=12c6d086").toURL),
         deviceId = DeviceClientId("12c6d086"),
         types = Seq(MailboxTypeName))
 
@@ -580,12 +580,12 @@
     val probe = server.getProbe(classOf[PushSubscriptionProbe])
     val pushSubscription1 = probe
       .createPushSubscription(username = BOB,
-        url = PushSubscriptionServerURL(new URL("https://example.com/push/?device=X8980fc&client=12c6d086")),
+        url = PushSubscriptionServerURL(new URI("https://example.com/push/?device=X8980fc&client=12c6d086").toURL()),
         deviceId = DeviceClientId("12c6d086"),
         types = Seq(MailboxTypeName))
     val pushSubscription2 = probe
       .createPushSubscription(username = BOB,
-        url = PushSubscriptionServerURL(new URL("https://example.com/push/?device=X8980fc&client=12c6d086")),
+        url = PushSubscriptionServerURL(new URI("https://example.com/push/?device=X8980fc&client=12c6d086").toURL()),
         deviceId = DeviceClientId("12c6d087"),
         types = Seq(EmailTypeName))
 
@@ -640,7 +640,7 @@
     val probe = server.getProbe(classOf[PushSubscriptionProbe])
     val pushSubscription1 = probe
       .createPushSubscription(username = BOB,
-        url = PushSubscriptionServerURL(new URL("https://example.com/push/?device=X8980fc&client=12c6d086")),
+        url = PushSubscriptionServerURL(new URI("https://example.com/push/?device=X8980fc&client=12c6d086").toURL()),
         deviceId = DeviceClientId("12c6d086"),
         types = Seq(MailboxTypeName))
     probe.validatePushSubscription(BOB, pushSubscription1.id)
@@ -691,17 +691,17 @@
     val probe = server.getProbe(classOf[PushSubscriptionProbe])
     val pushSubscription1 = probe
       .createPushSubscription(username = BOB,
-        url = PushSubscriptionServerURL(new URL("https://example.com/push/?device=X8980fc&client=12c6d086")),
+        url = PushSubscriptionServerURL(new URI("https://example.com/push/?device=X8980fc&client=12c6d086").toURL),
         deviceId = DeviceClientId("12c6d086"),
         types = Seq(MailboxTypeName))
     val pushSubscription2 = probe
       .createPushSubscription(username = BOB,
-        url = PushSubscriptionServerURL(new URL("https://example.com/push/?device=X8980fc&client=12c6d086")),
+        url = PushSubscriptionServerURL(new URI("https://example.com/push/?device=X8980fc&client=12c6d086").toURL),
         deviceId = DeviceClientId("12c6d087"),
         types = Seq(EmailTypeName))
     val pushSubscription3 = probe
       .createPushSubscription(username = BOB,
-        url = PushSubscriptionServerURL(new URL("https://example.com/push/?device=X8980fc&client=12c6d086")),
+        url = PushSubscriptionServerURL(new URI("https://example.com/push/?device=X8980fc&client=12c6d086").toURL),
         deviceId = DeviceClientId("12c6d088"),
         types = Seq(EmailTypeName))
 
@@ -758,7 +758,7 @@
     val probe = server.getProbe(classOf[PushSubscriptionProbe])
     val pushSubscription1 = probe
       .createPushSubscription(username = BOB,
-        url = PushSubscriptionServerURL(new URL("https://example.com/push/?device=X8980fc&client=12c6d086")),
+        url = PushSubscriptionServerURL(new URI("https://example.com/push/?device=X8980fc&client=12c6d086").toURL),
         deviceId = DeviceClientId("12c6d086"),
         types = Seq(MailboxTypeName))
 
@@ -840,12 +840,12 @@
     val probe = server.getProbe(classOf[PushSubscriptionProbe])
     val pushSubscription1 = probe
       .createPushSubscription(username = BOB,
-        url = PushSubscriptionServerURL(new URL("https://example.com/push/?device=X8980fc&client=12c6d086")),
+        url = PushSubscriptionServerURL(new URI("https://example.com/push/?device=X8980fc&client=12c6d086").toURL()),
         deviceId = DeviceClientId("12c6d086"),
         types = Seq(MailboxTypeName))
     val pushSubscription2 = probe
       .createPushSubscription(username = BOB,
-        url = PushSubscriptionServerURL(new URL("https://example.com/push/?device=X8980fc&client=12c6d086")),
+        url = PushSubscriptionServerURL(new URI("https://example.com/push/?device=X8980fc&client=12c6d086").toURL()),
         deviceId = DeviceClientId("12c6d087"),
         types = Seq(EmailTypeName))
 
@@ -1476,7 +1476,7 @@
     val probe = server.getProbe(classOf[PushSubscriptionProbe])
     val pushSubscription = probe
       .createPushSubscription(username = BOB,
-        url = PushSubscriptionServerURL(new URL("https://example.com/push/?device=X8980fc&client=12c6d086")),
+        url = PushSubscriptionServerURL(new URI("https://example.com/push/?device=X8980fc&client=12c6d086").toURL()),
         deviceId = DeviceClientId("12c6d086"),
         types = Seq(MailboxTypeName, EmailDeliveryTypeName, EmailTypeName))
 
@@ -1650,22 +1650,22 @@
     val probe = server.getProbe(classOf[PushSubscriptionProbe])
     val pushSubscription1 = probe
       .createPushSubscription(username = BOB,
-        url = PushSubscriptionServerURL(new URL("https://example.com/push/?device=X8980fc&client=12c6d086")),
+        url = PushSubscriptionServerURL(new URI("https://example.com/push/?device=X8980fc&client=12c6d086").toURL()),
         deviceId = DeviceClientId("12c6d081"),
         types = Seq(MailboxTypeName, EmailDeliveryTypeName, EmailTypeName))
     val pushSubscription2 = probe
       .createPushSubscription(username = BOB,
-        url = PushSubscriptionServerURL(new URL("https://example.com/push/?device=X8980fc&client=12c6d086")),
+        url = PushSubscriptionServerURL(new URI("https://example.com/push/?device=X8980fc&client=12c6d086").toURL()),
         deviceId = DeviceClientId("12c6d082"),
         types = Seq(MailboxTypeName, EmailDeliveryTypeName, EmailTypeName))
     val pushSubscription3 = probe
       .createPushSubscription(username = BOB,
-        url = PushSubscriptionServerURL(new URL("https://example.com/push/?device=X8980fc&client=12c6d086")),
+        url = PushSubscriptionServerURL(new URI("https://example.com/push/?device=X8980fc&client=12c6d086").toURL()),
         deviceId = DeviceClientId("12c6d083"),
         types = Seq(MailboxTypeName, EmailDeliveryTypeName, EmailTypeName))
     val pushSubscription4 = probe
       .createPushSubscription(username = BOB,
-        url = PushSubscriptionServerURL(new URL("https://example.com/push/?device=X8980fc&client=12c6d086")),
+        url = PushSubscriptionServerURL(new URI("https://example.com/push/?device=X8980fc&client=12c6d086").toURL()),
         deviceId = DeviceClientId("12c6d084"),
         types = Seq(MailboxTypeName, EmailDeliveryTypeName, EmailTypeName))
 
@@ -1754,7 +1754,7 @@
     val probe = server.getProbe(classOf[PushSubscriptionProbe])
     val pushSubscription = probe
       .createPushSubscription(username = BOB,
-        url = PushSubscriptionServerURL(new URL("https://example.com/push/?device=X8980fc&client=12c6d086")),
+        url = PushSubscriptionServerURL(new URI("https://example.com/push/?device=X8980fc&client=12c6d086").toURL),
         deviceId = DeviceClientId("12c6d086"),
         types = Seq(MailboxTypeName, EmailDeliveryTypeName, EmailTypeName))
 
@@ -1818,7 +1818,7 @@
     val probe = server.getProbe(classOf[PushSubscriptionProbe])
     val pushSubscription = probe
       .createPushSubscription(username = BOB,
-        url = PushSubscriptionServerURL(new URL("https://example.com/push/?device=X8980fc&client=12c6d086")),
+        url = PushSubscriptionServerURL(new URI("https://example.com/push/?device=X8980fc&client=12c6d086").toURL()),
         deviceId = DeviceClientId("12c6d086"),
         types = Seq(MailboxTypeName, EmailDeliveryTypeName, EmailTypeName))
 
@@ -1879,7 +1879,7 @@
     val probe = server.getProbe(classOf[PushSubscriptionProbe])
     val pushSubscription = probe
       .createPushSubscription(username = BOB,
-        url = PushSubscriptionServerURL(new URL("https://example.com/push/?device=X8980fc&client=12c6d086")),
+        url = PushSubscriptionServerURL(new URI("https://example.com/push/?device=X8980fc&client=12c6d086").toURL()),
         deviceId = DeviceClientId("12c6d086"),
         types = Seq(MailboxTypeName, EmailDeliveryTypeName, EmailTypeName))
 
@@ -1940,7 +1940,7 @@
     val probe = server.getProbe(classOf[PushSubscriptionProbe])
     val pushSubscription = probe
       .createPushSubscription(username = BOB,
-        url = PushSubscriptionServerURL(new URL("https://example.com/push/?device=X8980fc&client=12c6d086")),
+        url = PushSubscriptionServerURL(new URI("https://example.com/push/?device=X8980fc&client=12c6d086").toURL()),
         deviceId = DeviceClientId("12c6d086"),
         types = Seq(MailboxTypeName, EmailDeliveryTypeName, EmailTypeName))
 
@@ -2002,7 +2002,7 @@
     val probe = server.getProbe(classOf[PushSubscriptionProbe])
     val pushSubscription = probe
       .createPushSubscription(username = BOB,
-        url = PushSubscriptionServerURL(new URL("https://example.com/push/?device=X8980fc&client=12c6d086")),
+        url = PushSubscriptionServerURL(new URI("https://example.com/push/?device=X8980fc&client=12c6d086").toURL()),
         deviceId = DeviceClientId("12c6d086"),
         types = Seq(MailboxTypeName, EmailDeliveryTypeName, EmailTypeName))
 
@@ -2063,7 +2063,7 @@
     val probe = server.getProbe(classOf[PushSubscriptionProbe])
     val pushSubscription = probe
       .createPushSubscription(username = BOB,
-        url = PushSubscriptionServerURL(new URL("https://example.com/push/?device=X8980fc&client=12c6d086")),
+        url = PushSubscriptionServerURL(new URI("https://example.com/push/?device=X8980fc&client=12c6d086").toURL()),
         deviceId = DeviceClientId("12c6d086"),
         types = Seq(MailboxTypeName, EmailDeliveryTypeName, EmailTypeName))
 
@@ -2537,7 +2537,7 @@
     val probe = server.getProbe(classOf[PushSubscriptionProbe])
     val pushSubscription = probe
       .createPushSubscription(username = BOB,
-        url = PushSubscriptionServerURL(new URL("https://example.com/push/?device=X8980fc&client=12c6d086")),
+        url = PushSubscriptionServerURL(new URI("https://example.com/push/?device=X8980fc&client=12c6d086").toURL),
         deviceId = DeviceClientId("12c6d086"),
         types = Seq(MailboxTypeName, EmailDeliveryTypeName, EmailTypeName))
 
@@ -2682,12 +2682,12 @@
     val probe = server.getProbe(classOf[PushSubscriptionProbe])
     val pushSubscription1 = probe
       .createPushSubscription(username = BOB,
-        url = PushSubscriptionServerURL(new URL("https://example.com/push/?device=X8980fc&client=12c6d086")),
+        url = PushSubscriptionServerURL(new URI("https://example.com/push/?device=X8980fc&client=12c6d086").toURL),
         deviceId = DeviceClientId("12c6d086"),
         types = Seq(MailboxTypeName, EmailDeliveryTypeName, EmailTypeName))
     val pushSubscription2 = probe
       .createPushSubscription(username = BOB,
-        url = PushSubscriptionServerURL(new URL("https://example.com/push/?device=X8980fc&client=12c6d087")),
+        url = PushSubscriptionServerURL(new URI("https://example.com/push/?device=X8980fc&client=12c6d087").toURL),
         deviceId = DeviceClientId("12c6d087"),
         types = Seq(MailboxTypeName, EmailDeliveryTypeName, EmailTypeName))
 
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/Capability.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/Capability.scala
index 658d5bc..cf99b08 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/Capability.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/Capability.scala
@@ -92,7 +92,7 @@
 
   private def safeURI(string: String): Option[URI] = Option(string).flatMap(s => Try(new URI(s)).toOption)
 
-  private def safeURL(string: String): Option[URL] = Option(string).flatMap(s => Try(new URL(s)).toOption)
+  private def safeURL(string: String): Option[URL] = Option(string).flatMap(s => Try(new URI(s).toURL).toOption)
 }
 
 final case class UrlPrefixes(httpUrlPrefix: URI, webSocketURLPrefix: URI)
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/Session.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/Session.scala
index cd869ec..e6f6527 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/Session.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/Session.scala
@@ -109,6 +109,8 @@
   override def serialize: String = value.toString
 }
 
+case class URL(value: String) extends AnyVal
+
 final case class Session(capabilities: Capabilities,
                          accounts: List[Account],
                          primaryAccounts: Map[CapabilityIdentifier, AccountId],
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/ResponseSerializer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/ResponseSerializer.scala
index 04f78d1..597d713 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/ResponseSerializer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/ResponseSerializer.scala
@@ -20,7 +20,6 @@
 package org.apache.james.jmap.json
 
 import java.io.InputStream
-import java.net.URL
 
 import eu.timepit.refined.refineV
 import io.netty.handler.codec.http.HttpResponseStatus
@@ -30,7 +29,7 @@
 import org.apache.james.jmap.core.Id.IdConstraint
 import org.apache.james.jmap.core.Invocation.{Arguments, MethodCallId, MethodName}
 import org.apache.james.jmap.core.SetError.SetErrorDescription
-import org.apache.james.jmap.core.{Account, AccountId, Capabilities, Capability, ClientId, CoreCapabilityProperties, CreatedIds, EhloArg, EhloArgs, EhloName, Invocation, IsPersonal, IsReadOnly, MailCapabilityProperties, MaxCallsInRequest, MaxConcurrentRequests, MaxConcurrentUpload, MaxDelayedSend, MaxMailboxDepth, MaxMailboxesPerEmail, MaxObjectsInGet, MaxObjectsInSet, MaxSizeAttachmentsPerEmail, MaxSizeMailboxName, MaxSizeRequest, MaxSizeUpload, MayCreateTopLevelMailbox, ProblemDetails, Properties, RequestObject, ResponseObject, ServerId, Session, SetError, SubmissionProperties, SupportsPush, UuidState, WebSocketCapabilityProperties}
+import org.apache.james.jmap.core.{Account, AccountId, Capabilities, Capability, ClientId, CoreCapabilityProperties, CreatedIds, EhloArg, EhloArgs, EhloName, Invocation, IsPersonal, IsReadOnly, MailCapabilityProperties, MaxCallsInRequest, MaxConcurrentRequests, MaxConcurrentUpload, MaxDelayedSend, MaxMailboxDepth, MaxMailboxesPerEmail, MaxObjectsInGet, MaxObjectsInSet, MaxSizeAttachmentsPerEmail, MaxSizeMailboxName, MaxSizeRequest, MaxSizeUpload, MayCreateTopLevelMailbox, ProblemDetails, Properties, RequestObject, ResponseObject, ServerId, Session, SetError, SubmissionProperties, SupportsPush, UuidState, WebSocketCapabilityProperties, URL}
 import play.api.libs.functional.syntax._
 import play.api.libs.json._
 
@@ -85,7 +84,7 @@
   private implicit val mayCreateTopLevelMailboxWrites: Writes[MayCreateTopLevelMailbox] = Json.valueWrites[MayCreateTopLevelMailbox]
 
   private implicit val usernameWrites: Writes[Username] = username => JsString(username.asString)
-  private implicit val urlWrites: Writes[URL] = url => JsString(url.toString)
+  private implicit val urlWrites: Writes[URL] = url => JsString(url.value)
   val coreCapabilityWrites: OWrites[CoreCapabilityProperties] = Json.writes[CoreCapabilityProperties]
   val mailCapabilityWrites: OWrites[MailCapabilityProperties] = Json.writes[MailCapabilityProperties]
   private implicit val maxDelayedSendWrites: Writes[MaxDelayedSend] = Json.valueWrites[MaxDelayedSend]
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/SessionSupplier.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/SessionSupplier.scala
index 7abd901..7827265 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/SessionSupplier.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/SessionSupplier.scala
@@ -19,15 +19,13 @@
 
 package org.apache.james.jmap.routes
 
-import java.net.URL
-
 import cats.data.Validated
 import cats.implicits.toTraverseOps
 import cats.instances.list._
 import jakarta.inject.Inject
 import org.apache.james.core.Username
 import org.apache.james.jmap.core.CapabilityIdentifier.CapabilityIdentifier
-import org.apache.james.jmap.core.{Account, AccountId, Capabilities, Capability, CapabilityFactory, IsPersonal, IsReadOnly, Session, UrlPrefixes}
+import org.apache.james.jmap.core.{Account, AccountId, Capabilities, Capability, CapabilityFactory, IsPersonal, IsReadOnly, Session, URL, UrlPrefixes}
 
 import scala.jdk.CollectionConverters._
 
@@ -80,11 +78,11 @@
 }
 
 class JmapUrlEndpointResolver(val urlPrefixes: UrlPrefixes) {
-  val apiUrl: URL = new URL(urlPrefixes.httpUrlPrefix.toString + "/jmap")
+  val apiUrl: URL = URL(urlPrefixes.httpUrlPrefix.toString + "/jmap")
 
-  val downloadUrl: URL = new URL(urlPrefixes.httpUrlPrefix.toString + "/download/{accountId}/{blobId}?type={type}&name={name}")
+  val downloadUrl: URL = URL(urlPrefixes.httpUrlPrefix.toString + "/download/{accountId}/{blobId}?type={type}&name={name}")
 
-  val uploadUrl: URL = new URL(urlPrefixes.httpUrlPrefix.toString + "/upload/{accountId}")
+  val uploadUrl: URL = URL(urlPrefixes.httpUrlPrefix.toString + "/upload/{accountId}")
 
-  val eventSourceUrl: URL = new URL(urlPrefixes.httpUrlPrefix.toString + "/eventSource?types={types}&closeAfter={closeafter}&ping={ping}")
+  val eventSourceUrl: URL = URL(urlPrefixes.httpUrlPrefix.toString + "/eventSource?types={types}&closeAfter={closeafter}&ping={ping}")
 }
\ No newline at end of file
diff --git a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/core/JmapUrlEndpointResolverTest.scala b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/core/JmapUrlEndpointResolverTest.scala
index 325c32d..47791ec 100644
--- a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/core/JmapUrlEndpointResolverTest.scala
+++ b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/core/JmapUrlEndpointResolverTest.scala
@@ -19,7 +19,7 @@
 
 package org.apache.james.jmap.core
 
-import java.net.{URI, URL}
+import java.net.URI
 
 import org.apache.commons.configuration2.{Configuration, PropertiesConfiguration}
 import org.apache.james.jmap.core.JmapConfigProperties.{URL_PREFIX_PROPERTY, WEBSOCKET_URL_PREFIX_PROPERTY}
@@ -42,10 +42,10 @@
     "succeed to configuration urlPrefix when provided" in {
       val testee : JmapUrlEndpointResolver= new JmapUrlEndpointResolver(UrlPrefixes(new URI("http://random-domain.com"), new URI("ws://random-domain.com")))
 
-      testee.apiUrl must be(new URL("http://random-domain.com/jmap"))
-      testee.downloadUrl must be(new URL("http://random-domain.com/download/{accountId}/{blobId}?type={type}&name={name}"))
-      testee.uploadUrl must be(new URL("http://random-domain.com/upload/{accountId}"))
-      testee.eventSourceUrl must be(new URL("http://random-domain.com/eventSource?types={types}&closeAfter={closeafter}&ping={ping}"))
+      testee.apiUrl must be(URL("http://random-domain.com/jmap"))
+      testee.downloadUrl must be(URL("http://random-domain.com/download/{accountId}/{blobId}?type={type}&name={name}"))
+      testee.uploadUrl must be(URL("http://random-domain.com/upload/{accountId}"))
+      testee.eventSourceUrl must be(URL("http://random-domain.com/eventSource?types={types}&closeAfter={closeafter}&ping={ping}"))
     }
   }
 }
diff --git a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/json/SessionSerializationTest.scala b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/json/SessionSerializationTest.scala
index 8ec9101..4617998 100644
--- a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/json/SessionSerializationTest.scala
+++ b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/json/SessionSerializationTest.scala
@@ -19,8 +19,6 @@
 
 package org.apache.james.jmap.json
 
-import java.net.URL
-
 import eu.timepit.refined.auto._
 import org.apache.james.core.Username
 import org.apache.james.jmap.core.CapabilityIdentifier.CapabilityIdentifier
@@ -49,7 +47,7 @@
   private val COLLATION_ALGORITHMS : List[CollationAlgorithm] = List(ALGO_1, ALGO_2, ALGO_3)
   private val USER_1 = Username.of("user1@james.org")
   private val USER_2 = Username.of("user2@james.org")
-  private val URL = new URL("http://james.org")
+  private val URL = org.apache.james.jmap.core.URL("http://james.org")
 
   private val MAIL_IDENTIFIER: CapabilityIdentifier = "urn:ietf:params:jmap:mail"
   private val CONTACT_IDENTIFIER: CapabilityIdentifier = "urn:ietf:params:jmap:contact"
diff --git a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/pushsubscription/PushServerExtension.scala b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/pushsubscription/PushServerExtension.scala
index fde195c..ffb7236 100644
--- a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/pushsubscription/PushServerExtension.scala
+++ b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/pushsubscription/PushServerExtension.scala
@@ -19,7 +19,7 @@
 
 package org.apache.james.jmap.pushsubscription
 
-import java.net.URL
+import java.net.{URI, URL}
 import java.time.Clock
 import java.util.UUID
 
@@ -48,7 +48,7 @@
     MockPushServer.appendSpec(mockServer)
   }
 
-  def getBaseUrl: URL = new URL(s"http://127.0.0.1:${mockServer.getLocalPort}")
+  def getBaseUrl: URL = new URI(s"http://127.0.0.1:${mockServer.getLocalPort}").toURL
 }
 
 object MockPushServer {
diff --git a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/pushsubscription/PushSubscriptionSetCreateProcessorTest.scala b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/pushsubscription/PushSubscriptionSetCreateProcessorTest.scala
index b2e5b56..fa2749f 100644
--- a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/pushsubscription/PushSubscriptionSetCreateProcessorTest.scala
+++ b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/pushsubscription/PushSubscriptionSetCreateProcessorTest.scala
@@ -19,7 +19,7 @@
 
 package org.apache.james.jmap.pushsubscription
 
-import java.net.URL
+import java.net.URI
 import java.security.KeyPair
 import java.security.interfaces.ECPublicKey
 import java.util.{Base64, UUID}
@@ -55,7 +55,7 @@
   def setup(pushServer: ClientAndServer): Unit = {
     val webPushClient: WebPushClient = new DefaultWebPushClient(WebPushClientTestFixture.PUSH_CLIENT_CONFIGURATION)
     testee = new PushSubscriptionSetCreateProcessor(webPushClient)
-    pushServerUrl = PushSubscriptionServerURL(new URL(s"http://127.0.0.1:${pushServer.getLocalPort}/subscribe"))
+    pushServerUrl = PushSubscriptionServerURL(new URI(s"http://127.0.0.1:${pushServer.getLocalPort}/subscribe").toURL)
 
     pushServer
       .when(request
diff --git a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/pushsubscription/SafeWebPushClientContract.scala b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/pushsubscription/SafeWebPushClientContract.scala
index 71b536e..465f6d6 100644
--- a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/pushsubscription/SafeWebPushClientContract.scala
+++ b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/pushsubscription/SafeWebPushClientContract.scala
@@ -19,7 +19,7 @@
 
 package org.apache.james.jmap.pushsubscription
 
-import java.net.URL
+import java.net.URI
 import java.nio.charset.StandardCharsets
 
 import org.apache.james.jmap.api.model.PushSubscriptionServerURL
@@ -48,7 +48,7 @@
   @ParameterizedTest
   @ValueSource(strings = Array("127.0.0.1", "127.0.0.9", "10.9.0.3", "192.168.102.35"))
   def serverSideRequestForgeryAttemptsShouldBeRejected(ip: String): Unit = {
-    assertThatThrownBy(() => Mono.from(testee.push(PushSubscriptionServerURL(new URL(s"http://$ip")), PUSH_REQUEST_SAMPLE)).block)
+    assertThatThrownBy(() => Mono.from(testee.push(PushSubscriptionServerURL(new URI(s"http://$ip").toURL), PUSH_REQUEST_SAMPLE)).block)
       .isInstanceOf(classOf[IllegalArgumentException])
   }
 }
diff --git a/server/protocols/jwt/src/test/java/org/apache/james/jwt/JwksPublicKeyProviderTest.java b/server/protocols/jwt/src/test/java/org/apache/james/jwt/JwksPublicKeyProviderTest.java
index 4d1033b..68da3f5 100644
--- a/server/protocols/jwt/src/test/java/org/apache/james/jwt/JwksPublicKeyProviderTest.java
+++ b/server/protocols/jwt/src/test/java/org/apache/james/jwt/JwksPublicKeyProviderTest.java
@@ -23,6 +23,8 @@
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
 import java.net.URL;
 import java.nio.charset.StandardCharsets;
 import java.security.PublicKey;
@@ -83,8 +85,8 @@
 
     private URL getJwksURL() {
         try {
-            return new URL(String.format("http://127.0.0.1:%s%s", mockServer.getLocalPort(), JWKS_URI_PATH));
-        } catch (MalformedURLException e) {
+            return new URI(String.format("http://127.0.0.1:%s%s", mockServer.getLocalPort(), JWKS_URI_PATH)).toURL();
+        } catch (MalformedURLException | URISyntaxException e) {
             throw new RuntimeException(e);
         }
     }
@@ -106,13 +108,13 @@
     }
 
     @Test
-    void getShouldFailWhenBadJwksURL() throws MalformedURLException {
+    void getShouldFailWhenBadJwksURL() throws MalformedURLException, URISyntaxException {
         mockServer
             .when(HttpRequest.request().withPath("/invalid"))
             .respond(HttpResponse.response().withStatusCode(200)
                 .withBody("invalid body", StandardCharsets.UTF_8));
 
-        PublicKeyProvider testee = JwksPublicKeyProvider.of(new URL(String.format("http://127.0.0.1:%s/invalid", mockServer.getLocalPort())),
+        PublicKeyProvider testee = JwksPublicKeyProvider.of(new URI(String.format("http://127.0.0.1:%s/invalid", mockServer.getLocalPort())).toURL(),
             "wu-9VZEr0gHF986PYPVzvU-5IP1q26EzzQVK_sjG29Q");
         assertThatThrownBy(testee::get)
             .isInstanceOf(MissingOrInvalidKeyException.class);
diff --git a/server/protocols/jwt/src/test/java/org/apache/james/jwt/OidcJwtTokenVerifierTest.java b/server/protocols/jwt/src/test/java/org/apache/james/jwt/OidcJwtTokenVerifierTest.java
index 0d146a8..fedf69b 100644
--- a/server/protocols/jwt/src/test/java/org/apache/james/jwt/OidcJwtTokenVerifierTest.java
+++ b/server/protocols/jwt/src/test/java/org/apache/james/jwt/OidcJwtTokenVerifierTest.java
@@ -26,6 +26,8 @@
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
 import java.net.URL;
 import java.nio.charset.StandardCharsets;
 import java.util.Optional;
@@ -393,24 +395,24 @@
 
     private URL getJwksURL() {
         try {
-            return new URL(String.format("http://127.0.0.1:%s%s", mockServer.getLocalPort(), JWKS_URI_PATH));
-        } catch (MalformedURLException e) {
+            return new URI(String.format("http://127.0.0.1:%s%s", mockServer.getLocalPort(), JWKS_URI_PATH)).toURL();
+        } catch (MalformedURLException | URISyntaxException e) {
             throw new RuntimeException(e);
         }
     }
 
     private URL getUserInfoEndpoint() {
         try {
-            return new URL(String.format("http://127.0.0.1:%s%s", mockServer.getLocalPort(), USERINFO_PATH));
-        } catch (MalformedURLException e) {
+            return new URI(String.format("http://127.0.0.1:%s%s", mockServer.getLocalPort(), USERINFO_PATH)).toURL();
+        } catch (MalformedURLException | URISyntaxException e) {
             throw new RuntimeException(e);
         }
     }
 
     private URL getIntrospectionEndpoint() {
         try {
-            return new URL(String.format("http://127.0.0.1:%s%s", mockServer.getLocalPort(), INTROSPECTION_PATH));
-        } catch (MalformedURLException e) {
+            return new URI(String.format("http://127.0.0.1:%s%s", mockServer.getLocalPort(), INTROSPECTION_PATH)).toURL();
+        } catch (MalformedURLException | URISyntaxException e) {
             throw new RuntimeException(e);
         }
     }
diff --git a/server/protocols/jwt/src/test/java/org/apache/james/jwt/introspection/DefaultCheckTokenClientTest.java b/server/protocols/jwt/src/test/java/org/apache/james/jwt/introspection/DefaultCheckTokenClientTest.java
index e7d7101..e97391b 100644
--- a/server/protocols/jwt/src/test/java/org/apache/james/jwt/introspection/DefaultCheckTokenClientTest.java
+++ b/server/protocols/jwt/src/test/java/org/apache/james/jwt/introspection/DefaultCheckTokenClientTest.java
@@ -25,7 +25,8 @@
 import static org.assertj.core.api.SoftAssertions.assertSoftly;
 
 import java.net.MalformedURLException;
-import java.net.URL;
+import java.net.URI;
+import java.net.URISyntaxException;
 import java.nio.charset.StandardCharsets;
 import java.util.Optional;
 
@@ -57,9 +58,9 @@
 
     private IntrospectionEndpoint getIntrospectionTokenEndpoint() {
         try {
-            return new IntrospectionEndpoint(new URL(String.format("http://127.0.0.1:%s%s", mockServer.getLocalPort(), INTROSPECTION_TOKEN_URI_PATH)),
+            return new IntrospectionEndpoint(new URI(String.format("http://127.0.0.1:%s%s", mockServer.getLocalPort(), INTROSPECTION_TOKEN_URI_PATH)).toURL(),
                 Optional.empty());
-        } catch (MalformedURLException e) {
+        } catch (MalformedURLException | URISyntaxException e) {
             throw new RuntimeException(e);
         }
     }
diff --git a/server/protocols/protocols-imap4/src/main/java/org/apache/james/imapserver/netty/IMAPServer.java b/server/protocols/protocols-imap4/src/main/java/org/apache/james/imapserver/netty/IMAPServer.java
index a80fb2e..73bccc8 100644
--- a/server/protocols/protocols-imap4/src/main/java/org/apache/james/imapserver/netty/IMAPServer.java
+++ b/server/protocols/protocols-imap4/src/main/java/org/apache/james/imapserver/netty/IMAPServer.java
@@ -19,6 +19,7 @@
 package org.apache.james.imapserver.netty;
 
 import java.net.MalformedURLException;
+import java.net.URISyntaxException;
 import java.time.Duration;
 import java.util.Optional;
 import java.util.Set;
@@ -82,7 +83,7 @@
                         isRequireSSL,
                         isPlainAuthEnabled,
                         OidcSASLConfiguration.parse(configuration.configurationAt(OIDC_PATH)));
-                } catch (MalformedURLException | NullPointerException exception) {
+                } catch (MalformedURLException | NullPointerException | URISyntaxException exception) {
                     throw new ConfigurationException("Failed to retrieve oauth component", exception);
                 }
             }
diff --git a/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/netty/SMTPServer.java b/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/netty/SMTPServer.java
index c61e5cb..6924e9f 100644
--- a/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/netty/SMTPServer.java
+++ b/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/netty/SMTPServer.java
@@ -22,6 +22,7 @@
 import static org.apache.james.smtpserver.netty.SMTPServer.AuthenticationAnnounceMode.NEVER;
 
 import java.net.MalformedURLException;
+import java.net.URISyntaxException;
 import java.util.Locale;
 import java.util.Optional;
 import java.util.Set;
@@ -110,7 +111,7 @@
             if (haveOidcProperties) {
                 try {
                     return Optional.of(OidcSASLConfiguration.parse(configuration.configurationAt(OIDC_PATH)));
-                } catch (MalformedURLException exception) {
+                } catch (MalformedURLException | URISyntaxException exception) {
                    throw new ConfigurationException("Failed to retrieve oauth component", exception);
                 }
             } else {
diff --git a/server/protocols/webadmin-integration-test/memory-webadmin-integration-test/src/test/java/org/apache/james/webadmin/integration/memory/MemoryUserDeletionIntegrationTest.java b/server/protocols/webadmin-integration-test/memory-webadmin-integration-test/src/test/java/org/apache/james/webadmin/integration/memory/MemoryUserDeletionIntegrationTest.java
index d1361a3..29fe1ee 100644
--- a/server/protocols/webadmin-integration-test/memory-webadmin-integration-test/src/test/java/org/apache/james/webadmin/integration/memory/MemoryUserDeletionIntegrationTest.java
+++ b/server/protocols/webadmin-integration-test/memory-webadmin-integration-test/src/test/java/org/apache/james/webadmin/integration/memory/MemoryUserDeletionIntegrationTest.java
@@ -28,6 +28,7 @@
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.hamcrest.Matchers.hasSize;
 
+import java.net.URI;
 import java.net.URL;
 import java.util.List;
 import java.util.Optional;
@@ -288,7 +289,7 @@
         server.getProbe(MemoryUserDeletionIntegrationTestProbe.class)
             .addPushSubscriptions(ALICE, new PushSubscriptionCreationRequest(
                 "device",
-                new PushSubscriptionServerURL(new URL("http://whatever/toto")),
+                new PushSubscriptionServerURL(new URI("http://whatever/toto").toURL()),
                 Option.empty(),
                 Option.empty(),
                 PushSubscriptionCreationRequest.noTypes()));
diff --git a/third-party/crowdsec/src/test/java/org/apache/james/crowdsec/CrowdsecEhloHookTest.java b/third-party/crowdsec/src/test/java/org/apache/james/crowdsec/CrowdsecEhloHookTest.java
index d0041d0..f10414b 100644
--- a/third-party/crowdsec/src/test/java/org/apache/james/crowdsec/CrowdsecEhloHookTest.java
+++ b/third-party/crowdsec/src/test/java/org/apache/james/crowdsec/CrowdsecEhloHookTest.java
@@ -23,7 +23,8 @@
 import static org.assertj.core.api.Assertions.assertThat;
 
 import java.io.IOException;
-import java.net.URL;
+import java.net.URI;
+import java.net.URISyntaxException;
 
 import org.apache.james.crowdsec.client.CrowdsecClientConfiguration;
 import org.apache.james.protocols.smtp.SMTPSession;
@@ -40,9 +41,9 @@
     static CrowdsecExtension crowdsecExtension = new CrowdsecExtension();
 
     @BeforeEach
-    void setUpEach() throws IOException {
+    void setUpEach() throws IOException, URISyntaxException {
         int port = crowdsecExtension.getCrowdsecContainer().getMappedPort(CROWDSEC_PORT);
-        ehloHook = new CrowdsecEhloHook(new CrowdsecClientConfiguration(new URL("http://localhost:" + port + "/v1"), CrowdsecClientConfiguration.DEFAULT_API_KEY));
+        ehloHook = new CrowdsecEhloHook(new CrowdsecClientConfiguration(new URI("http://localhost:" + port + "/v1").toURL(), CrowdsecClientConfiguration.DEFAULT_API_KEY));
     }
 
     @Test
diff --git a/third-party/crowdsec/src/test/java/org/apache/james/crowdsec/CrowdsecExtension.java b/third-party/crowdsec/src/test/java/org/apache/james/crowdsec/CrowdsecExtension.java
index 2bfd554..eb834b5 100644
--- a/third-party/crowdsec/src/test/java/org/apache/james/crowdsec/CrowdsecExtension.java
+++ b/third-party/crowdsec/src/test/java/org/apache/james/crowdsec/CrowdsecExtension.java
@@ -22,6 +22,7 @@
 import static org.apache.james.crowdsec.client.CrowdsecClientConfiguration.DEFAULT_API_KEY;
 
 import java.io.IOException;
+import java.net.URI;
 import java.net.URL;
 import java.time.Duration;
 import java.util.UUID;
@@ -117,10 +118,14 @@
     }
 
     public URL getCrowdSecUrl() {
-        return Throwing.supplier(() -> new URL("http",
-            crowdsecContainer.getHost(),
-            crowdsecContainer.getMappedPort(CROWDSEC_PORT),
-            "/v1")).get();
+        return Throwing.supplier(() -> new URI("http://" +
+            crowdsecContainer.getHost() + ":" +
+            crowdsecContainer.getMappedPort(CROWDSEC_PORT) +
+            "/v1").toURL()).get();
+    }
+
+    public URL getLocalhostCrowdsecUrl() {
+        return Throwing.supplier(() -> new URI("http://localhost:" + crowdsecContainer.getMappedPort(CROWDSEC_PORT) + "/v1").toURL()).get();
     }
 
     public GenericContainer<?> getCrowdsecContainer() {
diff --git a/third-party/crowdsec/src/test/java/org/apache/james/crowdsec/CrowdsecHttpClientTest.java b/third-party/crowdsec/src/test/java/org/apache/james/crowdsec/CrowdsecHttpClientTest.java
index 538f1e2..3151e40 100644
--- a/third-party/crowdsec/src/test/java/org/apache/james/crowdsec/CrowdsecHttpClientTest.java
+++ b/third-party/crowdsec/src/test/java/org/apache/james/crowdsec/CrowdsecHttpClientTest.java
@@ -19,14 +19,12 @@
 
 package org.apache.james.crowdsec;
 
-import static org.apache.james.crowdsec.CrowdsecExtension.CROWDSEC_PORT;
 import static org.apache.james.crowdsec.client.CrowdsecClientConfiguration.DEFAULT_API_KEY;
 import static org.apache.james.crowdsec.model.CrowdsecDecision.BAN;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 import java.io.IOException;
-import java.net.URL;
 import java.util.List;
 
 import org.apache.james.crowdsec.client.CrowdsecClientConfiguration;
@@ -43,8 +41,7 @@
     @Test
     void getDecisionsWhenBanningAnIP() throws IOException, InterruptedException {
         banIP("--ip", "192.168.0.4");
-        int port = crowdsecExtension.getCrowdsecContainer().getMappedPort(CROWDSEC_PORT);
-        CrowdsecClientConfiguration config = new CrowdsecClientConfiguration(new URL("http://localhost:" + port + "/v1"), DEFAULT_API_KEY);
+        CrowdsecClientConfiguration config = new CrowdsecClientConfiguration(crowdsecExtension.getLocalhostCrowdsecUrl(), DEFAULT_API_KEY);
         CrowdsecHttpClient httpClient = new CrowdsecHttpClient(config);
         List<CrowdsecDecision> decisions = httpClient.getCrowdsecDecisions().block();
 
@@ -58,8 +55,7 @@
     @Test
     void getDecisionsWhenBanningAnIPRange() throws IOException, InterruptedException {
         banIP("--range", "192.168.0.0/16");
-        int port = crowdsecExtension.getCrowdsecContainer().getMappedPort(CROWDSEC_PORT);
-        CrowdsecClientConfiguration config = new CrowdsecClientConfiguration(new URL("http://localhost:" + port + "/v1"), DEFAULT_API_KEY);
+        CrowdsecClientConfiguration config = new CrowdsecClientConfiguration(crowdsecExtension.getLocalhostCrowdsecUrl(), DEFAULT_API_KEY);
         CrowdsecHttpClient httpClient = new CrowdsecHttpClient(config);
         List<CrowdsecDecision> decisions = httpClient.getCrowdsecDecisions().block();
 
@@ -73,8 +69,7 @@
     @Test
     void getDecisionsWithWrongApiKey() throws IOException, InterruptedException {
         banIP("--range", "192.168.0.0/16");
-        int port = crowdsecExtension.getCrowdsecContainer().getMappedPort(CROWDSEC_PORT);
-        CrowdsecClientConfiguration config = new CrowdsecClientConfiguration(new URL("http://localhost:" + port + "/v1"), "wrong-key");
+        CrowdsecClientConfiguration config = new CrowdsecClientConfiguration(crowdsecExtension.getLocalhostCrowdsecUrl(), "wrong-key");
         CrowdsecHttpClient httpClient = new CrowdsecHttpClient(config);
 
         assertThatThrownBy(() -> httpClient.getCrowdsecDecisions().block())
@@ -83,8 +78,7 @@
 
     @Test
     void getDecisionsWhenNoBanning() throws IOException {
-        int port = crowdsecExtension.getCrowdsecContainer().getMappedPort(CROWDSEC_PORT);
-        CrowdsecClientConfiguration config = new CrowdsecClientConfiguration(new URL("http://localhost:" + port + "/v1"), DEFAULT_API_KEY);
+        CrowdsecClientConfiguration config = new CrowdsecClientConfiguration(crowdsecExtension.getLocalhostCrowdsecUrl(), DEFAULT_API_KEY);
         CrowdsecHttpClient httpClient = new CrowdsecHttpClient(config);
         List<CrowdsecDecision> decisions = httpClient.getCrowdsecDecisions().block();
 
@@ -95,8 +89,7 @@
     void getDecisionsWhenBanningMultipleIP() throws IOException, InterruptedException {
         banIP("--ip", "192.168.0.4");
         banIP("--ip", "192.168.0.5");
-        int port = crowdsecExtension.getCrowdsecContainer().getMappedPort(CROWDSEC_PORT);
-        CrowdsecClientConfiguration config = new CrowdsecClientConfiguration(new URL("http://localhost:" + port + "/v1"), DEFAULT_API_KEY);
+        CrowdsecClientConfiguration config = new CrowdsecClientConfiguration(crowdsecExtension.getLocalhostCrowdsecUrl(), DEFAULT_API_KEY);
         CrowdsecHttpClient httpClient = new CrowdsecHttpClient(config);
         List<CrowdsecDecision> decisions = httpClient.getCrowdsecDecisions().block();
 
diff --git a/third-party/crowdsec/src/test/java/org/apache/james/crowdsec/CrowdsecImapConnectionCheckTest.java b/third-party/crowdsec/src/test/java/org/apache/james/crowdsec/CrowdsecImapConnectionCheckTest.java
index adc6ffe..1886fc7 100644
--- a/third-party/crowdsec/src/test/java/org/apache/james/crowdsec/CrowdsecImapConnectionCheckTest.java
+++ b/third-party/crowdsec/src/test/java/org/apache/james/crowdsec/CrowdsecImapConnectionCheckTest.java
@@ -40,8 +40,7 @@
     @Test
     void givenIPBannedByCrowdsecDecisionIp() throws IOException, InterruptedException {
         banIP("--ip", "127.0.0.3");
-        int port = crowdsecExtension.getCrowdsecContainer().getMappedPort(CROWDSEC_PORT);
-        CrowdsecClientConfiguration crowdsecClientConfiguration = new CrowdsecClientConfiguration(new URL("http://localhost:" + port + "/v1"), DEFAULT_API_KEY);
+        CrowdsecClientConfiguration crowdsecClientConfiguration = new CrowdsecClientConfiguration(crowdsecExtension.getLocalhostCrowdsecUrl(), DEFAULT_API_KEY);
 
         CrowdsecImapConnectionCheck connectionCheck = new CrowdsecImapConnectionCheck(crowdsecClientConfiguration);
         connectionCheck.validate(new InetSocketAddress("127.0.0.3", 8800));
@@ -52,8 +51,7 @@
     @Test
     void givenIPBannedByCrowdsecDecisionIpRange() throws IOException, InterruptedException {
         banIP("--range", "127.0.0.1/24");
-        int port = crowdsecExtension.getCrowdsecContainer().getMappedPort(CROWDSEC_PORT);
-        CrowdsecClientConfiguration crowdsecClientConfiguration = new CrowdsecClientConfiguration(new URL("http://localhost:" + port + "/v1"), DEFAULT_API_KEY);
+        CrowdsecClientConfiguration crowdsecClientConfiguration = new CrowdsecClientConfiguration(crowdsecExtension.getLocalhostCrowdsecUrl(), DEFAULT_API_KEY);
 
         CrowdsecImapConnectionCheck connectionCheck = new CrowdsecImapConnectionCheck(crowdsecClientConfiguration);
         connectionCheck.validate(new InetSocketAddress("127.0.0.3", 8800));
@@ -64,9 +62,7 @@
     @Test
     void givenIPNotBannedByCrowdsecDecisionIp() throws IOException, InterruptedException {
         banIP("--ip", "192.182.39.2");
-
-        int port = crowdsecExtension.getCrowdsecContainer().getMappedPort(CROWDSEC_PORT);
-        CrowdsecClientConfiguration crowdsecClientConfiguration = new CrowdsecClientConfiguration(new URL("http://localhost:" + port + "/v1"), DEFAULT_API_KEY);
+        CrowdsecClientConfiguration crowdsecClientConfiguration = new CrowdsecClientConfiguration(crowdsecExtension.getLocalhostCrowdsecUrl(), DEFAULT_API_KEY);
 
         CrowdsecImapConnectionCheck connectionCheck = new CrowdsecImapConnectionCheck(crowdsecClientConfiguration);
         connectionCheck.validate(new InetSocketAddress("127.0.0.3", 8800));
@@ -77,8 +73,7 @@
     void givenIPNotBannedByCrowdsecDecisionIpRange() throws IOException, InterruptedException {
         banIP("--range", "192.182.39.2/24");
 
-        int port = crowdsecExtension.getCrowdsecContainer().getMappedPort(CROWDSEC_PORT);
-        CrowdsecClientConfiguration crowdsecClientConfiguration = new CrowdsecClientConfiguration(new URL("http://localhost:" + port + "/v1"), DEFAULT_API_KEY);
+        CrowdsecClientConfiguration crowdsecClientConfiguration = new CrowdsecClientConfiguration(crowdsecExtension.getLocalhostCrowdsecUrl(), DEFAULT_API_KEY);
 
         CrowdsecImapConnectionCheck connectionCheck = new CrowdsecImapConnectionCheck(crowdsecClientConfiguration);
         connectionCheck.validate(new InetSocketAddress("127.0.0.3", 8800));
diff --git a/third-party/crowdsec/src/test/java/org/apache/james/crowdsec/CrowdsecSMTPConnectHandlerTest.java b/third-party/crowdsec/src/test/java/org/apache/james/crowdsec/CrowdsecSMTPConnectHandlerTest.java
index d1be4f7..0c20677 100644
--- a/third-party/crowdsec/src/test/java/org/apache/james/crowdsec/CrowdsecSMTPConnectHandlerTest.java
+++ b/third-party/crowdsec/src/test/java/org/apache/james/crowdsec/CrowdsecSMTPConnectHandlerTest.java
@@ -22,8 +22,7 @@
 
     @BeforeEach
     void setUpEach() throws IOException {
-        int port = crowdsecExtension.getCrowdsecContainer().getMappedPort(CROWDSEC_PORT);
-        var crowdsecClientConfiguration = new CrowdsecClientConfiguration(new URL("http://localhost:" + port + "/v1"), CrowdsecClientConfiguration.DEFAULT_API_KEY);
+        var crowdsecClientConfiguration = new CrowdsecClientConfiguration(crowdsecExtension.getLocalhostCrowdsecUrl(), CrowdsecClientConfiguration.DEFAULT_API_KEY);
         connectHandler = new CrowdsecSMTPConnectHandler(new CrowdsecService(crowdsecClientConfiguration));
     }
 
diff --git a/third-party/crowdsec/src/test/java/org/apache/james/crowdsec/CrowdsecServiceTest.java b/third-party/crowdsec/src/test/java/org/apache/james/crowdsec/CrowdsecServiceTest.java
index 384ebb5..8bc8c45 100644
--- a/third-party/crowdsec/src/test/java/org/apache/james/crowdsec/CrowdsecServiceTest.java
+++ b/third-party/crowdsec/src/test/java/org/apache/james/crowdsec/CrowdsecServiceTest.java
@@ -41,8 +41,7 @@
 
     @BeforeEach
     void setUpEach() throws IOException {
-        int port = crowdsecExtension.getCrowdsecContainer().getMappedPort(CROWDSEC_PORT);
-        service = new CrowdsecService(new CrowdsecClientConfiguration(new URL("http://localhost:" + port + "/v1"), CrowdsecClientConfiguration.DEFAULT_API_KEY));
+        service = new CrowdsecService(new CrowdsecClientConfiguration(crowdsecExtension.getLocalhostCrowdsecUrl(), CrowdsecClientConfiguration.DEFAULT_API_KEY));
     }
 
     @Test
diff --git a/third-party/linshare/src/main/java/org/apache/james/linshare/LinshareConfiguration.java b/third-party/linshare/src/main/java/org/apache/james/linshare/LinshareConfiguration.java
index 44f3add..c487ef1 100644
--- a/third-party/linshare/src/main/java/org/apache/james/linshare/LinshareConfiguration.java
+++ b/third-party/linshare/src/main/java/org/apache/james/linshare/LinshareConfiguration.java
@@ -20,12 +20,14 @@
 package org.apache.james.linshare;
 
 import java.net.MalformedURLException;
+import java.net.URI;
 import java.net.URL;
 import java.util.Objects;
 import java.util.UUID;
 
 import org.apache.commons.configuration2.Configuration;
 
+import com.github.fge.lambdas.Throwing;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Preconditions;
 
@@ -41,7 +43,10 @@
             RequireBasicAuthorization url(URL url);
 
             default RequireBasicAuthorization urlAsString(String url) throws MalformedURLException {
-                return url(new URL(url));
+                if (url == null) {
+                    throw new MalformedURLException("url can not be null");
+                }
+                return url(Throwing.supplier(() -> new URI(url).toURL()).get());
             }
         }
 
diff --git a/third-party/linshare/src/test/java/org/apache/james/linshare/LinshareConfigurationTest.java b/third-party/linshare/src/test/java/org/apache/james/linshare/LinshareConfigurationTest.java
index aabbd61..3d4a32c 100644
--- a/third-party/linshare/src/test/java/org/apache/james/linshare/LinshareConfigurationTest.java
+++ b/third-party/linshare/src/test/java/org/apache/james/linshare/LinshareConfigurationTest.java
@@ -23,7 +23,7 @@
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 import java.net.MalformedURLException;
-import java.net.URL;
+import java.net.URI;
 import java.util.UUID;
 
 import org.apache.commons.configuration2.PropertiesConfiguration;
@@ -111,7 +111,7 @@
         configuration.addProperty(LinshareConfiguration.PASSWORD_PROPERTY, LinshareFixture.TECHNICAL_ACCOUNT.getPassword());
         configuration.addProperty(LinshareConfiguration.URL_PROPERTY, "invalid");
 
-        assertThatThrownBy(() -> LinshareConfiguration.from(configuration)).isInstanceOf(MalformedURLException.class);
+        assertThatThrownBy(() -> LinshareConfiguration.from(configuration)).isInstanceOf(IllegalArgumentException.class);
     }
 
     @Test
@@ -145,7 +145,7 @@
         configuration.addProperty(LinshareConfiguration.URL_PROPERTY, url);
 
         assertThat(LinshareConfiguration.from(configuration)).isEqualTo(LinshareConfiguration.builder()
-            .url(new URL(url))
+            .url(new URI(url).toURL())
             .basicAuthorization(DEFAULT_UUID, password)
             .build());
     }
diff --git a/third-party/rspamd/src/test/java/org/apache/james/rspamd/RspamdExtension.java b/third-party/rspamd/src/test/java/org/apache/james/rspamd/RspamdExtension.java
index ab12fe5..8e307a4 100644
--- a/third-party/rspamd/src/test/java/org/apache/james/rspamd/RspamdExtension.java
+++ b/third-party/rspamd/src/test/java/org/apache/james/rspamd/RspamdExtension.java
@@ -20,6 +20,7 @@
 package org.apache.james.rspamd;
 
 import java.io.IOException;
+import java.net.URI;
 import java.net.URL;
 import java.time.Duration;
 import java.util.UUID;
@@ -123,10 +124,10 @@
     }
 
     public URL rspamdURL() {
-        return Throwing.supplier(() -> new URL("http",
-            rspamdContainer.getHost(),
-            rspamdContainer.getMappedPort(RSPAMD_DEFAULT_PORT),
-            "/")).get();
+        return Throwing.supplier(() -> new URI("http://"+
+            rspamdContainer.getHost() + ":" +
+            rspamdContainer.getMappedPort(RSPAMD_DEFAULT_PORT) +
+            "/").toURL()).get();
     }
 
     public URL getBaseUrl() {
diff --git a/third-party/rspamd/src/test/java/org/apache/james/rspamd/healthcheck/RspamdHealthcheckTest.java b/third-party/rspamd/src/test/java/org/apache/james/rspamd/healthcheck/RspamdHealthcheckTest.java
index ee88c42..3fd15bf 100644
--- a/third-party/rspamd/src/test/java/org/apache/james/rspamd/healthcheck/RspamdHealthcheckTest.java
+++ b/third-party/rspamd/src/test/java/org/apache/james/rspamd/healthcheck/RspamdHealthcheckTest.java
@@ -21,7 +21,7 @@
 
 import static org.assertj.core.api.Assertions.assertThat;
 
-import java.net.URL;
+import java.net.URI;
 import java.util.Optional;
 
 import org.apache.james.core.healthcheck.Result;
@@ -68,7 +68,7 @@
 
     @Test
     void checkShouldReturnUnhealthyWhenWrongRspamdURL() throws Exception {
-        RspamdClientConfiguration configuration = new RspamdClientConfiguration(new URL("http://wrongRspamdURL:11334"), "passwordDoesNotMatter", Optional.empty());
+        RspamdClientConfiguration configuration = new RspamdClientConfiguration(new URI("http://wrongRspamdURL:11334").toURL(), "passwordDoesNotMatter", Optional.empty());
         RspamdHttpClient client = new RspamdHttpClient(configuration);
         rspamdHealthCheck = new RspamdHealthCheck(client);
 
diff --git a/third-party/rspamd/src/test/java/org/apache/james/rspamd/task/FeedHamToRspamdTaskTest.java b/third-party/rspamd/src/test/java/org/apache/james/rspamd/task/FeedHamToRspamdTaskTest.java
index e133b0c..1f06b13 100644
--- a/third-party/rspamd/src/test/java/org/apache/james/rspamd/task/FeedHamToRspamdTaskTest.java
+++ b/third-party/rspamd/src/test/java/org/apache/james/rspamd/task/FeedHamToRspamdTaskTest.java
@@ -29,7 +29,7 @@
 import static org.mockito.Mockito.mock;
 
 import java.io.ByteArrayInputStream;
-import java.net.URL;
+import java.net.URI;
 import java.nio.ByteBuffer;
 import java.time.Clock;
 import java.time.Instant;
@@ -577,7 +577,7 @@
             .when(HttpRequest.request().withPath("/learnham"))
             .respond(httpRequest -> HttpResponse.response().withStatusCode(200), Delay.delay(TimeUnit.SECONDS, 10));
 
-        RspamdHttpClient httpClient = new RspamdHttpClient(new RspamdClientConfiguration(new URL(String.format("http://localhost:%s", mockServer.getLocalPort())),
+        RspamdHttpClient httpClient = new RspamdHttpClient(new RspamdClientConfiguration(new URI(String.format("http://localhost:%s", mockServer.getLocalPort())).toURL(),
             PASSWORD, Optional.of(3)));
 
         RunningOptions runningOptions = new RunningOptions(Optional.empty(),
diff --git a/third-party/rspamd/src/test/java/org/apache/james/rspamd/task/FeedSpamToRspamdTaskTest.java b/third-party/rspamd/src/test/java/org/apache/james/rspamd/task/FeedSpamToRspamdTaskTest.java
index 6d4b4ec..0223601 100644
--- a/third-party/rspamd/src/test/java/org/apache/james/rspamd/task/FeedSpamToRspamdTaskTest.java
+++ b/third-party/rspamd/src/test/java/org/apache/james/rspamd/task/FeedSpamToRspamdTaskTest.java
@@ -29,7 +29,7 @@
 import static org.mockito.Mockito.mock;
 
 import java.io.ByteArrayInputStream;
-import java.net.URL;
+import java.net.URI;
 import java.nio.ByteBuffer;
 import java.time.Clock;
 import java.time.Instant;
@@ -530,7 +530,7 @@
             .when(HttpRequest.request().withPath("/learnspam"))
             .respond(httpRequest -> HttpResponse.response().withStatusCode(200), Delay.delay(TimeUnit.SECONDS, 10));
 
-        RspamdHttpClient httpClient = new RspamdHttpClient(new RspamdClientConfiguration(new URL(String.format("http://localhost:%s", mockServer.getLocalPort())),
+        RspamdHttpClient httpClient = new RspamdHttpClient(new RspamdClientConfiguration(new URI(String.format("http://localhost:%s", mockServer.getLocalPort())).toURL(),
             PASSWORD, Optional.of(3)));
 
         RunningOptions runningOptions = new RunningOptions(Optional.empty(),