You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@james.apache.org by rc...@apache.org on 2020/12/30 03:35:22 UTC

[james-project] 10/29: JAMES-3431 RemoteDelivery: should cary over DSN parameters

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

commit 8d0889e783fa56c7c30381b2c744a7d56d850c75
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Tue Dec 29 11:10:42 2020 +0700

    JAMES-3431 RemoteDelivery: should cary over DSN parameters
    
    Limitations:
     - ORCPT parameter is not handled by javax.mail
     - NOTIFY parameter is global to a SMTPMessage instance, thus
       if NOTIFY parameters is not similar across recipients we
       need to transmit a copy of the message for each one of them
---
 .../java/org/apache/james/smtp/DSNRelayTest.java   | 335 +++++++++++++++++++++
 .../remote/delivery/MailDelivrerToHost.java        |  83 ++++-
 .../mock/smtp/server/ConfigurationClient.java      |   1 +
 3 files changed, 418 insertions(+), 1 deletion(-)

diff --git a/server/mailet/integration-testing/src/test/java/org/apache/james/smtp/DSNRelayTest.java b/server/mailet/integration-testing/src/test/java/org/apache/james/smtp/DSNRelayTest.java
new file mode 100644
index 0000000..a938790
--- /dev/null
+++ b/server/mailet/integration-testing/src/test/java/org/apache/james/smtp/DSNRelayTest.java
@@ -0,0 +1,335 @@
+/****************************************************************
+ * 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;
+
+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.calmlyAwait;
+import static org.apache.james.util.docker.Images.MOCK_SMTP_SERVER;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.awaitility.Duration.TEN_SECONDS;
+
+import org.apache.commons.net.smtp.AuthenticatingSMTPClient;
+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.ConfigurationClient;
+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.modules.protocols.SmtpGuiceProbe;
+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.transport.mailets.RemoteDelivery;
+import org.apache.james.transport.matchers.All;
+import org.apache.james.util.Host;
+import org.apache.james.util.docker.DockerContainer;
+import org.apache.james.utils.DataProbeImpl;
+import org.apache.james.utils.SMTPMessageSender;
+import org.apache.james.utils.TestIMAPClient;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class DSNRelayTest {
+    private static final Logger LOGGER = LoggerFactory.getLogger(DSNRelayTest.class);
+
+    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 static final String RECIPIENT1 = "touser1@" + ANOTHER_DOMAIN;
+    private static final String RECIPIENT2 = "touser2@" + ANOTHER_DOMAIN;
+
+    private InMemoryDNSService inMemoryDNSService;
+    private ConfigurationClient mockSMTPConfiguration;
+
+    @Rule
+    public TemporaryFolder temporaryFolder = new TemporaryFolder();
+    @Rule
+    public TestIMAPClient testIMAPClient = new TestIMAPClient();
+    @Rule
+    public SMTPMessageSender messageSender = new SMTPMessageSender(DEFAULT_DOMAIN);
+    @ClassRule
+    public static DockerContainer mockSmtp = DockerContainer.fromName(MOCK_SMTP_SERVER)
+        .withLogConsumer(outputFrame -> LOGGER.debug("MockSMTP 1: " + outputFrame.getUtf8String()));
+
+    private TemporaryJamesServer jamesServer;
+
+    @Before
+    public void setUp() throws Exception {
+        inMemoryDNSService = new InMemoryDNSService()
+            .registerMxRecord(DEFAULT_DOMAIN, LOCALHOST_IP)
+            .registerMxRecord(ANOTHER_DOMAIN, mockSmtp.getContainerIp());
+
+        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())
+                .putProcessor(CommonProcessors.bounces()))
+            .withSmtpConfiguration(SmtpConfiguration.builder()
+                .addHook(DSNEhloHook.class.getName())
+                .addHook(DSNMailParameterHook.class.getName())
+                .addHook(DSNRcptParameterHook.class.getName())
+                .addHook(DSNMessageHook.class.getName()))
+            .build(temporaryFolder.newFolder());
+        jamesServer.start();
+
+        jamesServer.getProbe(DataProbeImpl.class)
+            .fluent()
+            .addDomain(DEFAULT_DOMAIN)
+            .addUser(FROM, PASSWORD);
+
+        mockSMTPConfiguration = configurationClient(mockSmtp);
+        mockSMTPConfiguration.setSMTPExtensions(SMTPExtensions.of(SMTPExtension.of("dsn")));
+
+        assertThat(mockSMTPConfiguration.version()).isEqualTo("0.2");
+    }
+
+    @After
+    public void tearDown() {
+        jamesServer.shutdown();
+
+        mockSMTPConfiguration.cleanServer();
+    }
+
+    @Ignore("JAMES-3431 No javax.mail support for ORCPT DSN parameter...")
+    @Test
+    public void orcptIsUnsupported() throws Exception {
+        AuthenticatingSMTPClient smtpClient = new AuthenticatingSMTPClient("TLS", "UTF-8");
+
+        try {
+            smtpClient.connect("localhost", jamesServer.getProbe(SmtpGuiceProbe.class).getSmtpPort().getValue());
+            smtpClient.ehlo(DEFAULT_DOMAIN);
+            smtpClient.mail("<" + FROM + "> RET=HDRS ENVID=gabouzomeuh");
+            smtpClient.rcpt("<" + RECIPIENT + "> ORCPT=rfc822;" + RECIPIENT + " NOTIFY=FAILURE,DELAY");
+            smtpClient.sendShortMessageData("A short message...");
+        } finally {
+            smtpClient.disconnect();
+        }
+
+        calmlyAwait.atMost(TEN_SECONDS).untilAsserted(() -> assertThat(mockSMTPConfiguration.listMails())
+            .hasSize(3)
+            .extracting(Mail::getEnvelope)
+            .containsExactly(Mail.Envelope.builder()
+                .from(new MailAddress(FROM))
+                .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))
+                    .addParameter(Mail.Parameter.builder()
+                        .name("ORCPT")
+                        .value("rfc822;" + RECIPIENT)
+                        .build())
+                    .addParameter(Mail.Parameter.builder()
+                        .name("notify")
+                        .value("FAILURE,DELAY")
+                        .build())
+                    .build())
+                .build()));
+    }
+
+    @Test
+    public void remoteDeliveryShouldCarryOverDSNParameters() throws Exception {
+        AuthenticatingSMTPClient smtpClient = new AuthenticatingSMTPClient("TLS", "UTF-8");
+
+        try {
+            smtpClient.connect("localhost", jamesServer.getProbe(SmtpGuiceProbe.class).getSmtpPort().getValue());
+            smtpClient.ehlo(DEFAULT_DOMAIN);
+            smtpClient.mail("<" + FROM + "> RET=HDRS ENVID=gabouzomeuh");
+            smtpClient.rcpt("<" + RECIPIENT + "> NOTIFY=FAILURE,DELAY");
+            smtpClient.rcpt("<" + RECIPIENT1 + "> NOTIFY=NEVER");
+            smtpClient.rcpt("<" + RECIPIENT2 + ">");
+            smtpClient.sendShortMessageData("A short message...");
+        } finally {
+            smtpClient.disconnect();
+        }
+
+        calmlyAwait.atMost(TEN_SECONDS).untilAsserted(() -> assertThat(mockSMTPConfiguration.listMails())
+            .hasSize(3)
+            .extracting(Mail::getEnvelope)
+            .containsOnly(Mail.Envelope.builder()
+                .from(new MailAddress(FROM))
+                .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))
+                    .addParameter(Mail.Parameter.builder()
+                        .name("NOTIFY")
+                        .value("FAILURE,DELAY")
+                        .build())
+                    .build())
+                .build(),
+            Mail.Envelope.builder()
+                .from(new MailAddress(FROM))
+                .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(RECIPIENT1))
+                    .addParameter(Mail.Parameter.builder()
+                        .name("NOTIFY")
+                        .value("NEVER")
+                        .build())
+                    .build())
+                .build(),
+            Mail.Envelope.builder()
+                .from(new MailAddress(FROM))
+                .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(RECIPIENT2))
+                    .build())
+                .build()));
+    }
+
+    @Test
+    public void remoteDeliveryShouldCarryOverDSNParametersWhenSingleRecipient() throws Exception {
+        AuthenticatingSMTPClient smtpClient = new AuthenticatingSMTPClient("TLS", "UTF-8");
+
+        try {
+            smtpClient.connect("localhost", jamesServer.getProbe(SmtpGuiceProbe.class).getSmtpPort().getValue());
+            smtpClient.ehlo(DEFAULT_DOMAIN);
+            smtpClient.mail("<" + FROM + "> RET=HDRS ENVID=gabouzomeuh");
+            smtpClient.rcpt("<" + RECIPIENT + "> NOTIFY=FAILURE,DELAY");
+            smtpClient.sendShortMessageData("A short message...");
+        } finally {
+            smtpClient.disconnect();
+        }
+
+        calmlyAwait.atMost(TEN_SECONDS).untilAsserted(() -> assertThat(mockSMTPConfiguration.listMails())
+            .hasSize(1)
+            .extracting(Mail::getEnvelope)
+            .containsExactly(Mail.Envelope.builder()
+                .from(new MailAddress(FROM))
+                .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))
+                    .addParameter(Mail.Parameter.builder()
+                        .name("NOTIFY")
+                        .value("FAILURE,DELAY")
+                        .build())
+                    .build())
+                .build()));
+    }
+
+    @Test
+    public void remoteDeliveryShouldCarryOverDSNMailParameters() throws Exception {
+        AuthenticatingSMTPClient smtpClient = new AuthenticatingSMTPClient("TLS", "UTF-8");
+
+        try {
+            smtpClient.connect("localhost", jamesServer.getProbe(SmtpGuiceProbe.class).getSmtpPort().getValue());
+            smtpClient.ehlo(DEFAULT_DOMAIN);
+            smtpClient.mail("<" + FROM + "> RET=HDRS ENVID=gabouzomeuh");
+            smtpClient.rcpt("<" + RECIPIENT2 + ">");
+            smtpClient.sendShortMessageData("A short message...");
+        } finally {
+            smtpClient.disconnect();
+        }
+
+        calmlyAwait.atMost(TEN_SECONDS).untilAsserted(() -> assertThat(mockSMTPConfiguration.listMails())
+            .hasSize(1)
+            .extracting(Mail::getEnvelope)
+            .containsExactly(Mail.Envelope.builder()
+                .from(new MailAddress(FROM))
+                .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(RECIPIENT2))
+                    .build())
+                .build()));
+    }
+
+
+    private ProcessorConfiguration.Builder directResolutionTransport() {
+        return ProcessorConfiguration.transport()
+            .addMailet(MailetConfiguration.BCC_STRIPPER)
+            .addMailet(MailetConfiguration.LOCAL_DELIVERY)
+            .addMailet(MailetConfiguration.builder()
+                .mailet(RemoteDelivery.class)
+                .matcher(All.class)
+                .addProperty("outgoingQueue", "outgoing")
+                .addProperty("delayTime", "3 * 10 ms")
+                .addProperty("maxRetries", "3")
+                .addProperty("maxDnsProblemRetries", "0")
+                .addProperty("deliveryThreads", "2")
+                .addProperty("sendpartial", "true"));
+    }
+
+    private ConfigurationClient configurationClient(DockerContainer mockSmtp) {
+        return ConfigurationClient.from(
+            Host.from(mockSmtp.getHostIp(),
+                mockSmtp.getMappedPort(8000)));
+    }
+}
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 238a357..976b0c0 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
@@ -19,15 +19,28 @@
 
 package org.apache.james.transport.mailets.remote.delivery;
 
+import static com.sun.mail.smtp.SMTPMessage.NOTIFY_DELAY;
+import static com.sun.mail.smtp.SMTPMessage.NOTIFY_FAILURE;
+import static com.sun.mail.smtp.SMTPMessage.NOTIFY_NEVER;
+import static com.sun.mail.smtp.SMTPMessage.NOTIFY_SUCCESS;
+import static com.sun.mail.smtp.SMTPMessage.RETURN_FULL;
+import static com.sun.mail.smtp.SMTPMessage.RETURN_HDRS;
+
 import java.io.IOException;
 import java.util.Collection;
+import java.util.EnumSet;
+import java.util.Optional;
 import java.util.Properties;
 
+import javax.mail.Address;
 import javax.mail.MessagingException;
 import javax.mail.Session;
 import javax.mail.internet.InternetAddress;
 import javax.mail.internet.MimeMessage;
 
+import org.apache.commons.lang3.NotImplementedException;
+import org.apache.james.core.MailAddress;
+import org.apache.mailet.DsnParameters;
 import org.apache.mailet.HostAddress;
 import org.apache.mailet.Mail;
 import org.apache.mailet.MailetContext;
@@ -35,6 +48,8 @@ import org.apache.mailet.base.Converter7Bit;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.github.fge.lambdas.Throwing;
+import com.sun.mail.smtp.SMTPMessage;
 import com.sun.mail.smtp.SMTPTransport;
 
 @SuppressWarnings("deprecation")
@@ -68,7 +83,11 @@ public class MailDelivrerToHost {
             transport = (SMTPTransport) session.getTransport(outgoingMailServer);
             transport.setLocalHost(props.getProperty("mail.smtp.localhost", configuration.getHeloNameProvider().getHeloName()));
             connect(outgoingMailServer, transport);
-            transport.sendMessage(adaptToTransport(mail.getMessage(), transport), addr.toArray(InternetAddress[]::new));
+            if (mail.dsnParameters().isPresent()) {
+                sendDSNAwareEmail(mail, transport, addr);
+            } else {
+                transport.sendMessage(adaptToTransport(mail.getMessage(), transport), addr.toArray(InternetAddress[]::new));
+            }
             LOGGER.debug("Mail ({})  sent successfully to {} at {} from {} for {}", mail.getName(), outgoingMailServer.getHostName(),
                 outgoingMailServer.getHost(), props.get("mail.smtp.from"), mail.getRecipients());
         } finally {
@@ -76,6 +95,68 @@ public class MailDelivrerToHost {
         }
         return ExecutionResult.success();
     }
+    private void sendDSNAwareEmail(Mail mail, SMTPTransport transport, Collection<InternetAddress> addresses) {
+        addresses.forEach(Throwing.<InternetAddress>consumer(
+            address -> {
+                SMTPMessage smtpMessage = asSmtpMessage(mail, transport);
+                mail.dsnParameters()
+                    .flatMap(Throwing.<DsnParameters, Optional<DsnParameters.RecipientDsnParameters>>function(
+                        dsn -> Optional.ofNullable(dsn.getRcptParameters().get(new MailAddress(address.toString()))))
+                        .sneakyThrow())
+                    .flatMap(DsnParameters.RecipientDsnParameters::getNotifyParameter)
+                    .map(this::toJavaxNotify)
+                    .ifPresent(smtpMessage::setNotifyOptions);
+                InternetAddress[] rcpt = new InternetAddress[]{address};
+                transport.sendMessage(smtpMessage, rcpt);
+            }
+        ).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);
+        mail.dsnParameters().flatMap(DsnParameters::getEnvIdParameter)
+            .ifPresent(envId -> {
+                if (transport.supportsExtension("DSN")) {
+                    smtpMessage.setMailExtension("ENVID=" + envId.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 javax.mail parameters");
+        }
+    }
+
+    private int toJavaxNotify(EnumSet<DsnParameters.Notify> notifies) {
+        return notifies.stream()
+            .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 javax.mail parameters");
+        }
+    }
 
     private Properties getPropertiesForMail(Mail mail) {
         Properties props = session.getProperties();
diff --git a/server/mailet/mock-smtp-server/src/main/java/org/apache/james/mock/smtp/server/ConfigurationClient.java b/server/mailet/mock-smtp-server/src/main/java/org/apache/james/mock/smtp/server/ConfigurationClient.java
index dade1ba..484d628 100644
--- a/server/mailet/mock-smtp-server/src/main/java/org/apache/james/mock/smtp/server/ConfigurationClient.java
+++ b/server/mailet/mock-smtp-server/src/main/java/org/apache/james/mock/smtp/server/ConfigurationClient.java
@@ -210,6 +210,7 @@ public interface ConfigurationClient {
     default void cleanServer() {
         clearBehaviors();
         clearMails();
+        clearSMTPExtensions();
     }
 
     default BehaviorsParamsBuilder.CommandStep addNewBehavior() {


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