This is an automated email from the ASF dual-hosted git repository.

quantranhong1999 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git

commit e79eec96071165e5700e3c630f41376cea57d480
Author: Quan Tran <[email protected]>
AuthorDate: Thu Aug 13 09:46:32 2026 +0700

    JAMES-4210 Harden ManageSieve STARTTLS capability handling
    
    Reject STARTTLS when transport encryption is unavailable, avoid advertising 
unusable STARTTLS or empty SASL capabilities, and ensure malformed 
rejected-STARTTLS sequences close instead of hanging the connection.
---
 .../james/managesieve/core/CoreProcessor.java      |  3 +
 .../james/managesieveserver/CapabilityTest.java    | 73 ++++++++++++++++++++--
 .../james/managesieveserver/ManageSieveClient.java |  4 ++
 3 files changed, 74 insertions(+), 6 deletions(-)

diff --git 
a/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/CoreProcessor.java
 
b/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/CoreProcessor.java
index 4b6186e910..c91f955c90 100644
--- 
a/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/CoreProcessor.java
+++ 
b/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/CoreProcessor.java
@@ -227,6 +227,9 @@ public class CoreProcessor implements CoreCommands {
 
     @Override
     public String startTLS(Session session) {
+        if (!session.supportStartTLS()) {
+            return "NO STARTTLS is not available";
+        }
         if (session.getState() == Session.State.UNAUTHENTICATED) {
             if (session.isSslEnabled()) {
                 return "NO You can't enable two time SSL encryption";
diff --git 
a/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/CapabilityTest.java
 
b/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/CapabilityTest.java
index f13ffe3d8f..5ea4003657 100644
--- 
a/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/CapabilityTest.java
+++ 
b/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/CapabilityTest.java
@@ -19,7 +19,14 @@
 
 package org.apache.james.managesieveserver;
 
-import org.assertj.core.api.Assertions;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.io.EOFException;
+
+import org.apache.commons.configuration2.HierarchicalConfiguration;
+import org.apache.commons.configuration2.tree.ImmutableNode;
+import org.apache.james.server.core.configuration.FileConfigurationProvider;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.Test;
 
@@ -42,11 +49,11 @@ class CapabilityTest {
         ManageSieveClient client = new ManageSieveClient();
         client.connect(this.testSystem.getBindedIP(), 
this.testSystem.getBindedPort());
         ManageSieveClient.ServerResponse initialGreeting = 
client.readResponse();
-        
Assertions.assertThat(getSASLMechanisms(initialGreeting)).containsExactlyInAnyOrder("PLAIN");
+        
assertThat(getSASLMechanisms(initialGreeting)).containsExactlyInAnyOrder("PLAIN");
 
         client.sendCommand("CAPABILITY");
         ManageSieveClient.ServerResponse capabilityResponse = 
client.readResponse();
-        
Assertions.assertThat(getSASLMechanisms(capabilityResponse)).containsExactlyInAnyOrder("PLAIN");
+        
assertThat(getSASLMechanisms(capabilityResponse)).containsExactlyInAnyOrder("PLAIN");
     }
 
     @Test
@@ -56,15 +63,69 @@ class CapabilityTest {
         ManageSieveClient client = new ManageSieveClient();
         client.connect(this.testSystem.getBindedIP(), 
this.testSystem.getBindedPort());
         ManageSieveClient.ServerResponse initialGreeting = 
client.readResponse();
-        
Assertions.assertThat(getSASLMechanisms(initialGreeting)).containsExactlyInAnyOrder("PLAIN",
 "XOAUTH2", "OAUTHBEARER");
+        
assertThat(getSASLMechanisms(initialGreeting)).containsExactlyInAnyOrder("PLAIN",
 "XOAUTH2", "OAUTHBEARER");
 
         client.sendCommand("CAPABILITY");
         ManageSieveClient.ServerResponse capabilityResponse = 
client.readResponse();
-        
Assertions.assertThat(getSASLMechanisms(capabilityResponse)).containsExactlyInAnyOrder("PLAIN",
 "XOAUTH2", "OAUTHBEARER");
+        
assertThat(getSASLMechanisms(capabilityResponse)).containsExactlyInAnyOrder("PLAIN",
 "XOAUTH2", "OAUTHBEARER");
+    }
+
+    @Test
+    void shouldNotAnnounceOrAuthPlainOnClearTextWhenSslIsRequired() throws 
Exception {
+        HierarchicalConfiguration<ImmutableNode> configuration = 
FileConfigurationProvider.getConfig(
+            ClassLoader.getSystemResourceAsStream("managesieveserver.xml"));
+        configuration.addProperty("auth.requireSSL", true);
+        this.testSystem.setUp(configuration);
+
+        ManageSieveClient client = new ManageSieveClient();
+        client.connect(this.testSystem.getBindedIP(), 
this.testSystem.getBindedPort());
+        client.readResponse();
+
+        client.sendCommand("CAPABILITY");
+        ManageSieveClient.ServerResponse capabilityResponse = 
client.readResponse();
+        // RFC 5804 section 1.7 only permits an empty SASL capability when 
STARTTLS is advertised (which is not the case here)
+        assertThat(capabilityResponse.responseLines()).noneMatch(line -> 
line.startsWith("\"SASL\""));
+
+        client.sendCommand("AUTHENTICATE \"PLAIN\"");
+        ManageSieveClient.ServerResponse authenticationResponse = 
client.readResponse();
+        
assertThat(authenticationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.NO);
+        
assertThat(authenticationResponse.responseCode()).contains("ENCRYPT-NEEDED");
+    }
+
+    @Test
+    void shouldNotAnnounceOrAcceptStartTlsWhenTlsIsUnavailable() throws 
Exception {
+        this.testSystem.setUp();
+
+        ManageSieveClient client = new ManageSieveClient();
+        client.connect(this.testSystem.getBindedIP(), 
this.testSystem.getBindedPort());
+        ManageSieveClient.ServerResponse initialGreeting = 
client.readResponse();
+        
assertThat(initialGreeting.responseLines()).doesNotContain("\"STARTTLS\"");
+
+        client.sendCommand("CAPABILITY");
+        ManageSieveClient.ServerResponse capabilityResponse = 
client.readResponse();
+        
assertThat(capabilityResponse.responseLines()).doesNotContain("\"STARTTLS\"");
+
+        client.sendCommand("STARTTLS");
+        ManageSieveClient.ServerResponse startTlsResponse = 
client.readResponse();
+        
assertThat(startTlsResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.NO);
+        assertThat(startTlsResponse.explanation()).contains("STARTTLS is not 
available");
+    }
+
+    @Test
+    void shouldNotHangConnectionWhenAnotherCommandFollowsRejectedStartTls() 
throws Exception {
+        this.testSystem.setUp();
+
+        ManageSieveClient client = new ManageSieveClient();
+        client.connect(this.testSystem.getBindedIP(), 
this.testSystem.getBindedPort());
+        client.readResponse();
+
+        client.sendCommand("STARTTLS\r\nNOOP");
+
+        
assertThatThrownBy(client::readResponse).isInstanceOf(EOFException.class);
     }
 
     private String[] getSASLMechanisms(ManageSieveClient.ServerResponse 
response) {
-        String saslLine = Assertions.assertThat(response.responseLines())
+        String saslLine = assertThat(response.responseLines())
             .filteredOn(line -> line.startsWith("\"SASL\""))
             .hasSize(1)
             .first()
diff --git 
a/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/ManageSieveClient.java
 
b/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/ManageSieveClient.java
index 4a2a101b4d..104a546b28 100644
--- 
a/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/ManageSieveClient.java
+++ 
b/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/ManageSieveClient.java
@@ -21,6 +21,7 @@ package org.apache.james.managesieveserver;
 
 import java.io.BufferedReader;
 import java.io.BufferedWriter;
+import java.io.EOFException;
 import java.io.IOException;
 import java.io.InputStreamReader;
 import java.io.OutputStreamWriter;
@@ -72,6 +73,9 @@ public class ManageSieveClient extends SocketClient {
         ArrayList<String> lines = new ArrayList<>();
         while (response == null) {
             String line = this.reader.readLine();
+            if (line == null) {
+                throw new EOFException("ManageSieve connection closed without 
a response");
+            }
             String[] tokens = line.split(" ", 3);
             if (EnumUtils.isValidEnumIgnoreCase(ResponseType.class, 
tokens[0])) {
                 ResponseType responseType = 
EnumUtils.getEnumIgnoreCase(ResponseType.class, tokens[0]);


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to