parseImapMessage(ChannelHandlerContext ctx, ByteBu
// Also check if the session was logged out if so there is not need to try to decode it. See JAMES-1341
if (session != null && session.getState() != ImapSessionState.LOGOUT) {
try {
+ readerAndSize.getLeft().setUtf8Accept(session.utf8Enabled());
ImapMessage message = decoder.decode(readerAndSize.getLeft(), session);
diff --git a/server/protocols/protocols-imap4/src/test/java/org/apache/james/imapserver/netty/IMAPServerIDCommandTest.java b/server/protocols/protocols-imap4/src/test/java/org/apache/james/imapserver/netty/IMAPServerIDCommandTest.java
index a26b53c7efe..8fb6bbdbd31 100644
--- a/server/protocols/protocols-imap4/src/test/java/org/apache/james/imapserver/netty/IMAPServerIDCommandTest.java
+++ b/server/protocols/protocols-imap4/src/test/java/org/apache/james/imapserver/netty/IMAPServerIDCommandTest.java
@@ -24,6 +24,7 @@
import java.time.Duration;
import org.apache.james.util.concurrency.ConcurrentTestRunner;
+import org.apache.james.utils.TestIMAPClient;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
@@ -61,15 +62,21 @@ void idCommandShouldReturnConfiguredResponse() throws Exception {
}
@Test
- void concurrentIdCommandsInTheSameSessionShouldSucceed() throws Exception {
+ void concurrentIdCommandsShouldSucceed() throws Exception {
imapServer = createImapServer("imapServer.xml");
+ int port = imapServer.getListenAddresses().getFirst().getPort();
- testIMAPClient.connect("127.0.0.1", imapServer.getListenAddresses().getFirst().getPort());
+ // One client per thread: a single TestIMAPClient wraps one socket and one
+ // reader, so sharing it across threads interleaves the replies and a thread
+ // can read an empty string that belongs to nobody.
ConcurrentTestRunner.builder()
.operation((threadNumber, step) -> {
- assertThat(testIMAPClient.sendCommand("ID (\"name\" \"Apache James\")"))
- .contains("* ID NIL")
- .contains("OK ID completed.");
+ try (TestIMAPClient client = new TestIMAPClient()) {
+ client.connect("127.0.0.1", port);
+ assertThat(client.sendCommand("ID (\"name\" \"Apache James\")"))
+ .contains("* ID NIL")
+ .contains("OK ID completed.");
+ }
})
.threadCount(20)
.operationCount(1)
diff --git a/server/protocols/protocols-imap4/src/test/java/org/apache/james/imapserver/netty/IMAPServerSearchTest.java b/server/protocols/protocols-imap4/src/test/java/org/apache/james/imapserver/netty/IMAPServerSearchTest.java
index 884f35d4a33..7a5c629845a 100644
--- a/server/protocols/protocols-imap4/src/test/java/org/apache/james/imapserver/netty/IMAPServerSearchTest.java
+++ b/server/protocols/protocols-imap4/src/test/java/org/apache/james/imapserver/netty/IMAPServerSearchTest.java
@@ -21,26 +21,14 @@
import static org.apache.james.jmap.JMAPTestingConstants.LOCALHOST_IP;
import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatCode;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import java.io.ByteArrayOutputStream;
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.Properties;
-
-import jakarta.mail.Folder;
-import jakarta.mail.Message;
-import jakarta.mail.Session;
-import jakarta.mail.Store;
-import jakarta.mail.search.AndTerm;
-import jakarta.mail.search.BodyTerm;
-import jakarta.mail.search.FromStringTerm;
-import jakarta.mail.search.RecipientStringTerm;
-import jakarta.mail.search.SearchTerm;
-import jakarta.mail.search.SubjectTerm;
import org.apache.james.mailbox.MailboxSession;
import org.apache.james.mailbox.MessageManager;
@@ -303,34 +291,27 @@ void shouldRejectLongLiteralsWhenUnauthenticated() throws Exception {
@Test
void searchingShouldSupportMultipleUTF8Criteria() throws Exception {
- String host = "127.0.0.1";
- Properties props = new Properties();
- props.put("mail.debug", "true");
- Session session = Session.getDefaultInstance(props, null);
- Store store = session.getStore("imap");
- store.connect(host, port, USER.asString(), USER_PASS);
- Folder folder = store.getFolder("INBOX");
- folder.open(Folder.READ_ONLY);
-
- SearchTerm subjectTerm = new SubjectTerm("java培训");
- SearchTerm fromTerm = new FromStringTerm("采购");
- SearchTerm recipientTerm = new RecipientStringTerm(Message.RecipientType.TO, "张三");
- SearchTerm ccRecipientTerm = new RecipientStringTerm(Message.RecipientType.CC, "李四");
- SearchTerm bccRecipientTerm = new RecipientStringTerm(Message.RecipientType.BCC, "王五");
- SearchTerm bodyTerm = new BodyTerm("天天向上");
- SearchTerm[] searchTerms = new SearchTerm[6];
- searchTerms[0] = subjectTerm;
- searchTerms[1] = bodyTerm;
- searchTerms[2] = fromTerm;
- searchTerms[3] = recipientTerm;
- searchTerms[4] = ccRecipientTerm;
- searchTerms[5] = bccRecipientTerm;
- SearchTerm andTerm = new AndTerm(searchTerms);
-
- assertThatCode(() -> folder.search(andTerm)).doesNotThrowAnyException();
-
- folder.close(false);
- store.close();
+ MailboxSession mailboxSession = memoryIntegrationResources.getMailboxManager().createSystemSession(USER);
+ memoryIntegrationResources.getMailboxManager()
+ .createMailbox(MailboxPath.inbox(USER), mailboxSession);
+
+ enableUtf8AndSelectInbox();
+
+ // Six UTF-8 criteria in one command, each as an RFC 6855 literal of UTF-8
+ // octets. Driven over a raw socket rather than through jakarta.mail: once
+ // the server advertises UTF8=ACCEPT, angus-mail takes its supportsUtf8()
+ // branch and encodes search strings with ASCIIUtility.getBytes(), i.e.
+ // one truncated byte per char, so it never puts UTF-8 on the wire.
+ clientConnection.write(ByteBuffer.wrap(searchCommand("a3",
+ "SUBJECT", "java培训",
+ "BODY", "天天向上",
+ "FROM", "采购",
+ "TO", "张三",
+ "CC", "李四",
+ "BCC", "王五")));
+
+ assertThat(String.join("", readStringUntil(clientConnection, s -> s.contains("a3 "))))
+ .contains("a3 OK");
}
@Test
@@ -353,20 +334,58 @@ void searchingASingleUTF8CriterionShouldComplete() throws Exception {
"\r\n" +
"=E5=A4=A9=E5=A4=A9=E5=90=91=E4=B8=8A
\r\n"), mailboxSession);
- String host = "127.0.0.1";
- Properties props = new Properties();
- props.put("mail.debug", "true");
- Session session = Session.getDefaultInstance(props, null);
- Store store = session.getStore("imap");
- store.connect(host, port, USER.asString(), USER_PASS);
- Folder folder = store.getFolder("INBOX");
- folder.open(Folder.READ_ONLY);
+ enableUtf8AndSelectInbox();
+
+ clientConnection.write(ByteBuffer.wrap(searchCommand("a3", "BODY", "天天向上")));
+
+ assertThat(String.join("", readStringUntil(clientConnection, s -> s.contains("a3 "))))
+ .contains("* SEARCH 1")
+ .contains("a3 OK");
+ }
+
+ @Test
+ void searchingAUtf8QuotedStringShouldComplete() throws Exception {
+ MailboxSession mailboxSession = memoryIntegrationResources.getMailboxManager().createSystemSession(USER);
+ memoryIntegrationResources.getMailboxManager()
+ .createMailbox(MailboxPath.inbox(USER), mailboxSession);
+ memoryIntegrationResources.getMailboxManager()
+ .getMailbox(MailboxPath.inbox(USER), mailboxSession)
+ .appendMessage(MessageManager.AppendCommand.builder().build("Content-Type: text/plain; charset=UTF-8\r\n" +
+ "Subject: Test utf-8 charset\r\n" +
+ "\r\n" +
+ "天天向上\r\n"), mailboxSession);
+
+ enableUtf8AndSelectInbox();
+
+ clientConnection.write(ByteBuffer.wrap("a3 SEARCH BODY \"天天向上\" ALL\r\n".getBytes(StandardCharsets.UTF_8)));
- SearchTerm bodyTerm = new BodyTerm("天天向上");
+ assertThat(String.join("", readStringUntil(clientConnection, s -> s.contains("a3 "))))
+ .contains("* SEARCH 1")
+ .contains("a3 OK");
+ }
- assertThat(folder.search(bodyTerm)).hasSize(1);
+ private void enableUtf8AndSelectInbox() throws IOException {
+ clientConnection.write(ByteBuffer.wrap(String.format("a0 LOGIN %s %s\r\n", USER.asString(), USER_PASS).getBytes(StandardCharsets.UTF_8)));
+ readStringUntil(clientConnection, s -> s.contains("a0 OK"));
+ clientConnection.write(ByteBuffer.wrap("a1 ENABLE UTF8=ACCEPT\r\n".getBytes(StandardCharsets.UTF_8)));
+ readStringUntil(clientConnection, s -> s.contains("a1 OK"));
+ clientConnection.write(ByteBuffer.wrap("a2 SELECT INBOX\r\n".getBytes(StandardCharsets.UTF_8)));
+ readStringUntil(clientConnection, s -> s.contains("a2 OK"));
+ }
- folder.close(false);
- store.close();
+ /**
+ * Builds {@code SEARCH {+}CRLF ... ALL CRLF},
+ * the wire form a UTF8=ACCEPT client is meant to send for non-ASCII criteria.
+ */
+ private byte[] searchCommand(String tag, String... keysAndValues) throws IOException {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ out.write((tag + " SEARCH").getBytes(StandardCharsets.US_ASCII));
+ for (int i = 0; i < keysAndValues.length; i += 2) {
+ byte[] value = keysAndValues[i + 1].getBytes(StandardCharsets.UTF_8);
+ out.write((" " + keysAndValues[i] + " {" + value.length + "+}\r\n").getBytes(StandardCharsets.US_ASCII));
+ out.write(value);
+ }
+ out.write(" ALL\r\n".getBytes(StandardCharsets.US_ASCII));
+ return out.toByteArray();
}
}
diff --git a/server/protocols/protocols-imap4/src/test/java/org/apache/james/imapserver/netty/IMAPServerUtf8AcceptTest.java b/server/protocols/protocols-imap4/src/test/java/org/apache/james/imapserver/netty/IMAPServerUtf8AcceptTest.java
new file mode 100644
index 00000000000..c971ab14679
--- /dev/null
+++ b/server/protocols/protocols-imap4/src/test/java/org/apache/james/imapserver/netty/IMAPServerUtf8AcceptTest.java
@@ -0,0 +1,165 @@
+/****************************************************************
+ * 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.imapserver.netty;
+
+import static org.apache.james.jmap.JMAPTestingConstants.LOCALHOST_IP;
+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.List;
+import java.util.function.Predicate;
+
+import org.apache.james.mailbox.MailboxSession;
+import org.apache.james.mailbox.model.MailboxPath;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+import com.google.common.collect.ImmutableList;
+
+class IMAPServerUtf8AcceptTest extends AbstractIMAPServerTest {
+ IMAPServer imapServer;
+
+ @AfterEach
+ void tearDown() {
+ if (imapServer != null) {
+ imapServer.destroy();
+ }
+ }
+
+ @Test
+ void capabilityShouldAdvertiseUtf8Accept() throws Exception {
+ imapServer = createImapServer("imapServer.xml");
+ assertThat(
+ testIMAPClient.connect("127.0.0.1", imapServer.getListenAddresses().getFirst().getPort())
+ .sendCommand("CAPABILITY"))
+ .contains("UTF8=ACCEPT");
+ }
+
+ @Test
+ void enableUtf8AcceptShouldSucceed() throws Exception {
+ imapServer = createImapServer("imapServer.xml");
+ assertThat(
+ testIMAPClient.connect("127.0.0.1", imapServer.getListenAddresses().getFirst().getPort())
+ .login(USER.asString(), USER_PASS)
+ .sendCommand("ENABLE UTF8=ACCEPT"))
+ .contains("* ENABLED UTF8=ACCEPT")
+ .contains("OK ENABLE completed.");
+ }
+
+ @Test
+ void enableUtf8AcceptShouldNotEchoUnsupportedCapability() throws Exception {
+ imapServer = createImapServer("imapServer.xml");
+ assertThat(
+ testIMAPClient.connect("127.0.0.1", imapServer.getListenAddresses().getFirst().getPort())
+ .login(USER.asString(), USER_PASS)
+ .sendCommand("ENABLE BOGUS-CAPABILITY UTF8=ACCEPT"))
+ .contains("* ENABLED UTF8=ACCEPT")
+ .doesNotContain("BOGUS-CAPABILITY")
+ .contains("OK ENABLE completed.");
+ }
+
+ @Test
+ void listShouldEncodeMailboxNameAsModifiedUtf7WhenUtf8AcceptNotEnabled() throws Exception {
+ imapServer = createImapServer("imapServer.xml");
+ MailboxSession session = memoryIntegrationResources.getMailboxManager().createSystemSession(USER);
+ memoryIntegrationResources.getMailboxManager()
+ .createMailbox(MailboxPath.forUser(USER, "grå"), session);
+
+ try (SocketChannel c = SocketChannel.open(new InetSocketAddress(LOCALHOST_IP,
+ imapServer.getListenAddresses().getFirst().getPort()))) {
+ readUtf8Bytes(c);
+ c.write(ByteBuffer.wrap(String.format("a0 LOGIN %s %s\r\n", USER.asString(), USER_PASS).getBytes(StandardCharsets.UTF_8)));
+ readUtf8Until(c, s -> s.contains("a0 OK"));
+ c.write(ByteBuffer.wrap("a1 LIST \"\" \"*\"\r\n".getBytes(StandardCharsets.UTF_8)));
+ List replies = readUtf8Until(c, s -> s.contains("a1 OK"));
+
+ assertThat(String.join("", replies))
+ .contains("gr&AOU-")
+ .doesNotContain("grå");
+ }
+ }
+
+ @Test
+ void createWithUnicodeMailboxNameShouldSucceedAfterEnableUtf8Accept() throws Exception {
+ imapServer = createImapServer("imapServer.xml");
+
+ try (SocketChannel c = SocketChannel.open(new InetSocketAddress(LOCALHOST_IP,
+ imapServer.getListenAddresses().getFirst().getPort()))) {
+ readUtf8Bytes(c);
+ c.write(ByteBuffer.wrap(String.format("a0 LOGIN %s %s\r\n", USER.asString(), USER_PASS).getBytes(StandardCharsets.UTF_8)));
+ readUtf8Until(c, s -> s.contains("a0 OK"));
+ c.write(ByteBuffer.wrap("a1 ENABLE UTF8=ACCEPT\r\n".getBytes(StandardCharsets.UTF_8)));
+ readUtf8Until(c, s -> s.contains("a1 OK"));
+ c.write(ByteBuffer.wrap("a2 CREATE \"grå\"\r\n".getBytes(StandardCharsets.UTF_8)));
+ readUtf8Until(c, s -> s.contains("a2 OK"));
+ c.write(ByteBuffer.wrap("a3 LIST \"\" \"*\"\r\n".getBytes(StandardCharsets.UTF_8)));
+ List replies = readUtf8Until(c, s -> s.contains("a3 OK"));
+
+ assertThat(String.join("", replies)).contains("grå");
+ }
+ }
+
+ @Test
+ void listShouldEncodeMailboxNameAsRawUtf8WhenUtf8AcceptEnabled() throws Exception {
+ imapServer = createImapServer("imapServer.xml");
+ MailboxSession session = memoryIntegrationResources.getMailboxManager().createSystemSession(USER);
+ memoryIntegrationResources.getMailboxManager()
+ .createMailbox(MailboxPath.forUser(USER, "grå"), session);
+
+ try (SocketChannel c = SocketChannel.open(new InetSocketAddress(LOCALHOST_IP,
+ imapServer.getListenAddresses().getFirst().getPort()))) {
+ readUtf8Bytes(c);
+ c.write(ByteBuffer.wrap(String.format("a0 LOGIN %s %s\r\n", USER.asString(), USER_PASS).getBytes(StandardCharsets.UTF_8)));
+ readUtf8Until(c, s -> s.contains("a0 OK"));
+ c.write(ByteBuffer.wrap("a1 ENABLE UTF8=ACCEPT\r\n".getBytes(StandardCharsets.UTF_8)));
+ readUtf8Until(c, s -> s.contains("a1 OK"));
+ c.write(ByteBuffer.wrap("a2 LIST \"\" \"*\"\r\n".getBytes(StandardCharsets.UTF_8)));
+ List replies = readUtf8Until(c, s -> s.contains("a2 OK"));
+
+ assertThat(String.join("", replies))
+ .contains("grå")
+ .doesNotContain("gr&AOU-");
+ }
+ }
+
+ private byte[] readUtf8Bytes(SocketChannel channel) throws IOException {
+ ByteBuffer buf = ByteBuffer.allocate(8192);
+ channel.read(buf);
+ buf.flip();
+ byte[] out = new byte[buf.remaining()];
+ buf.get(out);
+ return out;
+ }
+
+ private List readUtf8Until(SocketChannel channel, Predicate condition) throws IOException {
+ ImmutableList.Builder result = ImmutableList.builder();
+ while (true) {
+ String line = new String(readUtf8Bytes(channel), StandardCharsets.UTF_8);
+ result.add(line);
+ if (condition.test(line)) {
+ return result.build();
+ }
+ }
+ }
+}
diff --git a/server/protocols/protocols-lmtp/src/main/java/org/apache/james/lmtpserver/CoreCmdHandlerLoader.java b/server/protocols/protocols-lmtp/src/main/java/org/apache/james/lmtpserver/CoreCmdHandlerLoader.java
index aac876e098c..353956eff79 100644
--- a/server/protocols/protocols-lmtp/src/main/java/org/apache/james/lmtpserver/CoreCmdHandlerLoader.java
+++ b/server/protocols/protocols-lmtp/src/main/java/org/apache/james/lmtpserver/CoreCmdHandlerLoader.java
@@ -1,82 +1,84 @@
-/****************************************************************
- * 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.lmtpserver;
-
-import java.util.List;
-
-import org.apache.james.lmtpserver.hook.MailboxDeliverToRecipientHandler;
-import org.apache.james.protocols.api.handler.CommandDispatcher;
-import org.apache.james.protocols.api.handler.CommandHandlerResultLogger;
-import org.apache.james.protocols.lib.handler.HandlersPackage;
-import org.apache.james.protocols.lmtp.core.LhloCmdHandler;
-import org.apache.james.protocols.lmtp.core.WelcomeMessageHandler;
-import org.apache.james.protocols.smtp.core.ExpnCmdHandler;
-import org.apache.james.protocols.smtp.core.NoopCmdHandler;
-import org.apache.james.protocols.smtp.core.PostmasterAbuseRcptHook;
-import org.apache.james.protocols.smtp.core.QuitCmdHandler;
-import org.apache.james.protocols.smtp.core.ReceivedDataLineFilter;
-import org.apache.james.protocols.smtp.core.RsetCmdHandler;
-import org.apache.james.protocols.smtp.core.VrfyCmdHandler;
-import org.apache.james.protocols.smtp.core.esmtp.MailSizeEsmtpExtension;
-import org.apache.james.protocols.smtp.core.esmtp.StartTlsCmdHandler;
-import org.apache.james.protocols.smtp.core.log.HookResultLogger;
-import org.apache.james.smtpserver.AuthRequiredToRelayRcptHook;
-import org.apache.james.smtpserver.JamesDataCmdHandler;
-import org.apache.james.smtpserver.JamesMailCmdHandler;
-import org.apache.james.smtpserver.JamesRcptCmdHandler;
-import org.apache.james.smtpserver.fastfail.ValidRcptHandler;
-
-/**
- * This class represent the base command handlers which are shipped with james.
- */
-public class CoreCmdHandlerLoader implements HandlersPackage {
-
- private static final List commands = List.of(
- WelcomeMessageHandler.class.getName(),
- CommandDispatcher.class.getName(),
- JamesDataCmdHandler.class.getName(),
- ExpnCmdHandler.class.getName(),
- LhloCmdHandler.class.getName(),
- JamesMailCmdHandler.class.getName(),
- NoopCmdHandler.class.getName(),
- QuitCmdHandler.class.getName(),
- JamesRcptCmdHandler.class.getName(),
- ValidRcptHandler.class.getName(),
- RsetCmdHandler.class.getName(),
- VrfyCmdHandler.class.getName(),
- MailSizeEsmtpExtension.class.getName(),
- StartTlsCmdHandler.class.getName(),
- AuthRequiredToRelayRcptHook.class.getName(),
- PostmasterAbuseRcptHook.class.getName(),
- ReceivedDataLineFilter.class.getName(),
- DataLineLMTPHandler.class.getName(),
- MailboxDeliverToRecipientHandler.class.getName(),
- CommandHandlerResultLogger.class.getName(),
- HookResultLogger.class.getName()
- );
-
- public CoreCmdHandlerLoader() {
- }
-
- @Override
- public List getHandlers() {
- return commands;
- }
-}
+/****************************************************************
+ * 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.lmtpserver;
+
+import java.util.List;
+
+import org.apache.james.lmtpserver.hook.MailboxDeliverToRecipientHandler;
+import org.apache.james.protocols.api.handler.CommandDispatcher;
+import org.apache.james.protocols.api.handler.CommandHandlerResultLogger;
+import org.apache.james.protocols.lib.handler.HandlersPackage;
+import org.apache.james.protocols.lmtp.core.LhloCmdHandler;
+import org.apache.james.protocols.lmtp.core.WelcomeMessageHandler;
+import org.apache.james.protocols.smtp.core.ExpnCmdHandler;
+import org.apache.james.protocols.smtp.core.NoopCmdHandler;
+import org.apache.james.protocols.smtp.core.PostmasterAbuseRcptHook;
+import org.apache.james.protocols.smtp.core.QuitCmdHandler;
+import org.apache.james.protocols.smtp.core.ReceivedDataLineFilter;
+import org.apache.james.protocols.smtp.core.RsetCmdHandler;
+import org.apache.james.protocols.smtp.core.VrfyCmdHandler;
+import org.apache.james.protocols.smtp.core.esmtp.MailSizeEsmtpExtension;
+import org.apache.james.protocols.smtp.core.esmtp.SMTPUTF8Extension;
+import org.apache.james.protocols.smtp.core.esmtp.StartTlsCmdHandler;
+import org.apache.james.protocols.smtp.core.log.HookResultLogger;
+import org.apache.james.smtpserver.AuthRequiredToRelayRcptHook;
+import org.apache.james.smtpserver.JamesDataCmdHandler;
+import org.apache.james.smtpserver.JamesMailCmdHandler;
+import org.apache.james.smtpserver.JamesRcptCmdHandler;
+import org.apache.james.smtpserver.fastfail.ValidRcptHandler;
+
+/**
+ * This class represent the base command handlers which are shipped with james.
+ */
+public class CoreCmdHandlerLoader implements HandlersPackage {
+
+ private static final List commands = List.of(
+ WelcomeMessageHandler.class.getName(),
+ CommandDispatcher.class.getName(),
+ JamesDataCmdHandler.class.getName(),
+ ExpnCmdHandler.class.getName(),
+ LhloCmdHandler.class.getName(),
+ JamesMailCmdHandler.class.getName(),
+ NoopCmdHandler.class.getName(),
+ QuitCmdHandler.class.getName(),
+ JamesRcptCmdHandler.class.getName(),
+ ValidRcptHandler.class.getName(),
+ RsetCmdHandler.class.getName(),
+ VrfyCmdHandler.class.getName(),
+ MailSizeEsmtpExtension.class.getName(),
+ SMTPUTF8Extension.class.getName(),
+ StartTlsCmdHandler.class.getName(),
+ AuthRequiredToRelayRcptHook.class.getName(),
+ PostmasterAbuseRcptHook.class.getName(),
+ ReceivedDataLineFilter.class.getName(),
+ DataLineLMTPHandler.class.getName(),
+ MailboxDeliverToRecipientHandler.class.getName(),
+ CommandHandlerResultLogger.class.getName(),
+ HookResultLogger.class.getName()
+ );
+
+ public CoreCmdHandlerLoader() {
+ }
+
+ @Override
+ public List getHandlers() {
+ return commands;
+ }
+}
diff --git a/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/CoreCmdHandlerLoader.java b/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/CoreCmdHandlerLoader.java
index 87d5e0e4927..12d0d69f035 100644
--- a/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/CoreCmdHandlerLoader.java
+++ b/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/CoreCmdHandlerLoader.java
@@ -37,6 +37,7 @@
import org.apache.james.protocols.smtp.core.esmtp.AuthCmdHandler;
import org.apache.james.protocols.smtp.core.esmtp.EhloCmdHandler;
import org.apache.james.protocols.smtp.core.esmtp.MailSizeEsmtpExtension;
+import org.apache.james.protocols.smtp.core.esmtp.SMTPUTF8Extension;
import org.apache.james.protocols.smtp.core.esmtp.StartTlsCmdHandler;
import org.apache.james.protocols.smtp.core.log.HookResultLogger;
@@ -61,6 +62,7 @@ public class CoreCmdHandlerLoader implements HandlersPackage {
RsetCmdHandler.class.getName(),
VrfyCmdHandler.class.getName(),
MailSizeEsmtpExtension.class.getName(),
+ SMTPUTF8Extension.class.getName(),
AuthRequiredToRelayRcptHook.class.getName(),
SenderAuthIdentifyVerificationHook.class.getName(),
AuthRequiredHook.class.getName(),
diff --git a/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/smtputf8/SmtpUtf8RcptHook.java b/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/smtputf8/SmtpUtf8RcptHook.java
deleted file mode 100644
index f8eeebbebf9..00000000000
--- a/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/smtputf8/SmtpUtf8RcptHook.java
+++ /dev/null
@@ -1,50 +0,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. *
- ****************************************************************/
-
-package org.apache.james.smtpserver.smtputf8;
-
-import java.util.Map;
-import java.util.Set;
-
-import org.apache.james.core.MailAddress;
-import org.apache.james.core.MaybeSender;
-import org.apache.james.protocols.smtp.SMTPSession;
-import org.apache.james.protocols.smtp.hook.HookResult;
-import org.apache.james.protocols.smtp.hook.RcptHook;
-import org.apache.mailet.Experimental;
-
-import com.google.common.collect.ImmutableSet;
-
-@Experimental
-public class SmtpUtf8RcptHook implements RcptHook {
- @Override
- public Set supportedParameters() {
- return ImmutableSet.of("SMTPUTF8");
- }
-
- @Override
- public HookResult doRcpt(SMTPSession session, MaybeSender sender, MailAddress rcpt) {
- return HookResult.DECLINED;
- }
-
- @Override
- public HookResult doRcpt(SMTPSession session, MaybeSender sender, MailAddress rcpt, Map parameters) {
- return HookResult.DECLINED;
- }
-}
diff --git a/server/protocols/protocols-smtp/src/test/java/org/apache/james/smtpserver/DSNTest.java b/server/protocols/protocols-smtp/src/test/java/org/apache/james/smtpserver/DSNTest.java
index 1e0323da2ec..e3a5cf840ac 100644
--- a/server/protocols/protocols-smtp/src/test/java/org/apache/james/smtpserver/DSNTest.java
+++ b/server/protocols/protocols-smtp/src/test/java/org/apache/james/smtpserver/DSNTest.java
@@ -71,7 +71,7 @@ void ehloShouldAdvertiseDsnExtension() throws Exception {
SoftAssertions.assertSoftly(softly -> {
softly.assertThat(smtpProtocol.getReplyCode()).isEqualTo(250);
- softly.assertThat(smtpProtocol.getReplyString()).contains("250 DSN");
+ softly.assertThat(smtpProtocol.getReplyString()).contains("250-DSN");
});
}
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
index a8d2d727147..c719adace88 100644
--- 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
@@ -72,7 +72,7 @@ void ehloShouldAdvertiseFutureReleaseExtension() throws Exception {
SoftAssertions.assertSoftly(softly -> {
softly.assertThat(smtpProtocol.getReplyCode()).isEqualTo(250);
- softly.assertThat(smtpProtocol.getReplyString()).contains("250 FUTURERELEASE 86400 2023-04-15T10:00:00Z");
+ softly.assertThat(smtpProtocol.getReplyString()).contains("250-FUTURERELEASE 86400 2023-04-15T10:00:00Z");
});
}
diff --git a/server/protocols/protocols-smtp/src/test/java/org/apache/james/smtpserver/SMTPServerTest.java b/server/protocols/protocols-smtp/src/test/java/org/apache/james/smtpserver/SMTPServerTest.java
index a62bea29d1c..29915c58f98 100644
--- a/server/protocols/protocols-smtp/src/test/java/org/apache/james/smtpserver/SMTPServerTest.java
+++ b/server/protocols/protocols-smtp/src/test/java/org/apache/james/smtpserver/SMTPServerTest.java
@@ -313,7 +313,10 @@ public void testSimpleMailSendWithEHLO() throws Exception {
assertThat(capabilitieslist)
.as("capabilities")
- .hasSize(3);
+ .hasSize(4);
+ assertThat(capabilitieslist.contains("SMTPUTF8"))
+ .as("capabilities present SMTPUTF8")
+ .isTrue();
assertThat(capabilitieslist.contains("PIPELINING"))
.as("capabilities present PIPELINING")
.isTrue();
@@ -425,10 +428,10 @@ public void testStartTLSInEHLO() throws Exception {
assertThat(capabilitieslist)
.as("capabilities")
- .hasSize(4);
+ .hasSize(5);
assertThat(capabilitieslist)
- .as("capabilities present PIPELINING ENHANCEDSTATUSCODES 8BITMIME STARTTLS")
- .containsOnly("PIPELINING", "ENHANCEDSTATUSCODES", "8BITMIME", "STARTTLS");
+ .as("capabilities present PIPELINING ENHANCEDSTATUSCODES 8BITMIME SMTPUTF8 STARTTLS")
+ .containsOnly("PIPELINING", "ENHANCEDSTATUSCODES", "8BITMIME", "SMTPUTF8", "STARTTLS");
smtpProtocol.quit();
smtpProtocol.disconnect();
diff --git a/server/protocols/protocols-smtp/src/test/java/org/apache/james/smtpserver/SmtpMtPriorityMessageHookTest.java b/server/protocols/protocols-smtp/src/test/java/org/apache/james/smtpserver/SmtpMtPriorityMessageHookTest.java
index f75f27fbc8e..eb15cdbf091 100644
--- a/server/protocols/protocols-smtp/src/test/java/org/apache/james/smtpserver/SmtpMtPriorityMessageHookTest.java
+++ b/server/protocols/protocols-smtp/src/test/java/org/apache/james/smtpserver/SmtpMtPriorityMessageHookTest.java
@@ -64,7 +64,7 @@ void ehloShouldAdvertiseMtPriorityExtension() throws Exception {
SoftAssertions.assertSoftly(softly -> {
softly.assertThat(smtpProtocol.getReplyCode()).isEqualTo(250);
- softly.assertThat(smtpProtocol.getReplyString()).contains("250 MT-PRIORITY");
+ softly.assertThat(smtpProtocol.getReplyString()).contains("250-MT-PRIORITY");
});
}
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
index c259f04ebd7..cde8dcf366c 100644
--- 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
@@ -72,7 +72,7 @@ void ehloShouldAdvertiseRequireTlsExtension() throws Exception {
SoftAssertions.assertSoftly(softly -> {
softly.assertThat(client.getReplyCode()).isEqualTo(250);
- softly.assertThat(client.getReplyString()).contains("250 REQUIRETLS");
+ softly.assertThat(client.getReplyString()).contains("250-REQUIRETLS");
});
}
diff --git a/server/protocols/protocols-smtp/src/test/java/org/apache/james/smtpserver/SmtpUtf8AnnounceTest.java b/server/protocols/protocols-smtp/src/test/java/org/apache/james/smtpserver/SmtpUtf8AnnounceTest.java
index bf133afff36..d3cb39ffa5f 100644
--- a/server/protocols/protocols-smtp/src/test/java/org/apache/james/smtpserver/SmtpUtf8AnnounceTest.java
+++ b/server/protocols/protocols-smtp/src/test/java/org/apache/james/smtpserver/SmtpUtf8AnnounceTest.java
@@ -19,27 +19,36 @@
package org.apache.james.smtpserver;
+import static org.apache.james.smtpserver.SMTPServerTestSystem.LOCAL_DOMAIN;
import static org.assertj.core.api.Assertions.assertThat;
+import java.io.IOException;
import java.net.InetSocketAddress;
import org.apache.commons.net.smtp.SMTPClient;
-import org.apache.james.server.core.configuration.FileConfigurationProvider;
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;
+/**
+ * RFC 6531 SMTPUTF8, exercised through a real {@link org.apache.james.smtpserver.netty.SMTPServer}
+ * and the stock handler chain, so that what is covered here is what an operator
+ * actually gets: {@code SMTPUTF8Extension} is pulled in by
+ * {@link CoreCmdHandlerLoader}, not wired by hand in the test configuration.
+ */
class SmtpUtf8AnnounceTest {
+ private static final String UTF8_SENDER = "expéditeur@remote.org";
+ private static final String UTF8_RECIPIENT = "réception@" + LOCAL_DOMAIN;
+ /** RFC 6531 §4.2 rejection: 553 5.6.7. */
+ private static final String NON_ASCII_WITHOUT_SMTPUTF8 = "553 5.6.7";
+
private final SMTPServerTestSystem testSystem = new SMTPServerTestSystem();
@BeforeEach
void setUp() throws Exception {
- testSystem.preSetUp();
- testSystem.smtpServer.configure(FileConfigurationProvider.getConfig(
- ClassLoader.getSystemResourceAsStream("smtpserver-utf8.xml")));
- testSystem.smtpServer.init();
+ testSystem.setUp("smtpserver-noauth.xml");
}
@AfterEach
@@ -49,30 +58,87 @@ void tearDown() {
@Test
void ehloShouldAnnounceSmtpUtf8() throws Exception {
- SMTPClient smtpProtocol = new SMTPClient();
- InetSocketAddress bindedAddress = testSystem.getBindedAddress();
- smtpProtocol.connect(bindedAddress.getAddress().getHostAddress(), bindedAddress.getPort());
+ SMTPClient smtpProtocol = connect();
smtpProtocol.sendCommand("EHLO localhost");
SoftAssertions.assertSoftly(softly -> {
softly.assertThat(smtpProtocol.getReplyCode()).isEqualTo(250);
- softly.assertThat(smtpProtocol.getReplyString())
- .contains("250-SMTPUTF8");
+ softly.assertThat(smtpProtocol.getReplyString()).contains("SMTPUTF8");
});
}
@Test
- void trivialEmailWithSmtpUtf8ShouldBeReceived() throws Exception {
- SMTPClient smtpProtocol = new SMTPClient();
- InetSocketAddress bindedAddress = testSystem.getBindedAddress();
- smtpProtocol.connect(bindedAddress.getAddress().getHostAddress(), bindedAddress.getPort());
+ void ehloShouldAnnounceSmtpUtf8Once() throws Exception {
+ SMTPClient smtpProtocol = connect();
+ smtpProtocol.sendCommand("EHLO localhost");
+
+ // EhloCmdHandler appends one line per EhloExtension without
+ // deduplicating, so a second handler advertising the keyword would
+ // silently produce a duplicate 250- line.
+ assertThat(smtpProtocol.getReplyString().split("SMTPUTF8", -1)).hasSize(2);
+ }
+ @Test
+ void unicodeAddressesShouldBeAcceptedWhenSmtpUtf8IsRequested() throws Exception {
+ SMTPClient smtpProtocol = connect();
smtpProtocol.sendCommand("EHLO remote.org");
- smtpProtocol.sendCommand("MAIL FROM: SMTPUTF8");
- smtpProtocol.sendCommand("RCPT TO: SMTPUTF8");
- smtpProtocol.sendShortMessageData("From: bob@localhost\r\n\r\nSubject: test mail\r\n\r\nTest body testSimpleMailSendWithDSN\r\n.\r\n");
+ smtpProtocol.sendCommand("MAIL FROM: <" + UTF8_SENDER + "> SMTPUTF8");
+ assertThat(smtpProtocol.getReplyCode()).isEqualTo(250);
+ smtpProtocol.sendCommand("RCPT TO:<" + UTF8_RECIPIENT + ">");
+ assertThat(smtpProtocol.getReplyCode()).isEqualTo(250);
+ smtpProtocol.sendShortMessageData("From: " + UTF8_SENDER + "\r\nSubject: test\r\n\r\nbody\r\n.\r\n");
Mail lastMail = testSystem.queue.getLastMail();
assertThat(lastMail).isNotNull();
+ SoftAssertions.assertSoftly(softly -> {
+ softly.assertThat(lastMail.getMaybeSender().asString()).isEqualTo(UTF8_SENDER);
+ softly.assertThat(lastMail.getRecipients())
+ .extracting(rcpt -> rcpt.asString())
+ .containsExactly(UTF8_RECIPIENT);
+ });
+ }
+
+ @Test
+ void nonAsciiSenderShouldBeRejectedWithoutSmtpUtf8() throws Exception {
+ SMTPClient smtpProtocol = connect();
+ smtpProtocol.sendCommand("EHLO remote.org");
+ smtpProtocol.sendCommand("MAIL FROM: <" + UTF8_SENDER + ">");
+
+ SoftAssertions.assertSoftly(softly -> {
+ softly.assertThat(smtpProtocol.getReplyCode()).isEqualTo(553);
+ softly.assertThat(smtpProtocol.getReplyString()).contains(NON_ASCII_WITHOUT_SMTPUTF8);
+ });
+ }
+
+ @Test
+ void nonAsciiRecipientShouldBeRejectedWithoutSmtpUtf8() throws Exception {
+ SMTPClient smtpProtocol = connect();
+ smtpProtocol.sendCommand("EHLO remote.org");
+ smtpProtocol.sendCommand("MAIL FROM: ");
+ assertThat(smtpProtocol.getReplyCode()).isEqualTo(250);
+ smtpProtocol.sendCommand("RCPT TO:<" + UTF8_RECIPIENT + ">");
+
+ SoftAssertions.assertSoftly(softly -> {
+ softly.assertThat(smtpProtocol.getReplyCode()).isEqualTo(553);
+ softly.assertThat(smtpProtocol.getReplyString()).contains(NON_ASCII_WITHOUT_SMTPUTF8);
+ });
+ }
+
+ @Test
+ void asciiEmailWithSmtpUtf8ShouldBeReceived() throws Exception {
+ SMTPClient smtpProtocol = connect();
+ smtpProtocol.sendCommand("EHLO remote.org");
+ smtpProtocol.sendCommand("MAIL FROM: SMTPUTF8");
+ smtpProtocol.sendCommand("RCPT TO:");
+ smtpProtocol.sendShortMessageData("From: bob@remote.org\r\nSubject: test\r\n\r\nbody\r\n.\r\n");
+
+ assertThat(testSystem.queue.getLastMail()).isNotNull();
+ }
+
+ private SMTPClient connect() throws IOException {
+ SMTPClient smtpProtocol = new SMTPClient("UTF-8");
+ InetSocketAddress bindedAddress = testSystem.getBindedAddress();
+ smtpProtocol.connect(bindedAddress.getAddress().getHostAddress(), bindedAddress.getPort());
+ return smtpProtocol;
}
}
diff --git a/server/protocols/protocols-smtp/src/test/resources/smtpserver-utf8.xml b/server/protocols/protocols-smtp/src/test/resources/smtpserver-utf8.xml
deleted file mode 100644
index 6be818709d1..00000000000
--- a/server/protocols/protocols-smtp/src/test/resources/smtpserver-utf8.xml
+++ /dev/null
@@ -1,52 +0,0 @@
-
-
-
-
-
-
-
- 0.0.0.0:0
- 200
-
- file://conf/keystore
- james72laBalle
- org.bouncycastle.jce.provider.BouncyCastleProvider
- SunX509
-
- 360
- 0
- 0
-
- forUnauthorizedAddresses
- false
-
- true
- 0
- true
- Apache JAMES awesome SMTP Server
-
-
-
-
-
- false
-
-
-
diff --git a/server/testing/src/main/java/org/apache/james/utils/SMTPMessageSender.java b/server/testing/src/main/java/org/apache/james/utils/SMTPMessageSender.java
index 9ee8ef6f355..21dfd2e2b75 100644
--- a/server/testing/src/main/java/org/apache/james/utils/SMTPMessageSender.java
+++ b/server/testing/src/main/java/org/apache/james/utils/SMTPMessageSender.java
@@ -138,6 +138,22 @@ public SMTPMessageSender sendMessageWithHeaders(String from, List recipi
return this;
}
+ /**
+ * Opens the transaction with EHLO and asserts the SMTPUTF8 extension (RFC 6531), which is
+ * required whenever the envelope carries non-ASCII addresses.
+ */
+ public SMTPMessageSender sendMessageWithHeadersSmtpUtf8(String from, String recipient, String message) throws IOException {
+ return sendMessageWithHeadersSmtpUtf8(from, ImmutableList.of(recipient), message);
+ }
+
+ public SMTPMessageSender sendMessageWithHeadersSmtpUtf8(String from, List recipients, String message) throws IOException {
+ doEhlo();
+ doSetSenderSmtpUtf8(from);
+ recipients.forEach(Throwing.consumer(this::doAddRcpt).sneakyThrow());
+ doData(message);
+ return this;
+ }
+
public SMTPMessageSender sendMessageNoSender(String from, String recipient) throws IOException {
doHelo();
doSetSender("");
@@ -186,6 +202,20 @@ private void doSetSender(String from) throws IOException {
}
}
+ private void doSetSenderSmtpUtf8(String from) throws IOException {
+ int code = smtpClient.mail("<" + from + "> SMTPUTF8");
+ if (code != 250) {
+ throw new SMTPSendingException(SmtpSendingStep.Sender, smtpClient.getReplyString());
+ }
+ }
+
+ private void doEhlo() throws IOException {
+ int code = smtpClient.ehlo(senderDomain);
+ if (code != 250) {
+ throw new SMTPSendingException(SmtpSendingStep.Helo, smtpClient.getReplyString());
+ }
+ }
+
private void doHelo() throws IOException {
int code = smtpClient.helo(senderDomain);
if (code != 250) {
diff --git a/server/testing/src/main/java/org/apache/james/utils/TestIMAPClient.java b/server/testing/src/main/java/org/apache/james/utils/TestIMAPClient.java
index 048da9a73ad..43be854c9ed 100644
--- a/server/testing/src/main/java/org/apache/james/utils/TestIMAPClient.java
+++ b/server/testing/src/main/java/org/apache/james/utils/TestIMAPClient.java
@@ -50,30 +50,68 @@ public class TestIMAPClient extends ExternalResource implements Closeable, After
private static final int MESSAGE_NUMBER_MATCHING_GROUP = 1;
public static final String INBOX = "INBOX";
- public static class Utf8IMAPSClient extends AuthenticatingIMAPClient {
+ /**
+ * commons-net announces and consumes IMAP literals in octets, but subtracts the
+ * {@link String#length()} of the lines it has decoded to know when a literal is over. Its
+ * streams therefore have to stay octet transparent - one char per octet, which is what its
+ * own ISO-8859-1 default gives. Decoding the socket as UTF-8 makes every multi-byte
+ * character count for one octet less than the server announced, so the client keeps reading
+ * past the literal, swallows the tagged reply as if it were message content and then blocks
+ * forever waiting for a completion line that has already gone by.
+ *
+ * UTF-8 is handled at {@link TestIMAPClient}'s own boundary instead: see
+ * {@link TestIMAPClient#asOctets(String)} and {@link TestIMAPClient#asText(String)}.
+ */
+ public static class OctetIMAPClient extends AuthenticatingIMAPClient {
@Override
protected void _connectAction_() throws IOException {
super._connectAction_();
- _reader = new CRLFLineReader(new InputStreamReader(_input_, StandardCharsets.UTF_8));
- __writer = new BufferedWriter(new OutputStreamWriter(_output_, StandardCharsets.UTF_8));
+ _reader = new CRLFLineReader(new InputStreamReader(_input_, StandardCharsets.ISO_8859_1));
+ __writer = new BufferedWriter(new OutputStreamWriter(_output_, StandardCharsets.ISO_8859_1));
}
}
+ /**
+ * Turns text into the octets to put on the wire: one char per UTF-8 octet, as
+ * {@link OctetIMAPClient} expects.
+ */
+ private static String asOctets(String text) {
+ return new String(text.getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1);
+ }
+
+ /**
+ * Reverse of {@link #asOctets(String)}: reads back the octets commons-net collected as UTF-8
+ * text.
+ */
+ private static String asText(String octets) {
+ return new String(octets.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
+ }
+
private final IMAPClient imapClient;
@VisibleForTesting
- TestIMAPClient(Utf8IMAPSClient imapClient) {
+ TestIMAPClient(OctetIMAPClient imapClient) {
this.imapClient = imapClient;
}
public TestIMAPClient() {
- this(new Utf8IMAPSClient());
+ this(new OctetIMAPClient());
}
public TestIMAPClient(IMAPClient imapClient) {
this.imapClient = imapClient;
}
+ private String replyString() {
+ return asText(imapClient.getReplyString());
+ }
+
+ private List replyStrings() {
+ return Stream.of(imapClient.getReplyStrings())
+ .map(TestIMAPClient::asText)
+ .collect(ImmutableList.toImmutableList());
+ }
+
public TestIMAPClient connect(String host, int port) throws IOException {
imapClient.connect(host, port);
return this;
@@ -81,7 +119,7 @@ public TestIMAPClient connect(String host, int port) throws IOException {
public String capability() throws IOException {
imapClient.capability();
- return imapClient.getReplyString();
+ return replyString();
}
public TestIMAPClient disconnect() throws IOException {
@@ -90,7 +128,7 @@ public TestIMAPClient disconnect() throws IOException {
}
public TestIMAPClient login(String user, String password) throws IOException {
- final boolean login = imapClient.login(user, password);
+ final boolean login = imapClient.login(asOctets(user), asOctets(password));
if (!login) {
throw new IOException("Login failed");
}
@@ -107,9 +145,9 @@ public TestIMAPClient authenticatePlain(String user, String password) throws Exc
}
public TestIMAPClient rawLogin(String user, String password) throws IOException {
- imapClient.sendCommand("LOGIN " + user + " " + password);
+ imapClient.sendCommand(asOctets("LOGIN " + user + " " + password));
- if (imapClient.getReplyString().contains("NO LOGIN failed.")) {
+ if (replyString().contains("NO LOGIN failed.")) {
throw new IOException("Login failed");
}
return this;
@@ -117,7 +155,7 @@ public TestIMAPClient rawLogin(String user, String password) throws IOException
public List list() throws IOException {
imapClient.list("", "*");
- return ImmutableList.copyOf(imapClient.getReplyStrings());
+ return replyStrings();
}
public TestIMAPClient login(Username user, String password) throws IOException {
@@ -125,13 +163,13 @@ public TestIMAPClient login(Username user, String password) throws IOException {
}
public TestIMAPClient select(String mailbox) throws IOException {
- imapClient.select(mailbox);
+ imapClient.select(asOctets(mailbox));
return this;
}
public TestIMAPClient create(String mailbox) throws IOException {
- if (!imapClient.create(mailbox)) {
- throw new RuntimeException(imapClient.getReplyString());
+ if (!imapClient.create(asOctets(mailbox))) {
+ throw new RuntimeException(replyString());
}
return this;
}
@@ -139,20 +177,20 @@ public TestIMAPClient create(String mailbox) throws IOException {
public TestIMAPClient append(String mailboxName, String message) throws IOException {
String noFlags = null;
String noDateTime = null;
- if (!imapClient.append(mailboxName, noFlags, noDateTime, message)) {
- throw new RuntimeException(imapClient.getReplyString());
+ if (!imapClient.append(asOctets(mailboxName), noFlags, noDateTime, asOctets(message))) {
+ throw new RuntimeException(replyString());
}
return this;
}
public TestIMAPClient delete(String mailbox) throws IOException {
- imapClient.delete(mailbox);
+ imapClient.delete(asOctets(mailbox));
return this;
}
public boolean hasAMessage() throws IOException {
imapClient.fetch("1", "UID");
- return imapClient.getReplyString()
+ return replyString()
.contains("OK FETCH completed");
}
@@ -172,7 +210,7 @@ public TestIMAPClient awaitMessageCount(ConditionFactory conditionFactory, int m
private long countFetchedEntries() {
return Splitter.on("\n")
.trimResults()
- .splitToStream(imapClient.getReplyString())
+ .splitToStream(replyString())
.filter(s -> s.startsWith("*"))
.count();
}
@@ -184,8 +222,7 @@ public TestIMAPClient awaitNoMessage(ConditionFactory conditionFactory) {
public boolean hasAMessageWithFlags(String flags) throws IOException {
imapClient.fetch("1:1", "ALL");
- String replyString = imapClient.getReplyString();
- return isCompletedWithFlags(flags, replyString);
+ return isCompletedWithFlags(flags, replyString());
}
@VisibleForTesting
@@ -197,12 +234,12 @@ boolean isCompletedWithFlags(String flags, String replyString) {
}
public boolean userGetNotifiedForNewMessagesWhenSelectingMailbox(int numOfNewMessage) {
- return imapClient.getReplyString().contains("OK [UNSEEN " + numOfNewMessage + "]");
+ return replyString().contains("OK [UNSEEN " + numOfNewMessage + "]");
}
public boolean userDoesNotReceiveMessage() throws IOException {
imapClient.fetch("1:1", "ALL");
- return imapClient.getReplyString()
+ return replyString()
.contains("BAD FETCH failed. Invalid messageset");
}
@@ -216,27 +253,26 @@ public String readFirstMessageHeaders() throws IOException {
public String setFlagsForAllMessagesInMailbox(String flag) throws IOException {
imapClient.store("1:*", "+FLAGS", flag);
- return imapClient.getReplyString();
+ return replyString();
}
public String copyAllMessagesInMailboxTo(String mailboxName) throws IOException {
- imapClient.copy("1:*", mailboxName);
- return imapClient.getReplyString();
+ imapClient.copy("1:*", asOctets(mailboxName));
+ return replyString();
}
public String readFirstMessageInMailbox(String parameters) throws IOException {
imapClient.fetch("1:1", parameters);
- return imapClient.getReplyString();
+ return replyString();
}
public boolean userGetNotifiedForNewMessages(int numberOfMessages) throws IOException {
imapClient.noop();
- String replyString = imapClient.getReplyString();
List parts = Splitter.on('\n')
.trimResults()
.omitEmptyStrings()
- .splitToList(replyString);
+ .splitToList(replyString());
return parts.size() == 3
&& parts.get(2).contains("OK NOOP completed.")
&& parts.contains("* " + numberOfMessages + " EXISTS")
@@ -246,11 +282,10 @@ public boolean userGetNotifiedForNewMessages(int numberOfMessages) throws IOExce
public boolean userGetNotifiedForDeletion(int msn) throws IOException {
imapClient.noop();
- String replyString = imapClient.getReplyString();
List parts = Splitter.on('\n')
.trimResults()
.omitEmptyStrings()
- .splitToList(replyString);
+ .splitToList(replyString());
return parts.size() == 2
&& parts.get(1).contains("OK NOOP completed.")
@@ -279,11 +314,11 @@ public void afterEach(ExtensionContext extensionContext) {
}
public void copyFirstMessage(String destMailbox) throws IOException {
- imapClient.copy("1", destMailbox);
+ imapClient.copy("1", asOctets(destMailbox));
}
public void moveFirstMessage(String destMailbox) throws IOException {
- imapClient.sendCommand("MOVE 1 " + destMailbox);
+ imapClient.sendCommand(asOctets("MOVE 1 " + destMailbox));
}
public void expunge() throws IOException {
@@ -291,18 +326,18 @@ public void expunge() throws IOException {
}
public String getQuotaRoot(String mailbox) throws IOException {
- imapClient.sendCommand("GETQUOTAROOT " + mailbox);
- return imapClient.getReplyString();
+ imapClient.sendCommand(asOctets("GETQUOTAROOT " + mailbox));
+ return replyString();
}
public String sendCommand(String command) throws IOException {
- imapClient.sendCommand(command);
- return imapClient.getReplyString();
+ imapClient.sendCommand(asOctets(command));
+ return replyString();
}
public long getMessageCount(String mailboxName) throws IOException {
- imapClient.examine(mailboxName);
- return Stream.of(imapClient.getReplyStrings())
+ imapClient.examine(asOctets(mailboxName));
+ return replyStrings().stream()
.map(EXAMINE_EXISTS::matcher)
.filter(Matcher::matches)
.map(m -> m.group(MESSAGE_NUMBER_MATCHING_GROUP))
diff --git a/server/testing/src/test/java/org/apache/james/utils/TestIMAPClientTest.java b/server/testing/src/test/java/org/apache/james/utils/TestIMAPClientTest.java
index 8bf5802d876..b4a8cdc1b4f 100644
--- a/server/testing/src/test/java/org/apache/james/utils/TestIMAPClientTest.java
+++ b/server/testing/src/test/java/org/apache/james/utils/TestIMAPClientTest.java
@@ -21,11 +21,11 @@
import static org.assertj.core.api.Assertions.assertThat;
-import org.apache.james.utils.TestIMAPClient.Utf8IMAPSClient;
+import org.apache.james.utils.TestIMAPClient.OctetIMAPClient;
import org.junit.jupiter.api.Test;
class TestIMAPClientTest {
- static final Utf8IMAPSClient NULL_IMAP_CLIENT = null;
+ static final OctetIMAPClient NULL_IMAP_CLIENT = null;
TestIMAPClient testee = new TestIMAPClient(NULL_IMAP_CLIENT);
@Test