This is an automated email from the ASF dual-hosted git repository. btellier pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/james-project.git
commit f2d6902b8b57f55efa9f9f57b3e1ae0c96958613 Author: Benoit Tellier <btell...@linagora.com> AuthorDate: Fri Apr 14 08:59:32 2023 +0700 JAMES-3822 RFC-4865 Implement delayed sends in SMTP Co-authored-by: Thanh Bui <vt...@linagora.com> --- .../protocols/smtp/core/esmtp/EhloCmdHandler.java | 2 +- .../apache/james/protocols/smtp/hook/HeloHook.java | 2 +- .../modules/ROOT/pages/configure/smtp-hooks.adoc | 15 + .../apache/james/smtpserver/SendMailHandler.java | 32 +- .../apache/james/smtpserver/dsn/DSNEhloHook.java | 2 +- .../FutureReleaseEHLOHook.java} | 31 +- .../FutureReleaseMailParameterHook.java | 123 +++++++ .../futurerelease/FutureReleaseParameters.java | 70 ++++ .../apache/james/smtpserver/FutureReleaseTest.java | 398 +++++++++++++++++++++ .../FutureReleaseParametersTest.java} | 25 +- .../test/resources/smtpserver-futurerelease.xml | 50 +++ 11 files changed, 720 insertions(+), 30 deletions(-) diff --git a/protocols/smtp/src/main/java/org/apache/james/protocols/smtp/core/esmtp/EhloCmdHandler.java b/protocols/smtp/src/main/java/org/apache/james/protocols/smtp/core/esmtp/EhloCmdHandler.java index 3c81c1cb2b..dc78bdd54c 100644 --- a/protocols/smtp/src/main/java/org/apache/james/protocols/smtp/core/esmtp/EhloCmdHandler.java +++ b/protocols/smtp/src/main/java/org/apache/james/protocols/smtp/core/esmtp/EhloCmdHandler.java @@ -162,7 +162,7 @@ public class EhloCmdHandler extends AbstractHookableCmdHandler<HeloHook> impleme return ImmutableList.<String>builder() .addAll(ESMTP_FEATURES) .addAll(getHooks().stream() - .flatMap(heloHook -> heloHook.implementedEsmtpFeatures().stream()) + .flatMap(heloHook -> heloHook.implementedEsmtpFeatures(session).stream()) .collect(ImmutableList.toImmutableList())) .build(); } diff --git a/protocols/smtp/src/main/java/org/apache/james/protocols/smtp/hook/HeloHook.java b/protocols/smtp/src/main/java/org/apache/james/protocols/smtp/hook/HeloHook.java index 3ca3c59710..49ffb97c2d 100644 --- a/protocols/smtp/src/main/java/org/apache/james/protocols/smtp/hook/HeloHook.java +++ b/protocols/smtp/src/main/java/org/apache/james/protocols/smtp/hook/HeloHook.java @@ -32,7 +32,7 @@ public interface HeloHook extends Hook { /** * @return ESMTP extensions to be advertised as part of EHLO answers */ - default Set<String> implementedEsmtpFeatures() { + default Set<String> implementedEsmtpFeatures(SMTPSession session) { return ImmutableSet.of(); } diff --git a/server/apps/distributed-app/docs/modules/ROOT/pages/configure/smtp-hooks.adoc b/server/apps/distributed-app/docs/modules/ROOT/pages/configure/smtp-hooks.adoc index b7253a4566..ab3a9f2b1b 100644 --- a/server/apps/distributed-app/docs/modules/ROOT/pages/configure/smtp-hooks.adoc +++ b/server/apps/distributed-app/docs/modules/ROOT/pages/configure/smtp-hooks.adoc @@ -301,4 +301,19 @@ Example configuration: <!-- ... --> <handler class="org.apache.james.smtpserver.fastfail.ValidSenderDomainHandler"/> </handlerchain> +.... + +== FUTURERELEASE hooks + +The Distributed server has optional support for FUTURERELEASE (link:https://www.rfc-editor.org/rfc/rfc4865.html[RFC-4865]) + +.... +<smtpserver enabled="true"> + <...> <!-- The rest of your SMTP configuration, unchanged --> + <handlerchain> + <handler class="org.apache.james.smtpserver.futurerelease.FutureReleaseEHLOHook"/> + <handler class="org.apache.james.smtpserver.futurerelease.FutureReleaseMailParameterHook"/> + <handler class="org.apache.james.smtpserver.CoreCmdHandlerLoader"/> + </handlerchain> +</smtpserver> .... \ No newline at end of file diff --git a/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/SendMailHandler.java b/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/SendMailHandler.java index a6c155fdd0..4b4296e4e5 100644 --- a/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/SendMailHandler.java +++ b/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/SendMailHandler.java @@ -19,23 +19,30 @@ package org.apache.james.smtpserver; +import static org.apache.james.smtpserver.futurerelease.FutureReleaseMailParameterHook.FUTURERELEASE_HOLDFOR; + import java.io.Closeable; import java.io.IOException; +import java.util.Optional; import javax.inject.Inject; import org.apache.commons.configuration2.Configuration; +import org.apache.james.protocols.api.ProtocolSession; import org.apache.james.protocols.smtp.SMTPSession; import org.apache.james.protocols.smtp.dsn.DSNStatus; import org.apache.james.protocols.smtp.hook.HookResult; import org.apache.james.protocols.smtp.hook.HookReturnCode; import org.apache.james.queue.api.MailQueue; import org.apache.james.queue.api.MailQueueFactory; +import org.apache.james.smtpserver.futurerelease.FutureReleaseParameters; import org.apache.james.util.MDCBuilder; import org.apache.mailet.Mail; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.github.fge.lambdas.Throwing; + /** * Queue the message */ @@ -72,12 +79,25 @@ public class SendMailHandler implements JamesMessageHook { LOGGER.debug("sending mail"); try (Closeable closeable = MDCBuilder.ofValue("messageId", mail.getMessage().getMessageID()).build()) { - queue.enQueue(mail); - LOGGER.info("Successfully spooled mail {} with messageId {} from {} on {} for {}", mail.getName(), - mail.getMessage().getMessageID(), - mail.getMaybeSender().asString(), - session.getRemoteAddress().getAddress(), - mail.getRecipients()); + Optional<FutureReleaseParameters.HoldFor> delays = session.getAttachment(FUTURERELEASE_HOLDFOR, ProtocolSession.State.Transaction); + + delays.ifPresentOrElse(Throwing.consumer(holdFor -> { + queue.enQueue(mail, holdFor.value()); + LOGGER.info("Successfully spooled mail {} with messageId {} from {} on {} for {} with delay {}", mail.getName(), + mail.getMessage().getMessageID(), + mail.getMaybeSender().asString(), + session.getRemoteAddress().getAddress(), + mail.getRecipients(), + holdFor.value()); + }), + Throwing.runnable(() -> { + queue.enQueue(mail); + LOGGER.info("Successfully spooled mail {} with messageId {} from {} on {} for {}", mail.getName(), + mail.getMessage().getMessageID(), + mail.getMaybeSender().asString(), + session.getRemoteAddress().getAddress(), + mail.getRecipients()); + })); } catch (Exception me) { LOGGER.error("Unknown error occurred while processing DATA.", me); return HookResult.builder() diff --git a/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/dsn/DSNEhloHook.java b/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/dsn/DSNEhloHook.java index a10a340fb3..5fe25e3dd4 100644 --- a/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/dsn/DSNEhloHook.java +++ b/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/dsn/DSNEhloHook.java @@ -29,7 +29,7 @@ import com.google.common.collect.ImmutableSet; public class DSNEhloHook implements HeloHook { @Override - public Set<String> implementedEsmtpFeatures() { + public Set<String> implementedEsmtpFeatures(SMTPSession session) { return ImmutableSet.of("DSN"); } diff --git a/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/dsn/DSNEhloHook.java b/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/futurerelease/FutureReleaseEHLOHook.java similarity index 60% copy from server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/dsn/DSNEhloHook.java copy to server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/futurerelease/FutureReleaseEHLOHook.java index a10a340fb3..767afad1e7 100644 --- a/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/dsn/DSNEhloHook.java +++ b/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/futurerelease/FutureReleaseEHLOHook.java @@ -16,21 +16,43 @@ * specific language governing permissions and limitations * * under the License. * ****************************************************************/ +package org.apache.james.smtpserver.futurerelease; -package org.apache.james.smtpserver.dsn; +import static org.apache.james.smtpserver.futurerelease.FutureReleaseParameters.MAX_HOLD_FOR_SUPPORTED; +import java.time.Clock; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; import java.util.Set; +import javax.inject.Inject; + import org.apache.james.protocols.smtp.SMTPSession; import org.apache.james.protocols.smtp.hook.HeloHook; import org.apache.james.protocols.smtp.hook.HookResult; import com.google.common.collect.ImmutableSet; -public class DSNEhloHook implements HeloHook { +public class FutureReleaseEHLOHook implements HeloHook { + private final Clock clock; + + @Inject + public FutureReleaseEHLOHook(Clock clock) { + this.clock = clock; + } + @Override - public Set<String> implementedEsmtpFeatures() { - return ImmutableSet.of("DSN"); + public Set<String> implementedEsmtpFeatures(SMTPSession session) { + if (session.getUsername() != null) { + Instant now = LocalDateTime.now(clock).toInstant(ZoneOffset.UTC); + String dateAsString = DateTimeFormatter.ISO_INSTANT.withZone(ZoneId.of("UTC")).format(now.plus(MAX_HOLD_FOR_SUPPORTED)); + + return ImmutableSet.of("FUTURERELEASE " + MAX_HOLD_FOR_SUPPORTED.toSeconds() + " " + dateAsString); + } + return ImmutableSet.of(); } @Override @@ -38,3 +60,4 @@ public class DSNEhloHook implements HeloHook { return HookResult.DECLINED; } } + diff --git a/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/futurerelease/FutureReleaseMailParameterHook.java b/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/futurerelease/FutureReleaseMailParameterHook.java new file mode 100644 index 0000000000..a215f6a72d --- /dev/null +++ b/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/futurerelease/FutureReleaseMailParameterHook.java @@ -0,0 +1,123 @@ +/**************************************************************** + * 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.futurerelease; + +import static org.apache.james.protocols.api.ProtocolSession.State.Transaction; +import static org.apache.james.smtpserver.futurerelease.FutureReleaseParameters.HOLDFOR_PARAMETER; +import static org.apache.james.smtpserver.futurerelease.FutureReleaseParameters.HOLDUNTIL_PARAMETER; +import static org.apache.james.smtpserver.futurerelease.FutureReleaseParameters.MAX_HOLD_FOR_SUPPORTED; + +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; + +import javax.inject.Inject; + +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 FutureReleaseMailParameterHook implements MailParametersHook { + private static final Logger LOGGER = LoggerFactory.getLogger(FutureReleaseMailParameterHook.class); + + public static final ProtocolSession.AttachmentKey<FutureReleaseParameters.HoldFor> FUTURERELEASE_HOLDFOR = ProtocolSession.AttachmentKey.of("FUTURERELEASE_HOLDFOR", FutureReleaseParameters.HoldFor.class); + + private final Clock clock; + + @Inject + public FutureReleaseMailParameterHook(Clock clock) { + this.clock = clock; + } + + @Override + public HookResult doMailParameter(SMTPSession session, String paramName, String paramValue) { + if (session.getUsername() == null) { + LOGGER.debug("Needs to be logged in in order to use future release extension"); + return HookResult.builder() + .hookReturnCode(HookReturnCode.deny()) + .smtpDescription("Needs to be logged in in order to use future release extension") + .build(); + } + + try { + Duration requestedHoldFor = evaluateHoldFor(paramName, paramValue); + + if (requestedHoldFor.compareTo(MAX_HOLD_FOR_SUPPORTED) > 0) { + LOGGER.debug("HoldFor is greater than max-future-release-interval or holdUntil exceeded max-future-release-date-time"); + return HookResult.builder() + .smtpReturnCode(SMTPRetCode.SYNTAX_ERROR_ARGUMENTS) + .hookReturnCode(HookReturnCode.deny()) + .smtpDescription("HoldFor is greater than max-future-release-interval or holdUntil exceeded max-future-release-date-time") + .build(); + } + if (requestedHoldFor.isNegative()) { + LOGGER.debug("HoldFor value is negative or holdUntil value is before now"); + return HookResult.builder() + .hookReturnCode(HookReturnCode.deny()) + .smtpReturnCode(SMTPRetCode.SYNTAX_ERROR_ARGUMENTS) + .smtpDescription("HoldFor value is negative or holdUntil value is before now") + .build(); + } + if (session.getAttachment(FUTURERELEASE_HOLDFOR, Transaction).isPresent()) { + LOGGER.debug("Mail parameter cannot contains both holdFor and holdUntil parameters"); + return HookResult.builder() + .hookReturnCode(HookReturnCode.deny()) + .smtpDescription("Mail parameter cannot contains both holdFor and holdUntil parameters") + .build(); + } + session.setAttachment(FUTURERELEASE_HOLDFOR, FutureReleaseParameters.HoldFor.of(requestedHoldFor), Transaction); + return HookResult.DECLINED; + } catch (IllegalArgumentException e) { + LOGGER.debug("Incorrect syntax when handling FUTURE-RELEASE mail parameter", e); + return HookResult.builder() + .hookReturnCode(HookReturnCode.deny()) + .smtpReturnCode(SMTPRetCode.SYNTAX_ERROR_ARGUMENTS) + .smtpDescription("Incorrect syntax when handling FUTURE-RELEASE mail parameter") + .build(); + } + } + + private Duration evaluateHoldFor(String paramName, String paramValue) { + if (paramName.equals(HOLDFOR_PARAMETER)) { + return Duration.ofSeconds(Long.parseLong(paramValue)); + } + if (paramName.equals(HOLDUNTIL_PARAMETER)) { + DateTimeFormatter formatter = DateTimeFormatter.ISO_INSTANT.withZone(ZoneId.of("Z")); + Instant now = LocalDateTime.now(clock).toInstant(ZoneOffset.UTC); + return Duration.between(now, ZonedDateTime.parse(paramValue, formatter).toInstant()); + } + throw new IllegalArgumentException("Invalid parameter name " + paramName); + } + + @Override + public String[] getMailParamNames() { + return new String[] {HOLDFOR_PARAMETER, HOLDUNTIL_PARAMETER}; + } +} \ No newline at end of file diff --git a/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/futurerelease/FutureReleaseParameters.java b/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/futurerelease/FutureReleaseParameters.java new file mode 100644 index 0000000000..cb297ed5b9 --- /dev/null +++ b/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/futurerelease/FutureReleaseParameters.java @@ -0,0 +1,70 @@ +/**************************************************************** + * 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.futurerelease; + +import java.time.Duration; +import java.util.Objects; + +import com.google.common.base.MoreObjects; +import com.google.common.base.Preconditions; + +public class FutureReleaseParameters { + public static final String HOLDFOR_PARAMETER = "HOLDFOR"; + public static final String HOLDUNTIL_PARAMETER = "HOLDUNTIL"; + public static final Duration MAX_HOLD_FOR_SUPPORTED = Duration.ofDays(1); + + public static class HoldFor { + public static HoldFor of(Duration value) { + Preconditions.checkNotNull(value); + return new HoldFor(value); + } + + private final Duration value; + + private HoldFor(Duration value) { + this.value = value; + } + + public Duration value() { + return value; + } + + @Override + public final boolean equals(Object o) { + if (o instanceof HoldFor) { + HoldFor holdFor = (HoldFor) o; + return Objects.equals(this.value, holdFor.value); + } + return false; + } + + @Override + public final int hashCode() { + return Objects.hash(value); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("value", value) + .toString(); + } + } +} diff --git a/server/protocols/protocols-smtp/src/test/java/org/apache/james/smtpserver/FutureReleaseTest.java b/server/protocols/protocols-smtp/src/test/java/org/apache/james/smtpserver/FutureReleaseTest.java new file mode 100644 index 0000000000..dc87df8f8e --- /dev/null +++ b/server/protocols/protocols-smtp/src/test/java/org/apache/james/smtpserver/FutureReleaseTest.java @@ -0,0 +1,398 @@ +/**************************************************************** + * 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 com.google.common.collect.ImmutableList; +import com.google.inject.TypeLiteral; +import org.apache.commons.configuration2.BaseHierarchicalConfiguration; +import org.apache.commons.net.smtp.SMTPClient; +import org.apache.james.UserEntityValidator; +import org.apache.james.core.Domain; +import org.apache.james.core.Username; +import org.apache.james.dnsservice.api.DNSService; +import org.apache.james.dnsservice.api.InMemoryDNSService; +import org.apache.james.domainlist.api.DomainList; +import org.apache.james.domainlist.lib.DomainListConfiguration; +import org.apache.james.domainlist.memory.MemoryDomainList; +import org.apache.james.filesystem.api.FileSystem; +import org.apache.james.mailbox.Authorizator; +import org.apache.james.mailrepository.api.MailRepositoryStore; +import org.apache.james.mailrepository.api.Protocol; +import org.apache.james.mailrepository.memory.*; +import org.apache.james.metrics.api.Metric; +import org.apache.james.metrics.api.MetricFactory; +import org.apache.james.metrics.tests.RecordingMetricFactory; +import org.apache.james.protocols.api.utils.ProtocolServerUtils; +import org.apache.james.protocols.lib.mock.MockProtocolHandlerLoader; +import org.apache.james.queue.api.MailQueueFactory; +import org.apache.james.queue.api.ManageableMailQueue; +import org.apache.james.queue.api.RawMailQueueItemDecoratorFactory; +import org.apache.james.queue.memory.MemoryMailQueueFactory; +import org.apache.james.rrt.api.AliasReverseResolver; +import org.apache.james.rrt.api.CanSendFrom; +import org.apache.james.rrt.api.RecipientRewriteTable; +import org.apache.james.rrt.api.RecipientRewriteTableConfiguration; +import org.apache.james.rrt.lib.AliasReverseResolverImpl; +import org.apache.james.rrt.lib.CanSendFromImpl; +import org.apache.james.rrt.memory.MemoryRecipientRewriteTable; +import org.apache.james.server.core.configuration.Configuration; +import org.apache.james.server.core.configuration.FileConfigurationProvider; +import org.apache.james.server.core.filesystem.FileSystemImpl; +import org.apache.james.smtpserver.netty.SMTPServer; +import org.apache.james.smtpserver.netty.SmtpMetricsImpl; +import org.apache.james.user.api.UsersRepository; +import org.apache.james.user.memory.MemoryUsersRepository; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneId; +import java.time.chrono.ChronoZonedDateTime; +import java.util.Base64; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class FutureReleaseTest { + public static final String LOCAL_DOMAIN = "example.local"; + public static final Username BOB = Username.of("bob@localhost"); + public static final String PASSWORD = "bobpwd"; + private static final Instant DATE = Instant.parse("2023-04-14T10:00:00.00Z"); + private static final Clock CLOCK = Clock.fixed(DATE, ZoneId.of("Z")); + + protected MemoryDomainList domainList; + protected MemoryUsersRepository usersRepository; + protected SMTPServerTest.AlterableDNSServer dnsServer; + protected MemoryMailRepositoryStore mailRepositoryStore; + protected FileSystemImpl fileSystem; + protected Configuration configuration; + protected MockProtocolHandlerLoader chain; + protected MemoryMailQueueFactory queueFactory; + protected MemoryMailQueueFactory.MemoryCacheableMailQueue queue; + + private SMTPServer smtpServer; + + @BeforeEach + void setUp() throws Exception { + domainList = new MemoryDomainList(new InMemoryDNSService()); + domainList.configure(DomainListConfiguration.DEFAULT); + + domainList.addDomain(Domain.of(LOCAL_DOMAIN)); + domainList.addDomain(Domain.of("examplebis.local")); + usersRepository = MemoryUsersRepository.withVirtualHosting(domainList); + usersRepository.addUser(BOB, PASSWORD); + + createMailRepositoryStore(); + + setUpFakeLoader(); + setUpSMTPServer(); + + smtpServer.configure(FileConfigurationProvider.getConfig( + ClassLoader.getSystemResourceAsStream("smtpserver-futurerelease.xml"))); + smtpServer.init(); + } + + protected void createMailRepositoryStore() throws Exception { + configuration = Configuration.builder() + .workingDirectory("../") + .configurationFromClasspath() + .build(); + fileSystem = new FileSystemImpl(configuration.directories()); + MemoryMailRepositoryUrlStore urlStore = new MemoryMailRepositoryUrlStore(); + + MailRepositoryStoreConfiguration configuration = MailRepositoryStoreConfiguration.forItems( + new MailRepositoryStoreConfiguration.Item( + ImmutableList.of(new Protocol("memory")), + MemoryMailRepository.class.getName(), + new BaseHierarchicalConfiguration())); + + mailRepositoryStore = new MemoryMailRepositoryStore(urlStore, new SimpleMailRepositoryLoader(), configuration); + mailRepositoryStore.init(); + } + + protected SMTPServer createSMTPServer(SmtpMetricsImpl smtpMetrics) { + return new SMTPServer(smtpMetrics); + } + + protected void setUpSMTPServer() { + SmtpMetricsImpl smtpMetrics = mock(SmtpMetricsImpl.class); + when(smtpMetrics.getCommandsMetric()).thenReturn(mock(Metric.class)); + when(smtpMetrics.getConnectionMetric()).thenReturn(mock(Metric.class)); + smtpServer = createSMTPServer(smtpMetrics); + smtpServer.setDnsService(dnsServer); + smtpServer.setFileSystem(fileSystem); + smtpServer.setProtocolHandlerLoader(chain); + } + + protected void setUpFakeLoader() { + dnsServer = new SMTPServerTest.AlterableDNSServer(); + + MemoryRecipientRewriteTable rewriteTable = new MemoryRecipientRewriteTable(); + rewriteTable.setConfiguration(RecipientRewriteTableConfiguration.DEFAULT_ENABLED); + AliasReverseResolver aliasReverseResolver = new AliasReverseResolverImpl(rewriteTable); + CanSendFrom canSendFrom = new CanSendFromImpl(rewriteTable, aliasReverseResolver); + queueFactory = new MemoryMailQueueFactory(new RawMailQueueItemDecoratorFactory(), CLOCK); + queue = queueFactory.createQueue(MailQueueFactory.SPOOL); + + chain = MockProtocolHandlerLoader.builder() + .put(binder -> binder.bind(DomainList.class).toInstance(domainList)) + .put(binder -> binder.bind(Clock.class).toInstance(CLOCK)) + .put(binder -> binder.bind(new TypeLiteral<MailQueueFactory<?>>() {}).toInstance(queueFactory)) + .put(binder -> binder.bind(RecipientRewriteTable.class).toInstance(rewriteTable)) + .put(binder -> binder.bind(CanSendFrom.class).toInstance(canSendFrom)) + .put(binder -> binder.bind(FileSystem.class).toInstance(fileSystem)) + .put(binder -> binder.bind(MailRepositoryStore.class).toInstance(mailRepositoryStore)) + .put(binder -> binder.bind(DNSService.class).toInstance(dnsServer)) + .put(binder -> binder.bind(UsersRepository.class).toInstance(usersRepository)) + .put(binder -> binder.bind(MetricFactory.class).to(RecordingMetricFactory.class)) + .put(binder -> binder.bind(UserEntityValidator.class).toInstance(UserEntityValidator.NOOP)) + .put(binder -> binder.bind(Authorizator.class).toInstance((userId, otherUserId) -> Authorizator.AuthorizationState.ALLOWED)) + .build(); + } + + @AfterEach + void tearDown() { + smtpServer.destroy(); + } + + @Test + void rejectFutureReleaseUsageWhenUnauthenticated() throws Exception { + SMTPClient smtpProtocol = new SMTPClient(); + InetSocketAddress bindedAddress = new ProtocolServerUtils(smtpServer).retrieveBindedAddress(); + smtpProtocol.connect(bindedAddress.getAddress().getHostAddress(), bindedAddress.getPort()); + + smtpProtocol.sendCommand("EHLO whatever.tld"); + smtpProtocol.sendCommand("MAIL FROM: <b...@whatever.tld> HOLDFOR=83200"); + + assertThat(smtpProtocol.getReplyString()).isEqualTo("554 Needs to be logged in in order to use future release extension\r\n"); + } + + @Test + void ehloShouldAdvertiseFutureReleaseExtension() throws Exception { + SMTPClient smtpProtocol = new SMTPClient(); + InetSocketAddress bindedAddress = new ProtocolServerUtils(smtpServer).retrieveBindedAddress(); + smtpProtocol.connect(bindedAddress.getAddress().getHostAddress(), bindedAddress.getPort()); + authenticate(smtpProtocol); + smtpProtocol.sendCommand("EHLO localhost"); + + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(smtpProtocol.getReplyCode()).isEqualTo(250); + softly.assertThat(smtpProtocol.getReplyString()).contains("250 FUTURERELEASE 86400 2023-04-15T10:00:00Z"); + }); + } + + @Test + void ehloShouldNotAdvertiseFutureReleaseExtensionWhenUnauthenticated() throws Exception { + SMTPClient smtpProtocol = new SMTPClient(); + InetSocketAddress bindedAddress = new ProtocolServerUtils(smtpServer).retrieveBindedAddress(); + smtpProtocol.connect(bindedAddress.getAddress().getHostAddress(), bindedAddress.getPort()); + smtpProtocol.sendCommand("EHLO localhost"); + + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(smtpProtocol.getReplyCode()).isEqualTo(250); + softly.assertThat(smtpProtocol.getReplyString()).doesNotContain("250 FUTURERELEASE 86400 2023-04-15T10:00:00Z"); + }); + } + + private void authenticate(SMTPClient 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); + } + + @Test + void testSuccessCaseWithHoldForParams() throws Exception { + SMTPClient smtpProtocol = new SMTPClient(); + InetSocketAddress bindedAddress = new ProtocolServerUtils(smtpServer).retrieveBindedAddress(); + smtpProtocol.connect(bindedAddress.getAddress().getHostAddress(), bindedAddress.getPort()); + authenticate(smtpProtocol); + + smtpProtocol.sendCommand("EHLO localhost"); + smtpProtocol.sendCommand("MAIL FROM: <bob@localhost> HOLDFOR=83200"); + smtpProtocol.sendCommand("RCPT TO:<rcpt@localhost>"); + smtpProtocol.sendShortMessageData("Subject: test mail\r\n\r\nTest body testSimpleMailSendWithFutureRelease\r\n.\r\n"); + + ManageableMailQueue.MailQueueIterator browse = queue.browse(); + assertThat(browse.hasNext()).isTrue(); + assertThat(browse.next().getNextDelivery().map(ChronoZonedDateTime::toInstant)) + .contains(DATE.plusSeconds(83200)); + } + + @Test + void testSuccessCaseWithHoldUntilParams() throws Exception { + SMTPClient smtpProtocol = new SMTPClient(); + InetSocketAddress bindedAddress = new ProtocolServerUtils(smtpServer).retrieveBindedAddress(); + smtpProtocol.connect(bindedAddress.getAddress().getHostAddress(), bindedAddress.getPort()); + authenticate(smtpProtocol); + + smtpProtocol.sendCommand("EHLO localhost"); + smtpProtocol.sendCommand("MAIL FROM: <bob@localhost> HOLDUNTIL=2023-04-14T10:30:00Z"); + smtpProtocol.sendCommand("RCPT TO:<rcpt@localhost>"); + smtpProtocol.sendShortMessageData("Subject: test mail\r\n\r\nTest body testSimpleMailSendWithFutureRelease\r\n.\r\n"); + + ManageableMailQueue.MailQueueIterator browse = queue.browse(); + assertThat(browse.hasNext()).isTrue(); + assertThat(browse.next().getNextDelivery().map(ChronoZonedDateTime::toInstant)) + .contains(Instant.parse("2023-04-14T10:30:00Z")); + } + + @Test + void mailShouldBeRejectedWhenExceedingMaxFutureReleaseInterval() throws Exception { + SMTPClient smtpProtocol = new SMTPClient(); + InetSocketAddress bindedAddress = new ProtocolServerUtils(smtpServer).retrieveBindedAddress(); + smtpProtocol.connect(bindedAddress.getAddress().getHostAddress(), bindedAddress.getPort()); + authenticate(smtpProtocol); + + smtpProtocol.sendCommand("EHLO localhost"); + smtpProtocol.sendCommand("MAIL FROM: <bob@localhost> HOLDFOR=93200"); + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(smtpProtocol.getReplyCode()).isEqualTo(501); + softly.assertThat(smtpProtocol.getReplyString()).contains("501 HoldFor is greater than max-future-release-interval or holdUntil exceeded max-future-release-date-time"); + }); + } + + @Test + void mailShouldBeRejectedWhenInvalidHoldFor() throws Exception { + SMTPClient smtpProtocol = new SMTPClient(); + InetSocketAddress bindedAddress = new ProtocolServerUtils(smtpServer).retrieveBindedAddress(); + smtpProtocol.connect(bindedAddress.getAddress().getHostAddress(), bindedAddress.getPort()); + authenticate(smtpProtocol); + + smtpProtocol.sendCommand("EHLO localhost"); + smtpProtocol.sendCommand("MAIL FROM: <bob@localhost> HOLDFOR=BAD"); + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(smtpProtocol.getReplyCode()).isEqualTo(501); + softly.assertThat(smtpProtocol.getReplyString()).contains("501 Incorrect syntax when handling FUTURE-RELEASE mail parameter"); + }); + } + + @Test + void mailShouldBeRejectedWhenInvalidHoldUntil() throws Exception { + SMTPClient smtpProtocol = new SMTPClient(); + InetSocketAddress bindedAddress = new ProtocolServerUtils(smtpServer).retrieveBindedAddress(); + smtpProtocol.connect(bindedAddress.getAddress().getHostAddress(), bindedAddress.getPort()); + authenticate(smtpProtocol); + + smtpProtocol.sendCommand("EHLO localhost"); + smtpProtocol.sendCommand("MAIL FROM: <bob@localhost> HOLDUNTIL=BAD"); + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(smtpProtocol.getReplyCode()).isNotEqualTo(250); + softly.assertThat(smtpProtocol.getReplyString()).doesNotContain("250"); + }); + } + + @Test + void mailShouldBeRejectedWhenHoldUntilIsADate() throws Exception { + SMTPClient smtpProtocol = new SMTPClient(); + InetSocketAddress bindedAddress = new ProtocolServerUtils(smtpServer).retrieveBindedAddress(); + smtpProtocol.connect(bindedAddress.getAddress().getHostAddress(), bindedAddress.getPort()); + authenticate(smtpProtocol); + + smtpProtocol.sendCommand("EHLO localhost"); + smtpProtocol.sendCommand("MAIL FROM: <bob@localhost> HOLDUNTIL=2023-04-15"); + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(smtpProtocol.getReplyCode()).isNotEqualTo(250); + softly.assertThat(smtpProtocol.getReplyString()).doesNotContain("250"); + }); + } + + @Test + void mailShouldBeRejectedWhenMaxFutureReleaseDateTime() throws Exception { + SMTPClient smtpProtocol = new SMTPClient(); + InetSocketAddress bindedAddress = new ProtocolServerUtils(smtpServer).retrieveBindedAddress(); + smtpProtocol.connect(bindedAddress.getAddress().getHostAddress(), bindedAddress.getPort()); + authenticate(smtpProtocol); + + smtpProtocol.sendCommand("EHLO localhost"); + smtpProtocol.sendCommand("MAIL FROM: <bob@localhost> HOLDUNTIL=2023-04-15T11:00:00Z"); + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(smtpProtocol.getReplyCode()).isEqualTo(501); + softly.assertThat(smtpProtocol.getReplyString()).contains("501 HoldFor is greater than max-future-release-interval or holdUntil exceeded max-future-release-date-time"); + }); + } + + @Test + void mailShouldBeRejectedWhenHoldForIsNegative() throws Exception { + SMTPClient smtpProtocol = new SMTPClient(); + InetSocketAddress bindedAddress = new ProtocolServerUtils(smtpServer).retrieveBindedAddress(); + smtpProtocol.connect(bindedAddress.getAddress().getHostAddress(), bindedAddress.getPort()); + authenticate(smtpProtocol); + + smtpProtocol.sendCommand("EHLO localhost"); + smtpProtocol.sendCommand("MAIL FROM: <bob@localhost> HOLDFOR=-30"); + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(smtpProtocol.getReplyCode()).isEqualTo(501); + softly.assertThat(smtpProtocol.getReplyString()).contains("501 HoldFor value is negative or holdUntil value is before now"); + }); + } + + @Test + void mailShouldBeRejectedWhenHoldUntilBeforeNow() throws Exception { + SMTPClient smtpProtocol = new SMTPClient(); + InetSocketAddress bindedAddress = new ProtocolServerUtils(smtpServer).retrieveBindedAddress(); + smtpProtocol.connect(bindedAddress.getAddress().getHostAddress(), bindedAddress.getPort()); + authenticate(smtpProtocol); + + smtpProtocol.sendCommand("EHLO localhost"); + smtpProtocol.sendCommand("MAIL FROM: <bob@localhost> HOLDUNTIL=2023-04-13T05:00:00Z"); + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(smtpProtocol.getReplyCode()).isEqualTo(501); + softly.assertThat(smtpProtocol.getReplyString()).contains("501 HoldFor value is negative or holdUntil value is before now"); + }); + } + + @Test + void mailShouldBeRejectedWhenMailParametersContainBothHoldForAndHoldUntil() throws Exception { + SMTPClient smtpProtocol = new SMTPClient(); + InetSocketAddress bindedAddress = new ProtocolServerUtils(smtpServer).retrieveBindedAddress(); + smtpProtocol.connect(bindedAddress.getAddress().getHostAddress(), bindedAddress.getPort()); + authenticate(smtpProtocol); + + smtpProtocol.sendCommand("EHLO localhost"); + smtpProtocol.sendCommand("MAIL FROM: <bob@localhost> HOLDFOR=83017 HOLDUNTIL=2023-04-12T11:00:00Z"); + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(smtpProtocol.getReplyCode()).isEqualTo(501); + softly.assertThat(smtpProtocol.getReplyString()).contains("501 HoldFor value is negative or holdUntil value is before now"); + }); + } + + @Test + void mailShouldBeSentWhenThereIsNoMailParameters() throws Exception { + SMTPClient smtpProtocol = new SMTPClient(); + InetSocketAddress bindedAddress = new ProtocolServerUtils(smtpServer).retrieveBindedAddress(); + smtpProtocol.connect(bindedAddress.getAddress().getHostAddress(), bindedAddress.getPort()); + authenticate(smtpProtocol); + + smtpProtocol.sendCommand("EHLO localhost"); + smtpProtocol.sendCommand("MAIL FROM: <bob@localhost>"); + smtpProtocol.sendCommand("RCPT TO:<rcpt@localhost>"); + smtpProtocol.sendShortMessageData("Subject: test mail\r\n\r\nTest body testSimpleMailSendWithFutureRelease\r\n.\r\n"); + + assertThat(queue.getSize()).isEqualTo(1L); + } +} diff --git a/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/dsn/DSNEhloHook.java b/server/protocols/protocols-smtp/src/test/java/org/apache/james/smtpserver/futurerelease/FutureReleaseParametersTest.java similarity index 68% copy from server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/dsn/DSNEhloHook.java copy to server/protocols/protocols-smtp/src/test/java/org/apache/james/smtpserver/futurerelease/FutureReleaseParametersTest.java index a10a340fb3..650d916f38 100644 --- a/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/dsn/DSNEhloHook.java +++ b/server/protocols/protocols-smtp/src/test/java/org/apache/james/smtpserver/futurerelease/FutureReleaseParametersTest.java @@ -17,24 +17,15 @@ * under the License. * ****************************************************************/ -package org.apache.james.smtpserver.dsn; +package org.apache.james.smtpserver.futurerelease; -import java.util.Set; +import org.junit.jupiter.api.Test; -import org.apache.james.protocols.smtp.SMTPSession; -import org.apache.james.protocols.smtp.hook.HeloHook; -import org.apache.james.protocols.smtp.hook.HookResult; +import nl.jqno.equalsverifier.EqualsVerifier; -import com.google.common.collect.ImmutableSet; - -public class DSNEhloHook implements HeloHook { - @Override - public Set<String> implementedEsmtpFeatures() { - return ImmutableSet.of("DSN"); - } - - @Override - public HookResult doHelo(SMTPSession session, String helo) { - return HookResult.DECLINED; +class FutureReleaseParametersTest { + @Test + void testEqualsVerifiersForHoldForClass() { + EqualsVerifier.forClass(FutureReleaseParameters.HoldFor.class).verify(); } -} +} \ No newline at end of file diff --git a/server/protocols/protocols-smtp/src/test/resources/smtpserver-futurerelease.xml b/server/protocols/protocols-smtp/src/test/resources/smtpserver-futurerelease.xml new file mode 100644 index 0000000000..d5b4327cf1 --- /dev/null +++ b/server/protocols/protocols-smtp/src/test/resources/smtpserver-futurerelease.xml @@ -0,0 +1,50 @@ +<?xml version="1.0"?> + +<!-- + 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. + --> + +<!-- Read https://james.apache.org/server/config-smtp-lmtp.html#SMTP_Configuration for further details --> + +<smtpserver enabled="true"> + <bind>0.0.0.0:0</bind> + <connectionBacklog>200</connectionBacklog> + <tls socketTLS="false" startTLS="false"> + <keystore>file://conf/keystore</keystore> + <secret>james72laBalle</secret> + <provider>org.bouncycastle.jce.provider.BouncyCastleProvider</provider> + <algorithm>SunX509</algorithm> + </tls> + <connectiontimeout>360</connectiontimeout> + <connectionLimit>0</connectionLimit> + <connectionLimitPerIP>0</connectionLimitPerIP> + <auth> + <announce>forUnauthorizedAddresses</announce> + <requireSSL>false</requireSSL> + </auth> + <verifyIdentity>true</verifyIdentity> + <maxmessagesize>0</maxmessagesize> + <addressBracketsEnforcement>true</addressBracketsEnforcement> + <smtpGreeting>Apache JAMES awesome SMTP Server</smtpGreeting> + <handlerchain> + <handler class="org.apache.james.smtpserver.futurerelease.FutureReleaseEHLOHook"/> + <handler class="org.apache.james.smtpserver.futurerelease.FutureReleaseMailParameterHook"/> + <handler class="org.apache.james.smtpserver.CoreCmdHandlerLoader"/> + </handlerchain> + <gracefulShutdown>false</gracefulShutdown> +</smtpserver> \ No newline at end of file --------------------------------------------------------------------- To unsubscribe, e-mail: notifications-unsubscr...@james.apache.org For additional commands, e-mail: notifications-h...@james.apache.org