You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by ol...@apache.org on 2019/12/15 18:19:39 UTC

[sling-org-apache-sling-commons-messaging-mail] 03/03: SLING-8920 Provide a simple API and implementation to build and send mails

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

olli pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-commons-messaging-mail.git

commit fd1350d97590bb335e57e19c3ad8c208670fe316
Author: Oliver Lietz <ol...@apache.org>
AuthorDate: Sun Dec 15 19:18:17 2019 +0100

    SLING-8920 Provide a simple API and implementation to build and send mails
---
 README.md                                          |  62 ++-
 pom.xml                                            | 146 +++++--
 ...lServiceConfiguration.java => MailService.java} |  26 +-
 .../commons/messaging/mail/MessageBuilder.java     |  85 ++++
 ...ceConfiguration.java => MessageIdProvider.java} |  22 +-
 .../messaging/mail/internal/SimpleMailService.java | 124 ++++--
 .../internal/SimpleMailServiceConfiguration.java   |  66 ++-
 .../mail/internal/SimpleMessageBuilder.java        | 473 +++++++++++++++++++++
 .../mail/internal/SimpleMessageIdProvider.java     |  76 ++++
 ...a => SimpleMessageIdProviderConfiguration.java} |  21 +-
 .../messaging/mail/it/tests/MailTestSupport.java   | 144 +++++++
 .../mail/it/tests/SimpleMailServiceIT.java         | 414 ++++++++++++++++++
 src/test/resources/SupportApache-small.png         | Bin 0 -> 96596 bytes
 src/test/resources/password                        |   1 +
 src/test/resources/sling.png                       | Bin 0 -> 2957 bytes
 src/test/resources/template-inlines.html           |  38 ++
 src/test/resources/template.html                   |  35 ++
 src/test/resources/template.txt                    |   3 +
 18 files changed, 1618 insertions(+), 118 deletions(-)

diff --git a/README.md b/README.md
index a055395..3d9141a 100644
--- a/README.md
+++ b/README.md
@@ -6,19 +6,57 @@
 
 This module is part of the [Apache Sling](https://sling.apache.org) project.
 
-Provide an OSGi Configuration for `SimpleMailBuilder` or a custom `MailBuilder` to send messages using [Apache Commons Email](https://commons.apache.org/proper/commons-email/).
+This module provides a simple layer on top of [Jakarta Mail](https://eclipse-ee4j.github.io/mail/) (former [JavaMail](https://javaee.github.io/javamail/)) including a message builder and a service to send mails via SMTPS.
 
-To extend or override `SimpleMailBuilder`​s configuration call `MessageService#send(String, String, Map):Future<Result>` and supply a configuration map `mail` within the third parameter:
+* Mail Service: sends MIME messages.
+* Message Builder: builds plain text and HTML messages with attachments and inline images 
+* Message ID Provider: allows overwriting default message IDs by custom ones
+
+
+## Example
 
 ```
-{
-  "mail" : {
-    "mail.subject": <String>,
-    "mail.from": <String>,
-    "mail.smtp.hostname": <String>,
-    "mail.smtp.port": <int>,
-    "mail.smtp.username": <String>,
-    "mail.smtp.password": <String>
-  }
-}
+    @Reference
+    MailService mailService;
+
+    String subject = "Rudy, A Message to You";
+    String text = "Stop your messing around\nBetter think of your future\nTime you straighten right out\nCreating problems in town\n…";
+    String html = […];
+    byte[] attachment = […];
+    byte[] inline = […];
+
+    MimeMessage message = mailService.getMessageBuilder()
+        .from("dandy.livingstone@kingston.jamaica.example.net", "Dandy Livingstone")
+        .to("the.specials@coventry.england.example.net", "The Specials")
+        .replyTo("rocksteady@jamaica.example.net");
+        .subject(subject)
+        .text(text)
+        .html(html)
+        .attachment(attachment, "image/png", "attachment.png")
+        .inline(inline, "image/png", "inline")
+        .build();
+
+    mailService.sendMessage(message);
 ```
+
+
+## Integration Tests
+
+Integration tests require a running SMTP server. By default a [GreenMail](http://www.icegreen.com/greenmail/) server is started.
+
+An external SMTP server for validating messages with real mail clients can be used by setting required properties:
+
+    mvn clean install\
+      -Dsling.test.mail.smtps.server.external=true\
+      -Dsling.test.mail.smtps.from=envelope-from@example.org\
+      -Dsling.test.mail.smtps.host=localhost\
+      -Dsling.test.mail.smtps.port=465\
+      -Dsling.test.mail.smtps.username=username\
+      -Dsling.test.mail.smtps.password=password\
+      -Dsling.test.mail.from.address=from@example.org\
+      -Dsling.test.mail.from.name=From\ Sender\
+      -Dsling.test.mail.to.address=to@example.org\
+      -Dsling.test.mail.to.name=To\ Recipient\
+      -Dsling.test.mail.replyTo.address=replyto@example.org\
+      -Dsling.test.mail.replyTo.name=Reply\ To
+
diff --git a/pom.xml b/pom.xml
index 983cc59..4a2baf2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -32,13 +32,13 @@
   <version>0.0.1-SNAPSHOT</version>
 
   <name>Apache Sling Commons Messaging Mail</name>
-  <description>Messaging service using Commons Email to send mails.</description>
+  <description>Send mails via SMTPS</description>
 
   <properties>
     <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
     <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
     <sling.java.version>8</sling.java.version>
-    <org.ops4j.pax.exam.version>4.9.1</org.ops4j.pax.exam.version>
+    <org.ops4j.pax.exam.version>4.13.1</org.ops4j.pax.exam.version>
   </properties>
 
   <scm>
@@ -61,6 +61,16 @@
         <artifactId>depends-maven-plugin</artifactId>
       </plugin>
       <plugin>
+        <groupId>org.apache.rat</groupId>
+        <artifactId>apache-rat-plugin</artifactId>
+        <configuration>
+          <excludes combine.children="append">
+            <exclude>**/*.txt</exclude>
+            <exclude>src/test/resources/password</exclude>
+          </excludes>
+        </configuration>
+      </plugin>
+      <plugin>
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-failsafe-plugin</artifactId>
         <executions>
@@ -72,6 +82,7 @@
           </execution>
         </executions>
         <configuration>
+          <redirectTestOutputToFile>true</redirectTestOutputToFile>
           <systemProperties>
             <property>
               <name>bundle.filename</name>
@@ -84,34 +95,45 @@
   </build>
 
   <dependencies>
-    <!-- javax -->
+    <!-- javax/jakarta -->
     <dependency>
       <groupId>javax.inject</groupId>
       <artifactId>javax.inject</artifactId>
       <scope>test</scope>
     </dependency>
     <dependency>
-      <groupId>javax.mail</groupId>
-      <artifactId>javax.mail-api</artifactId>
-      <version>1.5.5</version>
+      <groupId>jakarta.mail</groupId>
+      <artifactId>jakarta.mail-api</artifactId>
+      <version>1.6.4</version>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.servicemix.specs</groupId>
+      <artifactId>org.apache.servicemix.specs.activation-api-1.1</artifactId>
+      <version>2.9.0</version>
       <scope>provided</scope>
     </dependency>
     <!-- Sun -->
     <dependency>
       <groupId>com.sun.mail</groupId>
-      <artifactId>javax.mail</artifactId>
-      <version>1.5.5</version>
+      <artifactId>jakarta.mail</artifactId>
+      <version>1.6.4</version>
       <scope>provided</scope>
     </dependency>
     <!-- OSGi -->
     <dependency>
       <groupId>org.osgi</groupId>
-      <artifactId>osgi.core</artifactId>
+      <artifactId>org.osgi.annotation.versioning</artifactId>
       <scope>provided</scope>
     </dependency>
     <dependency>
       <groupId>org.osgi</groupId>
-      <artifactId>org.osgi.annotation.versioning</artifactId>
+      <artifactId>osgi.cmpn</artifactId>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.osgi</groupId>
+      <artifactId>osgi.core</artifactId>
       <scope>provided</scope>
     </dependency>
     <dependency>
@@ -126,34 +148,35 @@
     </dependency>
     <!-- Apache Commons -->
     <dependency>
-      <groupId>org.apache.commons</groupId>
-      <artifactId>commons-email</artifactId>
-      <version>1.4</version>
-      <scope>provided</scope>
+      <groupId>commons-io</groupId>
+      <artifactId>commons-io</artifactId>
+      <version>2.5</version>
+      <scope>test</scope>
     </dependency>
     <dependency>
       <groupId>org.apache.commons</groupId>
       <artifactId>commons-lang3</artifactId>
-      <version>3.4</version>
+      <version>3.9</version>
+      <scope>provided</scope>
     </dependency>
-    <!-- Apache Felix -->
     <dependency>
-      <groupId>org.apache.felix</groupId>
-      <artifactId>org.apache.felix.configadmin</artifactId>
-      <version>1.8.8</version>
+      <groupId>org.apache.commons</groupId>
+      <artifactId>commons-email</artifactId>
+      <version>1.5</version>
       <scope>test</scope>
     </dependency>
+    <!-- Apache Felix -->
     <dependency>
       <groupId>org.apache.felix</groupId>
-      <artifactId>org.apache.felix.scr</artifactId>
-      <version>2.0.2</version>
+      <artifactId>org.apache.felix.framework</artifactId>
+      <version>6.0.3</version>
       <scope>test</scope>
     </dependency>
     <!-- Apache Sling -->
     <dependency>
       <groupId>org.apache.sling</groupId>
-      <artifactId>org.apache.sling.commons.threads</artifactId>
-      <version>3.2.6</version>
+      <artifactId>org.apache.sling.commons.crypto</artifactId>
+      <version>1.0.0-SNAPSHOT</version>
       <scope>provided</scope>
     </dependency>
     <dependency>
@@ -162,6 +185,45 @@
       <version>0.0.1-SNAPSHOT</version>
       <scope>provided</scope>
     </dependency>
+    <dependency>
+      <groupId>org.apache.sling</groupId>
+      <artifactId>org.apache.sling.commons.threads</artifactId>
+      <version>3.2.6</version>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.sling</groupId>
+      <artifactId>org.apache.sling.testing.paxexam</artifactId>
+      <version>3.1.0</version>
+      <scope>test</scope>
+    </dependency>
+    <!-- Jasypt -->
+    <dependency>
+      <groupId>org.apache.servicemix.bundles</groupId>
+      <artifactId>org.apache.servicemix.bundles.jasypt</artifactId>
+      <version>1.9.3_1</version>
+      <scope>test</scope>
+    </dependency>
+    <!-- Google -->
+    <dependency>
+      <groupId>com.google.guava</groupId>
+      <artifactId>guava</artifactId>
+      <version>28.1-jre</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>com.google.guava</groupId>
+      <artifactId>failureaccess</artifactId>
+      <version>1.0.1</version>
+      <scope>test</scope>
+    </dependency>
+    <!-- Thymeleaf -->
+    <dependency>
+      <groupId>org.thymeleaf</groupId>
+      <artifactId>thymeleaf</artifactId>
+      <version>3.0.11.RELEASE</version>
+      <scope>test</scope>
+    </dependency>
     <!-- logging -->
     <dependency>
       <groupId>org.slf4j</groupId>
@@ -173,7 +235,7 @@
       <artifactId>slf4j-simple</artifactId>
       <scope>test</scope>
     </dependency>
-    <!-- JSR 305-->
+    <!-- nullability -->
     <dependency>
       <groupId>org.jetbrains</groupId>
       <artifactId>annotations</artifactId>
@@ -186,6 +248,24 @@
       <scope>test</scope>
     </dependency>
     <dependency>
+      <groupId>com.google.truth</groupId>
+      <artifactId>truth</artifactId>
+      <version>1.0</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency> <!-- truth dep -->
+      <groupId>com.googlecode.java-diff-utils</groupId>
+      <artifactId>diffutils</artifactId>
+      <version>1.3.0</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>com.icegreen</groupId>
+      <artifactId>greenmail</artifactId>
+      <version>1.5.11</version>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
       <groupId>org.ops4j.pax.exam</groupId>
       <artifactId>pax-exam-container-forked</artifactId>
       <version>${org.ops4j.pax.exam.version}</version>
@@ -206,31 +286,19 @@
     <dependency>
       <groupId>org.ops4j.pax.url</groupId>
       <artifactId>pax-url-aether</artifactId>
-      <version>2.4.6</version>
+      <version>2.6.2</version>
       <scope>test</scope>
     </dependency>
     <dependency>
       <groupId>org.ops4j.pax.url</groupId>
       <artifactId>pax-url-reference</artifactId>
-      <version>2.4.6</version>
+      <version>2.6.2</version>
       <scope>test</scope>
     </dependency>
     <dependency>
       <groupId>org.ops4j.pax.url</groupId>
       <artifactId>pax-url-wrap</artifactId>
-      <version>2.4.6</version>
-      <scope>test</scope>
-    </dependency>
-    <dependency>
-      <groupId>org.apache.felix</groupId>
-      <artifactId>org.apache.felix.framework</artifactId>
-      <version>5.4.0</version>
-      <scope>test</scope>
-    </dependency>
-    <dependency>
-      <groupId>org.subethamail</groupId>
-      <artifactId>subethasmtp</artifactId>
-      <version>3.1.7</version>
+      <version>2.6.2</version>
       <scope>test</scope>
     </dependency>
   </dependencies>
diff --git a/src/main/java/org/apache/sling/commons/messaging/mail/internal/SimpleMailServiceConfiguration.java b/src/main/java/org/apache/sling/commons/messaging/mail/MailService.java
similarity index 57%
copy from src/main/java/org/apache/sling/commons/messaging/mail/internal/SimpleMailServiceConfiguration.java
copy to src/main/java/org/apache/sling/commons/messaging/mail/MailService.java
index 493041b..258b619 100644
--- a/src/main/java/org/apache/sling/commons/messaging/mail/internal/SimpleMailServiceConfiguration.java
+++ b/src/main/java/org/apache/sling/commons/messaging/mail/MailService.java
@@ -16,21 +16,21 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.sling.commons.messaging.mail.internal;
+package org.apache.sling.commons.messaging.mail;
 
-import org.osgi.service.metatype.annotations.AttributeDefinition;
-import org.osgi.service.metatype.annotations.ObjectClassDefinition;
+import java.util.concurrent.CompletableFuture;
 
-@ObjectClassDefinition(
-    name = "Apache Sling Commons Messaging Mail “Simple Mail Service”",
-    description = "simple mail service for Sling Commons Messaging"
-)
-@interface SimpleMailServiceConfiguration {
+import javax.mail.internet.MimeMessage;
 
-    @AttributeDefinition(
-        name = "ThreadPool name",
-        description = "name of the ThreadPool to use for sending mails"
-    )
-    String threadpoolName() default "default";
+import org.apache.sling.commons.messaging.MessageService;
+import org.jetbrains.annotations.NotNull;
+import org.osgi.annotation.versioning.ProviderType;
+
+@ProviderType
+public interface MailService extends MessageService<MimeMessage> {
+
+    @NotNull MessageBuilder getMessageBuilder();
+
+    @NotNull CompletableFuture<MimeMessage> sendMessage(@NotNull final MimeMessage message);
 
 }
diff --git a/src/main/java/org/apache/sling/commons/messaging/mail/MessageBuilder.java b/src/main/java/org/apache/sling/commons/messaging/mail/MessageBuilder.java
new file mode 100644
index 0000000..a8f4af3
--- /dev/null
+++ b/src/main/java/org/apache/sling/commons/messaging/mail/MessageBuilder.java
@@ -0,0 +1,85 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.commons.messaging.mail;
+
+import javax.mail.Header;
+import javax.mail.MessagingException;
+import javax.mail.internet.AddressException;
+import javax.mail.internet.InternetAddress;
+import javax.mail.internet.InternetHeaders;
+import javax.mail.internet.MimeMessage;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.osgi.annotation.versioning.ProviderType;
+
+@ProviderType
+public interface MessageBuilder {
+
+    @NotNull MessageBuilder header(@NotNull final String name, @Nullable final String value);
+
+    @NotNull MessageBuilder headers(@NotNull final InternetHeaders headers);
+
+    @NotNull MessageBuilder from(@NotNull final InternetAddress from);
+
+    @NotNull MessageBuilder from(@NotNull final String address) throws AddressException;
+
+    @NotNull MessageBuilder from(@NotNull final String address, @NotNull final String name) throws AddressException;
+
+    @NotNull MessageBuilder to(@NotNull final InternetAddress to);
+
+    @NotNull MessageBuilder to(@NotNull final String address) throws AddressException;
+
+    @NotNull MessageBuilder to(@NotNull final String address, @NotNull final String name) throws AddressException;
+
+    @NotNull MessageBuilder cc(@NotNull final InternetAddress cc);
+
+    @NotNull MessageBuilder cc(@NotNull final String address) throws AddressException;
+
+    @NotNull MessageBuilder cc(@NotNull final String address, @NotNull final String name) throws AddressException;
+
+    @NotNull MessageBuilder bcc(@NotNull final InternetAddress bcc);
+
+    @NotNull MessageBuilder bcc(@NotNull final String address) throws AddressException;
+
+    @NotNull MessageBuilder bcc(@NotNull final String address, final String name) throws AddressException;
+
+    @NotNull MessageBuilder replyTo(@NotNull final InternetAddress replyTo);
+
+    @NotNull MessageBuilder replyTo(@NotNull final String address) throws AddressException;
+
+    @NotNull MessageBuilder replyTo(@NotNull final String address, final String name) throws AddressException;
+
+    @NotNull MessageBuilder subject(@NotNull final String subject);
+
+    @NotNull MessageBuilder text(@NotNull final String text);
+
+    @NotNull MessageBuilder html(@NotNull final String html);
+
+    @NotNull MessageBuilder attachment(@NotNull final byte[] content, @NotNull final String type, @NotNull final String filename);
+
+    @NotNull MessageBuilder attachment(@NotNull final byte[] content, @NotNull final String type, @NotNull final String filename, @Nullable Header[] headers);
+
+    @NotNull MessageBuilder inline(@NotNull final byte[] content, @NotNull final String type, @NotNull final String cid);
+
+    @NotNull MessageBuilder inline(@NotNull final byte[] content, @NotNull final String type, @NotNull final String cid, @Nullable Header[] headers);
+
+    @NotNull MimeMessage build() throws MessagingException;
+
+}
diff --git a/src/main/java/org/apache/sling/commons/messaging/mail/internal/SimpleMailServiceConfiguration.java b/src/main/java/org/apache/sling/commons/messaging/mail/MessageIdProvider.java
similarity index 57%
copy from src/main/java/org/apache/sling/commons/messaging/mail/internal/SimpleMailServiceConfiguration.java
copy to src/main/java/org/apache/sling/commons/messaging/mail/MessageIdProvider.java
index 493041b..4b0eb5b 100644
--- a/src/main/java/org/apache/sling/commons/messaging/mail/internal/SimpleMailServiceConfiguration.java
+++ b/src/main/java/org/apache/sling/commons/messaging/mail/MessageIdProvider.java
@@ -16,21 +16,17 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.sling.commons.messaging.mail.internal;
+package org.apache.sling.commons.messaging.mail;
 
-import org.osgi.service.metatype.annotations.AttributeDefinition;
-import org.osgi.service.metatype.annotations.ObjectClassDefinition;
+import javax.mail.MessagingException;
+import javax.mail.internet.MimeMessage;
 
-@ObjectClassDefinition(
-    name = "Apache Sling Commons Messaging Mail “Simple Mail Service”",
-    description = "simple mail service for Sling Commons Messaging"
-)
-@interface SimpleMailServiceConfiguration {
+import org.jetbrains.annotations.NotNull;
+import org.osgi.annotation.versioning.ProviderType;
 
-    @AttributeDefinition(
-        name = "ThreadPool name",
-        description = "name of the ThreadPool to use for sending mails"
-    )
-    String threadpoolName() default "default";
+@ProviderType
+public interface MessageIdProvider {
+
+    @NotNull String getMessageId(@NotNull final MimeMessage message) throws MessagingException;
 
 }
diff --git a/src/main/java/org/apache/sling/commons/messaging/mail/internal/SimpleMailService.java b/src/main/java/org/apache/sling/commons/messaging/mail/internal/SimpleMailService.java
index 20031f1..1e73eab 100644
--- a/src/main/java/org/apache/sling/commons/messaging/mail/internal/SimpleMailService.java
+++ b/src/main/java/org/apache/sling/commons/messaging/mail/internal/SimpleMailService.java
@@ -18,21 +18,23 @@
  */
 package org.apache.sling.commons.messaging.mail.internal;
 
-import java.io.IOException;
-import java.util.Collections;
-import java.util.Map;
+import java.util.List;
+import java.util.Properties;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionException;
 
 import javax.mail.MessagingException;
+import javax.mail.Session;
+import javax.mail.Transport;
+import javax.mail.event.TransportListener;
+import javax.mail.internet.MimeMessage;
 
-import org.apache.commons.mail.Email;
-import org.apache.commons.mail.EmailException;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.sling.commons.crypto.CryptoService;
 import org.apache.sling.commons.messaging.MessageService;
-import org.apache.sling.commons.messaging.Result;
-import org.apache.sling.commons.messaging.mail.MailBuilder;
-import org.apache.sling.commons.messaging.mail.MailResult;
-import org.apache.sling.commons.messaging.mail.MailUtil;
+import org.apache.sling.commons.messaging.mail.MailService;
+import org.apache.sling.commons.messaging.mail.MessageBuilder;
+import org.apache.sling.commons.messaging.mail.MessageIdProvider;
 import org.apache.sling.commons.threads.ThreadPool;
 import org.apache.sling.commons.threads.ThreadPoolManager;
 import org.jetbrains.annotations.NotNull;
@@ -50,23 +52,28 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 @Component(
-    service = MessageService.class,
+    service = {
+        MessageService.class,
+        MailService.class
+    },
     property = {
-        Constants.SERVICE_DESCRIPTION + "=Service to send messages by mail.",
-        Constants.SERVICE_VENDOR + "=The Apache Software Foundation"
+        Constants.SERVICE_DESCRIPTION + "=Apache Sling Commons Messaging Mail – Simple Mail Service",
+        Constants.SERVICE_VENDOR + "=The Apache Software Foundation",
+        "protocol=SMTPS"
     }
 )
 @Designate(
-    ocd = SimpleMailServiceConfiguration.class
+    ocd = SimpleMailServiceConfiguration.class,
+    factory = true
 )
-public class SimpleMailService implements MessageService {
+public class SimpleMailService implements MailService {
 
     @Reference(
-        cardinality = ReferenceCardinality.MANDATORY,
+        cardinality = ReferenceCardinality.OPTIONAL,
         policy = ReferencePolicy.DYNAMIC,
         policyOption = ReferencePolicyOption.GREEDY
     )
-    private volatile MailBuilder mailBuilder;
+    private volatile MessageIdProvider messageIdProvider;
 
     @Reference(
         cardinality = ReferenceCardinality.MANDATORY,
@@ -75,9 +82,34 @@ public class SimpleMailService implements MessageService {
     )
     private volatile ThreadPoolManager threadPoolManager;
 
-    // the ThreadPool used for sending mails
+    @Reference(
+        cardinality = ReferenceCardinality.MANDATORY,
+        policy = ReferencePolicy.DYNAMIC,
+        policyOption = ReferencePolicyOption.GREEDY
+    )
+    private volatile CryptoService cryptoService;
+
+    @Reference(
+        cardinality = ReferenceCardinality.MULTIPLE,
+        policy = ReferencePolicy.DYNAMIC,
+        policyOption = ReferencePolicyOption.GREEDY
+    )
+    private volatile List<TransportListener> transportListeners;
+
     private ThreadPool threadPool;
 
+    private SimpleMailServiceConfiguration configuration;
+
+    private Session session;
+
+    private static final String SMTPS_PROTOCOL = "smtps";
+
+    // https://javaee.github.io/javamail/docs/api/com/sun/mail/smtp/package-summary.html
+
+    private static final String MAIL_SMTPS_FROM = "mail.smtps.from";
+
+    private static final String MESSAGE_ID_HEADER = "Message-ID";
+
     private final Logger logger = LoggerFactory.getLogger(SimpleMailService.class);
 
     public SimpleMailService() {
@@ -85,46 +117,72 @@ public class SimpleMailService implements MessageService {
 
     @Activate
     private void activate(final SimpleMailServiceConfiguration configuration) {
-        logger.debug("activate");
+        logger.debug("activating");
+        this.configuration = configuration;
         configure(configuration);
     }
 
     @Modified
     private void modified(final SimpleMailServiceConfiguration configuration) {
-        logger.debug("modified");
+        logger.debug("modifying");
+        this.configuration = configuration;
         configure(configuration);
     }
 
     @Deactivate
-    protected void deactivate() {
-        logger.info("deactivate");
+    private void deactivate() {
+        logger.debug("deactivating");
+        this.configuration = null;
         threadPoolManager.release(threadPool);
         threadPool = null;
+        session = null;
     }
 
     private void configure(final SimpleMailServiceConfiguration configuration) {
         threadPoolManager.release(threadPool);
-        threadPool = threadPoolManager.get(configuration.threadpoolName());
+        threadPool = threadPoolManager.get(configuration.threadpool_name());
+
+        final Properties properties = new Properties();
+        final String from = configuration.mail_smtps_from();
+        if (StringUtils.isNotBlank(from)) {
+            properties.setProperty(MAIL_SMTPS_FROM, from);
+        }
+
+        session = Session.getInstance(properties);
     }
 
     @Override
-    public CompletableFuture<Result> send(@NotNull final String message, @NotNull final String recipient) {
-        return send(message, recipient, Collections.emptyMap());
+    public @NotNull MessageBuilder getMessageBuilder() {
+        return new SimpleMessageBuilder(session);
     }
 
     @Override
-    public CompletableFuture<Result> send(@NotNull final String message, @NotNull final String recipient, @NotNull final Map data) {
-        return CompletableFuture.supplyAsync(() -> sendMail(message, recipient, data, mailBuilder), runnable -> threadPool.submit(runnable));
+    public @NotNull CompletableFuture<MimeMessage> sendMessage(@NotNull final MimeMessage message) {
+        return CompletableFuture.supplyAsync(() -> send(message), runnable -> threadPool.submit(runnable));
     }
 
-    private MailResult sendMail(final String message, final String recipient, final Map data, final MailBuilder mailBuilder) {
+    private @NotNull MimeMessage send(@NotNull final MimeMessage message) {
         try {
-            final Email email = mailBuilder.build(message, recipient, data);
-            final String messageId = email.send();
-            logger.info("mail '{}' sent", messageId);
-            final byte[] bytes = MailUtil.toByteArray(email);
-            return new MailResult(bytes);
-        } catch (EmailException | MessagingException | IOException e) {
+            final ClassLoader tccl = Thread.currentThread().getContextClassLoader();
+            Thread.currentThread().setContextClassLoader(this.getClass().getClassLoader());
+            final String password = cryptoService.decrypt(configuration.password());
+            try (final Transport transport = session.getTransport(SMTPS_PROTOCOL)) {
+                final List<TransportListener> listeners = this.transportListeners;
+                listeners.forEach(transport::addTransportListener);
+                transport.connect(configuration.mail_smtps_host(), configuration.mail_smtps_port(), configuration.username(), password);
+                message.saveChanges();
+                final MessageIdProvider messageIdProvider = this.messageIdProvider;
+                if (messageIdProvider != null) {
+                    final String messageId = messageIdProvider.getMessageId(message);
+                    message.setHeader(MESSAGE_ID_HEADER, String.format("<%s>", messageId));
+                }
+                logger.debug("sending message '{}'", message.getMessageID());
+                transport.sendMessage(message, message.getAllRecipients());
+                return message;
+            } finally {
+                Thread.currentThread().setContextClassLoader(tccl);
+            }
+        } catch (MessagingException e) {
             throw new CompletionException(e);
         }
     }
diff --git a/src/main/java/org/apache/sling/commons/messaging/mail/internal/SimpleMailServiceConfiguration.java b/src/main/java/org/apache/sling/commons/messaging/mail/internal/SimpleMailServiceConfiguration.java
index 493041b..82a79fd 100644
--- a/src/main/java/org/apache/sling/commons/messaging/mail/internal/SimpleMailServiceConfiguration.java
+++ b/src/main/java/org/apache/sling/commons/messaging/mail/internal/SimpleMailServiceConfiguration.java
@@ -19,18 +19,80 @@
 package org.apache.sling.commons.messaging.mail.internal;
 
 import org.osgi.service.metatype.annotations.AttributeDefinition;
+import org.osgi.service.metatype.annotations.AttributeType;
 import org.osgi.service.metatype.annotations.ObjectClassDefinition;
 
 @ObjectClassDefinition(
     name = "Apache Sling Commons Messaging Mail “Simple Mail Service”",
-    description = "simple mail service for Sling Commons Messaging"
+    description = "Simple mail service sending MIME messages via SMTPS"
 )
 @interface SimpleMailServiceConfiguration {
 
     @AttributeDefinition(
+        name = "Names",
+        description = "names of this service",
+        required = false
+    )
+    String[] names() default {"default"};
+
+    @AttributeDefinition(
         name = "ThreadPool name",
         description = "name of the ThreadPool to use for sending mails"
     )
-    String threadpoolName() default "default";
+    String threadpool_name() default "default";
+
+    @AttributeDefinition(
+        name = "SMTP from",
+        description = "from address"
+    )
+    String mail_smtps_from();
+
+    @AttributeDefinition(
+        name = "SMTP host",
+        description = "host of SMTP server"
+    )
+    String mail_smtps_host() default "localhost";
+
+    @AttributeDefinition(
+        name = "SMTP port",
+        description = "port of SMTP server"
+    )
+    int mail_smtps_port() default 465;
+
+    @AttributeDefinition(
+        name = "Username",
+        description = "username for SMTP server"
+    )
+    String username();
+
+    @AttributeDefinition(
+        name = "Password",
+        description = "password for SMTP server",
+        type = AttributeType.PASSWORD
+    )
+    String password();
+
+    @AttributeDefinition(
+        name = "Message ID Provider target",
+        description = "filter expression to target a Message ID Provider",
+        required = false
+    )
+    String messageIdProvider_target();
+
+    @AttributeDefinition(
+        name = "Crypto Service target",
+        description = "filter expression to target a Crypto Service",
+        required = false
+    )
+    String cryptoService_target();
+
+    @AttributeDefinition(
+        name = "Transport Listeners target",
+        description = "filter expression to target Transport Listeners",
+        required = false
+    )
+    String transportListeners_target();
+
+    String webconsole_configurationFactory_nameHint() default "{names} {username}@{mail_smtps_host}:{mail_smtps_port}";
 
 }
diff --git a/src/main/java/org/apache/sling/commons/messaging/mail/internal/SimpleMessageBuilder.java b/src/main/java/org/apache/sling/commons/messaging/mail/internal/SimpleMessageBuilder.java
new file mode 100644
index 0000000..57a7b50
--- /dev/null
+++ b/src/main/java/org/apache/sling/commons/messaging/mail/internal/SimpleMessageBuilder.java
@@ -0,0 +1,473 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.commons.messaging.mail.internal;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.StandardCharsets;
+import java.util.LinkedList;
+import java.util.List;
+
+import javax.activation.DataHandler;
+import javax.activation.DataSource;
+import javax.mail.Address;
+import javax.mail.Header;
+import javax.mail.Message;
+import javax.mail.MessagingException;
+import javax.mail.Part;
+import javax.mail.Session;
+import javax.mail.internet.AddressException;
+import javax.mail.internet.InternetAddress;
+import javax.mail.internet.InternetHeaders;
+import javax.mail.internet.MimeBodyPart;
+import javax.mail.internet.MimeMessage;
+import javax.mail.internet.MimeMultipart;
+import javax.mail.util.ByteArrayDataSource;
+
+import org.apache.sling.commons.messaging.mail.MessageBuilder;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public class SimpleMessageBuilder implements MessageBuilder {
+
+    private final Session session;
+
+    private InternetHeaders headers = new InternetHeaders();
+
+    private InternetAddress from;
+
+    private List<InternetAddress> toRecipients = new LinkedList<>();
+
+    private List<InternetAddress> ccRecipients = new LinkedList<>();
+
+    private List<InternetAddress> bccRecipients = new LinkedList<>();
+
+    private List<InternetAddress> replyTos = new LinkedList<>();
+
+    private String subject;
+
+    private String text;
+
+    private String html;
+
+    private List<Attachment> attachments = new LinkedList<>();
+
+    private List<Inline> inlines = new LinkedList<>();
+
+    private static final String CONTENT_TYPE_TEXT_HTML = "text/html; charset=utf-8";
+
+    private static final String CONTENT_TYPE_TEXT_PLAIN = "text/plain; charset=utf-8";
+
+    private static final String CHARSET_UTF8 = "utf-8";
+
+    private static final String MULTIPART_SUBTYPE_MIXED = "mixed";
+
+    private static final String MULTIPART_SUBTYPE_ALTERNATIVE = "alternative";
+
+    private static final String MULTIPART_SUBTYPE_RELATED = "related";
+
+    SimpleMessageBuilder(@NotNull final Session session) {
+        this.session = session;
+    }
+
+    @Override
+    public @NotNull MessageBuilder header(@NotNull final String name, @Nullable final String value) {
+        headers.setHeader(name, value);
+        return this;
+    }
+
+    @Override
+    public @NotNull MessageBuilder headers(@NotNull final InternetHeaders headers) {
+        while (headers.getAllHeaders().hasMoreElements()) {
+            final Header header = headers.getAllHeaders().nextElement();
+            this.headers.setHeader(header.getName(), header.getValue());
+        }
+        return this;
+    }
+
+    @Override
+    public @NotNull MessageBuilder from(@NotNull final InternetAddress from) {
+        this.from = from;
+        return this;
+    }
+
+    @Override
+    public @NotNull MessageBuilder from(@NotNull final String address) throws AddressException {
+        final InternetAddress from = new InternetAddress(address);
+        return from(from);
+    }
+
+    @Override
+    public @NotNull MessageBuilder from(@NotNull final String address, @NotNull final String name) throws AddressException {
+        final InternetAddress from = new InternetAddress(address);
+        try {
+            from.setPersonal(name, StandardCharsets.UTF_8.name());
+        } catch (UnsupportedEncodingException e) {
+            //
+        }
+        return from(from);
+    }
+
+    @Override
+    public @NotNull MessageBuilder to(@NotNull final InternetAddress to) {
+        toRecipients.add(to);
+        return this;
+    }
+
+    @Override
+    public @NotNull MessageBuilder to(@NotNull final String address) throws AddressException {
+        final InternetAddress to = new InternetAddress(address);
+        return to(to);
+    }
+
+    @Override
+    public @NotNull MessageBuilder to(@NotNull final String address, @NotNull final String name) throws AddressException {
+        final InternetAddress to = new InternetAddress(address);
+        try {
+            to.setPersonal(name, StandardCharsets.UTF_8.name());
+        } catch (UnsupportedEncodingException e) {
+            //
+        }
+        return to(to);
+    }
+
+    @Override
+    public @NotNull MessageBuilder cc(@NotNull final InternetAddress cc) {
+        ccRecipients.add(cc);
+        return this;
+    }
+
+    @Override
+    public @NotNull MessageBuilder cc(@NotNull final String address) throws AddressException {
+        final InternetAddress cc = new InternetAddress(address);
+        return cc(cc);
+    }
+
+    @Override
+    public @NotNull MessageBuilder cc(@NotNull final String address, @NotNull final String name) throws AddressException {
+        final InternetAddress cc = new InternetAddress(address);
+        try {
+            cc.setPersonal(name, StandardCharsets.UTF_8.name());
+        } catch (UnsupportedEncodingException e) {
+            //
+        }
+        return cc(cc);
+    }
+
+    @Override
+    public @NotNull MessageBuilder bcc(@NotNull final InternetAddress bcc) {
+        bccRecipients.add(bcc);
+        return this;
+    }
+
+    @Override
+    public @NotNull MessageBuilder bcc(@NotNull final String address) throws AddressException {
+        final InternetAddress bcc = new InternetAddress(address);
+        return bcc(bcc);
+    }
+
+    public @NotNull MessageBuilder bcc(@NotNull final String address, final String name) throws AddressException {
+        final InternetAddress bcc = new InternetAddress(address);
+        try {
+            bcc.setPersonal(name, StandardCharsets.UTF_8.name());
+        } catch (UnsupportedEncodingException e) {
+            //
+        }
+        return bcc(bcc);
+    }
+
+    @Override
+    public @NotNull MessageBuilder replyTo(@NotNull final InternetAddress replyTo) {
+        replyTos.add(replyTo);
+        return this;
+    }
+
+    @Override
+    public @NotNull MessageBuilder replyTo(@NotNull final String address) throws AddressException {
+        final InternetAddress replyTo = new InternetAddress(address);
+        return replyTo(replyTo);
+    }
+
+    @Override
+    public @NotNull MessageBuilder replyTo(@NotNull final String address, @NotNull final String name) throws AddressException {
+        final InternetAddress replyTo = new InternetAddress(address);
+        try {
+            replyTo.setPersonal(name, StandardCharsets.UTF_8.name());
+        } catch (UnsupportedEncodingException e) {
+            //
+        }
+        return replyTo(replyTo);
+    }
+
+    @Override
+    public @NotNull MessageBuilder subject(@NotNull final String subject) {
+        this.subject = subject;
+        return this;
+    }
+
+    @Override
+    public @NotNull MessageBuilder text(@NotNull final String text) {
+        this.text = text;
+        return this;
+    }
+
+    @Override
+    public @NotNull MessageBuilder html(@NotNull final String html) {
+        this.html = html;
+        return this;
+    }
+
+    @Override
+    public @NotNull MessageBuilder attachment(@NotNull final byte[] content, @NotNull final String type, @NotNull final String filename) {
+        return attachment(content, type, filename, null);
+    }
+
+    @Override
+    public @NotNull MessageBuilder attachment(@NotNull final byte[] content, @NotNull final String type, @NotNull final String filename, @Nullable Header[] headers) {
+        final Attachment attachment = new Attachment(content, type, filename, null);
+        this.attachments.add(attachment);
+        return this;
+    }
+
+    @Override
+    public @NotNull MessageBuilder inline(@NotNull final byte[] content, @NotNull final String type, @NotNull final String cid) {
+        return inline(content, type, cid, null);
+    }
+
+    @Override
+    public @NotNull MessageBuilder inline(@NotNull final byte[] content, @NotNull final String type, @NotNull final String cid, @Nullable Header[] headers) {
+        final Inline inline = new Inline(content, type, cid, headers);
+        this.inlines.add(inline);
+        return this;
+    }
+
+    private InternetHeaders headers() {
+        return headers;
+    }
+
+    private InternetAddress from() {
+        return from;
+    }
+
+    private List<InternetAddress> to() {
+        return toRecipients;
+    }
+
+    private List<InternetAddress> cc() {
+        return ccRecipients;
+    }
+
+    private List<InternetAddress> bcc() {
+        return bccRecipients;
+    }
+
+    private List<InternetAddress> replyTo() {
+        return replyTos;
+    }
+
+    private String subject() {
+        return subject;
+    }
+
+    private String text() {
+        return text;
+    }
+
+    private String html() {
+        return html;
+    }
+
+    private List<Attachment> attachments() {
+        return attachments;
+    }
+
+    private List<Inline> inlines() {
+        return inlines;
+    }
+
+    private boolean hasText() {
+        return text() != null;
+    }
+
+    private boolean hasHtml() {
+        return html() != null;
+    }
+
+    private boolean hasAttachments() {
+        return !attachments().isEmpty();
+    }
+
+    private boolean hasInlines() {
+        return !inlines().isEmpty();
+    }
+
+    public @NotNull MimeMessage build() throws MessagingException {
+        final MimeMessage message = new MimeMessage(session);
+
+        while (headers().getAllHeaders().hasMoreElements()) {
+            final Header header = headers.getAllHeaders().nextElement();
+            message.setHeader(header.getName(), header.getValue());
+        }
+
+        message.setFrom(from());
+        message.setRecipients(Message.RecipientType.TO, to().toArray(new Address[0]));
+        message.setRecipients(Message.RecipientType.CC, cc().toArray(new Address[0]));
+        message.setRecipients(Message.RecipientType.BCC, bcc().toArray(new Address[0]));
+        message.setReplyTo(replyTos.toArray(new Address[0]));
+        message.setSubject(subject(), StandardCharsets.UTF_8.name());
+
+        if (hasHtml() || hasAttachments() || hasInlines()) {
+            final MimeMultipart content = new MimeMultipart(MULTIPART_SUBTYPE_MIXED);
+
+            if (hasText() && hasHtml()) { // text and html
+                final MimeMultipart alternative = new MimeMultipart(MULTIPART_SUBTYPE_ALTERNATIVE);
+                handleHtmlAndInlines(alternative, html(), inlines());
+                addText(alternative, text());
+                final MimeBodyPart part = new MimeBodyPart();
+                part.setContent(alternative);
+                content.addBodyPart(part);
+            } else if (hasHtml()) { // html only
+                handleHtmlAndInlines(content, html(), inlines());
+            } else { // text only
+                addText(content, text());
+            }
+
+            addAttachments(content, attachments);
+
+            message.setContent(content);
+        } else {
+            message.setText(text(), CHARSET_UTF8);
+        }
+        return message;
+    }
+
+    private static void handleHtmlAndInlines(final MimeMultipart parent, final String html, final List<Inline> inlines) throws MessagingException {
+        if (!inlines.isEmpty()) { // html and inlines
+            final MimeMultipart related = new MimeMultipart(MULTIPART_SUBTYPE_RELATED);
+            addHtml(related, html);
+            addInlines(related, inlines);
+            final MimeBodyPart part = new MimeBodyPart();
+            part.setContent(related);
+            parent.addBodyPart(part);
+        } else { // html
+            addHtml(parent, html);
+        }
+    }
+
+    private static void addText(final MimeMultipart parent, final String text) throws MessagingException {
+        final MimeBodyPart part = new MimeBodyPart();
+        part.setContent(text, CONTENT_TYPE_TEXT_PLAIN);
+        parent.addBodyPart(part);
+    }
+
+    private static void addHtml(final MimeMultipart parent, final String html) throws MessagingException {
+        final MimeBodyPart part = new MimeBodyPart();
+        part.setContent(html, CONTENT_TYPE_TEXT_HTML);
+        parent.addBodyPart(part);
+    }
+
+    private static void addAttachments(final MimeMultipart parent, final List<Attachment> attachments) throws MessagingException {
+        for (final Attachment attachment : attachments) {
+            try (final ByteArrayInputStream inputStream = new ByteArrayInputStream(attachment.content)) {
+                final MimeBodyPart part = new MimeBodyPart();
+                part.setDisposition(Part.ATTACHMENT);
+                part.setFileName(attachment.filename);
+                setDataHandler(part, inputStream, attachment.type);
+                if (attachment.headers != null) {
+                    setHeaders(part, attachment.headers);
+                }
+                parent.addBodyPart(part);
+            } catch (Exception e) {
+                final String message = String.format("Adding attachment failed: %s", attachment.filename);
+                throw new MessagingException(message, e);
+            }
+        }
+    }
+
+    private static void addInlines(final MimeMultipart parent, final List<Inline> inlines) throws MessagingException {
+        for (final Inline inline : inlines) {
+            try (final ByteArrayInputStream inputStream = new ByteArrayInputStream(inline.content)) {
+                final MimeBodyPart part = new MimeBodyPart();
+                part.setDisposition(Part.INLINE);
+                part.setContentID(String.format("<%s>", inline.cid));
+                setDataHandler(part, inputStream, inline.type);
+                if (inline.headers != null) {
+                    setHeaders(part, inline.headers);
+                }
+                parent.addBodyPart(part);
+            } catch (Exception e) {
+                final String message = String.format("Adding inline object failed: %s", inline.cid);
+                throw new MessagingException(message, e);
+            }
+        }
+    }
+
+    private static void setDataHandler(final MimeBodyPart part, final InputStream inputStream, final String type) throws MessagingException, IOException {
+        final DataSource source = new ByteArrayDataSource(inputStream, type);
+        final DataHandler handler = new DataHandler(source);
+        part.setDataHandler(handler);
+    }
+
+    private static void setHeaders(final MimeBodyPart part, final Header[] headers) throws MessagingException {
+        for (final Header header : headers) {
+            part.setHeader(header.getName(), header.getValue());
+        }
+    }
+
+    private static class Attachment {
+
+        final byte[] content;
+
+        final String type;
+
+        final String filename;
+
+        final Header[] headers;
+
+        Attachment(@NotNull final byte[] content, @NotNull final String type, @NotNull final String filename, @Nullable final Header[] headers) {
+            this.content = content;
+            this.type = type;
+            this.filename = filename;
+            this.headers = headers;
+        }
+
+    }
+
+    private static class Inline {
+
+        final byte[] content;
+
+        final String type;
+
+        final String cid;
+
+        final Header[] headers;
+
+        Inline(@NotNull final byte[] content, @NotNull final String type, @NotNull final String cid, @Nullable final Header[] headers) {
+            this.content = content;
+            this.type = type;
+            this.cid = cid;
+            this.headers = headers;
+        }
+
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/commons/messaging/mail/internal/SimpleMessageIdProvider.java b/src/main/java/org/apache/sling/commons/messaging/mail/internal/SimpleMessageIdProvider.java
new file mode 100644
index 0000000..e8c231c
--- /dev/null
+++ b/src/main/java/org/apache/sling/commons/messaging/mail/internal/SimpleMessageIdProvider.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.commons.messaging.mail.internal;
+
+import java.util.UUID;
+
+import javax.mail.MessagingException;
+import javax.mail.internet.MimeMessage;
+
+import org.apache.sling.commons.messaging.mail.MessageIdProvider;
+import org.jetbrains.annotations.NotNull;
+import org.osgi.framework.Constants;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Modified;
+import org.osgi.service.metatype.annotations.Designate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Component(
+    property = {
+        Constants.SERVICE_DESCRIPTION + "=Apache Sling Commons Messaging Mail – Simple Message ID Provider",
+        Constants.SERVICE_VENDOR + "=The Apache Software Foundation"
+    }
+)
+@Designate(
+    ocd = SimpleMessageIdProviderConfiguration.class,
+    factory = true
+)
+public class SimpleMessageIdProvider implements MessageIdProvider {
+
+    private SimpleMessageIdProviderConfiguration configuration;
+
+    private final Logger logger = LoggerFactory.getLogger(SimpleMessageIdProvider.class);
+
+    @Activate
+    private void activate(final SimpleMessageIdProviderConfiguration configuration) {
+        logger.debug("activating");
+        this.configuration = configuration;
+    }
+
+    @Modified
+    private void modified(final SimpleMessageIdProviderConfiguration configuration) {
+        logger.debug("modifying");
+        this.configuration = configuration;
+    }
+
+    @Deactivate
+    private void deactivate() {
+        logger.debug("deactivating");
+        this.configuration = null;
+    }
+
+    @Override
+    public @NotNull String getMessageId(@NotNull MimeMessage message) throws MessagingException {
+        return String.format("%s.%s@%s", UUID.randomUUID().toString(), System.currentTimeMillis(), configuration.host());
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/commons/messaging/mail/internal/SimpleMailServiceConfiguration.java b/src/main/java/org/apache/sling/commons/messaging/mail/internal/SimpleMessageIdProviderConfiguration.java
similarity index 63%
copy from src/main/java/org/apache/sling/commons/messaging/mail/internal/SimpleMailServiceConfiguration.java
copy to src/main/java/org/apache/sling/commons/messaging/mail/internal/SimpleMessageIdProviderConfiguration.java
index 493041b..d098909 100644
--- a/src/main/java/org/apache/sling/commons/messaging/mail/internal/SimpleMailServiceConfiguration.java
+++ b/src/main/java/org/apache/sling/commons/messaging/mail/internal/SimpleMessageIdProviderConfiguration.java
@@ -22,15 +22,24 @@ import org.osgi.service.metatype.annotations.AttributeDefinition;
 import org.osgi.service.metatype.annotations.ObjectClassDefinition;
 
 @ObjectClassDefinition(
-    name = "Apache Sling Commons Messaging Mail “Simple Mail Service”",
-    description = "simple mail service for Sling Commons Messaging"
+    name = "Apache Sling Commons Messaging Mail “Simple Message ID Provider”",
+    description = "Service to provide a Message ID based on random UUID, timestamp in ms and custom host"
 )
-@interface SimpleMailServiceConfiguration {
+@interface SimpleMessageIdProviderConfiguration {
 
     @AttributeDefinition(
-        name = "ThreadPool name",
-        description = "name of the ThreadPool to use for sending mails"
+        name = "Names",
+        description = "names of this service",
+        required = false
     )
-    String threadpoolName() default "default";
+    String[] names() default {"default"};
+
+    @AttributeDefinition(
+        name = "Host",
+        description = "Host to use in Message ID"
+    )
+    String host() default "localhost";
+
+    String webconsole_configurationFactory_nameHint() default "{names} {host}";
 
 }
diff --git a/src/test/java/org/apache/sling/commons/messaging/mail/it/tests/MailTestSupport.java b/src/test/java/org/apache/sling/commons/messaging/mail/it/tests/MailTestSupport.java
new file mode 100644
index 0000000..6938dee
--- /dev/null
+++ b/src/test/java/org/apache/sling/commons/messaging/mail/it/tests/MailTestSupport.java
@@ -0,0 +1,144 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.commons.messaging.mail.it.tests;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Locale;
+import java.util.Map;
+
+import javax.inject.Inject;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.sling.testing.paxexam.SlingOptions;
+import org.apache.sling.testing.paxexam.TestSupport;
+import org.ops4j.pax.exam.options.MavenArtifactProvisionOption;
+import org.ops4j.pax.exam.options.ModifiableCompositeOption;
+import org.osgi.service.cm.ConfigurationAdmin;
+import org.thymeleaf.ITemplateEngine;
+import org.thymeleaf.TemplateEngine;
+import org.thymeleaf.TemplateSpec;
+import org.thymeleaf.context.Context;
+import org.thymeleaf.context.IContext;
+import org.thymeleaf.templatemode.TemplateMode;
+
+import static org.apache.sling.testing.paxexam.SlingOptions.backing;
+import static org.apache.sling.testing.paxexam.SlingOptions.scr;
+import static org.apache.sling.testing.paxexam.SlingOptions.slingCommonsThreads;
+import static org.ops4j.pax.exam.CoreOptions.bootClasspathLibrary;
+import static org.ops4j.pax.exam.CoreOptions.composite;
+import static org.ops4j.pax.exam.CoreOptions.junitBundles;
+import static org.ops4j.pax.exam.CoreOptions.mavenBundle;
+import static org.ops4j.pax.exam.CoreOptions.wrappedBundle;
+
+public abstract class MailTestSupport extends TestSupport {
+
+    @Inject
+    protected ConfigurationAdmin configurationAdmin;
+
+    private ITemplateEngine templateEngine = new TemplateEngine();
+
+    protected ModifiableCompositeOption baseConfiguration() {
+        return composite(
+            super.baseConfiguration(),
+            // Sling Commons Messaging Mail
+            testBundle("bundle.filename"),
+            mavenBundle().groupId("org.apache.sling").artifactId("org.apache.sling.commons.messaging").versionAsInProject(),
+            mavenBundle().groupId("jakarta.mail").artifactId("jakarta.mail-api").versionAsInProject(),
+            mavenBundle().groupId("com.sun.mail").artifactId("jakarta.mail").versionAsInProject(),
+            mavenBundle().groupId("org.apache.commons").artifactId("commons-lang3").versionAsInProject(),
+            scr(),
+            slingCommonsCrypto(),
+            slingCommonsThreads(),
+            backing(),
+            // testing
+            junitBundles(),
+            wrappedBundle(mavenBundle().groupId("com.google.truth").artifactId("truth").versionAsInProject()),
+            mavenBundle().groupId("com.google.guava").artifactId("guava").versionAsInProject(),
+            mavenBundle().groupId("com.google.guava").artifactId("failureaccess").versionAsInProject(),
+            mavenBundle().groupId("com.googlecode.java-diff-utils").artifactId("diffutils").versionAsInProject(),
+            mavenBundle().groupId("commons-io").artifactId("commons-io").versionAsInProject(),
+            mavenBundle().groupId("org.apache.commons").artifactId("commons-email").versionAsInProject(),
+            greenmail(),
+            thymeleaf()
+        );
+    }
+
+    private static ModifiableCompositeOption greenmail() {
+        final MavenArtifactProvisionOption greenmail = mavenBundle().groupId("com.icegreen").artifactId("greenmail").versionAsInProject();
+        final MavenArtifactProvisionOption slf4j_api = mavenBundle().groupId("org.slf4j").artifactId("slf4j-api").versionAsInProject();
+        final MavenArtifactProvisionOption slf4j_simple = mavenBundle().groupId("org.slf4j").artifactId("slf4j-simple").versionAsInProject();
+        return composite(
+            greenmail,
+            // add GreenMail to boot classpath to allow setting ssl.SocketFactory.provider to GreenMail's DummySSLSocketFactory
+            bootClasspathLibrary(greenmail).afterFramework(),
+            bootClasspathLibrary(slf4j_api).afterFramework(), // GreenMail dependency
+            bootClasspathLibrary(slf4j_simple).afterFramework() // GreenMail dependency
+        );
+    }
+
+    private static ModifiableCompositeOption thymeleaf() {
+        return composite(
+            mavenBundle().groupId("org.apache.servicemix.bundles").artifactId("org.apache.servicemix.bundles.thymeleaf").version(SlingOptions.versionResolver),
+            mavenBundle().groupId("org.attoparser").artifactId("attoparser").version(SlingOptions.versionResolver),
+            mavenBundle().groupId("org.unbescape").artifactId("unbescape").version(SlingOptions.versionResolver),
+            mavenBundle().groupId("org.apache.servicemix.bundles").artifactId("org.apache.servicemix.bundles.ognl").version(SlingOptions.versionResolver),
+            mavenBundle().groupId("org.javassist").artifactId("javassist").version(SlingOptions.versionResolver)
+        );
+    }
+
+    private static ModifiableCompositeOption slingCommonsCrypto() {
+        return composite(
+            mavenBundle().groupId("org.apache.sling").artifactId("org.apache.sling.commons.crypto").versionAsInProject(),
+            mavenBundle().groupId("org.apache.commons").artifactId("commons-lang3").versionAsInProject(),
+            mavenBundle().groupId("org.apache.servicemix.bundles").artifactId("org.apache.servicemix.bundles.jasypt").versionAsInProject()
+        );
+    }
+
+    // helpers for attachments, inline objects and templates
+
+    String getResourceAsString(final String path) throws IOException {
+        try (final InputStream inputStream = getClass().getResourceAsStream(path)) {
+            return IOUtils.toString(inputStream, StandardCharsets.UTF_8);
+        }
+    }
+
+    byte[] getResourceAsByteArray(final String path) throws IOException {
+        try (final InputStream inputStream = getClass().getResourceAsStream(path)) {
+            return IOUtils.toByteArray(inputStream);
+        }
+    }
+
+    String renderHtmlTemplate(final String path, final Map<String, Object> variables) throws IOException {
+        return renderTemplate(path, variables, TemplateMode.HTML);
+    }
+
+    String renderTextTemplate(final String path, final Map<String, Object> variables) throws IOException {
+        return renderTemplate(path, variables, TemplateMode.TEXT);
+    }
+
+    String renderTemplate(final String path, final Map<String, Object> variables, final TemplateMode templateMode) throws IOException {
+        final String template = getResourceAsString(path);
+        final IContext context = new Context(Locale.ENGLISH, variables);
+        final TemplateSpec templateSpec = new TemplateSpec(template, templateMode);
+        return templateEngine.process(templateSpec, context);
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/commons/messaging/mail/it/tests/SimpleMailServiceIT.java b/src/test/java/org/apache/sling/commons/messaging/mail/it/tests/SimpleMailServiceIT.java
new file mode 100644
index 0000000..2557f28
--- /dev/null
+++ b/src/test/java/org/apache/sling/commons/messaging/mail/it/tests/SimpleMailServiceIT.java
@@ -0,0 +1,414 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.commons.messaging.mail.it.tests;
+
+import java.io.UnsupportedEncodingException;
+import java.security.Security;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Properties;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+
+import javax.activation.DataSource;
+import javax.inject.Inject;
+import javax.mail.Message;
+import javax.mail.Session;
+import javax.mail.internet.InternetAddress;
+import javax.mail.internet.MimeMessage;
+
+import com.icegreen.greenmail.util.DummySSLSocketFactory;
+import com.icegreen.greenmail.util.GreenMail;
+import com.icegreen.greenmail.util.ServerSetup;
+import org.apache.commons.mail.util.MimeMessageParser;
+import org.apache.sling.commons.messaging.MessageService;
+import org.apache.sling.commons.messaging.mail.MailService;
+import org.apache.sling.commons.messaging.mail.MessageBuilder;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.ops4j.pax.exam.Configuration;
+import org.ops4j.pax.exam.Option;
+import org.ops4j.pax.exam.junit.PaxExam;
+import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy;
+import org.ops4j.pax.exam.spi.reactors.PerClass;
+import org.ops4j.pax.exam.util.Filter;
+import org.ops4j.pax.exam.util.PathUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.ops4j.pax.exam.CoreOptions.options;
+import static org.ops4j.pax.exam.CoreOptions.propagateSystemProperties;
+import static org.ops4j.pax.exam.cm.ConfigurationAdminOptions.factoryConfiguration;
+
+@RunWith(PaxExam.class)
+@ExamReactorStrategy(PerClass.class)
+public class SimpleMailServiceIT extends MailTestSupport {
+
+    private static final boolean local = !Boolean.getBoolean("sling.test.mail.smtps.server.external");
+
+    private static InternetAddress from;
+
+    private static InternetAddress to;
+
+    private static InternetAddress cc;
+
+    private static InternetAddress bcc;
+
+    private static InternetAddress replyTo;
+
+    static {
+        final String from_address = local ? "from@example.org" : System.getProperty("sling.test.mail.from.address");
+        final String from_name = local ? "From Name" : System.getProperty("sling.test.mail.from.name");
+        final String to_address = local ? "to@example.org" : System.getProperty("sling.test.mail.to.address");
+        final String to_name = local ? "To Name" : System.getProperty("sling.test.mail.to.name");
+        final String replyTo_address = local ? "replyto@example.org" : System.getProperty("sling.test.mail.replyTo.address");
+        final String replyTo_name = local ? "ReplyTo Name" : System.getProperty("sling.test.mail.replyTo.name");
+        try {
+            from = new InternetAddress(from_address, from_name);
+            to = new InternetAddress(to_address, to_name);
+            replyTo = new InternetAddress(replyTo_address, replyTo_name);
+        } catch (UnsupportedEncodingException e) {
+            e.printStackTrace();
+        }
+    }
+
+    private GreenMail greenMail;
+
+    @Inject
+    protected MessageService<MimeMessage> messageService;
+
+    @Inject
+    @Filter(value = "(protocol=SMTPS)")
+    protected MailService mailService;
+
+    @Rule
+    public final ExpectedException exception = ExpectedException.none();
+
+    private final Logger logger = LoggerFactory.getLogger(SimpleMailServiceIT.class);
+
+    @Configuration
+    public Option[] configuration() {
+        final int port = findFreePort();
+        final String path = String.format("%s/src/test/resources/password", PathUtils.getBaseDir());
+        return options(
+            baseConfiguration(),
+            propagateSystemProperties(
+                "sling.test.mail.smtps.server.external",
+                "sling.test.mail.smtps.from",
+                "sling.test.mail.smtps.host",
+                "sling.test.mail.smtps.port",
+                "sling.test.mail.smtps.username",
+                "sling.test.mail.smtps.password",
+                "sling.test.mail.from.address",
+                "sling.test.mail.from.name",
+                "sling.test.mail.to.address",
+                "sling.test.mail.to.name",
+                "sling.test.mail.replyTo.address",
+                "sling.test.mail.replyTo.name"
+            ),
+            factoryConfiguration("org.apache.sling.commons.messaging.mail.internal.SimpleMessageIdProvider")
+                .put("host", "localhost")
+                .asOption(),
+            factoryConfiguration("org.apache.sling.commons.messaging.mail.internal.SimpleMailService")
+                .put("mail.smtps.from", local ? "envelope-from@example.org" : System.getProperty("sling.test.mail.smtps.from"))
+                .put("mail.smtps.host", local ? "localhost" : System.getProperty("sling.test.mail.smtps.host"))
+                .put("mail.smtps.port", local ? port : Integer.getInteger("sling.test.mail.smtps.port"))
+                .put("username", local ? "username" : System.getProperty("sling.test.mail.smtps.username"))
+                .put("password", local ? "OEKPFL5cVJRqVjh4QaDZhvBiqv8wgWBMJ8PGbYHTqev046oV6888mna9w1mIGCXK" : System.getProperty("sling.test.mail.smtps.password"))
+                .asOption(),
+            // Commons Crypto
+            factoryConfiguration("org.apache.sling.commons.crypto.jasypt.internal.JasyptStandardPBEStringCryptoService")
+                .put("algorithm", "PBEWITHHMACSHA512ANDAES_256")
+                .asOption(),
+            factoryConfiguration("org.apache.sling.commons.crypto.jasypt.internal.JasyptRandomIvGeneratorRegistrar")
+                .put("algorithm", "SHA1PRNG")
+                .asOption(),
+            factoryConfiguration("org.apache.sling.commons.crypto.internal.FilePasswordProvider")
+                .put("path", path)
+                .asOption()
+        );
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        logger.info("local server : {}", local);
+        if (local && Objects.isNull(greenMail)) {
+            // set up GreenMail server
+            Security.setProperty("ssl.SocketFactory.provider", DummySSLSocketFactory.class.getName());
+            final org.osgi.service.cm.Configuration[] configurations = configurationAdmin.listConfigurations("(service.factoryPid=org.apache.sling.commons.messaging.mail.internal.SimpleMailService)");
+            final org.osgi.service.cm.Configuration configuration = configurations[0];
+            final int port = (int) configuration.getProperties().get("mail.smtps.port");
+            final ServerSetup serverSetup = new ServerSetup(port, "127.0.0.1", "smtps");
+            greenMail = new GreenMail(serverSetup);
+            greenMail.setUser("username", "password");
+            greenMail.start();
+        }
+    }
+
+    @After
+    public void tearDown() {
+        if (local) {
+            greenMail.stop();
+            greenMail = null;
+        }
+    }
+
+    private MessageBuilder initializeMessageBuilder() {
+        return mailService.getMessageBuilder()
+            .from(from)
+            .to(to)
+            .replyTo(replyTo);
+    }
+
+    @Test
+    public void testMessageService() throws ExecutionException, InterruptedException {
+        exception.expect(ExecutionException.class);
+        assertThat(messageService).isNotNull();
+        final Properties properties = new Properties();
+        final Session session = Session.getDefaultInstance(properties);
+        final MimeMessage message = new MimeMessage(session);
+        final CompletableFuture<MimeMessage> future = messageService.sendMessage(message);
+        future.get();
+    }
+
+    @Test
+    public void testMailService() {
+        assertThat(mailService).isNotNull();
+    }
+
+    @Test
+    public void sendText() throws Exception {
+        final Map<String, Object> variables = Collections.singletonMap("date", new Date());
+        final String subject = "Sling Commons Mail: Text [æåëęïįœøüū] \uD83D\uDCE7";
+        final String text = renderTextTemplate("/template.txt", variables);
+        final MimeMessage message = initializeMessageBuilder()
+            .subject(subject)
+            .text(text)
+            .build();
+
+        final CompletableFuture<MimeMessage> future = mailService.sendMessage(message);
+        future.get();
+
+        if (local) {
+            greenMail.waitForIncomingEmail(1);
+            greenMail.getReceivedMessagesForDomain(to.getAddress());
+            final MimeMessage[] messages = greenMail.getReceivedMessages();
+            final MimeMessage received = messages[0];
+            final MimeMessageParser parser = new MimeMessageParser(message).parse();
+
+            assertThat(received.getMessageID()).endsWith("@localhost>");
+            assertThat(received.getSubject()).isEqualTo(subject);
+            assertThat(received.getFrom()[0]).isEqualTo(from);
+            assertThat(received.getRecipients(Message.RecipientType.TO)[0]).isEqualTo(to);
+            assertThat(received.getReplyTo()[0]).isEqualTo(replyTo);
+
+            assertThat(parser.getPlainContent()).isEqualTo(text);
+
+            assertThat(parser.getAttachmentList()).isEmpty();
+            assertThat(parser.getContentIds()).isEmpty();
+        }
+    }
+
+    @Test
+    public void sendTextAndAttachment() throws Exception {
+        final Map<String, Object> variables = Collections.singletonMap("date", new Date());
+        final String subject = "Sling Commons Mail: Text and Attachment [æåëęïįœøüū] \uD83D\uDCE7";
+        final String text = renderTextTemplate("/template.txt", variables);
+        final byte[] support = getResourceAsByteArray("/SupportApache-small.png");
+        final MimeMessage message = initializeMessageBuilder()
+            .subject(subject)
+            .text(text)
+            .attachment(support, "image/png", "SupportApache-small.png")
+            .build();
+
+        final CompletableFuture<MimeMessage> future = mailService.sendMessage(message);
+        future.get();
+
+        if (local) {
+            greenMail.waitForIncomingEmail(1);
+            final MimeMessage[] messages = greenMail.getReceivedMessages();
+            final MimeMessage received = messages[0];
+            final MimeMessageParser parser = new MimeMessageParser(message).parse();
+
+            assertThat(received.getMessageID()).endsWith("@localhost>");
+            assertThat(received.getSubject()).isEqualTo(subject);
+            assertThat(received.getFrom()[0]).isEqualTo(from);
+            assertThat(received.getRecipients(Message.RecipientType.TO)[0]).isEqualTo(to);
+            assertThat(received.getReplyTo()[0]).isEqualTo(replyTo);
+
+            assertThat(parser.getPlainContent()).isEqualTo(text);
+
+            assertThat(parser.getAttachmentList().get(0).getName()).isEqualTo("SupportApache-small.png");
+            assertThat(parser.getContentIds()).isEmpty();
+        }
+    }
+
+    @Test
+    public void sendHtml() throws Exception {
+        final Map<String, Object> variables = Collections.singletonMap("date", new Date());
+        final String subject = "Sling Commons Mail: HTML [æåëęïįœøüū] \uD83D\uDCE7";
+        final String html = renderHtmlTemplate("/template.html", variables);
+        final MimeMessage message = initializeMessageBuilder()
+            .subject(subject)
+            .html(html)
+            .build();
+
+        final CompletableFuture<MimeMessage> future = mailService.sendMessage(message);
+        future.get();
+
+        if (local) {
+            greenMail.waitForIncomingEmail(1);
+            final MimeMessage[] messages = greenMail.getReceivedMessages();
+            final MimeMessage received = messages[0];
+            final MimeMessageParser parser = new MimeMessageParser(message).parse();
+
+            assertThat(received.getMessageID()).endsWith("@localhost>");
+            assertThat(received.getSubject()).isEqualTo(subject);
+            assertThat(received.getFrom()[0]).isEqualTo(from);
+            assertThat(received.getRecipients(Message.RecipientType.TO)[0]).isEqualTo(to);
+            assertThat(received.getReplyTo()[0]).isEqualTo(replyTo);
+
+            assertThat(parser.getHtmlContent()).isEqualTo(html);
+
+            assertThat(parser.getAttachmentList()).isEmpty();
+            assertThat(parser.getContentIds()).isEmpty();
+        }
+    }
+
+    @Test
+    public void sendHtmlAndAttachment() throws Exception {
+        final Map<String, Object> variables = Collections.singletonMap("date", new Date());
+        final String subject = "Sling Commons Mail: HTML and Attachment [æåëęïįœøüū] \uD83D\uDCE7";
+        final String html = renderHtmlTemplate("/template.html", variables);
+        final byte[] support = getResourceAsByteArray("/SupportApache-small.png");
+        final MimeMessage message = initializeMessageBuilder()
+            .subject(subject)
+            .html(html)
+            .attachment(support, "image/png", "SupportApache-small.png")
+            .build();
+
+        final CompletableFuture<MimeMessage> future = mailService.sendMessage(message);
+        future.get();
+
+        if (local) {
+            greenMail.waitForIncomingEmail(1);
+            final MimeMessage[] messages = greenMail.getReceivedMessages();
+            final MimeMessage received = messages[0];
+            final MimeMessageParser parser = new MimeMessageParser(message).parse();
+
+            assertThat(received.getMessageID()).endsWith("@localhost>");
+            assertThat(received.getSubject()).isEqualTo(subject);
+            assertThat(received.getFrom()[0]).isEqualTo(from);
+            assertThat(received.getRecipients(Message.RecipientType.TO)[0]).isEqualTo(to);
+            assertThat(received.getReplyTo()[0]).isEqualTo(replyTo);
+
+            assertThat(parser.getHtmlContent()).isEqualTo(html);
+
+            assertThat(parser.getAttachmentList().get(0).getName()).isEqualTo("SupportApache-small.png");
+            assertThat(parser.getContentIds()).isEmpty();
+        }
+    }
+
+    @Test
+    public void sendHtmlWithInlineImageAndAttachment() throws Exception {
+        final Map<String, Object> variables = Collections.singletonMap("date", new Date());
+        final String subject = "Sling Commons Mail: HTML with Inline Images and Attachment [æåëęïįœøüū] \uD83D\uDCE7";
+        final String html = renderHtmlTemplate("/template-inlines.html", variables);
+        final byte[] sling = getResourceAsByteArray("/sling.png");
+        final byte[] support = getResourceAsByteArray("/SupportApache-small.png");
+        final MimeMessage message = initializeMessageBuilder()
+            .subject(subject)
+            .html(html)
+            .attachment(support, "image/png", "SupportApache-small.png")
+            .inline(sling, "image/png", "sling")
+            .build();
+
+        final CompletableFuture<MimeMessage> future = mailService.sendMessage(message);
+        future.get();
+
+        if (local) {
+            greenMail.waitForIncomingEmail(1);
+            final MimeMessage[] messages = greenMail.getReceivedMessages();
+            final MimeMessage received = messages[0];
+            final MimeMessageParser parser = new MimeMessageParser(message).parse();
+
+            assertThat(received.getMessageID()).endsWith("@localhost>");
+            assertThat(received.getSubject()).isEqualTo(subject);
+            assertThat(received.getFrom()[0]).isEqualTo(from);
+            assertThat(received.getRecipients(Message.RecipientType.TO)[0]).isEqualTo(to);
+            assertThat(received.getReplyTo()[0]).isEqualTo(replyTo);
+
+            assertThat(parser.getHtmlContent()).isEqualTo(html);
+
+            assertThat(parser.getContentIds()).contains("sling");
+        }
+    }
+
+    @Test
+    public void sendHtmlWithInlineImageAndTextAndAttachment() throws Exception {
+        final Map<String, Object> variables = Collections.singletonMap("date", new Date());
+        final String subject = "Sling Commons Mail: HTML with Inline Images and Text and Attachment [æåëęïįœøüū] \uD83D\uDCE7";
+        final String text = renderTextTemplate("/template.txt", variables);
+        final String html = renderHtmlTemplate("/template-inlines.html", variables);
+        final byte[] sling = getResourceAsByteArray("/sling.png");
+        final byte[] support = getResourceAsByteArray("/SupportApache-small.png");
+        final MimeMessage message = initializeMessageBuilder()
+            .subject(subject)
+            .text(text)
+            .html(html)
+            .attachment(support, "image/png", "SupportApache-small.png")
+            .inline(sling, "image/png", "sling")
+            .build();
+
+        final CompletableFuture<MimeMessage> future = mailService.sendMessage(message);
+        future.get();
+
+        if (local) {
+            greenMail.waitForIncomingEmail(1);
+            final MimeMessage[] messages = greenMail.getReceivedMessages();
+            final MimeMessage received = messages[0];
+            final MimeMessageParser parser = new MimeMessageParser(message).parse();
+
+            assertThat(received.getMessageID()).endsWith("@localhost>");
+            assertThat(received.getSubject()).isEqualTo(subject);
+            assertThat(received.getFrom()[0]).isEqualTo(from);
+            assertThat(received.getRecipients(Message.RecipientType.TO)[0]).isEqualTo(to);
+            assertThat(received.getReplyTo()[0]).isEqualTo(replyTo);
+
+            assertThat(parser.getPlainContent()).isEqualTo(text);
+            assertThat(parser.getHtmlContent()).isEqualTo(html);
+
+            final List<DataSource> sources = parser.getAttachmentList();
+            for (DataSource source : sources) {
+                logger.info("data source: {}, {}", source.getName(), source.getContentType());
+            }
+
+            assertThat(parser.getContentIds()).contains("sling");
+        }
+    }
+
+}
diff --git a/src/test/resources/SupportApache-small.png b/src/test/resources/SupportApache-small.png
new file mode 100644
index 0000000..4a23e05
Binary files /dev/null and b/src/test/resources/SupportApache-small.png differ
diff --git a/src/test/resources/password b/src/test/resources/password
new file mode 100644
index 0000000..ad66ce8
--- /dev/null
+++ b/src/test/resources/password
@@ -0,0 +1 @@
++AQ?aDes!'DBMkrCi:FE6q\sOn=Pbmn=PK8n=PK?
\ No newline at end of file
diff --git a/src/test/resources/sling.png b/src/test/resources/sling.png
new file mode 100644
index 0000000..69163d9
Binary files /dev/null and b/src/test/resources/sling.png differ
diff --git a/src/test/resources/template-inlines.html b/src/test/resources/template-inlines.html
new file mode 100644
index 0000000..33d4e0c
--- /dev/null
+++ b/src/test/resources/template-inlines.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<!--
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied.  See the License for the
+    specific language governing permissions and limitations
+    under the License.
+-->
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+  <title>Thymeleaf HTML Mail Template with Inline Image</title>
+</head>
+<body>
+<table>
+  <tr>
+    <td>This message was sent with Apache Sling Commons Messaging Mail. 📧</td>
+  </tr>
+  <tr>
+    <td><img src="cid:sling" width="124" height="63" alt="Apache Sling"></td>
+  </tr>
+  <tr>
+    <td><span data-th-text="${date}">date</span></td>
+  </tr>
+</table>
+</body>
+</html>
diff --git a/src/test/resources/template.html b/src/test/resources/template.html
new file mode 100644
index 0000000..e32bff0
--- /dev/null
+++ b/src/test/resources/template.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<!--
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied.  See the License for the
+    specific language governing permissions and limitations
+    under the License.
+-->
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+  <title>Thymeleaf HTML Mail Template</title>
+</head>
+<body>
+<table>
+  <tr>
+    <td>This message was sent with Apache Sling Commons Messaging Mail. 📧</td>
+  </tr>
+  <tr>
+    <td><span data-th-text="${date}">date</span></td>
+  </tr>
+</table>
+</body>
+</html>
diff --git a/src/test/resources/template.txt b/src/test/resources/template.txt
new file mode 100644
index 0000000..a66ee49
--- /dev/null
+++ b/src/test/resources/template.txt
@@ -0,0 +1,3 @@
+This message was sent with Apache Sling Commons Messaging Mail. 📧
+
+[[${date}]]