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