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