From 7fb44b31b1c6a281f4974db046ec6de29eab0baf Mon Sep 17 00:00:00 2001 From: Marcel May Date: Thu, 14 Sep 2023 22:54:07 +0200 Subject: [PATCH] Fixing SearchCommandParser cannot be cast to jakarta.mail.search.SearchTerm (fixes #591) --- .../imap/commands/SearchCommandParser.java | 45 +++++++----- .../commands/SearchCommandParserTest.java | 68 ++++++++++++++----- 2 files changed, 80 insertions(+), 33 deletions(-) diff --git a/greenmail-core/src/main/java/com/icegreen/greenmail/imap/commands/SearchCommandParser.java b/greenmail-core/src/main/java/com/icegreen/greenmail/imap/commands/SearchCommandParser.java index fe67275470..0ba66f1f88 100644 --- a/greenmail-core/src/main/java/com/icegreen/greenmail/imap/commands/SearchCommandParser.java +++ b/greenmail-core/src/main/java/com/icegreen/greenmail/imap/commands/SearchCommandParser.java @@ -50,6 +50,7 @@ protected enum DataFormats { STRING // Complete further types on demand } + /** * Parses the request argument into a valid search term. Not yet fully implemented - see SearchKey enum. *

@@ -57,7 +58,7 @@ protected enum DataFormats { * Throws an UnsupportedCharsetException if provided CHARSET is not supported. */ public SearchTerm searchTerm(ImapRequestLineReader request) - throws ProtocolException { + throws ProtocolException { Charset charset = StandardCharsets.US_ASCII; // Default // Stack contains mix of SearchOperator and SearchTerm instances // and will be processed in two steps @@ -80,16 +81,8 @@ public SearchTerm searchTerm(ImapRequestLineReader request) request.consume(); request.consumeAll(CHR_SPACE); - LinkedList groupItems = new LinkedList<>(); - Object item; - while ((item = stack.pop()) != SearchOperator.GROUP) { - groupItems.addFirst((SearchTerm) item); - } - if (groupItems.size() == 1) { - stack.push(groupItems.get(0)); // Single item - } else { - stack.push(new AndTerm(groupItems.toArray(new SearchTerm[0]))); - } + // Resolve group to single term + handleGroup(stack); } else { throw new IllegalStateException("Unsupported atom special char <" + next + ">"); } @@ -110,7 +103,7 @@ else if (CHARSET_TOKEN.equals(token)) { try { charset = Charset.forName(charsetName); } catch (UnsupportedCharsetException ex) { - log.error("Unsupported charset '{}", charsetName); + log.error("Unsupported charset '{}'", charsetName); throw ex; } } else { @@ -140,21 +133,37 @@ else if (CHARSET_TOKEN.equals(token)) { return handleOperators(stack); } + private void handleGroup(Deque stack) { + Deque groupItems = new LinkedList<>(); + Object item; + while ((item = stack.pop()) != SearchOperator.GROUP) { + groupItems.addLast(item); + } + final SearchTerm groupTerm = handleOperators(groupItems); + stack.push(groupTerm); + } + private void handleSearchArg(ImapRequestLineReader request, SearchKey key, SearchTermBuilder searchTermBuilder, Charset charset) throws ProtocolException { String paramValue; - switch(key.getArgDataFormat()) { + switch (key.getArgDataFormat()) { case STRING: paramValue = string(request, charset); break; case ATOM: paramValue = atomOnly(request); break; - default: throw new IllegalStateException("Argument type "+key.getArgDataFormat()+" not implemented for key "+key); + default: + throw new IllegalStateException("Argument type " + key.getArgDataFormat() + " not implemented for key " + key); } searchTermBuilder.addParameter(paramValue); } private SearchTerm handleOperators(Deque stack) { + // Must be single term + if (stack.size() == 1) { + return (SearchTerm) stack.pop(); + } + LinkedList params = new LinkedList<>(); while (!stack.isEmpty()) { final Object o = stack.pop(); @@ -184,10 +193,12 @@ private SearchTerm handleOperators(Deque stack) { } } - if (params.size() != 1) { - throw new IllegalStateException("Expected exactly one root search term but got " + params); + if (params.size() > 1) { + SearchTerm[] items = params.toArray(new SearchTerm[0]); + return new AndTerm(items); + } else { + return params.pop(); } - return params.pop(); } } diff --git a/greenmail-core/src/test/java/com/icegreen/greenmail/imap/commands/SearchCommandParserTest.java b/greenmail-core/src/test/java/com/icegreen/greenmail/imap/commands/SearchCommandParserTest.java index 09b1b75c29..97871a03e3 100644 --- a/greenmail-core/src/test/java/com/icegreen/greenmail/imap/commands/SearchCommandParserTest.java +++ b/greenmail-core/src/test/java/com/icegreen/greenmail/imap/commands/SearchCommandParserTest.java @@ -1,27 +1,27 @@ package com.icegreen.greenmail.imap.commands; -import java.io.ByteArrayInputStream; +import com.icegreen.greenmail.imap.ImapRequestLineReader; +import com.icegreen.greenmail.imap.ProtocolException; import jakarta.mail.Flags; import jakarta.mail.Message; import jakarta.mail.internet.AddressException; import jakarta.mail.internet.InternetAddress; import jakarta.mail.search.*; - -import com.icegreen.greenmail.imap.ImapRequestLineReader; -import com.icegreen.greenmail.imap.ProtocolException; import org.junit.Test; +import java.io.ByteArrayInputStream; + import static org.assertj.core.api.Assertions.assertThat; public class SearchCommandParserTest { @Test public void testHeader() throws ProtocolException { SearchTerm expectedTerm = new AndTerm( - new AndTerm(new SearchTerm[]{ - new HeaderTerm("Message-ID", "<1627010197.0.1593681191102@[192.168.242.10]>"), - new FlagTerm(new Flags(Flags.Flag.SEEN), true) - }), - SearchTermBuilder.create(SearchKey.ALL).build() + new AndTerm(new SearchTerm[]{ + new HeaderTerm("Message-ID", "<1627010197.0.1593681191102@[192.168.242.10]>"), + new FlagTerm(new Flags(Flags.Flag.SEEN), true) + }), + SearchTermBuilder.create(SearchKey.ALL).build() ); SearchTerm searchTerm = parse("(HEADER Message-ID <1627010197.0.1593681191102@[192.168.242.10]> SEEN) ALL"); @@ -56,12 +56,12 @@ public void testSmallerAndLargerParseCommand() throws ProtocolException { @Test public void testAndSubjectOrToFrom() throws ProtocolException, AddressException { SearchTerm expectedTerm = new AndTerm(new SearchTerm[]{ - new SubjectTerm("Greenmail"), - new OrTerm( - new RecipientTerm(Message.RecipientType.TO, new InternetAddress("to@localhost")), - new FromTerm(new InternetAddress("from@localhost")) - ), - SearchTermBuilder.create(SearchKey.ALL).build()}); + new SubjectTerm("Greenmail"), + new OrTerm( + new RecipientTerm(Message.RecipientType.TO, new InternetAddress("to@localhost")), + new FromTerm(new InternetAddress("from@localhost")) + ), + SearchTermBuilder.create(SearchKey.ALL).build()}); // SUBJECT Greenmail (OR TO from@localhost FROM from@localhost) ALL SearchTerm searchTerm = parse("SUBJECT Greenmail OR TO to@localhost FROM from@localhost ALL"); @@ -73,12 +73,48 @@ public void testAndSubjectOrToFrom() throws ProtocolException, AddressException public void testNotKeyword() throws ProtocolException { Flags flags = new Flags(); flags.add("ABC"); - SearchTerm expectedTerm = new NotTerm(new FlagTerm(flags, true)); + SearchTerm expectedTerm = new NotTerm(new FlagTerm(flags, true)); SearchTerm searchTerm = parse("NOT (KEYWORD ABC)"); assertThat(searchTerm).isEqualTo(expectedTerm); } + @Test + public void testSimpleOr() throws ProtocolException { + SearchTerm expectedTerm = new OrTerm( + new FlagTerm(new Flags(Flags.Flag.DRAFT), true), + new FlagTerm(new Flags(Flags.Flag.SEEN), true) + ); + SearchTerm searchTerm = parse("OR (DRAFT) (SEEN)"); + + assertThat(searchTerm).isEqualTo(expectedTerm); + } + + @Test + public void testIssue591simple() throws ProtocolException { + SearchTerm expectedTerm = new OrTerm( + new NotTerm(new FlagTerm(new Flags(Flags.Flag.SEEN), true)), + new FlagTerm(new Flags(Flags.Flag.SEEN), true) + ); + SearchTerm searchTerm = parse("OR (NOT (SEEN)) (SEEN)"); + + assertThat(searchTerm).isEqualTo(expectedTerm); + } + + @Test + public void testIssue591complex() throws ProtocolException { + SearchTerm expectedTerm = new OrTerm( + new NotTerm(new FlagTerm(new Flags(Flags.Flag.SEEN), true)), + new OrTerm( + new FlagTerm(new Flags("foo"), true), + new FlagTerm(new Flags("bar"), true) + ) + ); + SearchTerm searchTerm = parse("OR (NOT (SEEN)) (OR (KEYWORD foo) (KEYWORD bar))"); + + assertThat(searchTerm).isEqualTo(expectedTerm); + } + private SearchTerm parse(String line) throws ProtocolException { final byte[] bytes = (line.endsWith("\n") ? line : (line + '\n')).getBytes(); ByteArrayInputStream ins = new ByteArrayInputStream(bytes);