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

rcordier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git


The following commit(s) were added to refs/heads/master by this push:
     new 2b0c1bdee1 JAMES-3823 SMTP Require TLS Option (#2460)
2b0c1bdee1 is described below

commit 2b0c1bdee14ba2cc04f34ea5adcb51eca4b05e89
Author: Maksim <85022218+maxxx...@users.noreply.github.com>
AuthorDate: Mon Jan 13 12:30:48 2025 +0300

    JAMES-3823 SMTP Require TLS Option (#2460)
---
 .../architecture/implemented-standards.adoc        |   1 +
 .../servers/partials/configure/smtp-hooks.adoc     |  22 ++
 .../mailets/configuration/SmtpConfiguration.java   |  32 ++-
 .../src/main/resources/smtpserver.xml              |   2 +-
 .../configuration/SmtpConfigurationTest.java       |  18 ++
 .../mailets/remote/delivery/DeliveryRunnable.java  |   2 -
 .../remote/delivery/MailDelivrerToHost.java        | 159 ++++++-----
 .../remote/delivery/RemoteDeliveryTest.java        | 210 +++++++-------
 .../remote-delivery-integration-testing/pom.xml    |   7 +
 .../james/smtp/tls/SmtpRequireTlsRelayTest.java    | 303 +++++++++++++++++++++
 .../smtpserver/tls/SmtpRequireTlsEhloHook.java}    |  30 +-
 .../smtpserver/tls/SmtpRequireTlsMessageHook.java  |  77 ++++++
 .../tls/SmtpRequireTlsParameterHook.java           |  64 +++++
 .../smtpserver/SmtpRequireTlsMessageHookTest.java  | 179 ++++++++++++
 .../src/test/resources/smtpserver-requireTls.xml   |  22 ++
 .../james/queue/memory/MemoryMailQueueFactory.java |  16 +-
 .../java/org/apache/james/util/docker/Images.java  |   2 +-
 src/site/xdoc/server/rfcs.xml                      |   1 +
 18 files changed, 956 insertions(+), 191 deletions(-)

diff --git 
a/docs/modules/servers/partials/architecture/implemented-standards.adoc 
b/docs/modules/servers/partials/architecture/implemented-standards.adoc
index 5a338bc06e..e394cc0112 100644
--- a/docs/modules/servers/partials/architecture/implemented-standards.adoc
+++ b/docs/modules/servers/partials/architecture/implemented-standards.adoc
@@ -35,6 +35,7 @@ This page details standards implemented by the {server-name}.
 - link:https://datatracker.ietf.org/doc/html/rfc2197[RFC-2197] SMTP Service 
Extension for Command Pipelining
 - link:https://datatracker.ietf.org/doc/html/rfc2554[RFC-2554] ESMTP Service 
Extension for Authentication
 - link:https://datatracker.ietf.org/doc/rfc6710/[RFC-6710] SMTP Extension for 
Message Transfer Priorities
+- link:https://datatracker.ietf.org/doc/rfc8689/[RFC-8689] SMTP Require TLS 
Option
 
 == LMTP
 
diff --git a/docs/modules/servers/partials/configure/smtp-hooks.adoc 
b/docs/modules/servers/partials/configure/smtp-hooks.adoc
index afb1ce3e53..e99848a089 100644
--- a/docs/modules/servers/partials/configure/smtp-hooks.adoc
+++ b/docs/modules/servers/partials/configure/smtp-hooks.adoc
@@ -335,6 +335,7 @@ The Distributed server has optional support for SMTP 
Extension for Message Trans
 
 The SMTP server does not allow positive priorities from unauthorized sources 
and sets the priority to the default value (0).
 
+[source,xml]
 ....
 <smtpserver enabled="true">
     <...> <!-- The rest of your SMTP configuration, unchanged -->
@@ -347,6 +348,27 @@ The SMTP server does not allow positive priorities from 
unauthorized sources and
 </smtpserver>
 ....
 
+== SMTP Require TLS Option hooks
+
+These hooks are designed to support the SMTP service extension, REQUIRETLS, 
and a TLS-Required message header field.
+(link:https://www.rfc-editor.org/rfc/rfc8689.html[RFC-8689])
+
+[source,xml]
+....
+<smtpserver enabled="true">
+    <...> <!-- The rest of your SMTP configuration, unchanged -->
+    <tls socketTLS="false" startTLS="true">``
+        <!-- ... -->
+    </tls>
+ <handlerchain>
+        <handler 
class="org.apache.james.smtpserver.tls.SmtpRequireTlsEhloHook"/>
+        <handler 
class="org.apache.james.smtpserver.tls.SmtpRequireTlsParameterHook"/>
+        <handler 
class="org.apache.james.smtpserver.tls.SmtpRequireTlsMessageHook"/>
+        <handler class="org.apache.james.smtpserver.CoreCmdHandlerLoader"/>
+    </handlerchain>
+</smtpserver>
+....
+
 == DKIM checks hooks
 
 Hook for verifying DKIM signatures of incoming mails.
diff --git 
a/server/mailet/integration-testing/src/main/java/org/apache/james/mailets/configuration/SmtpConfiguration.java
 
b/server/mailet/integration-testing/src/main/java/org/apache/james/mailets/configuration/SmtpConfiguration.java
index 39088fa329..3bb0556257 100644
--- 
a/server/mailet/integration-testing/src/main/java/org/apache/james/mailets/configuration/SmtpConfiguration.java
+++ 
b/server/mailet/integration-testing/src/main/java/org/apache/james/mailets/configuration/SmtpConfiguration.java
@@ -70,15 +70,17 @@ public class SmtpConfiguration implements SerializableAsXml 
{
         private static final String DEFAULT_DISABLED = "0";
 
         private Optional<Boolean> authRequired;
+        private Optional<Boolean> startTls;
         private Optional<String> maxMessageSize;
         private Optional<SMTPConfiguration.SenderVerificationMode> 
verifyIndentity;
         private Optional<Boolean> bracketEnforcement;
         private Optional<String> authorizedAddresses;
-        private ImmutableList.Builder<HookConfigurationEntry> additionalHooks;
+        private final ImmutableList.Builder<HookConfigurationEntry> 
additionalHooks;
 
         public Builder() {
             authorizedAddresses = Optional.empty();
             authRequired = Optional.empty();
+            startTls = Optional.empty();
             verifyIndentity = Optional.empty();
             maxMessageSize = Optional.empty();
             bracketEnforcement = Optional.empty();
@@ -101,6 +103,11 @@ public class SmtpConfiguration implements 
SerializableAsXml {
             return this;
         }
 
+        public Builder requireStartTls() {
+            this.startTls = Optional.of(true);
+            return this;
+        }
+
         public Builder requireBracketEnforcement() {
             this.bracketEnforcement = Optional.of(true);
             return this;
@@ -138,11 +145,12 @@ public class SmtpConfiguration implements 
SerializableAsXml {
 
         public SmtpConfiguration build() {
             return new SmtpConfiguration(authorizedAddresses,
-                authRequired.orElse(!AUTH_REQUIRED),
-                bracketEnforcement.orElse(true),
-                
verifyIndentity.orElse(SMTPConfiguration.SenderVerificationMode.DISABLED),
-                maxMessageSize.orElse(DEFAULT_DISABLED),
-                additionalHooks.build());
+                    authRequired.orElse(!AUTH_REQUIRED),
+                    startTls.orElse(false),
+                    bracketEnforcement.orElse(true),
+                    
verifyIndentity.orElse(SMTPConfiguration.SenderVerificationMode.DISABLED),
+                    maxMessageSize.orElse(DEFAULT_DISABLED),
+                    additionalHooks.build());
         }
     }
 
@@ -152,6 +160,7 @@ public class SmtpConfiguration implements SerializableAsXml 
{
 
     private final Optional<String> authorizedAddresses;
     private final boolean authRequired;
+    private final boolean startTls;
     private final boolean bracketEnforcement;
     private final SMTPConfiguration.SenderVerificationMode verifyIndentity;
     private final String maxMessageSize;
@@ -159,6 +168,7 @@ public class SmtpConfiguration implements SerializableAsXml 
{
 
     private SmtpConfiguration(Optional<String> authorizedAddresses,
                               boolean authRequired,
+                              boolean startTls,
                               boolean bracketEnforcement,
                               SMTPConfiguration.SenderVerificationMode 
verifyIndentity,
                               String maxMessageSize,
@@ -168,6 +178,7 @@ public class SmtpConfiguration implements SerializableAsXml 
{
         this.bracketEnforcement = bracketEnforcement;
         this.verifyIndentity = verifyIndentity;
         this.maxMessageSize = maxMessageSize;
+        this.startTls = startTls;
         this.additionalHooks = additionalHooks;
     }
 
@@ -180,10 +191,11 @@ public class SmtpConfiguration implements 
SerializableAsXml {
         scopes.put("verifyIdentity", verifyIndentity.toString());
         scopes.put("maxmessagesize", maxMessageSize);
         scopes.put("bracketEnforcement", bracketEnforcement);
+        scopes.put("startTls", startTls);
 
         List<Map<String, Object>> additionalHooksWithConfig = 
additionalHooks.stream()
-            .map(HookConfigurationEntry::asMustacheScopes)
-            .collect(ImmutableList.toImmutableList());
+                .map(HookConfigurationEntry::asMustacheScopes)
+                .collect(ImmutableList.toImmutableList());
 
         scopes.put("hooks", additionalHooksWithConfig);
 
@@ -193,14 +205,14 @@ public class SmtpConfiguration implements 
SerializableAsXml {
         Mustache mustache = mf.compile(getPatternReader(), "example");
         mustache.execute(writer, scopes);
         writer.flush();
-        return new String(byteArrayOutputStream.toByteArray(), 
StandardCharsets.UTF_8);
+        return byteArrayOutputStream.toString(StandardCharsets.UTF_8);
     }
 
     private StringReader getPatternReader() throws IOException {
         InputStream patternStream = 
ClassLoader.getSystemResourceAsStream("smtpserver.xml");
         ByteArrayOutputStream byteArrayOutputStream = new 
ByteArrayOutputStream();
         IOUtils.copy(patternStream, byteArrayOutputStream);
-        String pattern = new String(byteArrayOutputStream.toByteArray(), 
StandardCharsets.UTF_8);
+        String pattern = 
byteArrayOutputStream.toString(StandardCharsets.UTF_8);
         return new StringReader(pattern);
     }
 }
diff --git 
a/server/mailet/integration-testing/src/main/resources/smtpserver.xml 
b/server/mailet/integration-testing/src/main/resources/smtpserver.xml
index 2d314ac085..0f94a58a45 100644
--- a/server/mailet/integration-testing/src/main/resources/smtpserver.xml
+++ b/server/mailet/integration-testing/src/main/resources/smtpserver.xml
@@ -24,7 +24,7 @@
         <jmxName>smtpserver-global</jmxName>
         <bind>0.0.0.0:0</bind>
         <connectionBacklog>200</connectionBacklog>
-        <tls socketTLS="false" startTLS="false">
+        <tls socketTLS="false" startTLS="{{startTls}}">
             <keystore>file://conf/keystore</keystore>
             <secret>james72laBalle</secret>
             
<provider>org.bouncycastle.jce.provider.BouncyCastleProvider</provider>
diff --git 
a/server/mailet/integration-testing/src/test/java/org/apache/james/mailets/configuration/SmtpConfigurationTest.java
 
b/server/mailet/integration-testing/src/test/java/org/apache/james/mailets/configuration/SmtpConfigurationTest.java
index 58724e8bf2..2648358ad2 100644
--- 
a/server/mailet/integration-testing/src/test/java/org/apache/james/mailets/configuration/SmtpConfigurationTest.java
+++ 
b/server/mailet/integration-testing/src/test/java/org/apache/james/mailets/configuration/SmtpConfigurationTest.java
@@ -74,6 +74,24 @@ public class SmtpConfigurationTest {
                 is("true")));
     }
 
+    @Test
+    public void startTlsDisabledByDefault() throws IOException {
+
+        assertThat(SmtpConfiguration.builder()
+                        .build()
+                        .serializeAsXml(),
+                hasXPath("//tls/@startTLS", is("false")));
+    }
+
+    @Test
+    public void startTlsCanBeCustomized() throws IOException {
+
+        assertThat(SmtpConfiguration.builder()
+                        .requireStartTls()
+                        .build().serializeAsXml(),
+                hasXPath("//tls/@startTLS", is("true")));
+    }
+
     @Test
     public void maxMessageSizeCanBeCustomized() throws IOException {
         assertThat(SmtpConfiguration.builder()
diff --git 
a/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remote/delivery/DeliveryRunnable.java
 
b/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remote/delivery/DeliveryRunnable.java
index 77e70bcf68..ca170e56c9 100644
--- 
a/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remote/delivery/DeliveryRunnable.java
+++ 
b/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remote/delivery/DeliveryRunnable.java
@@ -41,7 +41,6 @@ import org.apache.james.queue.api.MailQueue;
 import org.apache.james.util.AuditTrail;
 import org.apache.james.util.MDCBuilder;
 import org.apache.mailet.Attribute;
-import org.apache.mailet.AttributeName;
 import org.apache.mailet.AttributeValue;
 import org.apache.mailet.Mail;
 import org.apache.mailet.MailetContext;
@@ -66,7 +65,6 @@ public class DeliveryRunnable implements Disposable {
     public static final Supplier<Date> CURRENT_DATE_SUPPLIER = Date::new;
     public static final String OUTGOING_MAILS = "outgoingMails";
     public static final String REMOTE_DELIVERY_TRIAL = "RemoteDeliveryTrial";
-    private static final AttributeName MAIL_PRIORITY_ATTRIBUTE_NAME = 
AttributeName.of("MAIL_PRIORITY");
 
     private final MailQueue queue;
     private final RemoteDeliveryConfiguration configuration;
diff --git 
a/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remote/delivery/MailDelivrerToHost.java
 
b/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remote/delivery/MailDelivrerToHost.java
index 44cc1c7aa7..75efb77f75 100644
--- 
a/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remote/delivery/MailDelivrerToHost.java
+++ 
b/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remote/delivery/MailDelivrerToHost.java
@@ -29,10 +29,12 @@ import static 
org.eclipse.angus.mail.smtp.SMTPMessage.RETURN_HDRS;
 import java.io.IOException;
 import java.util.Collection;
 import java.util.EnumSet;
+import java.util.List;
 import java.util.Optional;
 import java.util.Properties;
 
 import jakarta.mail.MessagingException;
+import jakarta.mail.SendFailedException;
 import jakarta.mail.Session;
 import jakarta.mail.internet.InternetAddress;
 import jakarta.mail.internet.MimeMessage;
@@ -46,7 +48,6 @@ import org.apache.commons.pool2.impl.DefaultPooledObject;
 import org.apache.commons.pool2.impl.GenericObjectPool;
 import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
 import org.apache.james.core.MailAddress;
-import org.apache.mailet.Attribute;
 import org.apache.mailet.AttributeName;
 import org.apache.mailet.DsnParameters;
 import org.apache.mailet.HostAddress;
@@ -65,6 +66,12 @@ import com.google.common.collect.ImmutableListMultimap;
 public class MailDelivrerToHost {
     private static final Logger LOGGER = 
LoggerFactory.getLogger(MailDelivrerToHost.class);
     public static final String BIT_MIME_8 = "8BITMIME";
+    public static final String REQUIRE_TLS = "REQUIRETLS";
+    public static final String STARTTLS = "STARTTLS";
+    public static final String MT_PRIORITY = "MT-PRIORITY";
+    public static final String MAIL_PRIORITY_ATTRIBUTE_NAME = "MAIL_PRIORITY";
+    private static final List<String> supportedSmtpExtensionsList = 
List.of(MT_PRIORITY, STARTTLS);
+    private static final List<String> supportedMailAttributesList = 
List.of(MAIL_PRIORITY_ATTRIBUTE_NAME, REQUIRE_TLS);
 
     private final RemoteDeliveryConfiguration configuration;
     private final Converter7Bit converter7Bit;
@@ -110,8 +117,8 @@ public class MailDelivrerToHost {
         Session session = selectSession(outgoingMailServer);
         Properties props = getPropertiesForMail(mail, session);
         LOGGER.debug("Attempting delivery of {} with messageId {} to host {} 
at {} from {}",
-            mail.getName(), getMessageId(mail), 
outgoingMailServer.getHostName(),
-            outgoingMailServer.getHost(), props.get(inContext(session, 
"mail.smtp.from")));
+                mail.getName(), getMessageId(mail), 
outgoingMailServer.getHostName(),
+                outgoingMailServer.getHost(), props.get(inContext(session, 
"mail.smtp.from")));
 
         // Many of these properties are only in later JavaMail versions
         // "mail.smtp.ehlo"           //default true
@@ -124,16 +131,20 @@ public class MailDelivrerToHost {
             transport = (SMTPTransport) 
session.getTransport(outgoingMailServer);
             transport.setLocalHost(props.getProperty(inContext(session, 
"mail.smtp.localhost"), configuration.getHeloNameProvider().getHeloName()));
             connect(outgoingMailServer, transport);
+            if (receiverDoesNotProvideNecessaryStartTls(mail, transport)) {
+                return ExecutionResult.permanentFailure(new 
SendFailedException("Mail delivery failed; the receiving server does not 
support STARTTLS"));
+            }
             if (mail.dsnParameters().isPresent()) {
                 sendDSNAwareEmail(mail, transport, addr);
-            } else if (transport.supportsExtension("MT-PRIORITY")) {
-                sendEmailWithPriority(mail, transport, addr);
+            } else if (extensionsSupported(transport)) {
+                SMTPMessage smtpMessage = new 
SMTPMessage(adaptToTransport(mail.getMessage(), transport));
+                transport.sendMessage(toSmtpMessageWithExtensions(mail, 
smtpMessage), addr.toArray(InternetAddress[]::new));
             } else {
                 transport.sendMessage(adaptToTransport(mail.getMessage(), 
transport), addr.toArray(InternetAddress[]::new));
             }
             LOGGER.info("Mail ({}) with messageId {} sent successfully to {} 
at {} from {} for {}",
-                mail.getName(), getMessageId(mail), 
outgoingMailServer.getHostName(),
-                outgoingMailServer.getHost(), props.get(inContext(session, 
"mail.smtp.from")), mail.getRecipients());
+                    mail.getName(), getMessageId(mail), 
outgoingMailServer.getHostName(),
+                    outgoingMailServer.getHost(), props.get(inContext(session, 
"mail.smtp.from")), mail.getRecipients());
         } finally {
             closeTransport(mail, outgoingMailServer, transport);
             releaseSession(outgoingMailServer, session);
@@ -184,83 +195,101 @@ public class MailDelivrerToHost {
 
     private void sendDSNAwareEmail(Mail mail, SMTPTransport transport, 
Collection<InternetAddress> addresses) {
         addresses.stream()
-            .map(address -> Pair.of(
-                mail.dsnParameters()
-                    .flatMap(Throwing.<DsnParameters, 
Optional<DsnParameters.RecipientDsnParameters>>function(
-                            dsn -> 
Optional.ofNullable(dsn.getRcptParameters().get(new 
MailAddress(address.toString()))))
-                        .orReturn(Optional.empty()))
-                    
.flatMap(DsnParameters.RecipientDsnParameters::getNotifyParameter)
-                    .map(this::toJavaxNotify),
-                address))
-            .collect(ImmutableListMultimap.toImmutableListMultimap(
-                Pair::getKey,
-                Pair::getValue))
-            .asMap()
-            .forEach(Throwing.<Optional<Integer>, 
Collection<InternetAddress>>biConsumer((maybeNotify, recipients) -> {
-                SMTPMessage smtpMessage = asSmtpMessage(mail, transport);
-                maybeNotify.ifPresent(smtpMessage::setNotifyOptions);
-                transport.sendMessage(smtpMessage, 
recipients.toArray(InternetAddress[]::new));
-            }).sneakyThrow());
+                .map(address -> Pair.of(
+                        mail.dsnParameters()
+                                .flatMap(Throwing.<DsnParameters, 
Optional<DsnParameters.RecipientDsnParameters>>function(
+                                                dsn -> 
Optional.ofNullable(dsn.getRcptParameters().get(new 
MailAddress(address.toString()))))
+                                        .orReturn(Optional.empty()))
+                                
.flatMap(DsnParameters.RecipientDsnParameters::getNotifyParameter)
+                                .map(this::toJavaxNotify),
+                        address))
+                .collect(ImmutableListMultimap.toImmutableListMultimap(
+                        Pair::getKey,
+                        Pair::getValue))
+                .asMap()
+                .forEach(Throwing.<Optional<Integer>, 
Collection<InternetAddress>>biConsumer((maybeNotify, recipients) -> {
+                    SMTPMessage smtpMessage = asSmtpMessage(mail, transport);
+                    maybeNotify.ifPresent(smtpMessage::setNotifyOptions);
+                    transport.sendMessage(smtpMessage, 
recipients.toArray(InternetAddress[]::new));
+                }).sneakyThrow());
     }
 
     private SMTPMessage asSmtpMessage(Mail mail, SMTPTransport transport) 
throws MessagingException {
         SMTPMessage smtpMessage = new 
SMTPMessage(adaptToTransport(mail.getMessage(), transport));
         mail.dsnParameters().flatMap(DsnParameters::getRetParameter)
-            .map(this::toJavaxRet)
-            .ifPresent(smtpMessage::setReturnOption);
+                .map(this::toJavaxRet)
+                .ifPresent(smtpMessage::setReturnOption);
         mail.dsnParameters().flatMap(DsnParameters::getEnvIdParameter)
-            .ifPresent(envId -> {
-                if (transport.supportsExtension("DSN")) {
-                    smtpMessage.setMailExtension("ENVID=" + envId.asString());
-                }
-            });
-        if (transport.supportsExtension("MT-PRIORITY")) {
-            return toSmtpMessageWithPriorityExtension(mail, smtpMessage);
+                .ifPresent(envId -> {
+                    if (transport.supportsExtension("DSN")) {
+                        smtpMessage.setMailExtension("ENVID=" + 
envId.asString());
+                    }
+                });
+
+        if (extensionsSupported(transport)) {
+            return toSmtpMessageWithExtensions(mail, smtpMessage);
         }
         return smtpMessage;
     }
 
-    private void sendEmailWithPriority(Mail mail, SMTPTransport transport, 
Collection<InternetAddress> addresses) throws MessagingException {
-        SMTPMessage smtpMessage = new 
SMTPMessage(adaptToTransport(mail.getMessage(), transport));
-        transport.sendMessage(toSmtpMessageWithPriorityExtension(mail, 
smtpMessage), addresses.toArray(InternetAddress[]::new));
+    private static boolean extensionsSupported(SMTPTransport transport) {
+        return 
supportedSmtpExtensionsList.stream().anyMatch(transport::supportsExtension);
+    }
+
+    private static boolean receiverDoesNotProvideNecessaryStartTls(Mail mail, 
SMTPTransport transport) {
+        return !transport.getLastServerResponse().contains(STARTTLS) &&
+                
mail.attributesMap().containsKey(AttributeName.of(REQUIRE_TLS)) &&
+                isRequireTlsAttribute(mail);
     }
 
-    private SMTPMessage toSmtpMessageWithPriorityExtension(Mail mail, 
SMTPMessage smtpMessage) {
-        Optional<Attribute> priorityAttribute = 
Optional.ofNullable(mail.attributesMap().get(AttributeName.of("MAIL_PRIORITY")));
-        priorityAttribute.ifPresent(attribute -> 
smtpMessage.setMailExtension(smtpMessage.getMailExtension() + " MT-PRIORITY=" + 
attribute.getValue().value()));
+    private static boolean isRequireTlsAttribute(Mail mail) {
+        return 
mail.attributesMap().get(AttributeName.of(REQUIRE_TLS)).getValue().value().equals(Boolean.TRUE);
+    }
+
+    private SMTPMessage toSmtpMessageWithExtensions(Mail mail, SMTPMessage 
smtpMessage) {
+        mail.attributesMap().forEach((attributeName, attribute) -> {
+            if 
(supportedMailAttributesList.contains(attributeName.asString())) {
+                String existingMessageExtensions = 
Optional.ofNullable(smtpMessage.getMailExtension())
+                    .map(extension -> " " + extension)
+                    .orElse("");
+                switch (attributeName.asString()) {
+                    case MAIL_PRIORITY_ATTRIBUTE_NAME ->
+                        smtpMessage.setMailExtension(MT_PRIORITY + "=" + 
attribute.getValue().value() +
+                            existingMessageExtensions);
+                    case REQUIRE_TLS -> {
+                        if (isRequireTlsAttribute(mail)) {
+                            smtpMessage.setMailExtension(REQUIRE_TLS + 
existingMessageExtensions);
+                        }
+                    }
+                    default -> throw new NotImplementedException("Unknown mail 
attribute cannot be handled: {}", attributeName.asString());
+                }
+            }
+        });
         return smtpMessage;
     }
 
     private int toJavaxRet(DsnParameters.Ret ret) {
-        switch (ret) {
-            case FULL:
-                return RETURN_FULL;
-            case HDRS:
-                return RETURN_HDRS;
-            default:
-                throw new NotImplementedException(ret + " cannot be converted 
to jakarta.mail parameters");
-        }
+        return switch (ret) {
+            case FULL -> RETURN_FULL;
+            case HDRS -> RETURN_HDRS;
+            default -> throw new NotImplementedException(ret + " cannot be 
converted to jakarta.mail parameters");
+        };
     }
 
     private int toJavaxNotify(EnumSet<DsnParameters.Notify> notifies) {
         return notifies.stream()
-            .mapToInt(this::toJavaxNotify)
-            .sum();
+                .mapToInt(this::toJavaxNotify)
+                .sum();
     }
 
     private int toJavaxNotify(DsnParameters.Notify notify) {
-        switch (notify) {
-            case NEVER:
-                return NOTIFY_NEVER;
-            case SUCCESS:
-                return NOTIFY_SUCCESS;
-            case FAILURE:
-                return NOTIFY_FAILURE;
-            case DELAY:
-                return NOTIFY_DELAY;
-            default:
-                throw new NotImplementedException(notify + " cannot be 
converted to jakarta.mail parameters");
-        }
+        return switch (notify) {
+            case NEVER -> NOTIFY_NEVER;
+            case SUCCESS -> NOTIFY_SUCCESS;
+            case FAILURE -> NOTIFY_FAILURE;
+            case DELAY -> NOTIFY_DELAY;
+            default -> throw new NotImplementedException(notify + " cannot be 
converted to jakarta.mail parameters");
+        };
     }
 
     private Properties getPropertiesForMail(Mail mail, Session session) {
@@ -281,7 +310,7 @@ public class MailDelivrerToHost {
 
     private String getHostName(HostAddress outgoingMailServer) {
         String host = outgoingMailServer.getHostName();
-        if (host.length() > 0 && host.charAt(host.length() - 1) == '.') {
+        if (!host.isEmpty() && host.charAt(host.length() - 1) == '.') {
             return host.substring(0, host.length() - 1);
         } else {
             return host;
@@ -304,7 +333,7 @@ public class MailDelivrerToHost {
         // If the transport is not the one developed by Sun we are not sure of 
how it handles the 8 bit mime stuff, so I
         // convert the message to 7bit.
         return !transport.getClass().getName().endsWith(".SMTPTransport")
-            || !transport.supportsExtension(BIT_MIME_8);
+                || !transport.supportsExtension(BIT_MIME_8);
         // if the message is already 8bit or binary and the server doesn't 
support the 8bit extension it has to be converted
         // to 7bit. Javamail api doesn't perform that conversion, but it is 
required to be a rfc-compliant smtp server.
     }
@@ -319,8 +348,8 @@ public class MailDelivrerToHost {
                 transport.close();
             } catch (MessagingException e) {
                 LOGGER.error("Warning: could not close the SMTP transport 
after sending mail ({}) to {} at {} for {}; " +
-                        "probably the server has already closed the 
connection. Message is considered to be delivered. Exception: {}",
-                    mail.getName(), outgoingMailServer.getHostName(), 
outgoingMailServer.getHost(), mail.getRecipients(), e.getMessage());
+                                "probably the server has already closed the 
connection. Message is considered to be delivered. Exception: {}",
+                        mail.getName(), outgoingMailServer.getHostName(), 
outgoingMailServer.getHost(), mail.getRecipients(), e.getMessage());
             }
             transport = null;
         }
diff --git 
a/server/mailet/mailets/src/test/java/org/apache/james/transport/mailets/remote/delivery/RemoteDeliveryTest.java
 
b/server/mailet/mailets/src/test/java/org/apache/james/transport/mailets/remote/delivery/RemoteDeliveryTest.java
index 19c6f50096..45723fc6e0 100644
--- 
a/server/mailet/mailets/src/test/java/org/apache/james/transport/mailets/remote/delivery/RemoteDeliveryTest.java
+++ 
b/server/mailet/mailets/src/test/java/org/apache/james/transport/mailets/remote/delivery/RemoteDeliveryTest.java
@@ -50,6 +50,7 @@ import org.apache.james.transport.mailets.RemoteDelivery;
 import org.apache.james.util.MimeMessageUtil;
 import org.apache.mailet.Attribute;
 import org.apache.mailet.AttributeName;
+import org.apache.mailet.AttributeValue;
 import org.apache.mailet.Mail;
 import org.apache.mailet.base.MailAddressFixture;
 import org.apache.mailet.base.test.FakeMail;
@@ -83,12 +84,11 @@ class RemoteDeliveryTest {
 
         @Override
         public final boolean equals(Object o) {
-            if (o instanceof MailProjection) {
-                MailProjection mailProjection = (MailProjection) o;
+            if (o instanceof MailProjection mailProjection) {
 
                 return Objects.equals(this.name, mailProjection.name)
-                    && Objects.equals(this.attributes, 
mailProjection.attributes)
-                    && Objects.equals(this.recipients, 
mailProjection.recipients);
+                        && Objects.equals(this.attributes, 
mailProjection.attributes)
+                        && Objects.equals(this.recipients, 
mailProjection.recipients);
             }
             return false;
         }
@@ -100,6 +100,7 @@ class RemoteDeliveryTest {
     }
 
     public static final String MAIL_NAME = "mail_name";
+    public static final String REQUIRETLS = "REQUIRETLS";
 
     private RemoteDelivery remoteDelivery;
     private MemoryMailQueueFactory.MemoryCacheableMailQueue mailQueue;
@@ -109,117 +110,117 @@ class RemoteDeliveryTest {
         MemoryMailQueueFactory queueFactory = spy(new 
MemoryMailQueueFactory(new RawMailQueueItemDecoratorFactory()));
         mailQueue = 
spy(queueFactory.createQueue(RemoteDeliveryConfiguration.DEFAULT_OUTGOING_QUEUE_NAME));
         
when(queueFactory.createQueue(RemoteDeliveryConfiguration.DEFAULT_OUTGOING_QUEUE_NAME))
-            .thenReturn(mailQueue);
+                .thenReturn(mailQueue);
         DNSService dnsService = mock(DNSService.class);
         MemoryDomainList domainList = new MemoryDomainList(dnsService);
         
domainList.configure(DomainListConfiguration.builder().defaultDomain(JAMES_APACHE_ORG_DOMAIN));
         remoteDelivery = new RemoteDelivery(dnsService, domainList,
-            queueFactory, new RecordingMetricFactory(), 
RemoteDelivery.ThreadState.DO_NOT_START_THREADS);
+                queueFactory, new RecordingMetricFactory(), 
RemoteDelivery.ThreadState.DO_NOT_START_THREADS);
     }
 
     @Test
     void remoteDeliveryShouldAddEmailToSpool() throws Exception {
         remoteDelivery.init(FakeMailetConfig.builder()
-            .build());
+                .build());
 
         Mail mail = FakeMail.builder()
-            .name(MAIL_NAME)
-            .recipients(MailAddressFixture.ANY_AT_JAMES)
-            .mimeMessage(MimeMessageUtil.mimeMessageFromBytes("h: 
v\r\n".getBytes(UTF_8)))
-            .build();
+                .name(MAIL_NAME)
+                .recipients(MailAddressFixture.ANY_AT_JAMES)
+                .mimeMessage(MimeMessageUtil.mimeMessageFromBytes("h: 
v\r\n".getBytes(UTF_8)))
+                .build();
         remoteDelivery.service(mail);
 
 
         assertThat(mailQueue.browse())
-            .toIterable()
-            .extracting(MailProjection::from)
-            .containsOnly(MailProjection.from(
-                FakeMail.builder()
-                    .name(MAIL_NAME + RemoteDelivery.NAME_JUNCTION + 
JAMES_APACHE_ORG)
-                    .recipient(MailAddressFixture.ANY_AT_JAMES)
-                    .build()));
+                .toIterable()
+                .extracting(MailProjection::from)
+                .containsOnly(MailProjection.from(
+                        FakeMail.builder()
+                                .name(MAIL_NAME + RemoteDelivery.NAME_JUNCTION 
+ JAMES_APACHE_ORG)
+                                .recipient(MailAddressFixture.ANY_AT_JAMES)
+                                .build()));
     }
 
     @Test
     void remoteDeliveryShouldPropagateFailures() throws Exception {
         remoteDelivery.init(FakeMailetConfig.builder()
-            .build());
+                .build());
 
         doThrow(new MailQueue.MailQueueException("Injected failure"))
-            .when(mailQueue)
-            .enQueue(any(), any());
+                .when(mailQueue)
+                .enQueue(any(), any());
 
         Mail mail = FakeMail.builder()
-            .name(MAIL_NAME)
-            .recipients(MailAddressFixture.ANY_AT_JAMES)
-            .mimeMessage(MimeMessageUtil.mimeMessageFromBytes("h: 
v\r\n".getBytes(UTF_8)))
-            .build();
+                .name(MAIL_NAME)
+                .recipients(MailAddressFixture.ANY_AT_JAMES)
+                .mimeMessage(MimeMessageUtil.mimeMessageFromBytes("h: 
v\r\n".getBytes(UTF_8)))
+                .build();
 
         assertThatThrownBy(() -> remoteDelivery.service(mail))
-            .isInstanceOf(MailQueue.MailQueueException.class);
+                .isInstanceOf(MailQueue.MailQueueException.class);
     }
 
     @Test
     void remoteDeliveryShouldSplitMailsByServerWhenNoGateway() throws 
Exception {
         remoteDelivery.init(FakeMailetConfig.builder()
-            .build());
+                .build());
 
         Mail mail = FakeMail.builder()
-            .name(MAIL_NAME)
-            .recipients(MailAddressFixture.ANY_AT_JAMES, 
MailAddressFixture.ANY_AT_JAMES2, MailAddressFixture.OTHER_AT_JAMES)
-            .mimeMessage(MimeMessageUtil.mimeMessageFromBytes("h: 
v\r\n".getBytes(UTF_8)))
-            .build();
+                .name(MAIL_NAME)
+                .recipients(MailAddressFixture.ANY_AT_JAMES, 
MailAddressFixture.ANY_AT_JAMES2, MailAddressFixture.OTHER_AT_JAMES)
+                .mimeMessage(MimeMessageUtil.mimeMessageFromBytes("h: 
v\r\n".getBytes(UTF_8)))
+                .build();
         remoteDelivery.service(mail);
 
 
         assertThat(mailQueue.browse())
-            .toIterable()
-            .extracting(MailProjection::from)
-            .containsOnly(
-                MailProjection.from(FakeMail.builder()
-                    .name(MAIL_NAME + RemoteDelivery.NAME_JUNCTION + 
JAMES_APACHE_ORG)
-                    .recipients(MailAddressFixture.ANY_AT_JAMES, 
MailAddressFixture.OTHER_AT_JAMES)
-                    .build()),
-                MailProjection.from(FakeMail.builder()
-                    .name(MAIL_NAME + RemoteDelivery.NAME_JUNCTION + 
MailAddressFixture.JAMES2_APACHE_ORG)
-                    .recipients(MailAddressFixture.ANY_AT_JAMES2)
-                    .build()));
+                .toIterable()
+                .extracting(MailProjection::from)
+                .containsOnly(
+                        MailProjection.from(FakeMail.builder()
+                                .name(MAIL_NAME + RemoteDelivery.NAME_JUNCTION 
+ JAMES_APACHE_ORG)
+                                .recipients(MailAddressFixture.ANY_AT_JAMES, 
MailAddressFixture.OTHER_AT_JAMES)
+                                .build()),
+                        MailProjection.from(FakeMail.builder()
+                                .name(MAIL_NAME + RemoteDelivery.NAME_JUNCTION 
+ MailAddressFixture.JAMES2_APACHE_ORG)
+                                .recipients(MailAddressFixture.ANY_AT_JAMES2)
+                                .build()));
     }
 
     @Test
     void remoteDeliveryShouldNotSplitMailsByServerWhenGateway() throws 
Exception {
         remoteDelivery.init(FakeMailetConfig.builder()
-            .setProperty(RemoteDeliveryConfiguration.GATEWAY, 
MailAddressFixture.JAMES_LOCAL)
-            .build());
+                .setProperty(RemoteDeliveryConfiguration.GATEWAY, 
MailAddressFixture.JAMES_LOCAL)
+                .build());
 
         Mail mail = FakeMail.builder()
-            .name(MAIL_NAME)
-            .recipients(MailAddressFixture.ANY_AT_JAMES, 
MailAddressFixture.ANY_AT_JAMES2, MailAddressFixture.OTHER_AT_JAMES)
-            .mimeMessage(MimeMessageUtil.mimeMessageFromBytes("h: 
v\r\n".getBytes(UTF_8)))
-            .build();
+                .name(MAIL_NAME)
+                .recipients(MailAddressFixture.ANY_AT_JAMES, 
MailAddressFixture.ANY_AT_JAMES2, MailAddressFixture.OTHER_AT_JAMES)
+                .mimeMessage(MimeMessageUtil.mimeMessageFromBytes("h: 
v\r\n".getBytes(UTF_8)))
+                .build();
         remoteDelivery.service(mail);
 
 
         assertThat(mailQueue.browse())
-            .toIterable()
-            .extracting(MailProjection::from)
-            .containsOnly(
-                MailProjection.from(FakeMail.builder()
-                    .name(MAIL_NAME)
-                    .recipients(MailAddressFixture.ANY_AT_JAMES, 
MailAddressFixture.ANY_AT_JAMES2, MailAddressFixture.OTHER_AT_JAMES)
-                    .build()));
+                .toIterable()
+                .extracting(MailProjection::from)
+                .containsOnly(
+                        MailProjection.from(FakeMail.builder()
+                                .name(MAIL_NAME)
+                                .recipients(MailAddressFixture.ANY_AT_JAMES, 
MailAddressFixture.ANY_AT_JAMES2, MailAddressFixture.OTHER_AT_JAMES)
+                                .build()));
     }
 
     @Test
     void remoteDeliveryShouldGhostMails() throws Exception {
         remoteDelivery.init(FakeMailetConfig.builder()
-            .build());
+                .build());
 
         Mail mail = FakeMail.builder()
-            .name(MAIL_NAME)
-            .recipients(MailAddressFixture.ANY_AT_JAMES)
-            .mimeMessage(MimeMessageUtil.mimeMessageFromBytes("h: 
v\r\n".getBytes(UTF_8)))
-            .build();
+                .name(MAIL_NAME)
+                .recipients(MailAddressFixture.ANY_AT_JAMES)
+                .mimeMessage(MimeMessageUtil.mimeMessageFromBytes("h: 
v\r\n".getBytes(UTF_8)))
+                .build();
         remoteDelivery.service(mail);
 
         assertThat(mail.getState()).isEqualTo(Mail.GHOST);
@@ -228,74 +229,99 @@ class RemoteDeliveryTest {
     @Test
     void remoteDeliveryShouldDeletePriorityWhenUsePriorityIsFalse() throws 
Exception {
         remoteDelivery.init(FakeMailetConfig.builder()
-            .setProperty(RemoteDeliveryConfiguration.USE_PRIORITY, "false")
-            .build());
+                .setProperty(RemoteDeliveryConfiguration.USE_PRIORITY, "false")
+                .build());
 
         Mail mail = FakeMail.builder()
-            .name(MAIL_NAME)
-            .recipients(MailAddressFixture.ANY_AT_JAMES)
-            .attribute(MailPrioritySupport.NORMAL_PRIORITY_ATTRIBUTE)
-            .mimeMessage(MimeMessageUtil.mimeMessageFromBytes("h: 
v\r\n".getBytes(UTF_8)))
-            .build();
+                .name(MAIL_NAME)
+                .recipients(MailAddressFixture.ANY_AT_JAMES)
+                .attribute(MailPrioritySupport.NORMAL_PRIORITY_ATTRIBUTE)
+                .mimeMessage(MimeMessageUtil.mimeMessageFromBytes("h: 
v\r\n".getBytes(UTF_8)))
+                .build();
         remoteDelivery.service(mail);
 
 
         assertThat(mailQueue.browse())
-            .toIterable()
-            .extracting(MailProjection::from)
-            .containsOnly(MailProjection.from(FakeMail.builder()
-                .name(MAIL_NAME + RemoteDelivery.NAME_JUNCTION + 
MailAddressFixture.JAMES_APACHE_ORG)
-                .recipient(MailAddressFixture.ANY_AT_JAMES)
-                .build()));
+                .toIterable()
+                .extracting(MailProjection::from)
+                .containsOnly(MailProjection.from(FakeMail.builder()
+                        .name(MAIL_NAME + RemoteDelivery.NAME_JUNCTION + 
MailAddressFixture.JAMES_APACHE_ORG)
+                        .recipient(MailAddressFixture.ANY_AT_JAMES)
+                        .build()));
     }
 
     @Test
     void remoteDeliveryShouldPreservePriority() throws Exception {
         remoteDelivery.init(FakeMailetConfig.builder()
-            .setProperty(RemoteDeliveryConfiguration.USE_PRIORITY, "true")
-            .build());
+                .setProperty(RemoteDeliveryConfiguration.USE_PRIORITY, "true")
+                .build());
 
         Mail mail = FakeMail.builder()
-            .name(MAIL_NAME)
-            .recipients(MailAddressFixture.ANY_AT_JAMES)
-            .attribute(MailPrioritySupport.NORMAL_PRIORITY_ATTRIBUTE)
-            .mimeMessage(MimeMessageUtil.mimeMessageFromBytes("h: 
v\r\n".getBytes(UTF_8)))
-            .build();
+                .name(MAIL_NAME)
+                .recipients(MailAddressFixture.ANY_AT_JAMES)
+                .attribute(MailPrioritySupport.NORMAL_PRIORITY_ATTRIBUTE)
+                .mimeMessage(MimeMessageUtil.mimeMessageFromBytes("h: 
v\r\n".getBytes(UTF_8)))
+                .build();
         remoteDelivery.service(mail);
 
 
         assertThat(mailQueue.browse())
-            .toIterable()
-            .extracting(MailProjection::from)
-            .containsOnly(MailProjection.from(FakeMail.builder()
-                .name(MAIL_NAME + RemoteDelivery.NAME_JUNCTION + 
MailAddressFixture.JAMES_APACHE_ORG)
-                .attribute(MailPrioritySupport.NORMAL_PRIORITY_ATTRIBUTE)
-                .recipient(MailAddressFixture.ANY_AT_JAMES)
-                .build()));
+                .toIterable()
+                .extracting(MailProjection::from)
+                .containsOnly(MailProjection.from(FakeMail.builder()
+                        .name(MAIL_NAME + RemoteDelivery.NAME_JUNCTION + 
MailAddressFixture.JAMES_APACHE_ORG)
+                        
.attribute(MailPrioritySupport.NORMAL_PRIORITY_ATTRIBUTE)
+                        .recipient(MailAddressFixture.ANY_AT_JAMES)
+                        .build()));
+    }
+
+    @Test
+    void remoteDeliveryShouldPreserveRequireTls() throws Exception {
+        remoteDelivery.init(FakeMailetConfig.builder()
+                .setProperty(RemoteDeliveryConfiguration.START_TLS, "true")
+                .build());
+
+        Mail mail = FakeMail.builder()
+                .name(MAIL_NAME)
+                .recipients(MailAddressFixture.ANY_AT_JAMES)
+                .attribute(new Attribute(AttributeName.of(REQUIRETLS), 
AttributeValue.of(true)))
+                .mimeMessage(MimeMessageUtil.mimeMessageFromBytes("h: 
v\r\n".getBytes(UTF_8)))
+                .build();
+        remoteDelivery.service(mail);
+
+
+        assertThat(mailQueue.browse())
+                .toIterable()
+                .extracting(MailProjection::from)
+                .containsOnly(MailProjection.from(FakeMail.builder()
+                        .name(MAIL_NAME + RemoteDelivery.NAME_JUNCTION + 
MailAddressFixture.JAMES_APACHE_ORG)
+                        .attribute(new Attribute(AttributeName.of(REQUIRETLS), 
AttributeValue.of(true)))
+                        .recipient(MailAddressFixture.ANY_AT_JAMES)
+                        .build()));
     }
 
     @Test
     void remoteDeliveryShouldNotForwardMailsWithNoRecipients() throws 
Exception {
         remoteDelivery.init(FakeMailetConfig.builder()
-            .build());
+                .build());
 
         Mail mail = FakeMail.builder().name(MAIL_NAME).build();
         remoteDelivery.service(mail);
 
         assertThat(mailQueue.browse()).toIterable()
-            .isEmpty();
+                .isEmpty();
     }
 
     @Test
     void remoteDeliveryShouldNotForwardMailsWithNoRecipientsWithGateway() 
throws Exception {
         remoteDelivery.init(FakeMailetConfig.builder()
-            .setProperty(RemoteDeliveryConfiguration.GATEWAY, 
MailAddressFixture.JAMES_LOCAL)
-            .build());
+                .setProperty(RemoteDeliveryConfiguration.GATEWAY, 
MailAddressFixture.JAMES_LOCAL)
+                .build());
 
         Mail mail = FakeMail.builder().name(MAIL_NAME).build();
         remoteDelivery.service(mail);
 
         assertThat(mailQueue.browse()).toIterable()
-            .isEmpty();
+                .isEmpty();
     }
 }
diff --git a/server/mailet/remote-delivery-integration-testing/pom.xml 
b/server/mailet/remote-delivery-integration-testing/pom.xml
index cb61bf4b21..57cb6c9a53 100644
--- a/server/mailet/remote-delivery-integration-testing/pom.xml
+++ b/server/mailet/remote-delivery-integration-testing/pom.xml
@@ -38,6 +38,13 @@
             <artifactId>james-server-mailets-integration-testing</artifactId>
             <version>${project.version}</version>
         </dependency>
+        <dependency>
+            <groupId>${james.protocols.groupId}</groupId>
+            <artifactId>protocols-api</artifactId>
+            <version>${project.version}</version>
+            <type>test-jar</type>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 
     <build>
diff --git 
a/server/mailet/remote-delivery-integration-testing/src/test/java/org/apache/james/smtp/tls/SmtpRequireTlsRelayTest.java
 
b/server/mailet/remote-delivery-integration-testing/src/test/java/org/apache/james/smtp/tls/SmtpRequireTlsRelayTest.java
new file mode 100644
index 0000000000..fa86fa9797
--- /dev/null
+++ 
b/server/mailet/remote-delivery-integration-testing/src/test/java/org/apache/james/smtp/tls/SmtpRequireTlsRelayTest.java
@@ -0,0 +1,303 @@
+/****************************************************************
+ * 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.james.smtp.tls;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.apache.james.MemoryJamesServerMain.SMTP_AND_IMAP_MODULE;
+import static org.apache.james.mailets.configuration.Constants.DEFAULT_DOMAIN;
+import static org.apache.james.mailets.configuration.Constants.LOCALHOST_IP;
+import static org.apache.james.mailets.configuration.Constants.PASSWORD;
+import static 
org.apache.james.mailets.configuration.Constants.awaitAtMostOneMinute;
+import static org.apache.james.mailets.configuration.Constants.calmlyAwait;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.awaitility.Durations.TEN_SECONDS;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Base64;
+
+import org.apache.commons.net.smtp.SMTPSClient;
+import org.apache.commons.net.smtp.SimpleSMTPHeader;
+import org.apache.james.core.MailAddress;
+import org.apache.james.dnsservice.api.DNSService;
+import org.apache.james.dnsservice.api.InMemoryDNSService;
+import org.apache.james.mailets.TemporaryJamesServer;
+import org.apache.james.mailets.configuration.CommonProcessors;
+import org.apache.james.mailets.configuration.MailetConfiguration;
+import org.apache.james.mailets.configuration.MailetContainer;
+import org.apache.james.mailets.configuration.ProcessorConfiguration;
+import org.apache.james.mailets.configuration.SmtpConfiguration;
+import org.apache.james.mock.smtp.server.model.Mail;
+import org.apache.james.mock.smtp.server.model.SMTPExtension;
+import org.apache.james.mock.smtp.server.model.SMTPExtensions;
+import org.apache.james.mock.smtp.server.testing.MockSmtpServerExtension;
+import 
org.apache.james.mock.smtp.server.testing.MockSmtpServerExtension.DockerMockSmtp;
+import org.apache.james.modules.protocols.ImapGuiceProbe;
+import org.apache.james.modules.protocols.SmtpGuiceProbe;
+import org.apache.james.protocols.api.utils.BogusSslContextFactory;
+import org.apache.james.protocols.api.utils.BogusTrustManagerFactory;
+import org.apache.james.smtpserver.dsn.DSNEhloHook;
+import org.apache.james.smtpserver.dsn.DSNMailParameterHook;
+import org.apache.james.smtpserver.dsn.DSNMessageHook;
+import org.apache.james.smtpserver.dsn.DSNRcptParameterHook;
+import org.apache.james.smtpserver.priority.SmtpMtPriorityEhloHook;
+import org.apache.james.smtpserver.priority.SmtpMtPriorityMessageHook;
+import org.apache.james.smtpserver.priority.SmtpMtPriorityParameterHook;
+import org.apache.james.smtpserver.tls.SmtpRequireTlsEhloHook;
+import org.apache.james.smtpserver.tls.SmtpRequireTlsMessageHook;
+import org.apache.james.smtpserver.tls.SmtpRequireTlsParameterHook;
+import org.apache.james.transport.mailets.RemoteDelivery;
+import org.apache.james.transport.matchers.All;
+import org.apache.james.utils.DataProbeImpl;
+import org.apache.james.utils.TestIMAPClient;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInfo;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.junit.jupiter.api.io.TempDir;
+
+class SmtpRequireTlsRelayTest {
+    private static final String ANOTHER_DOMAIN = "other.com";
+    private static final String FROM = "from@" + DEFAULT_DOMAIN;
+    private static final String RECIPIENT = "touser@" + ANOTHER_DOMAIN;
+
+    private InMemoryDNSService inMemoryDNSService;
+
+    @RegisterExtension
+    public TestIMAPClient testIMAPClient = new TestIMAPClient();
+    @RegisterExtension
+    public static MockSmtpServerExtension mockSmtpExtension = new 
MockSmtpServerExtension();
+
+    private TemporaryJamesServer jamesServer;
+
+    @BeforeEach
+    void setUp(@TempDir File temporaryFolder, DockerMockSmtp mockSmtp, 
TestInfo testInfo) throws Exception {
+        boolean usePriority = testInfo.getTags().stream().anyMatch(tag -> 
tag.contains("usePriority=true"));
+
+        inMemoryDNSService = new InMemoryDNSService()
+                .registerMxRecord(DEFAULT_DOMAIN, LOCALHOST_IP)
+                .registerMxRecord(ANOTHER_DOMAIN, mockSmtp.getIPAddress());
+
+        SmtpConfiguration.Builder smtpConfiguration = 
SmtpConfiguration.builder()
+                .requireStartTls()
+                .addHook(DSNEhloHook.class.getName())
+                .addHook(DSNMailParameterHook.class.getName())
+                .addHook(DSNRcptParameterHook.class.getName())
+                .addHook(DSNMessageHook.class.getName())
+                .addHook(SmtpRequireTlsEhloHook.class.getName())
+                .addHook(SmtpRequireTlsParameterHook.class.getName())
+                .addHook(SmtpRequireTlsMessageHook.class.getName())
+                .withAutorizedAddresses("0.0.0.0/0.0.0.0");
+
+        if (usePriority) {
+            smtpConfiguration
+                    .addHook(SmtpMtPriorityEhloHook.class.getName())
+                    .addHook(SmtpMtPriorityParameterHook.class.getName())
+                    .addHook(SmtpMtPriorityMessageHook.class.getName());
+        }
+
+        jamesServer = TemporaryJamesServer.builder()
+                .withBase(SMTP_AND_IMAP_MODULE)
+                .withOverrides(binder -> 
binder.bind(DNSService.class).toInstance(inMemoryDNSService))
+                .withMailetContainer(MailetContainer.builder()
+                        .putProcessor(CommonProcessors.simpleRoot())
+                        .putProcessor(CommonProcessors.error())
+                        .putProcessor(directResolutionTransport(usePriority))
+                        .putProcessor(CommonProcessors.bounces()))
+                .withSmtpConfiguration(smtpConfiguration)
+                .build(temporaryFolder);
+        jamesServer.start();
+
+        jamesServer.getProbe(DataProbeImpl.class)
+                .fluent()
+                .addDomain(DEFAULT_DOMAIN)
+                .addUser(FROM, PASSWORD);
+
+        
mockSmtp.getConfigurationClient().setSMTPExtensions(SMTPExtensions.of(SMTPExtension.of("STARTTLS"),
 SMTPExtension.of("dsn")));
+        
assertThat(mockSmtp.getConfigurationClient().version()).isEqualTo("0.4");
+    }
+
+    private ProcessorConfiguration.Builder directResolutionTransport(Boolean 
usePriority) {
+        return ProcessorConfiguration.transport()
+                .addMailet(MailetConfiguration.BCC_STRIPPER)
+                .addMailet(MailetConfiguration.LOCAL_DELIVERY)
+                .addMailet(MailetConfiguration.builder()
+                        .mailet(RemoteDelivery.class)
+                        .matcher(All.class)
+                        .addProperty("usePriority", usePriority.toString())
+                        .addProperty("outgoingQueue", "outgoing")
+                        .addProperty("delayTime", "3 * 10 ms")
+                        .addProperty("maxRetries", "1")
+                        .addProperty("maxDnsProblemRetries", "0")
+                        .addProperty("deliveryThreads", "2")
+                        .addProperty("sendpartial", "true"));
+    }
+
+    @Test
+    @Tag("usePriority=false")
+    void remoteDeliveryShouldRequireTls(DockerMockSmtp mockSmtp) throws 
Exception {
+
+        SMTPSClient smtpClient = initSMTPSClient();
+        smtpClient.mail("<" + FROM + "> REQUIRETLS");
+        smtpClient.rcpt("<" + RECIPIENT + ">");
+        smtpClient.sendShortMessageData("A short message...");
+
+        calmlyAwait.atMost(TEN_SECONDS).untilAsserted(() -> 
assertThat(mockSmtp.getConfigurationClient().listMails())
+                .hasSize(1)
+                .extracting(Mail::getEnvelope)
+                .containsExactly(Mail.Envelope.builder()
+                        .from(new MailAddress(FROM))
+                        .addMailParameter(Mail.Parameter.builder()
+                                .name("REQUIRETLS")
+                                .build())
+                        .addRecipient(Mail.Recipient.builder()
+                                .address(new MailAddress(RECIPIENT))
+                                .build())
+                        .build()));
+    }
+
+    @Test
+    @Tag("usePriority=false")
+    void 
remoteDeliveryShouldRequireTlsWhenTlsRequiredHeaderExists(DockerMockSmtp 
mockSmtp) throws Exception {
+
+        SMTPSClient smtpClient = initSMTPSClient();
+        SimpleSMTPHeader header = new SimpleSMTPHeader(FROM, RECIPIENT, "Just 
testing");
+        header.addHeaderField("TLS-Required", "No");
+        smtpClient.mail("<" + FROM + "> REQUIRETLS");
+        smtpClient.rcpt("<" + RECIPIENT + ">");
+        smtpClient.sendShortMessageData(header + "A short message...");
+
+        calmlyAwait.atMost(TEN_SECONDS).untilAsserted(() -> 
assertThat(mockSmtp.getConfigurationClient().listMails())
+                .hasSize(1)
+                .extracting(Mail::getEnvelope)
+                .containsExactly(Mail.Envelope.builder()
+                        .from(new MailAddress(FROM))
+                        .addMailParameter(Mail.Parameter.builder()
+                                .name("REQUIRETLS")
+                                .build())
+                        .addRecipient(Mail.Recipient.builder()
+                                .address(new MailAddress(RECIPIENT))
+                                .build())
+                        .build()));
+    }
+
+    @Test
+    @Tag("usePriority=false")
+    void 
remoteDeliveryShouldNotRequireTlsWhenTlsRequiredHeaderExistsAndFromCommandDoesNotContainRequireTls(DockerMockSmtp
 mockSmtp) throws Exception {
+
+        SMTPSClient smtpClient = initSMTPSClient();
+        SimpleSMTPHeader header = new SimpleSMTPHeader(FROM, RECIPIENT, "Just 
testing");
+        header.addHeaderField("TLS-Required", "No");
+        smtpClient.mail("<" + FROM + ">");
+        smtpClient.rcpt("<" + RECIPIENT + ">");
+        smtpClient.sendShortMessageData(header + "A short message...");
+
+        calmlyAwait.atMost(TEN_SECONDS).untilAsserted(() -> 
assertThat(mockSmtp.getConfigurationClient().listMails())
+                .hasSize(1)
+                .extracting(Mail::getEnvelope)
+                .containsExactly(Mail.Envelope.builder()
+                        .from(new MailAddress(FROM))
+                        .addRecipient(Mail.Recipient.builder()
+                                .address(new MailAddress(RECIPIENT))
+                                .build())
+                        .build()));
+    }
+
+    @Test
+    @Tag("usePriority=true")
+    void 
remoteDeliveryWhenShouldRequireTlsAndDsnAndMtPriorityTogether(DockerMockSmtp 
mockSmtp) throws Exception {
+        String expectedPriorityValue = "3";
+
+        SMTPSClient smtpClient = initSMTPSClient();
+        smtpClient.mail("<" + FROM + "> MT-PRIORITY=" + expectedPriorityValue 
+ " REQUIRETLS RET=HDRS ENVID=gabouzomeuh");
+        smtpClient.rcpt("<" + RECIPIENT + ">");
+        smtpClient.sendShortMessageData("A short message...");
+
+        calmlyAwait.atMost(TEN_SECONDS).untilAsserted(() -> 
assertThat(mockSmtp.getConfigurationClient().listMails())
+                .hasSize(1)
+                .extracting(Mail::getEnvelope)
+                .containsExactly(Mail.Envelope.builder()
+                        .from(new MailAddress(FROM))
+                        .addMailParameter(Mail.Parameter.builder()
+                                .name("MT-PRIORITY")
+                                .value(expectedPriorityValue)
+                                .build())
+                        .addMailParameter(Mail.Parameter.builder()
+                                .name("REQUIRETLS")
+                                .build())
+                        .addMailParameter(Mail.Parameter.builder()
+                                .name("RET")
+                                .value("HDRS")
+                                .build())
+                        .addMailParameter(Mail.Parameter.builder()
+                                .name("ENVID")
+                                .value("gabouzomeuh")
+                                .build())
+                        .addRecipient(Mail.Recipient.builder()
+                                .address(new MailAddress(RECIPIENT))
+                                .build())
+                        .build()));
+    }
+
+    @Test
+    void remoteDeliveryShouldFailsWhenServerNotAllowStartTls(DockerMockSmtp 
mockSmtp) throws Exception {
+
+        mockSmtp.getConfigurationClient().clearSMTPExtensions();
+
+        SMTPSClient smtpClient = initSMTPSClient();
+        smtpClient.mail("<" + FROM + "> REQUIRETLS");
+        smtpClient.rcpt("<" + RECIPIENT + ">");
+        smtpClient.sendShortMessageData("A short message...");
+
+        String dsnMessage = testIMAPClient.connect(LOCALHOST_IP, 
jamesServer.getProbe(ImapGuiceProbe.class).getImapPort())
+                .login(FROM, PASSWORD)
+                .select(TestIMAPClient.INBOX)
+                .awaitMessageCount(awaitAtMostOneMinute, 1)
+                .readFirstMessage();
+
+        Assertions.assertThat(dsnMessage).contains("Mail delivery failed; the 
receiving server does not support STARTTLS");
+    }
+
+    @AfterEach
+    void tearDown() {
+        jamesServer.shutdown();
+    }
+
+    private void authenticate(SMTPSClient smtpProtocol) throws IOException {
+        smtpProtocol.sendCommand("AUTH PLAIN");
+        smtpProtocol.sendCommand(Base64.getEncoder().encodeToString(("\0" + 
FROM + "\0" + PASSWORD + "\0").getBytes(UTF_8)));
+        assertThat(smtpProtocol.getReplyCode())
+                .as("authenticated")
+                .isEqualTo(235);
+    }
+
+    private SMTPSClient initSMTPSClient() throws IOException {
+        SMTPSClient smtpClient = new SMTPSClient(false, 
BogusSslContextFactory.getClientContext());
+        
smtpClient.setTrustManager(BogusTrustManagerFactory.getTrustManagers()[0]);
+        smtpClient.connect("localhost", 
jamesServer.getProbe(SmtpGuiceProbe.class).getSmtpPort().getValue());
+        smtpClient.execTLS();
+        smtpClient.sendCommand("EHLO james.org");
+        authenticate(smtpClient);
+        return smtpClient;
+    }
+}
diff --git 
a/server/testing/src/main/java/org/apache/james/util/docker/Images.java 
b/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/tls/SmtpRequireTlsEhloHook.java
similarity index 66%
copy from server/testing/src/main/java/org/apache/james/util/docker/Images.java
copy to 
server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/tls/SmtpRequireTlsEhloHook.java
index 63a8e1cf86..82ac40db73 100644
--- a/server/testing/src/main/java/org/apache/james/util/docker/Images.java
+++ 
b/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/tls/SmtpRequireTlsEhloHook.java
@@ -17,15 +17,25 @@
  * under the License.                                           *
  ****************************************************************/
 
-package org.apache.james.util.docker;
+package org.apache.james.smtpserver.tls;
 
-public interface Images {
-    String FAKE_SMTP = "quanth99/rest-smtp-sink:1.0"; // Original Dockerfile: 
https://github.com/ambled/rest-smtp-sink/blob/master/Dockerfile
-    String RABBITMQ = "rabbitmq:3.13.3-management";
-    String ELASTICSEARCH_2 = "elasticsearch:2.4.6";
-    String ELASTICSEARCH_6 = 
"docker.elastic.co/elasticsearch/elasticsearch:6.3.2";
-    String OPENSEARCH = "opensearchproject/opensearch:2.14.0";
-    String TIKA = "apache/tika:3.0.0.0";
-    String MOCK_SMTP_SERVER = "linagora/mock-smtp-server:0.6";
-    String OPEN_LDAP = "osixia/openldap:1.5.0";
+import java.util.Set;
+
+import org.apache.james.protocols.smtp.SMTPSession;
+import org.apache.james.protocols.smtp.hook.HeloHook;
+import org.apache.james.protocols.smtp.hook.HookResult;
+
+public class SmtpRequireTlsEhloHook implements HeloHook {
+    @Override
+    public Set<String> implementedEsmtpFeatures(SMTPSession session) {
+        if (session.isTLSStarted()) {
+            return Set.of("REQUIRETLS");
+        }
+        return Set.of();
+    }
+
+    @Override
+    public HookResult doHelo(SMTPSession session, String helo) {
+        return HookResult.DECLINED;
+    }
 }
diff --git 
a/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/tls/SmtpRequireTlsMessageHook.java
 
b/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/tls/SmtpRequireTlsMessageHook.java
new file mode 100644
index 0000000000..f6de87f4e8
--- /dev/null
+++ 
b/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/tls/SmtpRequireTlsMessageHook.java
@@ -0,0 +1,77 @@
+/****************************************************************
+ * 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.james.smtpserver.tls;
+
+import static org.apache.james.protocols.api.ProtocolSession.State.Transaction;
+
+import java.util.Arrays;
+
+import jakarta.mail.MessagingException;
+
+import org.apache.james.protocols.api.ProtocolSession;
+import org.apache.james.protocols.smtp.SMTPRetCode;
+import org.apache.james.protocols.smtp.SMTPSession;
+import org.apache.james.protocols.smtp.hook.HookResult;
+import org.apache.james.protocols.smtp.hook.HookReturnCode;
+import org.apache.james.smtpserver.JamesMessageHook;
+import org.apache.mailet.Attribute;
+import org.apache.mailet.AttributeName;
+import org.apache.mailet.AttributeValue;
+import org.apache.mailet.Mail;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class SmtpRequireTlsMessageHook implements JamesMessageHook {
+    private static final Logger LOGGER = 
LoggerFactory.getLogger(SmtpRequireTlsMessageHook.class);
+    public static final String REQUIRETLS = "REQUIRETLS";
+    public static final String TLS_REQUIRED = "TLS-Required";
+
+    public static final ProtocolSession.AttachmentKey<Boolean> REQUIRETLS_KEY =
+        ProtocolSession.AttachmentKey.of(REQUIRETLS, Boolean.class);
+
+    @Override
+    public HookResult onMessage(SMTPSession session, Mail mail) {
+        try {
+            session.getAttachment(REQUIRETLS_KEY, 
Transaction).ifPresent(requireTsl ->
+                mail.setAttribute(new Attribute(AttributeName.of(REQUIRETLS), 
AttributeValue.of(true))));
+            if (isTlsNotRequired(mail) && 
!isRequireTlsAttributeContains(mail)) {
+                mail.setAttribute(new Attribute(AttributeName.of(REQUIRETLS), 
AttributeValue.of(false)));
+            }
+        } catch (MessagingException e) {
+            LOGGER.debug("Incorrect syntax when handling TLS-Required header 
field", e);
+            return HookResult.builder()
+                .smtpReturnCode(SMTPRetCode.SYNTAX_ERROR_ARGUMENTS)
+                .hookReturnCode(HookReturnCode.deny())
+                .smtpDescription("Incorrect syntax when handling TLS-Required 
header field")
+                .build();
+        }
+        return HookResult.DECLINED;
+    }
+
+    private static boolean isRequireTlsAttributeContains(Mail mail) {
+        return mail.attributeNames().map(AttributeName::asString)
+            .anyMatch(attributeName -> attributeName.equals(REQUIRETLS));
+    }
+
+    private static boolean isTlsNotRequired(Mail mail) throws 
MessagingException {
+        String[] headers = mail.getMessage().getHeader(TLS_REQUIRED);
+        return headers != null && headers.length == 1 && 
(Arrays.asList(headers).contains("No"));
+    }
+}
diff --git 
a/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/tls/SmtpRequireTlsParameterHook.java
 
b/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/tls/SmtpRequireTlsParameterHook.java
new file mode 100644
index 0000000000..0b1134b47a
--- /dev/null
+++ 
b/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/tls/SmtpRequireTlsParameterHook.java
@@ -0,0 +1,64 @@
+/****************************************************************
+ * 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.james.smtpserver.tls;
+
+import static org.apache.james.protocols.api.ProtocolSession.State.Transaction;
+
+import java.util.Optional;
+
+import org.apache.james.core.Username;
+import org.apache.james.protocols.api.ProtocolSession;
+import org.apache.james.protocols.smtp.SMTPRetCode;
+import org.apache.james.protocols.smtp.SMTPSession;
+import org.apache.james.protocols.smtp.hook.HookResult;
+import org.apache.james.protocols.smtp.hook.HookReturnCode;
+import org.apache.james.protocols.smtp.hook.MailParametersHook;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class SmtpRequireTlsParameterHook implements MailParametersHook {
+    private static final Logger LOGGER = 
LoggerFactory.getLogger(SmtpRequireTlsParameterHook.class);
+    private static final String REQUIRETLS = "REQUIRETLS";
+    public static final ProtocolSession.AttachmentKey<Boolean> REQUIRETLS_KEY =
+        ProtocolSession.AttachmentKey.of(REQUIRETLS, Boolean.class);
+
+    @Override
+    public HookResult doMailParameter(SMTPSession session, String paramName, 
String paramValue) {
+        if (session.getAttachment(REQUIRETLS_KEY, Transaction).isPresent()) {
+            LOGGER.debug("The Mail parameter cannot contain more than one 
REQUIRETLS parameter at the same time");
+            return HookResult.builder()
+                .smtpReturnCode(SMTPRetCode.SYNTAX_ERROR_ARGUMENTS)
+                .hookReturnCode(HookReturnCode.deny())
+                .smtpDescription("The Mail parameter cannot contain more than 
one REQUIRETLS parameter at the same time")
+                .build();
+        }
+        if (paramName.equals(REQUIRETLS) && session.isStartTLSSupported() && 
session.isTLSStarted()) {
+            session.setAttachment(REQUIRETLS_KEY, true, Transaction);
+            String userName = 
Optional.ofNullable(session.getUsername()).map(Username::asString).orElse("unauthorized");
+            LOGGER.info("SMTP sessionID: {}, User: {}, REQUIRETLS=true", 
session.getSessionID(), userName);
+        }
+        return HookResult.DECLINED;
+    }
+
+    @Override
+    public String[] getMailParamNames() {
+        return new String[]{REQUIRETLS};
+    }
+}
\ No newline at end of file
diff --git 
a/server/protocols/protocols-smtp/src/test/java/org/apache/james/smtpserver/SmtpRequireTlsMessageHookTest.java
 
b/server/protocols/protocols-smtp/src/test/java/org/apache/james/smtpserver/SmtpRequireTlsMessageHookTest.java
new file mode 100644
index 0000000000..c470a97272
--- /dev/null
+++ 
b/server/protocols/protocols-smtp/src/test/java/org/apache/james/smtpserver/SmtpRequireTlsMessageHookTest.java
@@ -0,0 +1,179 @@
+/****************************************************************
+ * 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.james.smtpserver;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.apache.james.smtpserver.SMTPServerTestSystem.BOB;
+import static org.apache.james.smtpserver.SMTPServerTestSystem.PASSWORD;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.util.Base64;
+
+import org.apache.commons.net.smtp.SMTPSClient;
+import org.apache.commons.net.smtp.SimpleSMTPHeader;
+import org.apache.james.protocols.api.utils.BogusSslContextFactory;
+import org.apache.james.protocols.api.utils.BogusTrustManagerFactory;
+import org.apache.mailet.Attribute;
+import org.apache.mailet.AttributeName;
+import org.apache.mailet.AttributeValue;
+import org.apache.mailet.Mail;
+import org.assertj.core.api.SoftAssertions;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class SmtpRequireTlsMessageHookTest {
+    private final SMTPServerTestSystem testSystem = new SMTPServerTestSystem();
+    private static final String REQUIRETLS = "REQUIRETLS";
+    private static final AttributeName REQUIRETLS_ATTRIBUTE_NAME = 
AttributeName.of(REQUIRETLS);
+
+    @BeforeEach
+    void setUp() throws Exception {
+        testSystem.setUp("smtpserver-requireTls.xml");
+    }
+
+    @AfterEach
+    void tearDown() {
+        testSystem.smtpServer.destroy();
+    }
+
+    private SMTPSClient initSMTPSClient() throws IOException {
+        SMTPSClient client = new SMTPSClient(false, 
BogusSslContextFactory.getClientContext());
+        client.setTrustManager(BogusTrustManagerFactory.getTrustManagers()[0]);
+        InetSocketAddress bindedAddress = testSystem.getBindedAddress();
+        client.connect(bindedAddress.getAddress().getHostAddress(), 
bindedAddress.getPort());
+        return client;
+    }
+
+    @Test
+    void ehloShouldAdvertiseRequireTlsExtension() throws Exception {
+        SMTPSClient client = initSMTPSClient();
+        client.execTLS();
+        client.sendCommand("EHLO localhost");
+
+        SoftAssertions.assertSoftly(softly -> {
+            softly.assertThat(client.getReplyCode()).isEqualTo(250);
+            softly.assertThat(client.getReplyString()).contains("250 
REQUIRETLS");
+        });
+    }
+
+    @Test
+    void ehloShouldNotAdvertiseRequireTlsExtensionWithoutExecTLS() throws 
Exception {
+        SMTPSClient client = initSMTPSClient();
+        client.sendCommand("EHLO localhost");
+
+        SoftAssertions.assertSoftly(softly -> {
+            softly.assertThat(client.getReplyCode()).isEqualTo(250);
+            softly.assertThat(client.getReplyString()).doesNotContain("250 
REQUIRETLS");
+        });
+    }
+
+    @Test
+    void mailShouldBeRejectedWhenInvalidTlsRequiredParameter() throws 
Exception {
+        SMTPSClient client = initSMTPSClient();
+        client.execTLS();
+        authenticate(client);
+        client.sendCommand("EHLO localhost");
+        client.sendCommand("MAIL FROM: <bob@localhost> REQUIRETLS REQUIRETLS");
+
+        SoftAssertions.assertSoftly(softly -> {
+            softly.assertThat(client.getReplyCode()).isEqualTo(501);
+            softly.assertThat(client.getReplyString()).contains("501 The Mail 
parameter cannot contain more than one REQUIRETLS parameter at the same time");
+        });
+    }
+
+    @Test
+    void requireTlsParameterShouldBeSetOnTheFinalEmail() throws Exception {
+        SMTPSClient client = initSMTPSClient();
+        client.execTLS();
+        authenticate(client);
+        client.sendCommand("EHLO localhost");
+        client.sendCommand("MAIL FROM:<bob@localhost> REQUIRETLS");
+        client.sendCommand("RCPT TO:<rcpt@localhost>");
+        client.sendShortMessageData("Subject: test mail\r\n\r\nTest body 
testSimpleMailSendWithMtPriority\r\n.\r\n");
+
+        Mail lastMail = testSystem.queue.getLastMail();
+
+        assertThat(lastMail.getAttribute(REQUIRETLS_ATTRIBUTE_NAME))
+            .hasValue(new Attribute(REQUIRETLS_ATTRIBUTE_NAME, 
AttributeValue.of(true)));
+
+    }
+
+    @Test
+    void requireTlsParameterShouldBeIgnoredInTheFinalEmail() throws Exception {
+        SMTPSClient client = initSMTPSClient();
+        authenticate(client);
+        client.sendCommand("EHLO localhost");
+        client.sendCommand("MAIL FROM:<bob@localhost> REQUIRETLS");
+        client.sendCommand("RCPT TO:<rcpt@localhost>");
+        client.sendShortMessageData("Subject: test mail\r\n\r\nTest body 
testSimpleMailSendWithMtPriority\r\n.\r\n");
+
+        Mail lastMail = testSystem.queue.getLastMail();
+
+        assertThat(lastMail.getAttribute(REQUIRETLS_ATTRIBUTE_NAME)).isEmpty();
+    }
+
+    @Test
+    void 
tlsRequiredHeaderFieldShouldBeIgnoredWhenTheRequireTlsMailFromParameterIsSpecified()
 throws Exception {
+        SMTPSClient client = initSMTPSClient();
+        client.execTLS();
+        authenticate(client);
+        client.sendCommand("EHLO localhost");
+        client.sendCommand("MAIL FROM:<bob@localhost> REQUIRETLS");
+        client.sendCommand("RCPT TO:<rcpt@localhost>");
+        SimpleSMTPHeader header = new SimpleSMTPHeader("bob@localhost", 
"rcpt@localhost", "Just testing");
+        header.addHeaderField("TLS-Required", "No");
+        client.sendShortMessageData(header + "Test body 
testRequireTlsEmail\r\n.\r\n");
+
+        Mail lastMail = testSystem.queue.getLastMail();
+
+        assertThat(lastMail.getAttribute(REQUIRETLS_ATTRIBUTE_NAME))
+            .hasValue(new Attribute(REQUIRETLS_ATTRIBUTE_NAME, 
AttributeValue.of(true)));
+    }
+
+    @Test
+    void 
tlsRequiredHeaderFieldShouldBeIncludedWhenTheRequireTlsMailFromParameterIsNotSpecified()
 throws Exception {
+        SMTPSClient client = initSMTPSClient();
+        client.execTLS();
+        authenticate(client);
+        client.sendCommand("EHLO localhost");
+        client.sendCommand("MAIL FROM:<bob@localhost>");
+        client.sendCommand("RCPT TO:<rcpt@localhost>");
+        SimpleSMTPHeader header = new SimpleSMTPHeader("bob@localhost", 
"rcpt@localhost", "Just testing");
+        header.addHeaderField("TLS-Required", "No");
+        client.sendShortMessageData(header + "Test body 
testRequireTlsEmail\r\n.\r\n");
+
+        Mail lastMail = testSystem.queue.getLastMail();
+
+        assertThat(lastMail.getAttribute(REQUIRETLS_ATTRIBUTE_NAME))
+                .hasValue(new Attribute(REQUIRETLS_ATTRIBUTE_NAME, 
AttributeValue.of(false)));
+    }
+
+    private void authenticate(SMTPSClient smtpProtocol) throws IOException {
+        smtpProtocol.sendCommand("AUTH PLAIN");
+        smtpProtocol.sendCommand(Base64.getEncoder().encodeToString(("\0" + 
BOB.asString() + "\0" + PASSWORD + "\0").getBytes(UTF_8)));
+        assertThat(smtpProtocol.getReplyCode())
+            .as("authenticated")
+            .isEqualTo(235);
+    }
+
+}
\ No newline at end of file
diff --git 
a/server/protocols/protocols-smtp/src/test/resources/smtpserver-requireTls.xml 
b/server/protocols/protocols-smtp/src/test/resources/smtpserver-requireTls.xml
new file mode 100644
index 0000000000..6dbc0e5d97
--- /dev/null
+++ 
b/server/protocols/protocols-smtp/src/test/resources/smtpserver-requireTls.xml
@@ -0,0 +1,22 @@
+<smtpserver enabled="true">
+    <bind>0.0.0.0:0</bind>
+    <tls socketTLS="false" startTLS="true">
+        <keystore>classpath://keystore</keystore>
+        <secret>james72laBalle</secret>
+        <provider>org.bouncycastle.jce.provider.BouncyCastleProvider</provider>
+        <algorithm>SunX509</algorithm>
+    </tls>
+    <auth>
+        <announce>forUnauthorizedAddresses</announce>
+        <requireSSL>false</requireSSL>
+    </auth>
+    <verifyIdentity>true</verifyIdentity>
+    <smtpGreeting>Apache JAMES awesome SMTP Server</smtpGreeting>
+    <handlerchain>
+        <handler 
class="org.apache.james.smtpserver.tls.SmtpRequireTlsEhloHook"/>
+        <handler 
class="org.apache.james.smtpserver.tls.SmtpRequireTlsParameterHook"/>
+        <handler 
class="org.apache.james.smtpserver.tls.SmtpRequireTlsMessageHook"/>
+        <handler class="org.apache.james.smtpserver.CoreCmdHandlerLoader"/>
+    </handlerchain>
+    <gracefulShutdown>false</gracefulShutdown>
+</smtpserver>
\ No newline at end of file
diff --git 
a/server/queue/queue-memory/src/main/java/org/apache/james/queue/memory/MemoryMailQueueFactory.java
 
b/server/queue/queue-memory/src/main/java/org/apache/james/queue/memory/MemoryMailQueueFactory.java
index c1f176eebd..967d63a1f3 100644
--- 
a/server/queue/queue-memory/src/main/java/org/apache/james/queue/memory/MemoryMailQueueFactory.java
+++ 
b/server/queue/queue-memory/src/main/java/org/apache/james/queue/memory/MemoryMailQueueFactory.java
@@ -251,20 +251,16 @@ public class MemoryMailQueueFactory implements 
MailQueueFactory<MemoryMailQueueF
         }
 
         public boolean shouldRemove(MailQueueItem item, Type type, String 
value) {
-            switch (type) {
-                case Name:
-                    return item.getMail().getName().equals(value);
-                case Recipient:
-                    return item.getMail().getRecipients().stream()
+            return switch (type) {
+                case Name -> item.getMail().getName().equals(value);
+                case Recipient -> item.getMail().getRecipients().stream()
                         .map(MailAddress::asString)
                         .anyMatch(value::equals);
-                case Sender:
-                    return item.getMail().getMaybeSender()
+                case Sender -> item.getMail().getMaybeSender()
                         .asString()
                         .equals(value);
-                default:
-                    throw new NotImplementedException("Unknown type " + type);
-            }
+                default -> throw new NotImplementedException("Unknown type " + 
type);
+            };
         }
 
         private void markProcessingAsFinished(MemoryMailQueueItem item) {
diff --git 
a/server/testing/src/main/java/org/apache/james/util/docker/Images.java 
b/server/testing/src/main/java/org/apache/james/util/docker/Images.java
index 63a8e1cf86..1e53a16c73 100644
--- a/server/testing/src/main/java/org/apache/james/util/docker/Images.java
+++ b/server/testing/src/main/java/org/apache/james/util/docker/Images.java
@@ -26,6 +26,6 @@ public interface Images {
     String ELASTICSEARCH_6 = 
"docker.elastic.co/elasticsearch/elasticsearch:6.3.2";
     String OPENSEARCH = "opensearchproject/opensearch:2.14.0";
     String TIKA = "apache/tika:3.0.0.0";
-    String MOCK_SMTP_SERVER = "linagora/mock-smtp-server:0.6";
+    String MOCK_SMTP_SERVER = "linagora/mock-smtp-server:0.7";
     String OPEN_LDAP = "osixia/openldap:1.5.0";
 }
diff --git a/src/site/xdoc/server/rfcs.xml b/src/site/xdoc/server/rfcs.xml
index c88720154f..d8ddf8b068 100644
--- a/src/site/xdoc/server/rfcs.xml
+++ b/src/site/xdoc/server/rfcs.xml
@@ -48,6 +48,7 @@
       <a href="rfclist/smtp/rfc2554.txt">RFC 2554: SMTP Service Extension for 
Authentication</a><br/>
       <a href="rfclist/smtp/rfc2821.txt">RFC 2821: Simple Mail Transfer 
Protocol (obsoletes RFC 821)</a><br/>
       <a href="rfclist/smtp/rfc6710.txt">RFC 6710: Simple Mail Transfer 
Protocol Extension for Message Transfer Priorities</a><br/>
+      <a href="https://tools.ietf.org/html/rfc8689";>RFC 8689: Simple Mail 
Transfer Protocol Require TLS Option</a><br/>
     </subsection>
     <subsection name="LMTP RFCs">
       <a href="rfclist/lmtp/rfc2033.txt">RFC 2033 : LMTP Local Mail Transfer 
Protocol</a><br/>


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscr...@james.apache.org
For additional commands, e-mail: notifications-h...@james.apache.org

Reply via email to