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]