From 6781a173975e0de69b3f86d5f404d76b67a3365c Mon Sep 17 00:00:00 2001
From: Jean-Pierre Portier <jean-pierre.portier@mailgun.com>
Date: Tue, 26 Nov 2024 18:03:17 +0100
Subject: [PATCH 1/8] feat (Java): Support 'AdditionalProperty' interface for
 form parameters serialization

---
 .../adapters/DeliveryTimeFormSerializer.java  |   5 +-
 .../sdk/core/databind/FormSerializer.java     |   4 +-
 .../core/databind/multipart/ObjectMapper.java |  67 +++++++---
 .../sdk/core/models/AdditionalProperties.java |  27 +++++
 .../utils/databind/RFC822FormSerializer.java  |   5 +-
 .../databind/multipart/ObjectMapperTest.java  | 114 ++++++++++++++++--
 .../v1/emails/request/SendEmailRequest.java   |  13 +-
 .../emails/request/SendMimeEmailRequest.java  |  13 +-
 .../internal/VerificationStartSmsOptions.java |  13 +-
 9 files changed, 200 insertions(+), 61 deletions(-)
 create mode 100644 core/src/main/com/sinch/sdk/core/models/AdditionalProperties.java

diff --git a/client/src/main/com/sinch/sdk/domains/mailgun/api/v1/adapters/DeliveryTimeFormSerializer.java b/client/src/main/com/sinch/sdk/domains/mailgun/api/v1/adapters/DeliveryTimeFormSerializer.java
index c68d4d28..65287349 100644
--- a/client/src/main/com/sinch/sdk/domains/mailgun/api/v1/adapters/DeliveryTimeFormSerializer.java
+++ b/client/src/main/com/sinch/sdk/domains/mailgun/api/v1/adapters/DeliveryTimeFormSerializer.java
@@ -1,11 +1,12 @@
 package com.sinch.sdk.domains.mailgun.api.v1.adapters;
 
 import com.sinch.sdk.core.databind.FormSerializer;
+import java.util.Map;
 
 public class DeliveryTimeFormSerializer extends FormSerializer<Integer> {
 
   @Override
-  public String serialize(Integer in) {
-    return String.format("%dh", in);
+  public void serialize(Integer in, String fieldName, Map<String, Object> out) {
+    out.put(fieldName, String.format("%dh", in));
   }
 }
diff --git a/core/src/main/com/sinch/sdk/core/databind/FormSerializer.java b/core/src/main/com/sinch/sdk/core/databind/FormSerializer.java
index 397eba1d..a61ad007 100644
--- a/core/src/main/com/sinch/sdk/core/databind/FormSerializer.java
+++ b/core/src/main/com/sinch/sdk/core/databind/FormSerializer.java
@@ -1,6 +1,8 @@
 package com.sinch.sdk.core.databind;
 
+import java.util.Map;
+
 public abstract class FormSerializer<T> {
 
-  public abstract Object serialize(T in);
+  public abstract void serialize(T in, String fieldName, Map<String, Object> out);
 }
diff --git a/core/src/main/com/sinch/sdk/core/databind/multipart/ObjectMapper.java b/core/src/main/com/sinch/sdk/core/databind/multipart/ObjectMapper.java
index b199fbd9..048927f6 100644
--- a/core/src/main/com/sinch/sdk/core/databind/multipart/ObjectMapper.java
+++ b/core/src/main/com/sinch/sdk/core/databind/multipart/ObjectMapper.java
@@ -5,6 +5,7 @@
 import com.sinch.sdk.core.databind.annotation.PropertiesOrder;
 import com.sinch.sdk.core.databind.annotation.Property;
 import com.sinch.sdk.core.exceptions.SerializationException;
+import com.sinch.sdk.core.models.AdditionalProperties;
 import com.sinch.sdk.core.models.OptionalValue;
 import com.sinch.sdk.core.utils.EnumDynamic;
 import com.sinch.sdk.core.utils.Pair;
@@ -16,6 +17,7 @@
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
@@ -23,6 +25,8 @@
 
 public class ObjectMapper {
 
+  static final String ADDITIONAL_PROPERTIES_IDENTIFIER = "additionalProperties";
+
   public Map<String, Object> serialize(Object value)
       throws IntrospectionException,
           InvocationTargetException,
@@ -33,7 +37,13 @@ public Map<String, Object> serialize(Object value)
 
     BeanInfo beanInfo = Introspector.getBeanInfo(value.getClass(), Object.class);
     List<Pair<String, Method>> serializableProperties = collectSerializableProperties(beanInfo);
-    return serializeProperties(serializableProperties, value);
+    Map<String, Object> output = serializeProperties(serializableProperties, value);
+    if (value instanceof AdditionalProperties) {
+      serializeAdditionalProperties(
+          getAdditionalPropertiesGetter(beanInfo).getMethod(), value, output);
+    }
+
+    return output;
   }
 
   private List<Pair<String, Method>> collectSerializableProperties(BeanInfo beanInfo) {
@@ -77,49 +87,78 @@ private Optional<Pair<String, Method>> getPropertyGetter(Method method) {
     return Optional.of(new Pair<>(property.value(), method));
   }
 
+  private MethodDescriptor getAdditionalPropertiesGetter(BeanInfo beanInfo) {
+
+    return Arrays.stream(beanInfo.getMethodDescriptors())
+        .filter(f -> f.getMethod().getName().equals(ADDITIONAL_PROPERTIES_IDENTIFIER))
+        .findFirst()
+        .orElseThrow(
+            () ->
+                new SerializationException(
+                    String.format("Missing '%s' getter", ADDITIONAL_PROPERTIES_IDENTIFIER)));
+  }
+
   private Map<String, Object> serializeProperties(
       List<Pair<String, Method>> serializableProperties, Object object)
       throws InvocationTargetException, IllegalAccessException {
-    Map<String, Object> properties = new LinkedHashMap<>();
+    Map<String, Object> out = new LinkedHashMap<>();
     for (Pair<String, Method> property : serializableProperties) {
+      serializeProperty(object, property.getRight(), property.getLeft(), out);
+    }
+    return out;
+  }
 
-      serializeProperty(object, property.getRight())
-          .ifPresent(v -> properties.put(property.getLeft(), v));
+  private void serializeAdditionalProperties(
+      Method method, Object object, Map<String, Object> output)
+      throws InvocationTargetException, IllegalAccessException {
+
+    @SuppressWarnings("unchecked")
+    Map<String, Object> propertyValue = (Map<String, Object>) method.invoke(object);
+    if (null == propertyValue) {
+      return;
     }
-    return properties;
+
+    FormSerialize formSerialize = method.getDeclaredAnnotation(FormSerialize.class);
+    if (null != formSerialize) {
+      handleOverriddenSerialization(formSerialize, propertyValue, "", output);
+      return;
+    }
+    output.putAll(propertyValue);
   }
 
-  private OptionalValue<?> serializeProperty(Object object, Method method)
+  private void serializeProperty(
+      Object object, Method method, String fieldName, Map<String, Object> out)
       throws InvocationTargetException, IllegalAccessException {
 
     OptionalValue<?> propertyValue = (OptionalValue<?>) method.invoke(object);
-
     if (!propertyValue.isPresent() || null == propertyValue.get()) {
-      return propertyValue;
+      return;
     }
 
     Object value = propertyValue.get();
 
     FormSerialize formSerialize = method.getDeclaredAnnotation(FormSerialize.class);
     if (null != formSerialize) {
-      value = handleOverriddenSerialization(formSerialize, value);
+      handleOverriddenSerialization(formSerialize, value, fieldName, out);
+      return;
     }
 
     if (value instanceof EnumDynamic) {
       EnumDynamic<?, ?> enumDynamic = (EnumDynamic<?, ?>) value;
-      return OptionalValue.of((enumDynamic.value().toString()));
+      out.put(fieldName, enumDynamic.value().toString());
+      return;
     }
-    return OptionalValue.of(value);
+    out.put(fieldName, value);
   }
 
-  private Object handleOverriddenSerialization(FormSerialize formSerialize, Object value) {
+  private void handleOverriddenSerialization(
+      FormSerialize formSerialize, Object in, String fieldName, Map<String, Object> out) {
     try {
       Class<?> clazz = Class.forName(formSerialize.using().getName());
       @SuppressWarnings("unchecked")
       Constructor<FormSerializer<Object>> ctor =
           (Constructor<FormSerializer<Object>>) clazz.getConstructor();
-      FormSerializer<Object> serializer = ctor.newInstance();
-      return serializer.serialize(value);
+      ctor.newInstance().serialize(in, fieldName, out);
     } catch (Exception e) {
       throw new SerializationException(e);
     }
diff --git a/core/src/main/com/sinch/sdk/core/models/AdditionalProperties.java b/core/src/main/com/sinch/sdk/core/models/AdditionalProperties.java
new file mode 100644
index 00000000..d708d11b
--- /dev/null
+++ b/core/src/main/com/sinch/sdk/core/models/AdditionalProperties.java
@@ -0,0 +1,27 @@
+package com.sinch.sdk.core.models;
+
+import com.sinch.sdk.domains.mailgun.models.v1.emails.request.SendEmailRequest.Builder;
+
+/** Interface definition for schemas supporting additional properties */
+public interface AdditionalProperties {
+
+  /**
+   * Return the additional property with the specified name.
+   *
+   * @param key the name of the property
+   * @return the additional property with the specified name
+   * @since __TO_BE_DEFINED__
+   */
+  Object get(String key);
+
+  interface Builder {
+
+    /**
+     * see getter
+     *
+     * @return Current builder
+     * @see #get
+     */
+    Builder put(String key, Object value);
+  }
+}
diff --git a/core/src/main/com/sinch/sdk/core/utils/databind/RFC822FormSerializer.java b/core/src/main/com/sinch/sdk/core/utils/databind/RFC822FormSerializer.java
index d6ba5385..4d22a8fc 100644
--- a/core/src/main/com/sinch/sdk/core/utils/databind/RFC822FormSerializer.java
+++ b/core/src/main/com/sinch/sdk/core/utils/databind/RFC822FormSerializer.java
@@ -4,11 +4,12 @@
 import java.time.Instant;
 import java.time.ZoneId;
 import java.time.format.DateTimeFormatter;
+import java.util.Map;
 
 public class RFC822FormSerializer extends FormSerializer<Instant> {
 
   @Override
-  public String serialize(Instant in) {
-    return DateTimeFormatter.RFC_1123_DATE_TIME.format(in.atZone(ZoneId.of("UTC")));
+  public void serialize(Instant in, String fieldName, Map<String, Object> out) {
+    out.put(fieldName, DateTimeFormatter.RFC_1123_DATE_TIME.format(in.atZone(ZoneId.of("UTC"))));
   }
 }
diff --git a/core/src/test/java/com/sinch/sdk/core/databind/multipart/ObjectMapperTest.java b/core/src/test/java/com/sinch/sdk/core/databind/multipart/ObjectMapperTest.java
index 27c80882..091bdb66 100644
--- a/core/src/test/java/com/sinch/sdk/core/databind/multipart/ObjectMapperTest.java
+++ b/core/src/test/java/com/sinch/sdk/core/databind/multipart/ObjectMapperTest.java
@@ -6,6 +6,7 @@
 import com.sinch.sdk.core.databind.annotation.PropertiesOrder;
 import com.sinch.sdk.core.databind.annotation.Property;
 import com.sinch.sdk.core.databind.multipart.ObjectMapperTest.SerializableObject.AnEnum;
+import com.sinch.sdk.core.models.AdditionalProperties;
 import com.sinch.sdk.core.models.OptionalValue;
 import com.sinch.sdk.core.utils.EnumDynamic;
 import com.sinch.sdk.core.utils.EnumSupportDynamic;
@@ -18,7 +19,9 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.LinkedHashMap;
 import java.util.Map;
+import java.util.Set;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import org.junit.jupiter.api.Assertions;
@@ -29,7 +32,7 @@ class ObjectMapperTest {
   static File fileAttachment1;
   static File fileAttachment2;
 
-  static Map<String, Object> serialized;
+  static Map<String, Object> serialized, defaultAdditionalPropertiesSerialized;
 
   static {
     ClassLoader classLoader = SendEmailRequestTest.class.getClassLoader();
@@ -40,6 +43,9 @@ class ObjectMapperTest {
         new File(
             classLoader.getResource("domains/mailgun/v1/emails/request/attachment2.txt").getFile());
 
+    Map<String, Object> additionalProperties =
+        fillMap("header:toto", "toto-value", "variable:foo", "foo-value", "raw", "raw-value");
+
     SerializableObject object =
         new SerializableObject(
             OptionalValue.of("text value"),
@@ -51,10 +57,16 @@ class ObjectMapperTest {
             OptionalValue.of(Arrays.asList(fileAttachment1, fileAttachment2)),
             OptionalValue.of(
                 Arrays.asList(
-                    Instant.parse("2024-11-25T10:06:54Z"), Instant.parse("2024-11-25T09:06:54Z"))));
+                    Instant.parse("2024-11-25T10:06:54Z"), Instant.parse("2024-11-25T09:06:54Z"))),
+            additionalProperties);
+
+    DefaultAdditionalPropertiesSerializableObject defaultAdditionalPropertiesSerializableObject =
+        new DefaultAdditionalPropertiesSerializableObject(additionalProperties);
 
     try {
       serialized = new ObjectMapper().serialize(object);
+      defaultAdditionalPropertiesSerialized =
+          new ObjectMapper().serialize(defaultAdditionalPropertiesSerializableObject);
     } catch (Exception e) {
       throw new RuntimeException(e);
     }
@@ -62,7 +74,7 @@ class ObjectMapperTest {
 
   @Test
   void countValue() {
-    Assertions.assertEquals(8, serialized.size());
+    Assertions.assertEquals(11, serialized.size());
   }
 
   @Test
@@ -76,7 +88,10 @@ void order() {
       SerializableObject.PROPERTY_FILE,
       SerializableObject.PROPERTY_ENUM,
       SerializableObject.PROPERTY_ENUM_COLLECTION,
-      SerializableObject.PROPERTY_TEXT_COLLECTION
+      SerializableObject.PROPERTY_TEXT_COLLECTION,
+      "h:toto",
+      "v:foo",
+      "raw"
     };
     String[] keys = serialized.keySet().toArray(new String[0]);
     TestHelpers.recursiveEquals(expectedOrder, keys);
@@ -128,6 +143,20 @@ void rfc822CollectionValue() {
         serialized.get("aRfc822Collection"));
   }
 
+  @Test
+  void additionalPropertiesValues() {
+    Assertions.assertEquals("toto-value", serialized.get("h:toto"));
+    Assertions.assertEquals("foo-value", serialized.get("v:foo"));
+    Assertions.assertEquals("raw-value", serialized.get("raw"));
+  }
+
+  @Test
+  void defaultAdditionalPropertiesValues() {
+    TestHelpers.recursiveEquals(
+        fillMap("header:toto", "toto-value", "variable:foo", "foo-value", "raw", "raw-value"),
+        defaultAdditionalPropertiesSerialized);
+  }
+
   @PropertiesOrder({
     SerializableObject.PROPERTY_RFC822_COLLECTION,
     SerializableObject.PROPERTY_TEXT,
@@ -139,7 +168,7 @@ void rfc822CollectionValue() {
     SerializableObject.PROPERTY_RFC822_COLLECTION,
     SerializableObject.PROPERTY_TEXT_COLLECTION
   })
-  static class SerializableObject {
+  static class SerializableObject implements AdditionalProperties {
     public static final String PROPERTY_TEXT = "aText";
     public static final String PROPERTY_ENUM = "anEnum";
     public static final String PROPERTY_FILE = "aFile";
@@ -159,6 +188,7 @@ static class SerializableObject {
     private final OptionalValue<Collection<AnEnum>> enumCollection;
     private final OptionalValue<Collection<File>> fileCollection;
     private final OptionalValue<Collection<Instant>> instantCollection;
+    private final Map<String, Object> additionalProperties;
 
     public SerializableObject(
         OptionalValue<String> text,
@@ -168,7 +198,8 @@ public SerializableObject(
         OptionalValue<Collection<String>> textCollection,
         OptionalValue<Collection<AnEnum>> enumCollection,
         OptionalValue<Collection<File>> fileCollection,
-        OptionalValue<Collection<Instant>> instantCollection) {
+        OptionalValue<Collection<Instant>> instantCollection,
+        Map<String, Object> additionalProperties) {
       this.text = text;
       this._enum = _enum;
       this.file = file;
@@ -177,6 +208,7 @@ public SerializableObject(
       this.textCollection = textCollection;
       this.enumCollection = enumCollection;
       this.fileCollection = fileCollection;
+      this.additionalProperties = additionalProperties;
     }
 
     @Property(PROPERTY_TEXT)
@@ -221,6 +253,19 @@ public OptionalValue<Collection<Instant>> instantCollection() {
       return instantCollection;
     }
 
+    @FormSerialize(using = AdditionalPropertiesFormSerializer.class)
+    public Map<String, Object> additionalProperties() {
+      return additionalProperties;
+    }
+
+    public Object get(String key) {
+      return null;
+    }
+
+    public Set<String> keys() {
+      return additionalProperties.keySet();
+    }
+
     public static class AnEnum extends EnumDynamic<String, AnEnum> {
       public static final AnEnum YES = new AnEnum("yes");
       public static final AnEnum NO = new AnEnum("no");
@@ -246,15 +291,60 @@ public static String valueOf(AnEnum e) {
     }
   }
 
+  static class DefaultAdditionalPropertiesSerializableObject implements AdditionalProperties {
+    private final Map<String, Object> additionalProperties;
+
+    public DefaultAdditionalPropertiesSerializableObject(Map<String, Object> additionalProperties) {
+      this.additionalProperties = additionalProperties;
+    }
+
+    public Map<String, Object> additionalProperties() {
+      return additionalProperties;
+    }
+
+    public Object get(String key) {
+      return null;
+    }
+
+    public Set<String> keys() {
+      return additionalProperties.keySet();
+    }
+  }
+
   public static class RFC822ListFormSerializer extends FormSerializer<Collection<Instant>> {
 
     @Override
-    public Collection<String> serialize(Collection<Instant> in) {
-      return in.stream()
-          .map(
-              instant ->
-                  DateTimeFormatter.RFC_1123_DATE_TIME.format(instant.atZone(ZoneId.of("UTC"))))
-          .collect(Collectors.toList());
+    public void serialize(Collection<Instant> in, String fieldName, Map<String, Object> out) {
+      out.put(
+          fieldName,
+          in.stream()
+              .map(
+                  instant ->
+                      DateTimeFormatter.RFC_1123_DATE_TIME.format(instant.atZone(ZoneId.of("UTC"))))
+              .collect(Collectors.toList()));
+    }
+  }
+
+  public static class AdditionalPropertiesFormSerializer
+      extends FormSerializer<Map<Object, Object>> {
+
+    @Override
+    public void serialize(Map<Object, Object> in, String fieldName, Map<String, Object> out) {
+
+      in.forEach(
+          (_key, value) -> {
+            String key =
+                ((String) _key).replaceAll("^header:", "h:").replaceAll("^variable:", "v:");
+            out.put(key, value);
+          });
+    }
+  }
+
+  private static Map<String, Object> fillMap(Object... pairs) {
+    LinkedHashMap<String, Object> map = new LinkedHashMap<>();
+    for (int i = 0; i < pairs.length; ) {
+      map.put((String) pairs[i++], pairs[i++]);
     }
+    return map;
   }
 }
diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/mailgun/models/v1/emails/request/SendEmailRequest.java b/openapi-contracts/src/main/com/sinch/sdk/domains/mailgun/models/v1/emails/request/SendEmailRequest.java
index bf845820..45afb119 100644
--- a/openapi-contracts/src/main/com/sinch/sdk/domains/mailgun/models/v1/emails/request/SendEmailRequest.java
+++ b/openapi-contracts/src/main/com/sinch/sdk/domains/mailgun/models/v1/emails/request/SendEmailRequest.java
@@ -11,6 +11,7 @@
 package com.sinch.sdk.domains.mailgun.models.v1.emails.request;
 
 import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.sinch.sdk.core.models.AdditionalProperties;
 import com.sinch.sdk.core.utils.EnumDynamic;
 import com.sinch.sdk.core.utils.EnumSupportDynamic;
 import java.io.File;
@@ -21,7 +22,7 @@
 
 /** SendEmailRequest */
 @JsonDeserialize(builder = SendEmailRequestImpl.Builder.class)
-public interface SendEmailRequest {
+public interface SendEmailRequest extends AdditionalProperties {
 
   /**
    * Email address for <code>From</code> header
@@ -540,14 +541,6 @@ public static String valueOf(SkipVerificationEnum e) {
    */
   String getRecipientVariables();
 
-  /**
-   * Return the additional property with the specified name.
-   *
-   * @param key the name of the property
-   * @return the additional property with the specified name
-   */
-  Object get(String key);
-
   /**
    * Getting builder
    *
@@ -558,7 +551,7 @@ static Builder builder() {
   }
 
   /** Dedicated Builder */
-  interface Builder {
+  interface Builder extends AdditionalProperties.Builder {
 
     /**
      * see getter
diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/mailgun/models/v1/emails/request/SendMimeEmailRequest.java b/openapi-contracts/src/main/com/sinch/sdk/domains/mailgun/models/v1/emails/request/SendMimeEmailRequest.java
index cadb8ac4..6b58b9ed 100644
--- a/openapi-contracts/src/main/com/sinch/sdk/domains/mailgun/models/v1/emails/request/SendMimeEmailRequest.java
+++ b/openapi-contracts/src/main/com/sinch/sdk/domains/mailgun/models/v1/emails/request/SendMimeEmailRequest.java
@@ -11,6 +11,7 @@
 package com.sinch.sdk.domains.mailgun.models.v1.emails.request;
 
 import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.sinch.sdk.core.models.AdditionalProperties;
 import com.sinch.sdk.core.utils.EnumDynamic;
 import com.sinch.sdk.core.utils.EnumSupportDynamic;
 import java.io.File;
@@ -21,7 +22,7 @@
 
 /** SendMimeEmailRequest */
 @JsonDeserialize(builder = SendMimeEmailRequestImpl.Builder.class)
-public interface SendMimeEmailRequest {
+public interface SendMimeEmailRequest extends AdditionalProperties {
 
   /**
    * Email address of the recipient(s). Example: <code>\&quot;Bob &lt;bob@host.com&gt;\&quot;</code>
@@ -478,14 +479,6 @@ public static String valueOf(SkipVerificationEnum e) {
    */
   String getRecipientVariables();
 
-  /**
-   * Return the additional property with the specified name.
-   *
-   * @param key the name of the property
-   * @return the additional property with the specified name
-   */
-  Object get(String key);
-
   /**
    * Getting builder
    *
@@ -496,7 +489,7 @@ static Builder builder() {
   }
 
   /** Dedicated Builder */
-  interface Builder {
+  interface Builder extends AdditionalProperties.Builder {
 
     /**
      * see getter
diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/verification/models/v1/start/request/internal/VerificationStartSmsOptions.java b/openapi-contracts/src/main/com/sinch/sdk/domains/verification/models/v1/start/request/internal/VerificationStartSmsOptions.java
index b9ae5e31..f5b8eb1b 100644
--- a/openapi-contracts/src/main/com/sinch/sdk/domains/verification/models/v1/start/request/internal/VerificationStartSmsOptions.java
+++ b/openapi-contracts/src/main/com/sinch/sdk/domains/verification/models/v1/start/request/internal/VerificationStartSmsOptions.java
@@ -11,6 +11,7 @@
 package com.sinch.sdk.domains.verification.models.v1.start.request.internal;
 
 import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.sinch.sdk.core.models.AdditionalProperties;
 import com.sinch.sdk.core.utils.EnumDynamic;
 import com.sinch.sdk.core.utils.EnumSupportDynamic;
 import java.util.Arrays;
@@ -21,7 +22,7 @@
  * not provided.
  */
 @JsonDeserialize(builder = VerificationStartSmsOptionsImpl.Builder.class)
-public interface VerificationStartSmsOptions {
+public interface VerificationStartSmsOptions extends AdditionalProperties {
 
   /**
    * The expiration time for a verification process is represented in the format <code>HH:MM:SS
@@ -82,14 +83,6 @@ public static String valueOf(CodeTypeEnum e) {
    */
   String getAcceptLanguage();
 
-  /**
-   * Return the additional property with the specified name.
-   *
-   * @param key the name of the property
-   * @return the additional property with the specified name
-   */
-  Object get(String key);
-
   /**
    * Getting builder
    *
@@ -100,7 +93,7 @@ static Builder builder() {
   }
 
   /** Dedicated Builder */
-  interface Builder {
+  interface Builder extends AdditionalProperties.Builder {
 
     /**
      * see getter

From 3cafb58f59f7c784a887b7a447f4fb3d458e8c00 Mon Sep 17 00:00:00 2001
From: Jean-Pierre Portier <jean-pierre.portier@mailgun.com>
Date: Wed, 27 Nov 2024 07:07:05 +0100
Subject: [PATCH 2/8] test (Mailgun): Unit tests

---
 .../api/v1/adapters/MailgunService.java       |   3 +-
 .../api/v1/adapters/EmailsServiceTest.java    | 103 ++++++++++++++++++
 .../api/v1/adapters/MailgunServiceTest.java   |  62 +++++++++++
 .../databind/multipart/ObjectMapperTest.java  |   4 +-
 .../emails/request/SendMimeEmailRequest.java  |  18 +--
 .../request/SendMimeEmailRequestImpl.java     |  28 ++---
 .../emails/request/SendEmailRequestTest.java  |  14 +--
 .../request/SendMimeEmailRequestTest.java     |  95 ++++++++++++++++
 8 files changed, 289 insertions(+), 38 deletions(-)
 create mode 100644 client/src/test/java/com/sinch/sdk/domains/mailgun/api/v1/adapters/EmailsServiceTest.java
 create mode 100644 client/src/test/java/com/sinch/sdk/domains/mailgun/api/v1/adapters/MailgunServiceTest.java
 create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/mailgun/models/v1/emails/request/SendMimeEmailRequestTest.java

diff --git a/client/src/main/com/sinch/sdk/domains/mailgun/api/v1/adapters/MailgunService.java b/client/src/main/com/sinch/sdk/domains/mailgun/api/v1/adapters/MailgunService.java
index 16e45858..3dc66f4b 100644
--- a/client/src/main/com/sinch/sdk/domains/mailgun/api/v1/adapters/MailgunService.java
+++ b/client/src/main/com/sinch/sdk/domains/mailgun/api/v1/adapters/MailgunService.java
@@ -34,8 +34,7 @@ public MailgunService(
         credentials.getApiUser(), "Mailgun service requires 'apiUser' to be defined");
     StringUtil.requireNonEmpty(
         credentials.getApiKey(), "Mailgun service requires 'apiKey' to be defined");
-    StringUtil.requireNonEmpty(
-        context.getUrl(), "'Mailgun service requires mailgunUrl' to be defined");
+    StringUtil.requireNonEmpty(context.getUrl(), "'Mailgun service requires 'url' to be defined");
 
     LOGGER.fine("Activate Mailgun API with server='" + context.getServer().getUrl() + "'");
 
diff --git a/client/src/test/java/com/sinch/sdk/domains/mailgun/api/v1/adapters/EmailsServiceTest.java b/client/src/test/java/com/sinch/sdk/domains/mailgun/api/v1/adapters/EmailsServiceTest.java
new file mode 100644
index 00000000..4b8185ef
--- /dev/null
+++ b/client/src/test/java/com/sinch/sdk/domains/mailgun/api/v1/adapters/EmailsServiceTest.java
@@ -0,0 +1,103 @@
+package com.sinch.sdk.domains.mailgun.api.v1.adapters;
+
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.adelean.inject.resources.junit.jupiter.TestWithResources;
+import com.sinch.sdk.BaseTest;
+import com.sinch.sdk.core.TestHelpers;
+import com.sinch.sdk.core.http.AuthManager;
+import com.sinch.sdk.core.http.HttpClient;
+import com.sinch.sdk.domains.mailgun.api.v1.internal.EmailsApi;
+import com.sinch.sdk.domains.mailgun.models.v1.emails.request.SendEmailRequestTest;
+import com.sinch.sdk.domains.mailgun.models.v1.emails.request.SendMimeEmailRequestTest;
+import com.sinch.sdk.domains.mailgun.models.v1.emails.response.GetStoredEmailResponse;
+import com.sinch.sdk.domains.mailgun.models.v1.emails.response.GetStoredEmailResponseTest;
+import com.sinch.sdk.domains.mailgun.models.v1.emails.response.SendEmailResponse;
+import com.sinch.sdk.domains.mailgun.models.v1.emails.response.SendEmailResponseTest;
+import com.sinch.sdk.domains.mailgun.models.v1.emails.response.SendingQueuesStatusResponse;
+import com.sinch.sdk.domains.mailgun.models.v1.emails.response.SendingQueuesStatusResponseTest;
+import com.sinch.sdk.models.MailgunContext;
+import java.util.Map;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+
+@TestWithResources
+class EmailsServiceTest extends BaseTest {
+
+  @Mock MailgunContext context;
+  @Mock EmailsApi api;
+  @Mock HttpClient httpClient;
+  @Mock Map<String, AuthManager> authManagers;
+  @Captor ArgumentCaptor<String> domainNameCaptor;
+
+  EmailsService service;
+
+  String domainName = "fooDomain";
+  String storageKey = "fooStorageKey";
+
+  @BeforeEach
+  public void initMocks() {
+    service = spy(new EmailsService(context, httpClient, authManagers));
+    doReturn(api).when(service).getApi();
+  }
+
+  @Test
+  void send() {
+    when(api.sendEmail(eq(domainName), eq(SendEmailRequestTest.sendEmailRequest)))
+        .thenReturn(SendEmailResponseTest.expectedSendEmailResponse);
+
+    SendEmailResponse response = service.send(domainName, SendEmailRequestTest.sendEmailRequest);
+
+    TestHelpers.recursiveEquals(response, SendEmailResponseTest.expectedSendEmailResponse);
+  }
+
+  @Test
+  void sendMime() {
+    when(api.sendMimeEmail(eq(domainName), eq(SendMimeEmailRequestTest.sendMimEmailRequest)))
+        .thenReturn(SendEmailResponseTest.expectedSendEmailResponse);
+
+    SendEmailResponse response =
+        service.sendMime(domainName, SendMimeEmailRequestTest.sendMimEmailRequest);
+
+    TestHelpers.recursiveEquals(response, SendEmailResponseTest.expectedSendEmailResponse);
+  }
+
+  @Test
+  void get() {
+    when(api.getStoredEmail(eq(domainName), eq(storageKey)))
+        .thenReturn(GetStoredEmailResponseTest.expectedGetEmailResponse);
+
+    GetStoredEmailResponse response = service.get(domainName, storageKey);
+
+    TestHelpers.recursiveEquals(response, GetStoredEmailResponseTest.expectedGetEmailResponse);
+  }
+
+  @Test
+  void getSendingQueuesStatus() {
+    when(api.getSendingQueuesStatus(eq(domainName)))
+        .thenReturn(SendingQueuesStatusResponseTest.expectedSendingQueuesStatusResponse);
+
+    SendingQueuesStatusResponse response = service.getSendingQueuesStatus(domainName);
+
+    TestHelpers.recursiveEquals(
+        response, SendingQueuesStatusResponseTest.expectedSendingQueuesStatusResponse);
+  }
+
+  @Test
+  void purgeDomainQueues() {
+
+    service.purgeDomainQueues(domainName);
+
+    verify(api).purgeDomainQueues(domainNameCaptor.capture());
+
+    Assertions.assertThat(domainNameCaptor.getValue()).isEqualTo(domainName);
+  }
+}
diff --git a/client/src/test/java/com/sinch/sdk/domains/mailgun/api/v1/adapters/MailgunServiceTest.java b/client/src/test/java/com/sinch/sdk/domains/mailgun/api/v1/adapters/MailgunServiceTest.java
new file mode 100644
index 00000000..e44be529
--- /dev/null
+++ b/client/src/test/java/com/sinch/sdk/domains/mailgun/api/v1/adapters/MailgunServiceTest.java
@@ -0,0 +1,62 @@
+package com.sinch.sdk.domains.mailgun.api.v1.adapters;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import com.sinch.sdk.core.http.HttpClient;
+import com.sinch.sdk.models.MailgunContext;
+import com.sinch.sdk.models.MailgunCredentials;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+
+class MailgunServiceTest {
+  @Mock HttpClient httpClient;
+
+  @Test
+  void doNotAcceptNullKey() {
+    MailgunCredentials credentials = MailgunCredentials.builder().setApiKey(null).build();
+    MailgunContext context = MailgunContext.builder().build();
+    Exception exception =
+        assertThrows(
+            IllegalArgumentException.class,
+            () -> new MailgunService(credentials, context, httpClient));
+    assertTrue(exception.getMessage().contains("apiKey"));
+  }
+
+  @Test
+  void doNotAcceptNullContext() {
+    MailgunCredentials credentials = MailgunCredentials.builder().setApiKey("foo").build();
+    Exception exception =
+        assertThrows(
+            NullPointerException.class, () -> new MailgunService(credentials, null, httpClient));
+    assertTrue(exception.getMessage().contains("Mailgun service requires context to be defined"));
+  }
+
+  @Test
+  void doNotAcceptEmptyURL() {
+    MailgunCredentials credentials = MailgunCredentials.builder().setApiKey("foo").build();
+    MailgunContext context = MailgunContext.builder().setUrl("").build();
+    Exception exception =
+        assertThrows(
+            IllegalArgumentException.class,
+            () -> new MailgunService(credentials, context, httpClient));
+    assertTrue(exception.getMessage().contains("Mailgun service requires 'url' to be defined"));
+  }
+
+  @Test
+  void doNotAcceptNullURL() {
+    MailgunCredentials credentials = MailgunCredentials.builder().setApiKey("foo").build();
+    MailgunContext context = MailgunContext.builder().build();
+    Exception exception =
+        assertThrows(
+            IllegalArgumentException.class,
+            () -> new MailgunService(credentials, context, httpClient));
+    assertTrue(exception.getMessage().contains("Mailgun service requires 'url' to be defined"));
+  }
+
+  @Test
+  void passInit() {
+    MailgunCredentials credentials = MailgunCredentials.builder().setApiKey("foo").build();
+    MailgunContext context = MailgunContext.builder().setUrl("foo").build();
+    assertDoesNotThrow(() -> new MailgunService(credentials, context, httpClient), "Init passed");
+  }
+}
diff --git a/core/src/test/java/com/sinch/sdk/core/databind/multipart/ObjectMapperTest.java b/core/src/test/java/com/sinch/sdk/core/databind/multipart/ObjectMapperTest.java
index 091bdb66..da8b9ed5 100644
--- a/core/src/test/java/com/sinch/sdk/core/databind/multipart/ObjectMapperTest.java
+++ b/core/src/test/java/com/sinch/sdk/core/databind/multipart/ObjectMapperTest.java
@@ -27,7 +27,7 @@
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
-class ObjectMapperTest {
+public class ObjectMapperTest {
 
   static File fileAttachment1;
   static File fileAttachment2;
@@ -340,7 +340,7 @@ public void serialize(Map<Object, Object> in, String fieldName, Map<String, Obje
     }
   }
 
-  private static Map<String, Object> fillMap(Object... pairs) {
+  public static Map<String, Object> fillMap(Object... pairs) {
     LinkedHashMap<String, Object> map = new LinkedHashMap<>();
     for (int i = 0; i < pairs.length; ) {
       map.put((String) pairs[i++], pairs[i++]);
diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/mailgun/models/v1/emails/request/SendMimeEmailRequest.java b/openapi-contracts/src/main/com/sinch/sdk/domains/mailgun/models/v1/emails/request/SendMimeEmailRequest.java
index 6b58b9ed..9ab62142 100644
--- a/openapi-contracts/src/main/com/sinch/sdk/domains/mailgun/models/v1/emails/request/SendMimeEmailRequest.java
+++ b/openapi-contracts/src/main/com/sinch/sdk/domains/mailgun/models/v1/emails/request/SendMimeEmailRequest.java
@@ -224,9 +224,9 @@ public static String valueOf(TestModeEnum e) {
    * href="https://documentation.mailgun.com/docs/mailgun/user-manual/sending-messages/#sending-in-test-mode">Sending
    * in Test Mode</a>
    *
-   * @return isTestMode
+   * @return testMode
    */
-  TestModeEnum getIsTestMode();
+  TestModeEnum getTestMode();
 
   /**
    * Toggles both click and open tracking on a per-message basis, see <a
@@ -320,10 +320,10 @@ public static String valueOf(TrackingClicksEnum e) {
    * Opens</a>. Has higher priority than domain-level setting.
    */
   public class TrackingOpensEnum extends EnumDynamic<String, TrackingOpensEnum> {
-    public static final TrackingOpensEnum YES = new TrackingOpensEnum("test:yes");
-    public static final TrackingOpensEnum NO = new TrackingOpensEnum("test:no");
-    public static final TrackingOpensEnum TRUE = new TrackingOpensEnum("test:true");
-    public static final TrackingOpensEnum FALSE = new TrackingOpensEnum("test:false");
+    public static final TrackingOpensEnum YES = new TrackingOpensEnum("yes");
+    public static final TrackingOpensEnum NO = new TrackingOpensEnum("no");
+    public static final TrackingOpensEnum TRUE = new TrackingOpensEnum("true");
+    public static final TrackingOpensEnum FALSE = new TrackingOpensEnum("false");
 
     private static final EnumSupportDynamic<String, TrackingOpensEnum> ENUM_SUPPORT =
         new EnumSupportDynamic<>(
@@ -611,11 +611,11 @@ interface Builder extends AdditionalProperties.Builder {
     /**
      * see getter
      *
-     * @param isTestMode see getter
+     * @param testMode see getter
      * @return Current builder
-     * @see #getIsTestMode
+     * @see #getTestMode
      */
-    Builder setIsTestMode(TestModeEnum isTestMode);
+    Builder setTestMode(TestModeEnum testMode);
 
     /**
      * see getter
diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/mailgun/models/v1/emails/request/SendMimeEmailRequestImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/mailgun/models/v1/emails/request/SendMimeEmailRequestImpl.java
index 7270ef18..94ebe4e5 100644
--- a/openapi-contracts/src/main/com/sinch/sdk/domains/mailgun/models/v1/emails/request/SendMimeEmailRequestImpl.java
+++ b/openapi-contracts/src/main/com/sinch/sdk/domains/mailgun/models/v1/emails/request/SendMimeEmailRequestImpl.java
@@ -94,7 +94,7 @@ public class SendMimeEmailRequestImpl implements SendMimeEmailRequest {
 
   public static final String PROPERTY_O_COLON_TESTMODE = "o:testmode";
 
-  private OptionalValue<TestModeEnum> isTestMode;
+  private OptionalValue<TestModeEnum> testMode;
 
   public static final String PROPERTY_O_COLON_TRACKING = "o:tracking";
 
@@ -155,7 +155,7 @@ protected SendMimeEmailRequestImpl(
       OptionalValue<Instant> deliveryTime,
       OptionalValue<Integer> deliveryTimeOptimizePeriod,
       OptionalValue<String> timezoneLocalize,
-      OptionalValue<TestModeEnum> isTestMode,
+      OptionalValue<TestModeEnum> testMode,
       OptionalValue<TrackingEnum> tracking,
       OptionalValue<TrackingClicksEnum> trackingClicks,
       OptionalValue<TrackingOpensEnum> trackingOpens,
@@ -179,7 +179,7 @@ protected SendMimeEmailRequestImpl(
     this.deliveryTime = deliveryTime;
     this.deliveryTimeOptimizePeriod = deliveryTimeOptimizePeriod;
     this.timezoneLocalize = timezoneLocalize;
-    this.isTestMode = isTestMode;
+    this.testMode = testMode;
     this.tracking = tracking;
     this.trackingClicks = trackingClicks;
     this.trackingOpens = trackingOpens;
@@ -312,13 +312,13 @@ public OptionalValue<String> timezoneLocalize() {
     return timezoneLocalize;
   }
 
-  public TestModeEnum getIsTestMode() {
-    return isTestMode.orElse(null);
+  public TestModeEnum getTestMode() {
+    return testMode.orElse(null);
   }
 
   @Property(PROPERTY_O_COLON_TESTMODE)
-  public OptionalValue<TestModeEnum> isTestMode() {
-    return isTestMode;
+  public OptionalValue<TestModeEnum> testMode() {
+    return testMode;
   }
 
   public TrackingEnum getTracking() {
@@ -465,7 +465,7 @@ public boolean equals(Object o) {
             this.timezoneLocalize,
             poSTV3DomainNameMessagesMimeMultipartFormDataRequestBody.timezoneLocalize)
         && Objects.equals(
-            this.isTestMode, poSTV3DomainNameMessagesMimeMultipartFormDataRequestBody.isTestMode)
+            this.testMode, poSTV3DomainNameMessagesMimeMultipartFormDataRequestBody.testMode)
         && Objects.equals(
             this.tracking, poSTV3DomainNameMessagesMimeMultipartFormDataRequestBody.tracking)
         && Objects.equals(
@@ -512,7 +512,7 @@ public int hashCode() {
         deliveryTime,
         deliveryTimeOptimizePeriod,
         timezoneLocalize,
-        isTestMode,
+        testMode,
         tracking,
         trackingClicks,
         trackingOpens,
@@ -550,7 +550,7 @@ public String toString() {
         .append(toIndentedString(deliveryTimeOptimizePeriod))
         .append("\n");
     sb.append("    timezoneLocalize: ").append(toIndentedString(timezoneLocalize)).append("\n");
-    sb.append("    isTestMode: ").append(toIndentedString(isTestMode)).append("\n");
+    sb.append("    testMode: ").append(toIndentedString(testMode)).append("\n");
     sb.append("    tracking: ").append(toIndentedString(tracking)).append("\n");
     sb.append("    trackingClicks: ").append(toIndentedString(trackingClicks)).append("\n");
     sb.append("    trackingOpens: ").append(toIndentedString(trackingOpens)).append("\n");
@@ -593,7 +593,7 @@ static class Builder implements SendMimeEmailRequest.Builder {
     OptionalValue<Instant> deliveryTime = OptionalValue.empty();
     OptionalValue<Integer> deliveryTimeOptimizePeriod = OptionalValue.empty();
     OptionalValue<String> timezoneLocalize = OptionalValue.empty();
-    OptionalValue<TestModeEnum> isTestMode = OptionalValue.empty();
+    OptionalValue<TestModeEnum> testMode = OptionalValue.empty();
     OptionalValue<TrackingEnum> tracking = OptionalValue.empty();
     OptionalValue<TrackingClicksEnum> trackingClicks = OptionalValue.empty();
     OptionalValue<TrackingOpensEnum> trackingOpens = OptionalValue.empty();
@@ -684,8 +684,8 @@ public Builder setTimezoneLocalize(String timezoneLocalize) {
     }
 
     @Property(value = PROPERTY_O_COLON_TESTMODE)
-    public Builder setIsTestMode(TestModeEnum isTestMode) {
-      this.isTestMode = OptionalValue.of(isTestMode);
+    public Builder setTestMode(TestModeEnum testMode) {
+      this.testMode = OptionalValue.of(testMode);
       return this;
     }
 
@@ -766,7 +766,7 @@ public SendMimeEmailRequest build() {
           deliveryTime,
           deliveryTimeOptimizePeriod,
           timezoneLocalize,
-          isTestMode,
+          testMode,
           tracking,
           trackingClicks,
           trackingOpens,
diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/mailgun/models/v1/emails/request/SendEmailRequestTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/mailgun/models/v1/emails/request/SendEmailRequestTest.java
index f5d52dd2..69e8c267 100644
--- a/openapi-contracts/src/test/java/com/sinch/sdk/domains/mailgun/models/v1/emails/request/SendEmailRequestTest.java
+++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/mailgun/models/v1/emails/request/SendEmailRequestTest.java
@@ -3,6 +3,7 @@
 import com.adelean.inject.resources.junit.jupiter.TestWithResources;
 import com.sinch.sdk.BaseTest;
 import com.sinch.sdk.core.TestHelpers;
+import com.sinch.sdk.core.databind.multipart.ObjectMapperTest;
 import com.sinch.sdk.core.http.HttpMapper;
 import com.sinch.sdk.domains.mailgun.models.v1.emails.request.SendEmailRequest.DkimSignatureEnum;
 import com.sinch.sdk.domains.mailgun.models.v1.emails.request.SendEmailRequest.RequireTlsEnum;
@@ -15,14 +16,13 @@
 import java.io.File;
 import java.time.Instant;
 import java.util.Arrays;
-import java.util.LinkedHashMap;
 import java.util.Map;
 import org.junit.jupiter.api.Test;
 
 @TestWithResources
 public class SendEmailRequestTest extends BaseTest {
 
-  static Map<Object, Object> expected;
+  static Map<String, Object> expected;
   static File fileAttachment1;
   static File fileAttachment2;
 
@@ -36,7 +36,7 @@ public class SendEmailRequestTest extends BaseTest {
             classLoader.getResource("domains/mailgun/v1/emails/request/attachment2.txt").getFile());
 
     expected =
-        fillMap(
+        ObjectMapperTest.fillMap(
             // spotless:off
               "from", "User <JavaSdkUser@sinch.com>",
               "to", Arrays.asList("aRecipient@mailgun-by-sinch.com"),
@@ -116,12 +116,4 @@ void serialize() {
 
     TestHelpers.recursiveEquals(expected, serialized);
   }
-
-  private static Map<Object, Object> fillMap(Object... pairs) {
-    LinkedHashMap<Object, Object> map = new LinkedHashMap<>();
-    for (int i = 0; i < pairs.length; ) {
-      map.put(pairs[i++], pairs[i++]);
-    }
-    return map;
-  }
 }
diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/mailgun/models/v1/emails/request/SendMimeEmailRequestTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/mailgun/models/v1/emails/request/SendMimeEmailRequestTest.java
new file mode 100644
index 00000000..c37ca6c7
--- /dev/null
+++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/mailgun/models/v1/emails/request/SendMimeEmailRequestTest.java
@@ -0,0 +1,95 @@
+package com.sinch.sdk.domains.mailgun.models.v1.emails.request;
+
+import com.adelean.inject.resources.junit.jupiter.TestWithResources;
+import com.sinch.sdk.BaseTest;
+import com.sinch.sdk.core.TestHelpers;
+import com.sinch.sdk.core.databind.multipart.ObjectMapperTest;
+import com.sinch.sdk.core.http.HttpMapper;
+import java.io.File;
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+
+@TestWithResources
+public class SendMimeEmailRequestTest extends BaseTest {
+
+  static Map<String, Object> expected;
+  static File fileAttachment1;
+  static File fileAttachment2;
+
+  static {
+    ClassLoader classLoader = SendMimeEmailRequestTest.class.getClassLoader();
+    fileAttachment1 =
+        new File(
+            classLoader.getResource("domains/mailgun/v1/emails/request/attachment1.txt").getFile());
+    fileAttachment2 =
+        new File(
+            classLoader.getResource("domains/mailgun/v1/emails/request/attachment2.txt").getFile());
+
+    expected =
+        ObjectMapperTest.fillMap(
+            // spotless:off
+              "to", Arrays.asList("aRecipient@mailgun-by-sinch.com"),
+            "message", fileAttachment1,
+            "template","template value",
+            "t:version","2",
+            "t:text","yes",
+            "t:variables","{\"key\": \"value\"}",
+            "o:tag", Arrays.asList("tag1", "tag2"),
+            "o:dkim","true",
+            "o:secondary-dkim","example.com/s1",
+            "o:secondary-dkim-public","public.example.com/s1",
+            "o:deliverytime", "Sat, 22 Jan 2000 11:23:45 GMT",
+            "o:deliverytime-optimize-period", "29h",
+            "o:time-zone-localize","02:04PM",
+            "o:testmode", "no",
+            "o:tracking","htmlonly",
+            "o:tracking-clicks","true",
+            "o:tracking-opens","false",
+            "o:require-tls","true",
+            "o:skip-verification","no",
+            "o:sending-ip","192.168.0.10",
+            "o:sending-ip-pool","sending pool ID",
+            "o:tracking-pixel-location-top","foo",
+            "recipient-variables","{\"cc-dest@sinch.com\": {\"variable1\": \"value1\"}}"
+              // spotless:on
+            );
+  }
+
+  public static SendMimeEmailRequest sendMimEmailRequest =
+      SendMimeEmailRequest.builder()
+          .setMessage(fileAttachment1)
+          .setTag(Arrays.asList("tag1", "tag2"))
+          .setDeliveryTime(Instant.parse("2000-01-22T11:23:45Z"))
+          .setTestMode(SendMimeEmailRequest.TestModeEnum.from("no"))
+          .setTo(Arrays.asList("aRecipient@mailgun-by-sinch.com"))
+          .setTemplateText(SendMimeEmailRequest.TemplateTextEnum.YES)
+          .setTemplateVersion("2")
+          .setTemplateVariables("{\"key\": \"value\"}")
+          .setEnableDkimSignature(SendMimeEmailRequest.DkimSignatureEnum.TRUE)
+          .setSecondaryDkim("example.com/s1")
+          .setSecondaryDkimPublic("public.example.com/s1")
+          .setDeliveryTimeOptimizePeriod(29)
+          .setTimezoneLocalize("02:04PM")
+          .setTracking(SendMimeEmailRequest.TrackingEnum.HTMLONLY)
+          .setTrackingClicks(SendMimeEmailRequest.TrackingClicksEnum.TRUE)
+          .setTrackingOpens(SendMimeEmailRequest.TrackingOpensEnum.FALSE)
+          .setRequireTls(SendMimeEmailRequest.RequireTlsEnum.TRUE)
+          .setSkipVerification(SendMimeEmailRequest.SkipVerificationEnum.NO)
+          .setSendingIp("192.168.0.10")
+          .setSendingIpPool("sending pool ID")
+          .setTrackingPixelLocationTop("foo")
+          .setRecipientVariables("{\"cc-dest@sinch.com\": {\"variable1\": \"value1\"}}")
+          .setTemplate("template value")
+          .build();
+
+  @Test
+  void serialize() {
+    Object serialized =
+        new HttpMapper()
+            .serializeFormParameters(Arrays.asList("multipart/form-data"), sendMimEmailRequest);
+
+    TestHelpers.recursiveEquals(expected, serialized);
+  }
+}

From 6db38a147c32eb127595a4e7c29210858e25c998 Mon Sep 17 00:00:00 2001
From: Jean-Pierre Portier <jean-pierre.portier@mailgun.com>
Date: Wed, 27 Nov 2024 12:15:27 +0100
Subject: [PATCH 3/8] test (Mailgun/Emails): Adding Mailgun emails e2e tests

---
 .../test/java/com/sinch/sdk/e2e/Config.java   |  10 +
 .../e2e/domains/mailgun/v1/EmailsSteps.java   | 183 ++++++++++++++++++
 .../sdk/e2e/domains/mailgun/v1/MailgunIT.java |  16 ++
 pom.xml                                       |   1 +
 4 files changed, 210 insertions(+)
 create mode 100644 client/src/test/java/com/sinch/sdk/e2e/domains/mailgun/v1/EmailsSteps.java
 create mode 100644 client/src/test/java/com/sinch/sdk/e2e/domains/mailgun/v1/MailgunIT.java

diff --git a/client/src/test/java/com/sinch/sdk/e2e/Config.java b/client/src/test/java/com/sinch/sdk/e2e/Config.java
index 4c3d940f..e0eec03a 100644
--- a/client/src/test/java/com/sinch/sdk/e2e/Config.java
+++ b/client/src/test/java/com/sinch/sdk/e2e/Config.java
@@ -4,7 +4,9 @@
 import com.sinch.sdk.models.Configuration;
 import com.sinch.sdk.models.ConversationContext;
 import com.sinch.sdk.models.ConversationRegion;
+import com.sinch.sdk.models.MailgunContext;
 import com.sinch.sdk.models.VoiceContext;
+import java.util.Arrays;
 
 public class Config {
 
@@ -21,6 +23,10 @@ public class Config {
   public static final String VOICE_HOST_NAME = "http://localhost:3019";
   public static final String VOICE_MANAGEMENT_HOST_NAME = "http://localhost:3020";
 
+  public static final String MAILGUN_HOST_NAME = "http://localhost:3021";
+  public static final String MAILGUN_API_KEY = "apiKey";
+  public static final String MAILGUN_STORAGE = "http://localhost:3021";
+
   private final SinchClient client;
 
   private Config() {
@@ -44,6 +50,10 @@ private Config() {
                     .setVoiceApplicationMngmtUrl(VOICE_MANAGEMENT_HOST_NAME)
                     .setVoiceUrl(VOICE_HOST_NAME)
                     .build())
+            .setMailgunContext(
+                MailgunContext.builder().setStorageUrls(Arrays.asList(MAILGUN_STORAGE)).build())
+            .setMailgunApiKey(MAILGUN_API_KEY)
+            .setMailgunUrl(MAILGUN_HOST_NAME)
             .build();
 
     client = new SinchClient(configuration);
diff --git a/client/src/test/java/com/sinch/sdk/e2e/domains/mailgun/v1/EmailsSteps.java b/client/src/test/java/com/sinch/sdk/e2e/domains/mailgun/v1/EmailsSteps.java
new file mode 100644
index 00000000..9af0035d
--- /dev/null
+++ b/client/src/test/java/com/sinch/sdk/e2e/domains/mailgun/v1/EmailsSteps.java
@@ -0,0 +1,183 @@
+package com.sinch.sdk.e2e.domains.mailgun.v1;
+
+import com.sinch.sdk.core.TestHelpers;
+import com.sinch.sdk.domains.mailgun.api.v1.EmailsService;
+import com.sinch.sdk.domains.mailgun.models.v1.emails.request.SendEmailRequest;
+import com.sinch.sdk.domains.mailgun.models.v1.emails.request.SendMimeEmailRequest;
+import com.sinch.sdk.domains.mailgun.models.v1.emails.response.ExceededQueueQuotaRegular;
+import com.sinch.sdk.domains.mailgun.models.v1.emails.response.ExceededQueueQuotaScheduled;
+import com.sinch.sdk.domains.mailgun.models.v1.emails.response.GetStoredEmailResponse;
+import com.sinch.sdk.domains.mailgun.models.v1.emails.response.QueueStatusDisabledDetails;
+import com.sinch.sdk.domains.mailgun.models.v1.emails.response.SendEmailResponse;
+import com.sinch.sdk.domains.mailgun.models.v1.emails.response.SendingQueuesStatusResponse;
+import com.sinch.sdk.e2e.Config;
+import io.cucumber.java.en.Given;
+import io.cucumber.java.en.Then;
+import io.cucumber.java.en.When;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import org.junit.jupiter.api.Assertions;
+
+public class EmailsSteps {
+
+  EmailsService service;
+
+  SendEmailResponse sendEmailResponse;
+  SendEmailResponse sendMimeEmailResponse;
+  GetStoredEmailResponse getStoredEmailResponse;
+  SendingQueuesStatusResponse sendingQueuesStatusResponse;
+  Boolean purgeDomainQueuesPassed;
+
+  static final String domainName = "domainName";
+  static final String storageKey = "storageKey";
+
+  @Given("^the Mailgun service \"Emails\" is available$")
+  public void serviceAvailable() {
+    service = Config.getSinchClient().mailgun().v1().emails();
+  }
+
+  @When("^I send a request to send a text email$")
+  public void send() {
+
+    SendEmailRequest request =
+        SendEmailRequest.builder()
+            .setText("Hello, this is a text message for E2E testing.")
+            .setTo(Collections.singletonList("destination@e2e.tst"))
+            .setFrom("Excited E2E user \uD83D\uDCE7 <sender@e2e.tst>")
+            .setSubject("E2E test text email")
+            .build();
+
+    sendEmailResponse = service.send(domainName, request);
+  }
+
+  @When("^I send a request to send a MIME email$")
+  public void sendMime() {
+
+    File tempFile = new File("MimeMessage");
+    tempFile.deleteOnExit();
+    try (BufferedWriter out = new BufferedWriter(new FileWriter(tempFile))) {
+      out.write("\uD83D\uDCE7 Sample content \uD83D\uDCE7");
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+
+    SendMimeEmailRequest request =
+        SendMimeEmailRequest.builder()
+            .setTo(Collections.singletonList("destination@e2e.tst"))
+            .setMessage(tempFile)
+            .build();
+
+    sendMimeEmailResponse = service.sendMime(domainName, request);
+  }
+
+  @When("^I send a request to retrieve a stored email$")
+  public void getStoredEmail() {
+
+    getStoredEmailResponse = service.get(domainName, storageKey);
+  }
+
+  @When("^I send a request to get the sending queue status$")
+  public void getSendingQueuesStatus() {
+
+    sendingQueuesStatusResponse = service.getSendingQueuesStatus(domainName);
+  }
+
+  @When("^I send a request to purge the domain queues$")
+  public void purgeDomainQueues() {
+
+    service.purgeDomainQueues(domainName);
+    purgeDomainQueuesPassed = true;
+  }
+
+  @Then("the sendEmail response contains information about the text email")
+  public void sendResult() {
+
+    SendEmailResponse expected =
+        SendEmailResponse.builder()
+            .setId("<20240606154318.027ac0b5fc80da62@sandbox123.mailgun.org>")
+            .setMessage("Queued. Thank you.")
+            .build();
+
+    TestHelpers.recursiveEquals(expected, sendEmailResponse);
+  }
+
+  @Then("the sendMimeEmail response contains information about the email")
+  public void sendMimeResult() {
+
+    SendEmailResponse expected =
+        SendEmailResponse.builder()
+            .setId("<20240606154852.a3fafd8a5230e166@sandbox123.mailgun.org>")
+            .setMessage("Queued. Thank you.")
+            .build();
+
+    TestHelpers.recursiveEquals(expected, sendMimeEmailResponse);
+  }
+
+  @Then("the getEmail response contains the email details")
+  public void getStoredEmailResult() {
+
+    GetStoredEmailResponse expected =
+        GetStoredEmailResponse.builder()
+            .setFrom("sender@e2e.tst")
+            .setSubject("Hello from mailgun")
+            .setTo("%recipient%")
+            .setSender("postmaster@sandbox123.mailgun.org")
+            .setRecipients("recipient@e2e.tst")
+            .setBodyHtml(
+                "<h1>Hello %recipient.name%</h1><span style=\"color:blue\">This is an HTML"
+                    + " email</span>")
+            .setStrippedHtml(
+                "<h1>Hello %recipient.name%</h1><span style=\"color:blue\">This is an HTML"
+                    + " email</span>")
+            .setBodyPlain("Message text only")
+            .setStrippedText("Message text only")
+            .setStrippedSignature("")
+            .setMessageHeaders(
+                Arrays.asList(
+                    Arrays.asList("Mime-Version", "1.0"),
+                    Arrays.asList(
+                        "Content-Type",
+                        "multipart/alternative;"
+                            + " boundary=\"44eea75a00c7df3bdd541c89727faec0ce8d5b09663245a35789d6b264c6\""),
+                    Arrays.asList("Subject", "Hello from mailgun"),
+                    Arrays.asList("From", "sender@e2e.tst"),
+                    Arrays.asList("To", "%recipient%"),
+                    Arrays.asList("X-Mailgun-Deliver-By", "Thu, 06 Jun 2024 07:40:00 +0000"),
+                    Arrays.asList(
+                        "Message-Id", "<20240606162145.5f329edde3b4ed71@sandbox123.mailgun.org>")))
+            .build();
+
+    TestHelpers.recursiveEquals(expected, getStoredEmailResponse);
+  }
+
+  @Then("the response contains the sending queues status")
+  public void getSendingQueuesStatusResult() {
+
+    SendingQueuesStatusResponse expected =
+        SendingQueuesStatusResponse.builder()
+            .setRegular(
+                ExceededQueueQuotaRegular.builder()
+                    .setIsDisabled(false)
+                    .setDisabled(
+                        QueueStatusDisabledDetails.builder().setUntil("").setReason("").build())
+                    .build())
+            .setScheduled(
+                ExceededQueueQuotaScheduled.builder()
+                    .setIsDisabled(false)
+                    .setDisabled(
+                        QueueStatusDisabledDetails.builder().setUntil("").setReason("").build())
+                    .build())
+            .build();
+
+    TestHelpers.recursiveEquals(expected, sendingQueuesStatusResponse);
+  }
+
+  @Then("the response indicates the purge has been done")
+  public void purgeDomainQueuesValue() {
+    Assertions.assertTrue(purgeDomainQueuesPassed);
+  }
+}
diff --git a/client/src/test/java/com/sinch/sdk/e2e/domains/mailgun/v1/MailgunIT.java b/client/src/test/java/com/sinch/sdk/e2e/domains/mailgun/v1/MailgunIT.java
new file mode 100644
index 00000000..68b48dfd
--- /dev/null
+++ b/client/src/test/java/com/sinch/sdk/e2e/domains/mailgun/v1/MailgunIT.java
@@ -0,0 +1,16 @@
+package com.sinch.sdk.e2e.domains.mailgun.v1;
+
+import static io.cucumber.junit.platform.engine.Constants.GLUE_PROPERTY_NAME;
+
+import org.junit.platform.suite.api.ConfigurationParameter;
+import org.junit.platform.suite.api.IncludeEngines;
+import org.junit.platform.suite.api.SelectClasspathResource;
+import org.junit.platform.suite.api.Suite;
+import org.junit.platform.suite.api.SuiteDisplayName;
+
+@Suite
+@SuiteDisplayName("Mailgun V1")
+@IncludeEngines("cucumber")
+@SelectClasspathResource("features/mailgun")
+@ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "com.sinch.sdk.e2e.domains.mailgun.v1")
+public class MailgunIT {}
diff --git a/pom.xml b/pom.xml
index 7132a23a..73ec1815 100644
--- a/pom.xml
+++ b/pom.xml
@@ -321,6 +321,7 @@
                         </goals>
                         <configuration>
                             <includes>
+                                <include>com.sinch.sdk.e2e.domains.mailgun.v1.MailgunIT</include>
                                 <include>com.sinch.sdk.e2e.domains.conversation.ConversationIT</include>
                                 <include>com.sinch.sdk.e2e.domains.voice.v0.VoiceIT</include>
                                 <include>com.sinch.sdk.e2e.domains.voice.v1.VoiceIT</include>

From d45ce941c4e56f52515c035439f129c7236277ce Mon Sep 17 00:00:00 2001
From: Jean-Pierre Portier <jean-pierre.portier@mailgun.com>
Date: Wed, 27 Nov 2024 14:28:05 +0100
Subject: [PATCH 4/8] refactor (FormSerializer/Tests): Remove duplicated
 propery key from ordered list

---
 .../com/sinch/sdk/core/databind/multipart/ObjectMapperTest.java  | 1 -
 1 file changed, 1 deletion(-)

diff --git a/core/src/test/java/com/sinch/sdk/core/databind/multipart/ObjectMapperTest.java b/core/src/test/java/com/sinch/sdk/core/databind/multipart/ObjectMapperTest.java
index da8b9ed5..15fc0cc0 100644
--- a/core/src/test/java/com/sinch/sdk/core/databind/multipart/ObjectMapperTest.java
+++ b/core/src/test/java/com/sinch/sdk/core/databind/multipart/ObjectMapperTest.java
@@ -165,7 +165,6 @@ void defaultAdditionalPropertiesValues() {
     SerializableObject.PROPERTY_FILE,
     SerializableObject.PROPERTY_ENUM,
     SerializableObject.PROPERTY_ENUM_COLLECTION,
-    SerializableObject.PROPERTY_RFC822_COLLECTION,
     SerializableObject.PROPERTY_TEXT_COLLECTION
   })
   static class SerializableObject implements AdditionalProperties {

From 02bfb0e6d81c76b97780d0da906da682269552d4 Mon Sep 17 00:00:00 2001
From: Jean-Pierre Portier <jean-pierre.portier@mailgun.com>
Date: Wed, 27 Nov 2024 16:01:34 +0100
Subject: [PATCH 5/8] feat (HttpCLient/Multipart): Set a default charset to
 'UTF-8' is not specified

---
 .../src/main/com/sinch/sdk/http/HttpClientApache.java | 11 ++++++++---
 .../sinch/sdk/e2e/domains/mailgun/v1/EmailsSteps.java |  2 +-
 2 files changed, 9 insertions(+), 4 deletions(-)

diff --git a/client/src/main/com/sinch/sdk/http/HttpClientApache.java b/client/src/main/com/sinch/sdk/http/HttpClientApache.java
index ef9b6de2..d6fc2b70 100644
--- a/client/src/main/com/sinch/sdk/http/HttpClientApache.java
+++ b/client/src/main/com/sinch/sdk/http/HttpClientApache.java
@@ -134,7 +134,7 @@ public HttpResponse invokeAPI(
         addBody(requestBuilder, body);
       }
 
-      addFormParams(requestBuilder, formParams);
+      addFormParams(requestBuilder, contentType, formParams);
 
       addAuth(requestBuilder, authManagersByOasSecuritySchemes, authNames, body);
 
@@ -225,14 +225,19 @@ private void addBody(ClassicRequestBuilder requestBuilder, String body) {
     requestBuilder.setEntity(new StringEntity(body, charset));
   }
 
-  private void addFormParams(ClassicRequestBuilder requestBuilder, Map<String, Object> formParams) {
+  private void addFormParams(
+      ClassicRequestBuilder requestBuilder,
+      Collection<String> contentType,
+      Map<String, Object> formParams) {
 
     if (null == formParams) {
       return;
     }
 
     MultipartEntityBuilder multiPartBuilder = MultipartEntityBuilder.create();
-
+    if (contentType.stream().noneMatch(cType -> cType.toLowerCase().contains("charset="))) {
+      multiPartBuilder.setCharset(StandardCharsets.UTF_8);
+    }
     formParams.forEach((key, value) -> addMultiPart(requestBuilder, multiPartBuilder, key, value));
 
     requestBuilder.setEntity(multiPartBuilder.build());
diff --git a/client/src/test/java/com/sinch/sdk/e2e/domains/mailgun/v1/EmailsSteps.java b/client/src/test/java/com/sinch/sdk/e2e/domains/mailgun/v1/EmailsSteps.java
index 9af0035d..8776e7d4 100644
--- a/client/src/test/java/com/sinch/sdk/e2e/domains/mailgun/v1/EmailsSteps.java
+++ b/client/src/test/java/com/sinch/sdk/e2e/domains/mailgun/v1/EmailsSteps.java
@@ -47,7 +47,7 @@ public void send() {
         SendEmailRequest.builder()
             .setText("Hello, this is a text message for E2E testing.")
             .setTo(Collections.singletonList("destination@e2e.tst"))
-            .setFrom("Excited E2E user \uD83D\uDCE7 <sender@e2e.tst>")
+            .setFrom("Excited E2E user ✉️ <sender@e2e.tst>")
             .setSubject("E2E test text email")
             .build();
 

From b2d7acb077439ed9a113463befb0b5de4e880a75 Mon Sep 17 00:00:00 2001
From: Jean-Pierre Portier <jean-pierre.portier@mailgun.com>
Date: Wed, 27 Nov 2024 17:42:14 +0100
Subject: [PATCH 6/8] refactor (FormSerializer): Sorting properties
 optimization

---
 .../core/databind/multipart/ObjectMapper.java | 27 +++++++++----------
 1 file changed, 12 insertions(+), 15 deletions(-)

diff --git a/core/src/main/com/sinch/sdk/core/databind/multipart/ObjectMapper.java b/core/src/main/com/sinch/sdk/core/databind/multipart/ObjectMapper.java
index 048927f6..3f97b718 100644
--- a/core/src/main/com/sinch/sdk/core/databind/multipart/ObjectMapper.java
+++ b/core/src/main/com/sinch/sdk/core/databind/multipart/ObjectMapper.java
@@ -18,6 +18,7 @@
 import java.lang.reflect.Method;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Comparator;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
@@ -54,28 +55,24 @@ private List<Pair<String, Method>> collectSerializableProperties(BeanInfo beanIn
     for (MethodDescriptor methodDescriptor : methodDescriptors) {
       getPropertyGetter(methodDescriptor.getMethod()).ifPresent(result::add);
     }
-    return sortProperties(beanInfo, result);
+
+    sortProperties(beanInfo, result);
+    return result;
   }
 
-  private List<Pair<String, Method>> sortProperties(
-      BeanInfo beanInfo, List<Pair<String, Method>> properties) {
+  private void sortProperties(BeanInfo beanInfo, List<Pair<String, Method>> properties) {
 
-    PropertiesOrder propertyOrder =
+    PropertiesOrder propertiesOrder =
         beanInfo.getBeanDescriptor().getBeanClass().getAnnotation(PropertiesOrder.class);
 
-    if (null == propertyOrder) {
-      return properties;
+    if (null == propertiesOrder
+        || null == propertiesOrder.value()
+        || 0 == propertiesOrder.value().length) {
+      return;
     }
 
-    ArrayList<Pair<String, Method>> sorted = new ArrayList<>(properties.size());
-
-    for (String property : propertyOrder.value()) {
-      properties.stream()
-          .filter(p -> p.getLeft().equals(property))
-          .findFirst()
-          .ifPresent(sorted::add);
-    }
-    return sorted;
+    List<String> order = java.util.Arrays.asList(propertiesOrder.value());
+    properties.sort(Comparator.comparingInt(l -> order.indexOf(l.getLeft())));
   }
 
   private Optional<Pair<String, Method>> getPropertyGetter(Method method) {

From cc4789e2ef78047742ae1e95b350c64c45dcc894 Mon Sep 17 00:00:00 2001
From: Jean-Pierre Portier <jean-pierre.portier@mailgun.com>
Date: Thu, 28 Nov 2024 09:59:10 +0100
Subject: [PATCH 7/8] feat (FormSerializer): Remove 'PropertiesOrder' feature

---
 .../databind/annotation/PropertiesOrder.java  | 14 --------
 .../core/databind/multipart/ObjectMapper.java | 18 ----------
 .../databind/multipart/ObjectMapperTest.java  | 31 -----------------
 .../emails/request/SendEmailRequestImpl.java  | 34 -------------------
 .../request/SendMimeEmailRequestImpl.java     | 26 --------------
 5 files changed, 123 deletions(-)
 delete mode 100644 core/src/main/com/sinch/sdk/core/databind/annotation/PropertiesOrder.java

diff --git a/core/src/main/com/sinch/sdk/core/databind/annotation/PropertiesOrder.java b/core/src/main/com/sinch/sdk/core/databind/annotation/PropertiesOrder.java
deleted file mode 100644
index 9fedef10..00000000
--- a/core/src/main/com/sinch/sdk/core/databind/annotation/PropertiesOrder.java
+++ /dev/null
@@ -1,14 +0,0 @@
-package com.sinch.sdk.core.databind.annotation;
-
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
-@Target({ElementType.TYPE})
-@Retention(RetentionPolicy.RUNTIME)
-@SinchAnnotation
-public @interface PropertiesOrder {
-
-  String[] value() default {};
-}
diff --git a/core/src/main/com/sinch/sdk/core/databind/multipart/ObjectMapper.java b/core/src/main/com/sinch/sdk/core/databind/multipart/ObjectMapper.java
index 3f97b718..2401658d 100644
--- a/core/src/main/com/sinch/sdk/core/databind/multipart/ObjectMapper.java
+++ b/core/src/main/com/sinch/sdk/core/databind/multipart/ObjectMapper.java
@@ -2,7 +2,6 @@
 
 import com.sinch.sdk.core.databind.FormSerializer;
 import com.sinch.sdk.core.databind.annotation.FormSerialize;
-import com.sinch.sdk.core.databind.annotation.PropertiesOrder;
 import com.sinch.sdk.core.databind.annotation.Property;
 import com.sinch.sdk.core.exceptions.SerializationException;
 import com.sinch.sdk.core.models.AdditionalProperties;
@@ -18,7 +17,6 @@
 import java.lang.reflect.Method;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Comparator;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
@@ -56,25 +54,9 @@ private List<Pair<String, Method>> collectSerializableProperties(BeanInfo beanIn
       getPropertyGetter(methodDescriptor.getMethod()).ifPresent(result::add);
     }
 
-    sortProperties(beanInfo, result);
     return result;
   }
 
-  private void sortProperties(BeanInfo beanInfo, List<Pair<String, Method>> properties) {
-
-    PropertiesOrder propertiesOrder =
-        beanInfo.getBeanDescriptor().getBeanClass().getAnnotation(PropertiesOrder.class);
-
-    if (null == propertiesOrder
-        || null == propertiesOrder.value()
-        || 0 == propertiesOrder.value().length) {
-      return;
-    }
-
-    List<String> order = java.util.Arrays.asList(propertiesOrder.value());
-    properties.sort(Comparator.comparingInt(l -> order.indexOf(l.getLeft())));
-  }
-
   private Optional<Pair<String, Method>> getPropertyGetter(Method method) {
 
     Property property = method.getDeclaredAnnotation(Property.class);
diff --git a/core/src/test/java/com/sinch/sdk/core/databind/multipart/ObjectMapperTest.java b/core/src/test/java/com/sinch/sdk/core/databind/multipart/ObjectMapperTest.java
index 15fc0cc0..db12a092 100644
--- a/core/src/test/java/com/sinch/sdk/core/databind/multipart/ObjectMapperTest.java
+++ b/core/src/test/java/com/sinch/sdk/core/databind/multipart/ObjectMapperTest.java
@@ -3,7 +3,6 @@
 import com.sinch.sdk.core.TestHelpers;
 import com.sinch.sdk.core.databind.FormSerializer;
 import com.sinch.sdk.core.databind.annotation.FormSerialize;
-import com.sinch.sdk.core.databind.annotation.PropertiesOrder;
 import com.sinch.sdk.core.databind.annotation.Property;
 import com.sinch.sdk.core.databind.multipart.ObjectMapperTest.SerializableObject.AnEnum;
 import com.sinch.sdk.core.models.AdditionalProperties;
@@ -77,26 +76,6 @@ void countValue() {
     Assertions.assertEquals(11, serialized.size());
   }
 
-  @Test
-  void order() {
-
-    String[] expectedOrder = {
-      SerializableObject.PROPERTY_RFC822_COLLECTION,
-      SerializableObject.PROPERTY_TEXT,
-      SerializableObject.PROPERTY_FILE_COLLECTION,
-      SerializableObject.PROPERTY_RFC822,
-      SerializableObject.PROPERTY_FILE,
-      SerializableObject.PROPERTY_ENUM,
-      SerializableObject.PROPERTY_ENUM_COLLECTION,
-      SerializableObject.PROPERTY_TEXT_COLLECTION,
-      "h:toto",
-      "v:foo",
-      "raw"
-    };
-    String[] keys = serialized.keySet().toArray(new String[0]);
-    TestHelpers.recursiveEquals(expectedOrder, keys);
-  }
-
   @Test
   void textValue() {
     TestHelpers.recursiveEquals("text value", serialized.get("aText"));
@@ -157,16 +136,6 @@ void defaultAdditionalPropertiesValues() {
         defaultAdditionalPropertiesSerialized);
   }
 
-  @PropertiesOrder({
-    SerializableObject.PROPERTY_RFC822_COLLECTION,
-    SerializableObject.PROPERTY_TEXT,
-    SerializableObject.PROPERTY_FILE_COLLECTION,
-    SerializableObject.PROPERTY_RFC822,
-    SerializableObject.PROPERTY_FILE,
-    SerializableObject.PROPERTY_ENUM,
-    SerializableObject.PROPERTY_ENUM_COLLECTION,
-    SerializableObject.PROPERTY_TEXT_COLLECTION
-  })
   static class SerializableObject implements AdditionalProperties {
     public static final String PROPERTY_TEXT = "aText";
     public static final String PROPERTY_ENUM = "anEnum";
diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/mailgun/models/v1/emails/request/SendEmailRequestImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/mailgun/models/v1/emails/request/SendEmailRequestImpl.java
index 9befa99a..6233c9e1 100644
--- a/openapi-contracts/src/main/com/sinch/sdk/domains/mailgun/models/v1/emails/request/SendEmailRequestImpl.java
+++ b/openapi-contracts/src/main/com/sinch/sdk/domains/mailgun/models/v1/emails/request/SendEmailRequestImpl.java
@@ -1,7 +1,6 @@
 package com.sinch.sdk.domains.mailgun.models.v1.emails.request;
 
 import com.sinch.sdk.core.databind.annotation.FormSerialize;
-import com.sinch.sdk.core.databind.annotation.PropertiesOrder;
 import com.sinch.sdk.core.databind.annotation.Property;
 import com.sinch.sdk.core.models.OptionalValue;
 import java.io.File;
@@ -11,39 +10,6 @@
 import java.util.Map;
 import java.util.Objects;
 
-@PropertiesOrder({
-  SendEmailRequestImpl.PROPERTY_FROM,
-  SendEmailRequestImpl.PROPERTY_TO,
-  SendEmailRequestImpl.PROPERTY_CC,
-  SendEmailRequestImpl.PROPERTY_BCC,
-  SendEmailRequestImpl.PROPERTY_SUBJECT,
-  SendEmailRequestImpl.PROPERTY_TEXT,
-  SendEmailRequestImpl.PROPERTY_HTML,
-  SendEmailRequestImpl.PROPERTY_AMP_HTML,
-  SendEmailRequestImpl.PROPERTY_ATTACHMENT,
-  SendEmailRequestImpl.PROPERTY_INLINE,
-  SendEmailRequestImpl.PROPERTY_TEMPLATE,
-  SendEmailRequestImpl.PROPERTY_T_COLON_VERSION,
-  SendEmailRequestImpl.PROPERTY_T_COLON_TEXT,
-  SendEmailRequestImpl.PROPERTY_T_COLON_VARIABLES,
-  SendEmailRequestImpl.PROPERTY_O_COLON_TAG,
-  SendEmailRequestImpl.PROPERTY_O_COLON_DKIM,
-  SendEmailRequestImpl.PROPERTY_O_COLON_SECONDARY_DKIM,
-  SendEmailRequestImpl.PROPERTY_O_COLON_SECONDARY_DKIM_PUBLIC,
-  SendEmailRequestImpl.PROPERTY_O_COLON_DELIVERYTIME,
-  SendEmailRequestImpl.PROPERTY_O_COLON_DELIVERYTIME_OPTIMIZE_PERIOD,
-  SendEmailRequestImpl.PROPERTY_O_COLON_TIME_ZONE_LOCALIZE,
-  SendEmailRequestImpl.PROPERTY_O_COLON_TESTMODE,
-  SendEmailRequestImpl.PROPERTY_O_COLON_TRACKING,
-  SendEmailRequestImpl.PROPERTY_O_COLON_TRACKING_CLICKS,
-  SendEmailRequestImpl.PROPERTY_O_COLON_TRACKING_OPENS,
-  SendEmailRequestImpl.PROPERTY_O_COLON_REQUIRE_TLS,
-  SendEmailRequestImpl.PROPERTY_O_COLON_SKIP_VERIFICATION,
-  SendEmailRequestImpl.PROPERTY_O_COLON_SENDING_IP,
-  SendEmailRequestImpl.PROPERTY_O_COLON_SENDING_IP_POOL,
-  SendEmailRequestImpl.PROPERTY_O_COLON_TRACKING_PIXEL_LOCATION_TOP,
-  SendEmailRequestImpl.PROPERTY_RECIPIENT_VARIABLES
-})
 public class SendEmailRequestImpl implements SendEmailRequest {
   private static final long serialVersionUID = 1L;
 
diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/mailgun/models/v1/emails/request/SendMimeEmailRequestImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/mailgun/models/v1/emails/request/SendMimeEmailRequestImpl.java
index 94ebe4e5..05cd1024 100644
--- a/openapi-contracts/src/main/com/sinch/sdk/domains/mailgun/models/v1/emails/request/SendMimeEmailRequestImpl.java
+++ b/openapi-contracts/src/main/com/sinch/sdk/domains/mailgun/models/v1/emails/request/SendMimeEmailRequestImpl.java
@@ -1,7 +1,6 @@
 package com.sinch.sdk.domains.mailgun.models.v1.emails.request;
 
 import com.sinch.sdk.core.databind.annotation.FormSerialize;
-import com.sinch.sdk.core.databind.annotation.PropertiesOrder;
 import com.sinch.sdk.core.databind.annotation.Property;
 import com.sinch.sdk.core.models.OptionalValue;
 import java.io.File;
@@ -11,31 +10,6 @@
 import java.util.Map;
 import java.util.Objects;
 
-@PropertiesOrder({
-  SendMimeEmailRequestImpl.PROPERTY_TO,
-  SendMimeEmailRequestImpl.PROPERTY_MESSAGE,
-  SendMimeEmailRequestImpl.PROPERTY_TEMPLATE,
-  SendMimeEmailRequestImpl.PROPERTY_T_COLON_VERSION,
-  SendMimeEmailRequestImpl.PROPERTY_T_COLON_TEXT,
-  SendMimeEmailRequestImpl.PROPERTY_T_COLON_VARIABLES,
-  SendMimeEmailRequestImpl.PROPERTY_O_COLON_TAG,
-  SendMimeEmailRequestImpl.PROPERTY_O_COLON_DKIM,
-  SendMimeEmailRequestImpl.PROPERTY_O_COLON_SECONDARY_DKIM,
-  SendMimeEmailRequestImpl.PROPERTY_O_COLON_SECONDARY_DKIM_PUBLIC,
-  SendMimeEmailRequestImpl.PROPERTY_O_COLON_DELIVERYTIME,
-  SendMimeEmailRequestImpl.PROPERTY_O_COLON_DELIVERYTIME_OPTIMIZE_PERIOD,
-  SendMimeEmailRequestImpl.PROPERTY_O_COLON_TIME_ZONE_LOCALIZE,
-  SendMimeEmailRequestImpl.PROPERTY_O_COLON_TESTMODE,
-  SendMimeEmailRequestImpl.PROPERTY_O_COLON_TRACKING,
-  SendMimeEmailRequestImpl.PROPERTY_O_COLON_TRACKING_CLICKS,
-  SendMimeEmailRequestImpl.PROPERTY_O_COLON_TRACKING_OPENS,
-  SendMimeEmailRequestImpl.PROPERTY_O_COLON_REQUIRE_TLS,
-  SendMimeEmailRequestImpl.PROPERTY_O_COLON_SKIP_VERIFICATION,
-  SendMimeEmailRequestImpl.PROPERTY_O_COLON_SENDING_IP,
-  SendMimeEmailRequestImpl.PROPERTY_O_COLON_SENDING_IP_POOL,
-  SendMimeEmailRequestImpl.PROPERTY_O_COLON_TRACKING_PIXEL_LOCATION_TOP,
-  SendMimeEmailRequestImpl.PROPERTY_RECIPIENT_VARIABLES
-})
 public class SendMimeEmailRequestImpl implements SendMimeEmailRequest {
   private static final long serialVersionUID = 1L;
 

From d075b64e8d1a33e950e1d054b98bc3dbe50b3994 Mon Sep 17 00:00:00 2001
From: Jean-Pierre Portier <jean-pierre.portier@mailgun.com>
Date: Thu, 28 Nov 2024 16:55:50 +0100
Subject: [PATCH 8/8] refactor (Core/RFC822 formatter): Use Core RFC822
 formatter from ObjectMapper test

---
 .../core/utils/databind/RFC822FormSerializer.java  |  6 +++++-
 .../core/databind/multipart/ObjectMapperTest.java  | 14 +++++---------
 2 files changed, 10 insertions(+), 10 deletions(-)

diff --git a/core/src/main/com/sinch/sdk/core/utils/databind/RFC822FormSerializer.java b/core/src/main/com/sinch/sdk/core/utils/databind/RFC822FormSerializer.java
index 4d22a8fc..51c84e3a 100644
--- a/core/src/main/com/sinch/sdk/core/utils/databind/RFC822FormSerializer.java
+++ b/core/src/main/com/sinch/sdk/core/utils/databind/RFC822FormSerializer.java
@@ -10,6 +10,10 @@ public class RFC822FormSerializer extends FormSerializer<Instant> {
 
   @Override
   public void serialize(Instant in, String fieldName, Map<String, Object> out) {
-    out.put(fieldName, DateTimeFormatter.RFC_1123_DATE_TIME.format(in.atZone(ZoneId.of("UTC"))));
+    out.put(fieldName, format(in));
+  }
+
+  public static String format(Instant instant) {
+    return DateTimeFormatter.RFC_1123_DATE_TIME.format(instant.atZone(ZoneId.of("UTC")));
   }
 }
diff --git a/core/src/test/java/com/sinch/sdk/core/databind/multipart/ObjectMapperTest.java b/core/src/test/java/com/sinch/sdk/core/databind/multipart/ObjectMapperTest.java
index db12a092..c1f1668b 100644
--- a/core/src/test/java/com/sinch/sdk/core/databind/multipart/ObjectMapperTest.java
+++ b/core/src/test/java/com/sinch/sdk/core/databind/multipart/ObjectMapperTest.java
@@ -13,8 +13,6 @@
 import com.sinch.sdk.domains.mailgun.models.v1.emails.request.SendEmailRequestTest;
 import java.io.File;
 import java.time.Instant;
-import java.time.ZoneId;
-import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -283,13 +281,11 @@ public static class RFC822ListFormSerializer extends FormSerializer<Collection<I
 
     @Override
     public void serialize(Collection<Instant> in, String fieldName, Map<String, Object> out) {
-      out.put(
-          fieldName,
-          in.stream()
-              .map(
-                  instant ->
-                      DateTimeFormatter.RFC_1123_DATE_TIME.format(instant.atZone(ZoneId.of("UTC"))))
-              .collect(Collectors.toList()));
+
+      Collection<String> formatted =
+          in.stream().map(RFC822FormSerializer::format).collect(Collectors.toList());
+
+      out.put(fieldName, formatted);
     }
   }