You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@james.apache.org by bt...@apache.org on 2022/11/22 07:11:49 UTC

[james-project] 04/12: JAMES-3858 Add THREADID search key in IMAP SEARCH

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

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

commit b25588f17b0839faed052ca0e10c534df07d3fcb
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Thu Nov 17 11:47:14 2022 +0700

    JAMES-3858 Add THREADID search key in IMAP SEARCH
---
 .../org/apache/james/mailbox/MailboxManager.java   |   3 +
 .../james/mailbox/store/StoreMailboxManager.java   |   1 +
 .../james/imap/api/message/request/SearchKey.java  | 103 ++++++++++++---------
 .../imap/decode/parser/SearchCommandParser.java    |  15 +++
 .../james/imap/processor/SearchProcessor.java      |   3 +
 .../james/imapserver/netty/IMAPServerTest.java     |  38 +++++++-
 6 files changed, 118 insertions(+), 45 deletions(-)

diff --git a/mailbox/api/src/main/java/org/apache/james/mailbox/MailboxManager.java b/mailbox/api/src/main/java/org/apache/james/mailbox/MailboxManager.java
index 1221e21661..1f1552ca12 100644
--- a/mailbox/api/src/main/java/org/apache/james/mailbox/MailboxManager.java
+++ b/mailbox/api/src/main/java/org/apache/james/mailbox/MailboxManager.java
@@ -423,4 +423,7 @@ public interface MailboxManager extends RequestAware, RightManager, MailboxAnnot
             c -> toBeWrapped,
             Runnable::run);
     }
+
+    MessageId.Factory getMessageIdFactory();
+
 }
diff --git a/mailbox/store/src/main/java/org/apache/james/mailbox/store/StoreMailboxManager.java b/mailbox/store/src/main/java/org/apache/james/mailbox/store/StoreMailboxManager.java
index e506b44671..01eec15d01 100644
--- a/mailbox/store/src/main/java/org/apache/james/mailbox/store/StoreMailboxManager.java
+++ b/mailbox/store/src/main/java/org/apache/james/mailbox/store/StoreMailboxManager.java
@@ -164,6 +164,7 @@ public class StoreMailboxManager implements MailboxManager {
         return quotaComponents;
     }
 
+    @Override
     public Factory getMessageIdFactory() {
         return messageIdFactory;
     }
diff --git a/protocols/imap/src/main/java/org/apache/james/imap/api/message/request/SearchKey.java b/protocols/imap/src/main/java/org/apache/james/imap/api/message/request/SearchKey.java
index 8bb3adb150..89655e0ce6 100644
--- a/protocols/imap/src/main/java/org/apache/james/imap/api/message/request/SearchKey.java
+++ b/protocols/imap/src/main/java/org/apache/james/imap/api/message/request/SearchKey.java
@@ -50,6 +50,7 @@ import static org.apache.james.imap.api.message.request.SearchKey.Type.TYPE_SINC
 import static org.apache.james.imap.api.message.request.SearchKey.Type.TYPE_SMALLER;
 import static org.apache.james.imap.api.message.request.SearchKey.Type.TYPE_SUBJECT;
 import static org.apache.james.imap.api.message.request.SearchKey.Type.TYPE_TEXT;
+import static org.apache.james.imap.api.message.request.SearchKey.Type.TYPE_THREADID;
 import static org.apache.james.imap.api.message.request.SearchKey.Type.TYPE_TO;
 import static org.apache.james.imap.api.message.request.SearchKey.Type.TYPE_UID;
 import static org.apache.james.imap.api.message.request.SearchKey.Type.TYPE_UNANSWERED;
@@ -117,44 +118,45 @@ public final class SearchKey {
         TYPE_AND,
         TYPE_YOUNGER,
         TYPE_OLDER,
-        TYPE_MODSEQ
+        TYPE_MODSEQ,
+        TYPE_THREADID
     }
 
-    private static final SearchKey UNSEEN = new SearchKey(TYPE_UNSEEN, null, null, 0, null, null, null, null, -1, -1);
+    private static final SearchKey UNSEEN = new SearchKey(TYPE_UNSEEN, null, null, 0, null, null, null, null, -1, -1, null);
 
-    private static final SearchKey UNFLAGGED = new SearchKey(TYPE_UNFLAGGED, null, null, 0, null, null, null, null, -1, -1);
+    private static final SearchKey UNFLAGGED = new SearchKey(TYPE_UNFLAGGED, null, null, 0, null, null, null, null, -1, -1, null);
 
-    private static final SearchKey UNDRAFT = new SearchKey(TYPE_UNDRAFT, null, null, 0, null, null, null, null, -1, -1);
+    private static final SearchKey UNDRAFT = new SearchKey(TYPE_UNDRAFT, null, null, 0, null, null, null, null, -1, -1, null);
 
-    private static final SearchKey UNDELETED = new SearchKey(TYPE_UNDELETED, null, null, 0, null, null, null, null, -1, -1);
+    private static final SearchKey UNDELETED = new SearchKey(TYPE_UNDELETED, null, null, 0, null, null, null, null, -1, -1, null);
 
-    private static final SearchKey UNANSWERED = new SearchKey(TYPE_UNANSWERED, null, null, 0, null, null, null, null, -1, -1);
+    private static final SearchKey UNANSWERED = new SearchKey(TYPE_UNANSWERED, null, null, 0, null, null, null, null, -1, -1, null);
 
-    private static final SearchKey SEEN = new SearchKey(TYPE_SEEN, null, null, 0, null, null, null, null, -1, -1);
+    private static final SearchKey SEEN = new SearchKey(TYPE_SEEN, null, null, 0, null, null, null, null, -1, -1, null);
 
-    private static final SearchKey RECENT = new SearchKey(TYPE_RECENT, null, null, 0, null, null, null, null, -1, -1);
+    private static final SearchKey RECENT = new SearchKey(TYPE_RECENT, null, null, 0, null, null, null, null, -1, -1, null);
 
-    private static final SearchKey OLD = new SearchKey(TYPE_OLD, null, null, 0, null, null, null, null, -1, -1);
+    private static final SearchKey OLD = new SearchKey(TYPE_OLD, null, null, 0, null, null, null, null, -1, -1, null);
 
-    private static final SearchKey NEW = new SearchKey(TYPE_NEW, null, null, 0, null, null, null, null, -1, -1);
+    private static final SearchKey NEW = new SearchKey(TYPE_NEW, null, null, 0, null, null, null, null, -1, -1, null);
 
-    private static final SearchKey FLAGGED = new SearchKey(TYPE_FLAGGED, null, null, 0, null, null, null, null, -1, -1);
+    private static final SearchKey FLAGGED = new SearchKey(TYPE_FLAGGED, null, null, 0, null, null, null, null, -1, -1, null);
 
-    private static final SearchKey DRAFT = new SearchKey(TYPE_DRAFT, null, null, 0, null, null, null, null, -1, -1);
+    private static final SearchKey DRAFT = new SearchKey(TYPE_DRAFT, null, null, 0, null, null, null, null, -1, -1, null);
 
-    private static final SearchKey DELETED = new SearchKey(TYPE_DELETED, null, null, 0, null, null, null, null, -1, -1);
+    private static final SearchKey DELETED = new SearchKey(TYPE_DELETED, null, null, 0, null, null, null, null, -1, -1, null);
 
-    private static final SearchKey ANSWERED = new SearchKey(TYPE_ANSWERED, null, null, 0, null, null, null, null, -1, -1);
+    private static final SearchKey ANSWERED = new SearchKey(TYPE_ANSWERED, null, null, 0, null, null, null, null, -1, -1, null);
 
-    private static final SearchKey ALL = new SearchKey(TYPE_ALL, null, null, 0, null, null, null, null, -1, -1);
+    private static final SearchKey ALL = new SearchKey(TYPE_ALL, null, null, 0, null, null, null, null, -1, -1, null);
 
     // NUMBERS
     public static SearchKey buildSequenceSet(IdRange[] ids) {
-        return new SearchKey(TYPE_SEQUENCE_SET, null, null, 0, null, null, null, ids, -1, -1);
+        return new SearchKey(TYPE_SEQUENCE_SET, null, null, 0, null, null, null, ids, -1, -1, null);
     }
 
     public static SearchKey buildUidSet(UidRange[] ids) {
-        return new SearchKey(TYPE_UID, null, null, 0, null, null, ids, null, -1, -1);
+        return new SearchKey(TYPE_UID, null, null, 0, null, null, ids, null, -1, -1, null);
     }
 
     // NO PARAMETERS
@@ -216,95 +218,99 @@ public final class SearchKey {
 
     // ONE VALUE
     public static SearchKey buildBcc(String value) {
-        return new SearchKey(TYPE_BCC, null, null, 0, null, value, null, null, -1, -1);
+        return new SearchKey(TYPE_BCC, null, null, 0, null, value, null, null, -1, -1, null);
     }
 
     public static SearchKey buildBody(String value) {
-        return new SearchKey(TYPE_BODY, null, null, 0, null, value, null, null, -1, -1);
+        return new SearchKey(TYPE_BODY, null, null, 0, null, value, null, null, -1, -1, null);
     }
 
     public static SearchKey buildCc(String value) {
-        return new SearchKey(TYPE_CC, null, null, 0, null, value, null, null, -1, -1);
+        return new SearchKey(TYPE_CC, null, null, 0, null, value, null, null, -1, -1, null);
     }
 
     public static SearchKey buildFrom(String value) {
-        return new SearchKey(TYPE_FROM, null, null, 0, null, value, null, null, -1, -1);
+        return new SearchKey(TYPE_FROM, null, null, 0, null, value, null, null, -1, -1, null);
     }
 
     public static SearchKey buildKeyword(String value) {
-        return new SearchKey(TYPE_KEYWORD, null, null, 0, null, value, null, null, -1, -1);
+        return new SearchKey(TYPE_KEYWORD, null, null, 0, null, value, null, null, -1, -1, null);
     }
 
     public static SearchKey buildSubject(String value) {
-        return new SearchKey(TYPE_SUBJECT, null, null, 0, null, value, null, null, -1, -1);
+        return new SearchKey(TYPE_SUBJECT, null, null, 0, null, value, null, null, -1, -1, null);
     }
 
     public static SearchKey buildText(String value) {
-        return new SearchKey(TYPE_TEXT, null, null, 0, null, value, null, null, -1, -1);
+        return new SearchKey(TYPE_TEXT, null, null, 0, null, value, null, null, -1, -1, null);
     }
 
     public static SearchKey buildTo(String value) {
-        return new SearchKey(TYPE_TO, null, null, 0, null, value, null, null, -1, -1);
+        return new SearchKey(TYPE_TO, null, null, 0, null, value, null, null, -1, -1, null);
+    }  
+    
+    public static SearchKey buildThreadId(String value) {
+        return new SearchKey(TYPE_THREADID, null, null, 0, null, null, null, null, -1, -1, value);
     }
 
     public static SearchKey buildUnkeyword(String value) {
-        return new SearchKey(TYPE_UNKEYWORD, null, null, 0, null, value, null, null, -1, -1);
+        return new SearchKey(TYPE_UNKEYWORD, null, null, 0, null, value, null, null, -1, -1, null);
     }
     
     // ONE DATE
     public static SearchKey buildYounger(long seconds) {
-        return new SearchKey(TYPE_YOUNGER, null, null, 0, null, null, null, null, seconds, -1);
+        return new SearchKey(TYPE_YOUNGER, null, null, 0, null, null, null, null, seconds, -1, null);
     }
 
     public static SearchKey buildOlder(long seconds) {
-        return new SearchKey(TYPE_OLDER, null, null, 0, null, null, null, null, seconds, -1);
+        return new SearchKey(TYPE_OLDER, null, null, 0, null, null, null, null, seconds, -1, null);
     }
 
     
     // ONE DATE
     public static SearchKey buildBefore(DayMonthYear date) {
-        return new SearchKey(TYPE_BEFORE, date, null, 0, null, null, null, null, -1, -1);
+        return new SearchKey(TYPE_BEFORE, date, null, 0, null, null, null, null, -1, -1, null);
     }
 
     public static SearchKey buildOn(DayMonthYear date) {
-        return new SearchKey(TYPE_ON, date, null, 0, null, null, null, null, -1, -1);
+        return new SearchKey(TYPE_ON, date, null, 0, null, null, null, null, -1, -1, null);
     }
 
     public static SearchKey buildSentBefore(DayMonthYear date) {
-        return new SearchKey(TYPE_SENTBEFORE, date, null, 0, null, null, null, null, -1, -1);
+        return new SearchKey(TYPE_SENTBEFORE, date, null, 0, null, null, null, null, -1, -1, null);
     }
 
     public static SearchKey buildSentOn(DayMonthYear date) {
-        return new SearchKey(TYPE_SENTON, date, null, 0, null, null, null, null, -1, -1);
+        return new SearchKey(TYPE_SENTON, date, null, 0, null, null, null, null, -1, -1, null);
     }
 
     public static SearchKey buildSentSince(DayMonthYear date) {
-        return new SearchKey(TYPE_SENTSINCE, date, null, 0, null, null, null, null, -1, -1);
+        return new SearchKey(TYPE_SENTSINCE, date, null, 0, null, null, null, null, -1, -1, null);
     }
 
     public static SearchKey buildSince(DayMonthYear date) {
-        return new SearchKey(TYPE_SINCE, date, null, 0, null, null, null, null, -1, -1);
+        return new SearchKey(TYPE_SINCE, date, null, 0, null, null, null, null, -1, -1, null);
     }
 
     // FIELD VALUE
     public static SearchKey buildHeader(String name, String value) {
-        return new SearchKey(TYPE_HEADER, null, null, 0, name, value, null, null, -1, -1);
+        return new SearchKey(TYPE_HEADER, null, null, 0, name, value, null, null, -1, -1, null);
     }
 
     // ONE NUMBER
     public static SearchKey buildLarger(long size) {
-        return new SearchKey(TYPE_LARGER, null, null, size, null, null, null, null, -1, -1);
+        return new SearchKey(TYPE_LARGER, null, null, size, null, null, null, null, -1, -1, null);
     }
 
     public static SearchKey buildSmaller(long size) {
-        return new SearchKey(TYPE_SMALLER, null, null, size, null, null, null, null, -1, -1);
+        return new SearchKey(TYPE_SMALLER, null, null, size, null, null, null, null, -1, -1, null);
     }
 
     // NOT
     public static SearchKey buildNot(SearchKey key) {
         final List<SearchKey> keys = new ArrayList<>();
         keys.add(key);
-        return new SearchKey(TYPE_NOT, null, keys, 0, null, null, null, null, -1, -1);
+        return new SearchKey(TYPE_NOT, null, keys, 0, null, null, null, null, -1, -1, null);
     }
 
     // OR
@@ -312,7 +318,7 @@ public final class SearchKey {
         final List<SearchKey> keys = new ArrayList<>();
         keys.add(keyOne);
         keys.add(keyTwo);
-        return new SearchKey(TYPE_OR, null, keys, 0, null, null, null, null, -1, -1);
+        return new SearchKey(TYPE_OR, null, keys, 0, null, null, null, null, -1, -1, null);
     }
 
     /**
@@ -323,11 +329,11 @@ public final class SearchKey {
      * @return <code>SearchKey</code>, not null
      */
     public static SearchKey buildAnd(List<SearchKey> keys) {
-        return new SearchKey(TYPE_AND, null, keys, 0, null, null, null, null, -1, -1);
+        return new SearchKey(TYPE_AND, null, keys, 0, null, null, null, null, -1, -1, null);
     }
 
     public static SearchKey buildModSeq(long modSeq) {
-        return new SearchKey(TYPE_MODSEQ, null, null, 0, null, null, null, null, -1, modSeq);
+        return new SearchKey(TYPE_MODSEQ, null, null, 0, null, null, null, null, -1, modSeq, null);
     }
     
     private final Type type;
@@ -349,8 +355,10 @@ public final class SearchKey {
     private final long seconds;
 
     private final long modSeq;
+
+    private final String threadId;
     
-    private SearchKey(Type type, DayMonthYear date, List<SearchKey> keys, long number, String name, String value, UidRange[] uids, IdRange[] sequence, long seconds, long modSeq) {
+    private SearchKey(Type type, DayMonthYear date, List<SearchKey> keys, long number, String name, String value, UidRange[] uids, IdRange[] sequence, long seconds, long modSeq, String threadId) {
         this.type = type;
         this.date = date;
         this.keys = keys;
@@ -361,6 +369,11 @@ public final class SearchKey {
         this.modSeq = modSeq;
         this.uids = uids;
         this.sequence = sequence;
+        this.threadId = threadId;
+    }
+
+    public String getThreadId() {
+        return threadId;
     }
     
     /**
@@ -468,7 +481,8 @@ public final class SearchKey {
                 && Objects.equals(this.name, searchKey.name)
                 && Objects.equals(this.value, searchKey.value)
                 && Arrays.equals(this.sequence, searchKey.sequence)
-                && Arrays.equals(this.uids, searchKey.uids);
+                && Arrays.equals(this.uids, searchKey.uids)
+                && Objects.equals(this.threadId, searchKey.threadId);
         }
         return false;
     }
@@ -476,7 +490,7 @@ public final class SearchKey {
     @Override
     public final int hashCode() {
         return Objects.hash(type, date, keys, size, name, value,
-            Arrays.hashCode(sequence), Arrays.hashCode(uids), seconds, modSeq);
+            Arrays.hashCode(sequence), Arrays.hashCode(uids), seconds, modSeq, threadId);
     }
 
     @Override
@@ -499,6 +513,7 @@ public final class SearchKey {
             .add("uids", Optional.ofNullable(uids).map(ImmutableList::copyOf).orElse(null))
             .add("sequences", Optional.ofNullable(sequence).map(ImmutableList::copyOf).orElse(null))
             .add("keys", Optional.ofNullable(keys).map(ImmutableList::copyOf).orElse(null))
+            .add("threadId", threadId)
             .toString();
     }
 }
diff --git a/protocols/imap/src/main/java/org/apache/james/imap/decode/parser/SearchCommandParser.java b/protocols/imap/src/main/java/org/apache/james/imap/decode/parser/SearchCommandParser.java
index f6d0e3e3c6..3283b754b5 100644
--- a/protocols/imap/src/main/java/org/apache/james/imap/decode/parser/SearchCommandParser.java
+++ b/protocols/imap/src/main/java/org/apache/james/imap/decode/parser/SearchCommandParser.java
@@ -43,6 +43,7 @@ import org.apache.james.imap.api.process.ImapSession;
 import org.apache.james.imap.decode.DecodingException;
 import org.apache.james.imap.decode.ImapRequestLineReader;
 import org.apache.james.imap.message.request.SearchRequest;
+import org.apache.james.mailbox.model.ThreadId;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -270,6 +271,8 @@ public class SearchCommandParser extends AbstractUidCommandParser {
             return text(request, charset);
         case 'O':
             return to(request, charset);
+        case 'H':
+            return threadId(request, charset);
         default:
             throw new DecodingException(HumanReadableText.ILLEGAL_ARGUMENTS, "Unknown search key");
         }
@@ -748,6 +751,18 @@ public class SearchCommandParser extends AbstractUidCommandParser {
         return result;
     }
 
+    private SearchKey threadId(ImapRequestLineReader request, Charset charset) throws DecodingException {
+        nextIsR(request);
+        nextIsE(request);
+        nextIsA(request);
+        nextIsD(request);
+        nextIsI(request);
+        nextIsD(request);
+        nextIsSpace(request);
+        String astring = request.astring(charset);
+        return SearchKey.buildThreadId(astring);
+    }
+
     private SearchKey subject(ImapRequestLineReader request, Charset charset) throws DecodingException {
         final SearchKey result;
         nextIsB(request);
diff --git a/protocols/imap/src/main/java/org/apache/james/imap/processor/SearchProcessor.java b/protocols/imap/src/main/java/org/apache/james/imap/processor/SearchProcessor.java
index 191eab4b7d..0805f7f65b 100644
--- a/protocols/imap/src/main/java/org/apache/james/imap/processor/SearchProcessor.java
+++ b/protocols/imap/src/main/java/org/apache/james/imap/processor/SearchProcessor.java
@@ -61,6 +61,7 @@ import org.apache.james.mailbox.model.SearchQuery;
 import org.apache.james.mailbox.model.SearchQuery.AddressType;
 import org.apache.james.mailbox.model.SearchQuery.Criterion;
 import org.apache.james.mailbox.model.SearchQuery.DateResolution;
+import org.apache.james.mailbox.model.ThreadId;
 import org.apache.james.metrics.api.MetricFactory;
 import org.apache.james.util.MDCBuilder;
 import org.apache.james.util.ReactorUtils;
@@ -372,6 +373,8 @@ public class SearchProcessor extends AbstractMailboxProcessor<SearchRequest> imp
             session.setAttribute(SEARCH_MODSEQ, true);
             long modSeq = key.getModSeq();
             return SearchQuery.or(SearchQuery.modSeqEquals(modSeq), SearchQuery.modSeqGreaterThan(modSeq));
+        case TYPE_THREADID:
+            return SearchQuery.threadId(ThreadId.fromBaseMessageId(getMailboxManager().getMessageIdFactory().fromString(key.getThreadId())));
         default:
             LOGGER.warn("Ignoring unknown search key {}", type);
             return SearchQuery.all();
diff --git a/server/protocols/protocols-imap4/src/test/java/org/apache/james/imapserver/netty/IMAPServerTest.java b/server/protocols/protocols-imap4/src/test/java/org/apache/james/imapserver/netty/IMAPServerTest.java
index fcfb6f0606..c7d20cf0f5 100644
--- a/server/protocols/protocols-imap4/src/test/java/org/apache/james/imapserver/netty/IMAPServerTest.java
+++ b/server/protocols/protocols-imap4/src/test/java/org/apache/james/imapserver/netty/IMAPServerTest.java
@@ -1390,18 +1390,54 @@ class IMAPServerTest {
     class Search {
         IMAPServer imapServer;
         private int port;
+        private SocketChannel clientConnection;
 
         @BeforeEach
         void beforeEach() throws Exception {
             imapServer = createImapServer("imapServer.xml");
             port = imapServer.getListenAddresses().get(0).getPort();
+
+            clientConnection = SocketChannel.open();
+            clientConnection.connect(new InetSocketAddress(LOCALHOST_IP, port));
+            readBytes(clientConnection);
         }
 
         @AfterEach
-        void tearDown() {
+        void tearDown() throws Exception {
+            clientConnection.close();
             imapServer.destroy();
         }
 
+        // Not an MPT test as ThreadId is a variable server-set and implementation specific
+        @Test
+        void imapSearchShouldSupportThreadId() throws Exception {
+            MailboxSession mailboxSession = memoryIntegrationResources.getMailboxManager().createSystemSession(USER);
+            memoryIntegrationResources.getMailboxManager()
+                .createMailbox(MailboxPath.inbox(USER), mailboxSession);
+            MessageManager.AppendResult appendResult = memoryIntegrationResources.getMailboxManager()
+                .getMailbox(MailboxPath.inbox(USER), mailboxSession)
+                .appendMessage(MessageManager.AppendCommand.builder().build("MIME-Version: 1.0\r\n" +
+                    "Content-Type: text/html; charset=UTF-8\r\n" +
+                    "Content-Transfer-Encoding: quoted-printable\r\n" +
+                    "From: =?ISO-8859-1?Q?Beno=EEt_TELLIER?= <b...@linagora.com>\r\n" +
+                    "Sender: =?ISO-8859-1?Q?Beno=EEt_TELLIER?= <b...@linagora.com>\r\n" +
+                    "Reply-To: b@linagora.com\r\n" +
+                    "To: =?ISO-8859-1?Q?Beno=EEt_TELLIER?= <b...@linagora.com>\r\n" +
+                    "Subject: Test utf-8 charset\r\n" +
+                    "Message-ID: <Mi...@linagora.com>\r\n" +
+                    "Date: Sun, 28 Mar 2021 03:58:06 +0000\r\n" +
+                    "\r\n" +
+                    "<p>=E5=A4=A9=E5=A4=A9=E5=90=91=E4=B8=8A<br></p>\r\n"), mailboxSession);
+
+            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 SELECT INBOX\r\n".getBytes(StandardCharsets.UTF_8)));
+            readStringUntil(clientConnection, s -> s.contains("a1 OK"));
+            clientConnection.write(ByteBuffer.wrap(String.format("a2 UID SEARCH THREADID %s\r\n", appendResult.getThreadId().serialize()).getBytes(StandardCharsets.UTF_8)));
+
+            readStringUntil(clientConnection, s -> s.contains(("* SEARCH " + appendResult.getId().getUid().asLong())));
+        }
+
         @Test
         void searchingShouldSupportMultipleUTF8Criteria() throws Exception {
             String host = "127.0.0.1";


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org