You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@james.apache.org by bt...@apache.org on 2021/05/14 02:04:39 UTC

[james-project] 04/09: JAMES-3574 Verify DSN support for LMTP

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 8f55920b5eee3e270e0f6966b8fa9835d89e2b8d
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Thu May 13 10:08:52 2021 +0700

    JAMES-3574 Verify DSN support for LMTP
    
     - We were missing the calls to James message hooks, eventually positionning the DSNs on the
     mail envelope.
    
    Also we slightly refactor tests:
     - Read LMTP data to be synchronous and thus NOT need to await
---
 .../james/lmtpserver/MailetContainerHandler.java   |   1 +
 .../apache/james/lmtpserver/LmtpServerTest.java    |  28 ++--
 .../lmtpserver/MailetContainerHandlerTest.java     | 143 ++++++++++++++++++---
 .../protocols-lmtp/src/test/resources/lmtpdsn.xml  |  39 ++++++
 4 files changed, 182 insertions(+), 29 deletions(-)

diff --git a/server/protocols/protocols-lmtp/src/main/java/org/apache/james/lmtpserver/MailetContainerHandler.java b/server/protocols/protocols-lmtp/src/main/java/org/apache/james/lmtpserver/MailetContainerHandler.java
index 3382331..628f88e 100644
--- a/server/protocols/protocols-lmtp/src/main/java/org/apache/james/lmtpserver/MailetContainerHandler.java
+++ b/server/protocols/protocols-lmtp/src/main/java/org/apache/james/lmtpserver/MailetContainerHandler.java
@@ -44,6 +44,7 @@ public class MailetContainerHandler extends DataLineJamesMessageHookHandler {
     @Override
     protected Response processExtensions(SMTPSession session, Mail mail) {
         try {
+            super.processExtensions(session, mail);
             mailProcessor.service(mail);
 
             return AbstractHookableCmdHandler.calcDefaultSMTPResponse(HookResult.builder()
diff --git a/server/protocols/protocols-lmtp/src/test/java/org/apache/james/lmtpserver/LmtpServerTest.java b/server/protocols/protocols-lmtp/src/test/java/org/apache/james/lmtpserver/LmtpServerTest.java
index 8db8842..dc2009c 100644
--- a/server/protocols/protocols-lmtp/src/test/java/org/apache/james/lmtpserver/LmtpServerTest.java
+++ b/server/protocols/protocols-lmtp/src/test/java/org/apache/james/lmtpserver/LmtpServerTest.java
@@ -61,7 +61,6 @@ import org.apache.james.server.core.configuration.Configuration;
 import org.apache.james.server.core.filesystem.FileSystemImpl;
 import org.apache.james.user.api.UsersRepository;
 import org.apache.james.user.memory.MemoryUsersRepository;
-import org.awaitility.Awaitility;
 import org.jboss.netty.util.HashedWheelTimer;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
@@ -145,29 +144,32 @@ class LmtpServerTest {
     void emailsShouldWellBeReceived() throws Exception {
         SocketChannel server = SocketChannel.open();
         server.connect(new InetSocketAddress(LOCALHOST_IP, getLmtpPort(lmtpServerFactory)));
+        server.read(ByteBuffer.allocate(1024));
 
         server.write(ByteBuffer.wrap(("LHLO <" + DOMAIN + ">\r\n").getBytes(StandardCharsets.UTF_8)));
+        server.read(ByteBuffer.allocate(1024));
         server.write(ByteBuffer.wrap(("MAIL FROM: <bob@" + DOMAIN + ">\r\n").getBytes(StandardCharsets.UTF_8)));
+        server.read(ByteBuffer.allocate(1024));
         server.write(ByteBuffer.wrap(("RCPT TO: <bo...@examplebis.local>\r\n").getBytes(StandardCharsets.UTF_8)));
+        server.read(ByteBuffer.allocate(1024));
         server.write(ByteBuffer.wrap(("DATA\r\n").getBytes(StandardCharsets.UTF_8)));
         server.read(ByteBuffer.allocate(1024)); // needed to synchronize
         server.write(ByteBuffer.wrap(("header:value\r\n\r\nbody").getBytes(StandardCharsets.UTF_8)));
         server.write(ByteBuffer.wrap(("\r\n").getBytes(StandardCharsets.UTF_8)));
         server.write(ByteBuffer.wrap((".").getBytes(StandardCharsets.UTF_8)));
         server.write(ByteBuffer.wrap(("\r\n").getBytes(StandardCharsets.UTF_8)));
+        server.read(ByteBuffer.allocate(1024));
         server.write(ByteBuffer.wrap(("QUIT\r\n").getBytes(StandardCharsets.UTF_8)));
 
-        Awaitility.await()
-            .untilAsserted(() -> {
-                Username username = Username.of("bob@examplebis.local");
-                MailboxSession systemSession = mailboxManager.createSystemSession(username);
-                assertThatCode(() ->
-                    assertThat(Flux.from(mailboxManager.getMailbox(MailboxPath.inbox(username), systemSession)
-                        .listMessagesMetadata(MessageRange.all(), systemSession))
-                        .count()
-                        .block())
-                        .isEqualTo(1))
-                    .doesNotThrowAnyException();
-            });
+
+        Username username = Username.of("bob@examplebis.local");
+        MailboxSession systemSession = mailboxManager.createSystemSession(username);
+        assertThatCode(() ->
+            assertThat(Flux.from(mailboxManager.getMailbox(MailboxPath.inbox(username), systemSession)
+                .listMessagesMetadata(MessageRange.all(), systemSession))
+                .count()
+                .block())
+                .isEqualTo(1))
+            .doesNotThrowAnyException();
     }
 }
\ No newline at end of file
diff --git a/server/protocols/protocols-lmtp/src/test/java/org/apache/james/lmtpserver/MailetContainerHandlerTest.java b/server/protocols/protocols-lmtp/src/test/java/org/apache/james/lmtpserver/MailetContainerHandlerTest.java
index 2087d14..ff4a7a4 100644
--- a/server/protocols/protocols-lmtp/src/test/java/org/apache/james/lmtpserver/MailetContainerHandlerTest.java
+++ b/server/protocols/protocols-lmtp/src/test/java/org/apache/james/lmtpserver/MailetContainerHandlerTest.java
@@ -22,16 +22,22 @@ package org.apache.james.lmtpserver;
 import static org.apache.james.jmap.JMAPTestingConstants.DOMAIN;
 import static org.apache.james.jmap.JMAPTestingConstants.LOCALHOST_IP;
 import static org.apache.james.lmtpserver.LmtpServerTest.getLmtpPort;
+import static org.apache.mailet.DsnParameters.Notify.DELAY;
+import static org.apache.mailet.DsnParameters.Notify.FAILURE;
+import static org.apache.mailet.DsnParameters.Notify.SUCCESS;
 import static org.assertj.core.api.Assertions.assertThat;
 
+import java.io.IOException;
 import java.net.InetSocketAddress;
 import java.nio.ByteBuffer;
 import java.nio.channels.SocketChannel;
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
+import java.util.EnumSet;
 import java.util.List;
 
 import org.apache.james.core.Domain;
+import org.apache.james.core.MailAddress;
 import org.apache.james.core.Username;
 import org.apache.james.dnsservice.api.DNSService;
 import org.apache.james.dnsservice.api.InMemoryDNSService;
@@ -56,14 +62,16 @@ import org.apache.james.server.core.configuration.Configuration;
 import org.apache.james.server.core.filesystem.FileSystemImpl;
 import org.apache.james.user.api.UsersRepository;
 import org.apache.james.user.memory.MemoryUsersRepository;
+import org.apache.mailet.DsnParameters;
 import org.apache.mailet.Mail;
-import org.awaitility.Awaitility;
 import org.jboss.netty.util.HashedWheelTimer;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.Test;
 
+import com.github.fge.lambdas.Throwing;
+
 class MailetContainerHandlerTest {
     static class RecordingMailProcessor implements MailProcessor {
         private final ArrayList<Mail> mails = new ArrayList<>();
@@ -86,7 +94,7 @@ class MailetContainerHandlerTest {
     }
 
     @Nested
-    class Normal {
+    class NormalTest {
 
         private RecordingMailProcessor recordingMailProcessor;
         private LMTPServerFactory lmtpServerFactory;
@@ -142,25 +150,127 @@ class MailetContainerHandlerTest {
         void emailShouldTriggerTheMailProcessing() throws Exception {
             SocketChannel server = SocketChannel.open();
             server.connect(new InetSocketAddress(LOCALHOST_IP, getLmtpPort(lmtpServerFactory)));
+            readBytes(server);
 
             server.write(ByteBuffer.wrap(("LHLO <" + DOMAIN + ">\r\n").getBytes(StandardCharsets.UTF_8)));
+            readBytes(server);
             server.write(ByteBuffer.wrap(("MAIL FROM: <bob@" + DOMAIN + ">\r\n").getBytes(StandardCharsets.UTF_8)));
+            readBytes(server);
             server.write(ByteBuffer.wrap(("RCPT TO: <bo...@examplebis.local>\r\n").getBytes(StandardCharsets.UTF_8)));
+            readBytes(server);
+            server.write(ByteBuffer.wrap(("DATA\r\n").getBytes(StandardCharsets.UTF_8)));
+            readBytes(server); // needed to synchronize
+            server.write(ByteBuffer.wrap(("header:value\r\n\r\nbody").getBytes(StandardCharsets.UTF_8)));
+            server.write(ByteBuffer.wrap(("\r\n").getBytes(StandardCharsets.UTF_8)));
+            server.write(ByteBuffer.wrap((".").getBytes(StandardCharsets.UTF_8)));
+            server.write(ByteBuffer.wrap(("\r\n").getBytes(StandardCharsets.UTF_8)));
+            readBytes(server);
+            server.write(ByteBuffer.wrap(("QUIT\r\n").getBytes(StandardCharsets.UTF_8)));
+            readBytes(server);
+
+            assertThat(recordingMailProcessor.getMails()).hasSize(1);
+        }
+    }
+
+    @Nested
+    class DSNTest {
+
+        private RecordingMailProcessor recordingMailProcessor;
+        private LMTPServerFactory lmtpServerFactory;
+
+        @BeforeEach
+        void setUp()  throws Exception {
+            InMemoryDNSService dnsService = new InMemoryDNSService()
+                .registerMxRecord(Domain.LOCALHOST.asString(), "127.0.0.1")
+                .registerMxRecord("examplebis.local", "127.0.0.1")
+                .registerMxRecord("127.0.0.1", "127.0.0.1");
+            MemoryDomainList domainList = new MemoryDomainList(dnsService);
+            domainList.configure(DomainListConfiguration.builder()
+                .autoDetect(false)
+                .autoDetectIp(false)
+                .build());
+            recordingMailProcessor = new RecordingMailProcessor();
+
+            domainList.addDomain(Domain.of("examplebis.local"));
+            MemoryUsersRepository usersRepository = MemoryUsersRepository.withVirtualHosting(domainList);
+
+            usersRepository.addUser(Username.of("bob@examplebis.local"), "pwd");
+
+            FileSystem fileSystem = new FileSystemImpl(Configuration.builder()
+                .workingDirectory("../")
+                .configurationFromClasspath()
+                .build().directories());
+            MemoryRecipientRewriteTable rewriteTable = new MemoryRecipientRewriteTable();
+            rewriteTable.setConfiguration(RecipientRewriteTableConfiguration.DEFAULT_ENABLED);
+            AliasReverseResolver aliasReverseResolver = new AliasReverseResolverImpl(rewriteTable);
+            CanSendFrom canSendFrom = new CanSendFromImpl(rewriteTable, aliasReverseResolver);
+            MockProtocolHandlerLoader loader = MockProtocolHandlerLoader.builder()
+                .put(binder -> binder.bind(DomainList.class).toInstance(domainList))
+                .put(binder -> binder.bind(RecipientRewriteTable.class).toInstance(rewriteTable))
+                .put(binder -> binder.bind(CanSendFrom.class).toInstance(canSendFrom))
+                .put(binder -> binder.bind(MailProcessor.class).toInstance(recordingMailProcessor))
+                .put(binder -> binder.bind(FileSystem.class).toInstance(fileSystem))
+                .put(binder -> binder.bind(DNSService.class).toInstance(dnsService))
+                .put(binder -> binder.bind(UsersRepository.class).toInstance(usersRepository))
+                .put(binder -> binder.bind(MetricFactory.class).to(RecordingMetricFactory.class))
+                .build();
+            lmtpServerFactory = new LMTPServerFactory(loader, fileSystem, new RecordingMetricFactory(), new HashedWheelTimer());
+
+            lmtpServerFactory.configure(ConfigLoader.getConfig(ClassLoader.getSystemResourceAsStream("lmtpdsn.xml")));
+            lmtpServerFactory.init();
+        }
+
+        @AfterEach
+        void tearDown() {
+            lmtpServerFactory.destroy();
+        }
+
+        @Test
+        void emailShouldTriggerTheMailProcessing() throws Exception {
+            SocketChannel server = SocketChannel.open();
+            server.connect(new InetSocketAddress(LOCALHOST_IP, getLmtpPort(lmtpServerFactory)));
+            readBytes(server);
+            server.write(ByteBuffer.wrap(("LHLO <" + DOMAIN + ">\r\n").getBytes(StandardCharsets.UTF_8)));
+            readBytes(server);
+            server.write(ByteBuffer.wrap(("MAIL FROM: <bob@" + DOMAIN + "> RET=HDRS ENVID=QQ314159\r\n").getBytes(StandardCharsets.UTF_8)));
+            readBytes(server);
+            server.write(ByteBuffer.wrap(("RCPT TO: <bo...@examplebis.local> NOTIFY=SUCCESS,FAILURE,DELAY ORCPT=rfc822;orcpt1@localhost\r\n").getBytes(StandardCharsets.UTF_8)));
+            readBytes(server);
             server.write(ByteBuffer.wrap(("DATA\r\n").getBytes(StandardCharsets.UTF_8)));
-            server.read(ByteBuffer.allocate(1024)); // needed to synchronize
+            readBytes(server);
             server.write(ByteBuffer.wrap(("header:value\r\n\r\nbody").getBytes(StandardCharsets.UTF_8)));
             server.write(ByteBuffer.wrap(("\r\n").getBytes(StandardCharsets.UTF_8)));
             server.write(ByteBuffer.wrap((".").getBytes(StandardCharsets.UTF_8)));
             server.write(ByteBuffer.wrap(("\r\n").getBytes(StandardCharsets.UTF_8)));
+            readBytes(server);
             server.write(ByteBuffer.wrap(("QUIT\r\n").getBytes(StandardCharsets.UTF_8)));
+            readBytes(server);
+
+            assertThat(recordingMailProcessor.getMails())
+                .first()
+                .extracting(Mail::dsnParameters)
+                .satisfies(Throwing.consumer(maybeDSN -> assertThat(maybeDSN)
+                    .isEqualTo(DsnParameters.builder()
+                        .envId(DsnParameters.EnvId.of("QQ314159"))
+                        .ret(DsnParameters.Ret.HDRS)
+                        .addRcptParameter(new MailAddress("bob@examplebis.local"), DsnParameters.RecipientDsnParameters.of(
+                            EnumSet.of(SUCCESS, FAILURE, DELAY), new MailAddress("orcpt1@localhost")))
+                        .build())));
+        }
 
-            Awaitility.await()
-                .untilAsserted(() -> assertThat(recordingMailProcessor.getMails()).hasSize(1));
+        @Test
+        void lhloShouldAdvertizeDSN() throws Exception {
+            SocketChannel server = SocketChannel.open();
+            server.connect(new InetSocketAddress(LOCALHOST_IP, getLmtpPort(lmtpServerFactory)));
+            readBytes(server);
+            server.write(ByteBuffer.wrap(("LHLO <" + DOMAIN + ">\r\n").getBytes(StandardCharsets.UTF_8)));
+
+            assertThat(new String(readBytes(server), StandardCharsets.UTF_8)).contains("250 DSN\r\n");
         }
     }
 
     @Nested
-    class Throwing {
+    class ThrowingTest {
         private LMTPServerFactory lmtpServerFactory;
 
         @BeforeEach
@@ -213,31 +323,32 @@ class MailetContainerHandlerTest {
         void emailShouldTriggerTheMailProcessing() throws Exception {
             SocketChannel server = SocketChannel.open();
             server.connect(new InetSocketAddress(LOCALHOST_IP, getLmtpPort(lmtpServerFactory)));
+            readBytes(server);
 
             server.write(ByteBuffer.wrap(("LHLO <" + DOMAIN + ">\r\n").getBytes(StandardCharsets.UTF_8)));
+            readBytes(server);
             server.write(ByteBuffer.wrap(("MAIL FROM: <bob@" + DOMAIN + ">\r\n").getBytes(StandardCharsets.UTF_8)));
+            readBytes(server);
             server.write(ByteBuffer.wrap(("RCPT TO: <bo...@examplebis.local>\r\n").getBytes(StandardCharsets.UTF_8)));
+            readBytes(server);
             server.write(ByteBuffer.wrap(("DATA\r\n").getBytes(StandardCharsets.UTF_8)));
-            server.read(ByteBuffer.allocate(1024)); // Read Welcome message
+            readBytes(server);
             server.write(ByteBuffer.wrap(("header:value\r\n\r\nbody").getBytes(StandardCharsets.UTF_8)));
             server.write(ByteBuffer.wrap(("\r\n").getBytes(StandardCharsets.UTF_8)));
             server.write(ByteBuffer.wrap((".").getBytes(StandardCharsets.UTF_8)));
             server.write(ByteBuffer.wrap(("\r\n").getBytes(StandardCharsets.UTF_8)));
+            byte[] dataResponse = readBytes(server);
             server.write(ByteBuffer.wrap(("QUIT\r\n").getBytes(StandardCharsets.UTF_8)));
+            readBytes(server);
 
-            server.read(ByteBuffer.allocate(1024)); // Read Capabilities
-            server.read(ByteBuffer.allocate(1024)); // Read LHLO reply string
-            server.read(ByteBuffer.allocate(1024)); // Read MAIL reply string
-            server.read(ByteBuffer.allocate(1024)); // Read RCPT reply string
-
-            ByteBuffer buffer = ByteBuffer.allocate(1024);
-            server.read(buffer); // Read DATA reply string
-            assertThat(new String(readBytes(buffer), StandardCharsets.UTF_8))
+            assertThat(new String(dataResponse, StandardCharsets.UTF_8))
                 .startsWith("451 4.0.0 Temporary error deliver message");
         }
     }
 
-    private byte[] readBytes(ByteBuffer line) {
+    private byte[] readBytes(SocketChannel channel) throws IOException {
+        ByteBuffer line = ByteBuffer.allocate(1024);
+        channel.read(line);
         line.rewind();
         byte[] bline = new byte[line.remaining()];
         line.get(bline);
diff --git a/server/protocols/protocols-lmtp/src/test/resources/lmtpdsn.xml b/server/protocols/protocols-lmtp/src/test/resources/lmtpdsn.xml
new file mode 100644
index 0000000..a012ea3
--- /dev/null
+++ b/server/protocols/protocols-lmtp/src/test/resources/lmtpdsn.xml
@@ -0,0 +1,39 @@
+<?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.
+ -->
+
+<lmtpservers>
+    <lmtpserver enabled="true">
+        <jmxName>lmtpserver</jmxName>
+        <bind>0.0.0.0:0</bind>
+        <connectionBacklog>200</connectionBacklog>
+        <connectiontimeout>1200</connectiontimeout>
+        <connectionLimit>0</connectionLimit>
+        <connectionLimitPerIP>0</connectionLimitPerIP>
+        <maxmessagesize>0</maxmessagesize>
+        <handlerchain coreHandlersPackage="org.apache.james.lmtpserver.MailetContainerCmdHandlerLoader">
+            <handler class="org.apache.james.lmtpserver.MailetContainerCmdHandlerLoader"/>
+
+            <handler class="org.apache.james.smtpserver.dsn.DSNEhloHook"/>
+            <handler class="org.apache.james.smtpserver.dsn.DSNMailParameterHook"/>
+            <handler class="org.apache.james.smtpserver.dsn.DSNRcptParameterHook"/>
+            <handler class="org.apache.james.smtpserver.dsn.DSNMessageHook"/>
+        </handlerchain>
+    </lmtpserver>
+</lmtpservers>

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