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

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


The following commit(s) were added to refs/heads/master by this push:
     new 0ca0a7d8d2 [FIX] Adhere to managesieve filename syntax (#3127)
0ca0a7d8d2 is described below

commit 0ca0a7d8d2b9d367b909693c3a616d0aa39046d6
Author: Benoit TELLIER <[email protected]>
AuthorDate: Sat Aug 22 13:44:20 2026 +0700

    [FIX] Adhere to managesieve filename syntax (#3127)
---
 .../james/managesieve/scripts/putscript.test       |  25 ++-
 .../managesieve/transcode/ArgumentParser.java      | 202 +++++++++++----------
 .../apache/james/managesieve/util/ParserUtils.java |  99 ++++++++++
 .../james/managesieve/util/ParserUtilsTest.java    |  72 ++++++++
 4 files changed, 301 insertions(+), 97 deletions(-)

diff --git 
a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/putscript.test
 
b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/putscript.test
index c233f6b6f6..348eb7c649 100644
--- 
a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/putscript.test
+++ 
b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/putscript.test
@@ -30,7 +30,7 @@ C: PUTSCRIPT "scriptname" error
 S: NO "error is an invalid size literal : it should be at least 4 char looking 
like \{_\+\}"
 
 C: PUTSCRIPT "scriptname" {error+}
-S: NO "Size is not a long : For input string: "error""
+S: NO "Size is not a long : For input string: \\"error\\""
 
 C: PUTSCRIPT "foo" {31+}
 C: #comment
@@ -43,6 +43,29 @@ S: ""
 C: user password
 S: OK
 
+C: PUTSCRIPT "new script" {7+}
+C: #test
+S: OK
+
+C: GETSCRIPT "new script"
+S: \{7\}
+S: #test
+S:
+S: OK
+
+C: LISTSCRIPTS
+S: "new script"
+S: OK
+
+C: RENAMESCRIPT "new script" "renamed script"
+S: OK
+
+C: DELETESCRIPT "renamed script"
+S: OK
+
+C: PUTSCRIPT "unterminated script {7+}
+S: NO "Unterminated quoted string"
+
 C: PUTSCRIPT "mysievescript" {97+}
 C: require ["fileinto"];
 C:
diff --git 
a/protocols/managesieve/src/main/java/org/apache/james/managesieve/transcode/ArgumentParser.java
 
b/protocols/managesieve/src/main/java/org/apache/james/managesieve/transcode/ArgumentParser.java
index 5132191b5d..7318a1c9cf 100644
--- 
a/protocols/managesieve/src/main/java/org/apache/james/managesieve/transcode/ArgumentParser.java
+++ 
b/protocols/managesieve/src/main/java/org/apache/james/managesieve/transcode/ArgumentParser.java
@@ -21,6 +21,8 @@
 package org.apache.james.managesieve.transcode;
 
 import java.util.Iterator;
+import java.util.List;
+import java.util.function.Function;
 
 import org.apache.james.managesieve.api.ArgumentException;
 import org.apache.james.managesieve.api.Session;
@@ -52,7 +54,7 @@ public class ArgumentParser {
 
     public String capability(Session session, String args) {
         if (!args.trim().isEmpty()) {
-            return "NO \"Too many arguments: " + args + "\"";
+            return no("Too many arguments: " + args);
         }
         return core.capability(session);
     }
@@ -74,28 +76,28 @@ public class ArgumentParser {
     }
 
     public String deleteScript(Session session, String args) {
-        Iterator<String> argumentIterator = Splitter.on(' 
').omitEmptyStrings().split(args).iterator();
-        if (!argumentIterator.hasNext()) {
-            return "NO \"Missing argument: script name\"";
-        }
-        String scriptName = ParserUtils.unquote(argumentIterator.next());
-        if (argumentIterator.hasNext()) {
-            return "NO \"Too many arguments: " + argumentIterator.next() + 
"\"";
-        }
-        return core.deleteScript(session, scriptName);
-    }    
-    
+        return withArguments(args, arguments -> {
+            if (arguments.isEmpty()) {
+                return no("Missing argument: script name");
+            }
+            if (arguments.size() > 1) {
+                return no("Too many arguments: " + arguments.get(1));
+            }
+            return core.deleteScript(session, arguments.get(0));
+        });
+    }
+
     public String getScript(Session session, String args) {
-        Iterator<String> argumentIterator = Splitter.on(' 
').omitEmptyStrings().split(args).iterator();
-        if (!argumentIterator.hasNext()) {
-            return "NO \"Missing argument: script name\"";
-        }
-        String scriptName = ParserUtils.unquote(argumentIterator.next());
-        if (argumentIterator.hasNext()) {
-            return "NO \"Too many arguments: " + argumentIterator.next() + 
"\"";
-        }
-        return core.getScript(session, scriptName);
-    }     
+        return withArguments(args, arguments -> {
+            if (arguments.isEmpty()) {
+                return no("Missing argument: script name");
+            }
+            if (arguments.size() > 1) {
+                return no("Too many arguments: " + arguments.get(1));
+            }
+            return core.getScript(session, arguments.get(0));
+        });
+    }
     
     public String checkScript(Session session, String args) {
         Iterator<String> firstLine = 
Splitter.on("\r\n").split(args.trim()).iterator();
@@ -108,121 +110,129 @@ public class ArgumentParser {
             try {
                 size = ParserUtils.getSize(arguments.next());
             } catch (ArgumentException e) {
-                return "NO \"" + e.getMessage() + "\"";
+                return no(e.getMessage());
             }
         }
         if (arguments.hasNext()) {
-            return "NO \"Extra arguments not supported\"";
+            return no("Extra arguments not supported");
         } else {
-            String content = Joiner.on("\r\n").join(firstLine);
-            if (validatePutSize) {
-                content += "\r\n";
-            }
+            String content = readContent(firstLine);
             if (content.length() < size && validatePutSize) {
                 throw new NotEnoughDataException();
             }
             if (Strings.isNullOrEmpty(content)) {
-                return "NO \"Missing argument: script content\"";
+                return no("Missing argument: script content");
             }
             return core.checkScript(session, content);
         }
     }
 
     public String haveSpace(Session session, String args) {
-        Iterator<String> argumentIterator = Splitter.on(' 
').omitEmptyStrings().split(args.trim()).iterator();
-        if (!argumentIterator.hasNext()) {
-            return "NO \"Missing argument: script name\"";
-        }
-        String scriptName = ParserUtils.unquote(argumentIterator.next());
-        long size;
-        if (!argumentIterator.hasNext()) {
-            return "NO \"Missing argument: script size\"";
-        }
-        try {
-            size = Long.parseLong(argumentIterator.next());
-        } catch (NumberFormatException e) {
-            return "NO \"Invalid argument: script size\"";
-        }
-        if (argumentIterator.hasNext()) {
-            return "NO \"Too many arguments: " + 
argumentIterator.next().trim() + "\"";
-        }
-        return core.haveSpace(session, scriptName, size);
+        return withArguments(args.trim(), arguments -> {
+            if (arguments.isEmpty()) {
+                return no("Missing argument: script name");
+            }
+            if (arguments.size() < 2) {
+                return no("Missing argument: script size");
+            }
+            try {
+                long size = Long.parseLong(arguments.get(1));
+                if (arguments.size() > 2) {
+                    return no("Too many arguments: " + arguments.get(2));
+                }
+                return core.haveSpace(session, arguments.get(0), size);
+            } catch (NumberFormatException e) {
+                return no("Invalid argument: script size");
+            }
+        });
     }
 
     public String listScripts(Session session, String args) {
         if (!args.trim().isEmpty()) {
-            return "NO \"Too many arguments: " + args + "\"";
+            return no("Too many arguments: " + args);
         }
         return core.listScripts(session);
     }
 
     public String putScript(Session session, String args) {
-        Iterator<String> firstLine = 
Splitter.on("\r\n").split(args.trim()).iterator();
-        Iterator<String> arguments = Splitter.on(' 
').split(firstLine.next().trim()).iterator();
-
-        String scriptName;
-        long size;
-        if (! arguments.hasNext()) {
-             return "NO \"Missing argument: script name\"";
-        } else {
-            scriptName = ParserUtils.unquote(arguments.next());
-            if (Strings.isNullOrEmpty(scriptName)) {
-               return "NO \"Missing argument: script name\"";
+        Iterator<String> lines = 
Splitter.on("\r\n").split(args.trim()).iterator();
+        return withArguments(lines.next().trim(), arguments -> {
+            if (arguments.isEmpty() || 
Strings.isNullOrEmpty(arguments.get(0))) {
+                return no("Missing argument: script name");
             }
-        }
-        if (! arguments.hasNext()) {
-            return "NO \"Missing argument: script size\"";
-        } else {
+            if (arguments.size() < 2) {
+                return no("Missing argument: script size");
+            }
+            long size;
             try {
-                size = ParserUtils.getSize(arguments.next());
+                size = ParserUtils.getSize(arguments.get(1));
             } catch (ArgumentException e) {
-                return "NO \"" + e.getMessage() + "\"";
+                return no(e.getMessage());
             }
-        }
-        if (arguments.hasNext()) {
-            return "NO \"Extra arguments not supported\"";
-        } else {
-            String content = Joiner.on("\r\n").join(firstLine);
-            if (validatePutSize) {
-                content += "\r\n";
+            if (arguments.size() > 2) {
+                return no("Extra arguments not supported");
             }
+            String content = readContent(lines);
             if (content.length() < size && validatePutSize) {
                 throw new NotEnoughDataException();
             }
-            return core.putScript(session, ParserUtils.unquote(scriptName), 
content);
-        }
+            return core.putScript(session, arguments.get(0), content);
+        });
     }
 
     public String renameScript(Session session, String args) {
-        Iterator<String> argumentIterator = Splitter.on(' 
').omitEmptyStrings().split(args).iterator();
-        if (!argumentIterator.hasNext()) {
-            return "NO \"Missing argument: old script name\"";
-        }
-        String oldName = ParserUtils.unquote(argumentIterator.next());
-        if (!argumentIterator.hasNext()) {
-            return "NO \"Missing argument: new script name\"";
-        }
-        String newName = ParserUtils.unquote(argumentIterator.next());
-        if (argumentIterator.hasNext()) {
-            return "NO \"Too many arguments: " + argumentIterator.next() + 
"\"";
-        }
-        return core.renameScript(session, oldName, newName);
+        return withArguments(args, arguments -> {
+            if (arguments.isEmpty()) {
+                return no("Missing argument: old script name");
+            }
+            if (arguments.size() < 2) {
+                return no("Missing argument: new script name");
+            }
+            if (arguments.size() > 2) {
+                return no("Too many arguments: " + arguments.get(2));
+            }
+            return core.renameScript(session, arguments.get(0), 
arguments.get(1));
+        });
     }
 
     public String setActive(Session session, String args) {
-        Iterator<String> argumentIterator = Splitter.on(' 
').omitEmptyStrings().split(args).iterator();
-        if (!argumentIterator.hasNext()) {
-            return "NO \"Missing argument: script name\"";
-        }
-        String scriptName = ParserUtils.unquote(argumentIterator.next());
-        if (argumentIterator.hasNext()) {
-            return "NO \"Too many arguments: " + argumentIterator.next() + 
"\"";
-        }
-        return core.setActive(session, scriptName);
+        return withArguments(args, arguments -> {
+            if (arguments.isEmpty()) {
+                return no("Missing argument: script name");
+            }
+            if (arguments.size() > 1) {
+                return no("Too many arguments: " + arguments.get(1));
+            }
+            return core.setActive(session, arguments.get(0));
+        });
     }
 
     public String startTLS(Session session) {
         return core.startTLS(session);
     }
 
+    /**
+     * Splits the arguments of a command line, then hands them over to the 
command implementation. Command lines
+     * violating the string syntax of RFC 5804 are rejected before reaching it.
+     */
+    private String withArguments(String args, Function<List<String>, String> 
command) {
+        try {
+            return command.apply(ParserUtils.splitArguments(args));
+        } catch (ArgumentException e) {
+            return no(e.getMessage());
+        }
+    }
+
+    private String readContent(Iterator<String> remainingLines) {
+        String content = Joiner.on("\r\n").join(remainingLines);
+        if (validatePutSize) {
+            return content + "\r\n";
+        }
+        return content;
+    }
+
+    private static String no(String message) {
+        return "NO " + ParserUtils.quote(message);
+    }
+
 }
diff --git 
a/protocols/managesieve/src/main/java/org/apache/james/managesieve/util/ParserUtils.java
 
b/protocols/managesieve/src/main/java/org/apache/james/managesieve/util/ParserUtils.java
index 86fbd7c02d..411d65c83f 100644
--- 
a/protocols/managesieve/src/main/java/org/apache/james/managesieve/util/ParserUtils.java
+++ 
b/protocols/managesieve/src/main/java/org/apache/james/managesieve/util/ParserUtils.java
@@ -20,10 +20,109 @@
 
 package org.apache.james.managesieve.util;
 
+import java.util.List;
+
 import org.apache.james.managesieve.api.ArgumentException;
 
+import com.google.common.collect.ImmutableList;
+
 public class ParserUtils {
 
+    private static final char QUOTE = '"';
+    private static final char ESCAPE = '\\';
+
+    /**
+     * States of the automaton splitting a ManageSieve command line into its 
arguments.
+     */
+    private enum SplitState {
+        /** Between two arguments: white spaces are the only thing expected 
here. */
+        BETWEEN_ARGUMENTS,
+        /** Within an argument that is not enclosed in quotes: the next white 
space ends it. */
+        WITHIN_ATOM,
+        /** Within an argument enclosed in quotes: white spaces belong to it, 
only the closing quote ends it. */
+        WITHIN_QUOTES,
+        /** Right after a backslash within quotes: the next character is taken 
literally, be it a quote or a backslash. */
+        AFTER_ESCAPE
+    }
+
+    /**
+     * Splits a ManageSieve command line into its arguments.
+     *
+     * Contrary to a plain whitespace split, the quoted-string syntax of RFC 
5804 section 4 is honoured: an argument
+     * enclosed in double quotes may contain spaces, and both '"' and '\' can 
be escaped with a leading '\'.
+     *
+     * Returned arguments are unquoted.
+     */
+    public static List<String> splitArguments(String line) throws 
ArgumentException {
+        ImmutableList.Builder<String> arguments = ImmutableList.builder();
+        StringBuilder pending = new StringBuilder();
+        SplitState state = SplitState.BETWEEN_ARGUMENTS;
+
+        for (char current : line.toCharArray()) {
+            state = switch (state) {
+                case BETWEEN_ARGUMENTS -> {
+                    if (isWhiteSpace(current)) {
+                        yield SplitState.BETWEEN_ARGUMENTS;
+                    }
+                    if (current == QUOTE) {
+                        yield SplitState.WITHIN_QUOTES;
+                    }
+                    pending.append(current);
+                    yield SplitState.WITHIN_ATOM;
+                }
+                case WITHIN_ATOM -> {
+                    if (isWhiteSpace(current)) {
+                        arguments.add(unquote(flush(pending)));
+                        yield SplitState.BETWEEN_ARGUMENTS;
+                    }
+                    pending.append(current);
+                    yield SplitState.WITHIN_ATOM;
+                }
+                case WITHIN_QUOTES -> {
+                    if (current == ESCAPE) {
+                        yield SplitState.AFTER_ESCAPE;
+                    }
+                    if (current == QUOTE) {
+                        arguments.add(flush(pending));
+                        yield SplitState.BETWEEN_ARGUMENTS;
+                    }
+                    pending.append(current);
+                    yield SplitState.WITHIN_QUOTES;
+                }
+                case AFTER_ESCAPE -> {
+                    pending.append(current);
+                    yield SplitState.WITHIN_QUOTES;
+                }
+            };
+        }
+
+        return switch (state) {
+            case WITHIN_QUOTES, AFTER_ESCAPE -> throw new 
ArgumentException("Unterminated quoted string");
+            case WITHIN_ATOM -> arguments.add(unquote(flush(pending))).build();
+            case BETWEEN_ARGUMENTS -> arguments.build();
+        };
+    }
+
+    private static boolean isWhiteSpace(char c) {
+        return c == ' ' || c == '\t';
+    }
+
+    private static String flush(StringBuilder pending) {
+        String value = pending.toString();
+        pending.setLength(0);
+        return value;
+    }
+
+    /**
+     * Renders a value as a ManageSieve quoted string, escaping the 
QUOTED-SPECIALS of RFC 5804 section 4 so that
+     * user supplied data can safely be echoed back within a response.
+     */
+    public static String quote(String value) {
+        return QUOTE + value
+            .replace(String.valueOf(ESCAPE), "\\\\")
+            .replace(String.valueOf(QUOTE), "\\\"") + QUOTE;
+    }
+
     public static long getSize(String args) throws ArgumentException {
         if (args != null && args.length() > 3
             && args.charAt(0) == '{'
diff --git 
a/protocols/managesieve/src/test/java/org/apache/james/managesieve/util/ParserUtilsTest.java
 
b/protocols/managesieve/src/test/java/org/apache/james/managesieve/util/ParserUtilsTest.java
index de6bcda745..f55b50573b 100644
--- 
a/protocols/managesieve/src/test/java/org/apache/james/managesieve/util/ParserUtilsTest.java
+++ 
b/protocols/managesieve/src/test/java/org/apache/james/managesieve/util/ParserUtilsTest.java
@@ -27,6 +27,78 @@ import org.apache.james.managesieve.api.ArgumentException;
 import org.junit.jupiter.api.Test;
 
 class ParserUtilsTest {
+    @Test
+    void splitArgumentsShouldReturnEmptyListOnEmptyInput() throws Exception {
+        assertThat(ParserUtils.splitArguments("")).isEmpty();
+    }
+
+    @Test
+    void splitArgumentsShouldSplitOnSpaces() throws Exception {
+        assertThat(ParserUtils.splitArguments("name 
{12+}")).containsExactly("name", "{12+}");
+    }
+
+    @Test
+    void splitArgumentsShouldIgnoreExtraSpaces() throws Exception {
+        assertThat(ParserUtils.splitArguments("  name   {12+}  
")).containsExactly("name", "{12+}");
+    }
+
+    @Test
+    void splitArgumentsShouldUnquoteArguments() throws Exception {
+        assertThat(ParserUtils.splitArguments("\"name\" 
{12+}")).containsExactly("name", "{12+}");
+    }
+
+    @Test
+    void splitArgumentsShouldNotSplitQuotedArgumentsContainingSpaces() throws 
Exception {
+        assertThat(ParserUtils.splitArguments("\"new script\" 
{12+}")).containsExactly("new script", "{12+}");
+    }
+
+    @Test
+    void splitArgumentsShouldSupportEscapedQuotes() throws Exception {
+        assertThat(ParserUtils.splitArguments("\"new 
\\\"script\\\"\"")).containsExactly("new \"script\"");
+    }
+
+    @Test
+    void splitArgumentsShouldSupportEscapedBackslashes() throws Exception {
+        
assertThat(ParserUtils.splitArguments("\"new\\\\script\"")).containsExactly("new\\script");
+    }
+
+    @Test
+    void splitArgumentsShouldSupportEmptyQuotedArgument() throws Exception {
+        assertThat(ParserUtils.splitArguments("\"\" 
{12+}")).containsExactly("", "{12+}");
+    }
+
+    @Test
+    void splitArgumentsShouldThrowOnTrailingEscapeWithinQuotedArgument() {
+        assertThatThrownBy(() -> ParserUtils.splitArguments("\"new script\\"))
+            .isInstanceOf(ArgumentException.class);
+    }
+
+    @Test
+    void splitArgumentsShouldNotTreatQuotesWithinAtomsAsDelimiters() throws 
Exception {
+        assertThat(ParserUtils.splitArguments("a\"b 
c")).containsExactly("a\"b", "c");
+    }
+
+    @Test
+    void splitArgumentsShouldThrowOnUnterminatedQuotedArgument() {
+        assertThatThrownBy(() -> ParserUtils.splitArguments("\"new script 
{12+}"))
+            .isInstanceOf(ArgumentException.class);
+    }
+
+    @Test
+    void quoteShouldEscapeQuotes() {
+        assertThat(ParserUtils.quote("For input string: 
\"error\"")).isEqualTo("\"For input string: \\\"error\\\"\"");
+    }
+
+    @Test
+    void quoteShouldEscapeBackslashes() {
+        assertThat(ParserUtils.quote("a\\b")).isEqualTo("\"a\\\\b\"");
+    }
+
+    @Test
+    void quoteShouldNotAlterRegularMessages() {
+        assertThat(ParserUtils.quote("Missing argument: script 
name")).isEqualTo("\"Missing argument: script name\"");
+    }
+
     @Test
     void getSizeShouldThrowOnNullInput() {
         assertThatThrownBy(() -> ParserUtils.getSize(null))


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

Reply via email to