diff --git a/src/agents/imap/CMakeLists.txt b/src/agents/imap/CMakeLists.txt
index 91890fc..b9b8f39 100644
--- a/src/agents/imap/CMakeLists.txt
+++ b/src/agents/imap/CMakeLists.txt
@@ -4,6 +4,7 @@ add_executable(bongoimap
idle.c
uidplus.c
special-use.c
+ list-extended.c
copy.c
event.c
fetch.c
@@ -63,4 +64,13 @@ if(BUILD_TESTING)
${CMAKE_CURRENT_SOURCE_DIR}
)
add_test(NAME imap-special-use COMMAND imap-special-use-test)
+
+ add_executable(imap-list-extended-test
+ tests/list-extended-test.c
+ list-extended.c
+ )
+ target_include_directories(imap-list-extended-test PRIVATE
+ ${CMAKE_CURRENT_SOURCE_DIR}
+ )
+ add_test(NAME imap-list-extended COMMAND imap-list-extended-test)
endif()
diff --git a/src/agents/imap/capabilities.c b/src/agents/imap/capabilities.c
index 2133de9..b14893a 100644
--- a/src/agents/imap/capabilities.c
+++ b/src/agents/imap/capabilities.c
@@ -56,7 +56,7 @@ IMAPFormatCapabilities(char *buffer, size_t buffer_size,
if (!Append(buffer, buffer_size, &length,
"* CAPABILITY IMAP4rev1 IMAP4 NAMESPACE IDLE UIDPLUS MOVE "
- "SPECIAL-USE CREATE-SPECIAL-USE XSENDER") ||
+ "SPECIAL-USE CREATE-SPECIAL-USE LIST-EXTENDED XSENDER") ||
(!authenticated && tls_active &&
!Append(buffer, buffer_size, &length,
" AUTH=PLAIN AUTH=LOGIN SASL-IR")) ||
diff --git a/src/agents/imap/imapd.c b/src/agents/imap/imapd.c
index 8d98db7..aba24fd 100644
--- a/src/agents/imap/imapd.c
+++ b/src/agents/imap/imapd.c
@@ -47,6 +47,7 @@
#include "idle.h"
#include "uidplus.h"
#include "special-use.h"
+#include "list-extended.h"
ImapGlobal Imap;
@@ -1797,6 +1798,247 @@ SendFolderInfo(Connection *conn, const char *command, FolderInformation *folder)
return(STATUS_ABORT);
}
+#define IMAP_LIST_MAX_PATTERNS 16
+
+typedef struct {
+ unsigned int selectionOptions;
+ unsigned int returnOptions;
+ size_t patternCount;
+ char patterns[IMAP_LIST_MAX_PATTERNS][STORE_MAX_COLLECTION * 2];
+} ExtendedListRequest;
+
+static long
+ListAddPattern(ExtendedListRequest *request, const char *reference,
+ const char *pattern)
+{
+ int written;
+
+ if (request->patternCount == IMAP_LIST_MAX_PATTERNS)
+ return STATUS_INVALID_ARGUMENT;
+ if (!*reference) {
+ written = snprintf(request->patterns[request->patternCount],
+ sizeof(request->patterns[0]), "%s", pattern);
+ } else if (!*pattern) {
+ written = snprintf(request->patterns[request->patternCount],
+ sizeof(request->patterns[0]), "%s", reference);
+ } else {
+ written = snprintf(request->patterns[request->patternCount],
+ sizeof(request->patterns[0]), "%s/%s",
+ reference, pattern);
+ }
+ if (written < 0 || (size_t)written >= sizeof(request->patterns[0]))
+ return STATUS_PATH_TOO_LONG;
+ request->patternCount++;
+ return STATUS_CONTINUE;
+}
+
+static long
+ParseExtendedListRequest(ImapSession *session, char *start,
+ ExtendedListRequest *request)
+{
+ FolderPath reference;
+ FolderPath pattern;
+ char *ptr;
+ char *argument = NULL;
+ long ccode;
+
+ memset(request, 0, sizeof(*request));
+ ptr = FindNextArgument(start);
+ if (!ptr) return STATUS_INVALID_ARGUMENT;
+
+ if (*ptr == '(') {
+ ccode = GrabArgument(session, &ptr, &argument);
+ if (ccode != STATUS_CONTINUE) return ccode;
+ if (!IMAPListParseSelectionOptions(argument,
+ &request->selectionOptions)) {
+ MemFree(argument);
+ return STATUS_INVALID_ARGUMENT;
+ }
+ MemFree(argument);
+ argument = NULL;
+ if (request->selectionOptions == 0) return STATUS_INVALID_ARGUMENT;
+ }
+
+ ccode = GetPathArgument(session, ptr, &ptr, &reference, TRUE);
+ if (ccode != STATUS_CONTINUE) return ccode;
+
+ {
+ char *patternStart = FindNextArgument(ptr);
+ if (!patternStart) {
+ FreePathArgument(&reference);
+ return STATUS_INVALID_ARGUMENT;
+ }
+ if (*patternStart == '(') {
+ char *patterns[IMAP_LIST_MAX_PATTERNS];
+ size_t count;
+ size_t i;
+
+ ptr = patternStart;
+ ccode = GrabArgument(session, &ptr, &argument);
+ if (ccode != STATUS_CONTINUE) {
+ FreePathArgument(&reference);
+ return ccode;
+ }
+ count = IMAPListSplitPatterns(argument, patterns,
+ IMAP_LIST_MAX_PATTERNS);
+ if (count == 0) ccode = STATUS_INVALID_ARGUMENT;
+ for (i = 0; i < count && ccode == STATUS_CONTINUE; i++) {
+ ccode = ListAddPattern(request, reference.name, patterns[i]);
+ }
+ MemFree(argument);
+ argument = NULL;
+ } else {
+ ccode = GetPathArgument(session, ptr, &ptr, &pattern, TRUE);
+ if (ccode == STATUS_CONTINUE) {
+ ccode = ListAddPattern(request, reference.name, pattern.name);
+ FreePathArgument(&pattern);
+ }
+ }
+ }
+ FreePathArgument(&reference);
+ if (ccode != STATUS_CONTINUE) return ccode;
+
+ ptr = FindNextArgument(ptr);
+ if (ptr) {
+ ccode = GrabArgument(session, &ptr, &argument);
+ if (ccode != STATUS_CONTINUE) return ccode;
+ if (XplStrCaseCmp(argument, "RETURN") != 0) {
+ MemFree(argument);
+ return STATUS_INVALID_ARGUMENT;
+ }
+ MemFree(argument);
+ argument = NULL;
+
+ ptr = FindNextArgument(ptr);
+ if (!ptr || *ptr != '(') return STATUS_INVALID_ARGUMENT;
+ ccode = GrabArgument(session, &ptr, &argument);
+ if (ccode != STATUS_CONTINUE) return ccode;
+ if (!IMAPListParseReturnOptions(argument, &request->returnOptions)) {
+ MemFree(argument);
+ return STATUS_INVALID_ARGUMENT;
+ }
+ MemFree(argument);
+ if (request->returnOptions == 0) return STATUS_INVALID_ARGUMENT;
+ if (FindNextArgument(ptr)) return STATUS_INVALID_ARGUMENT;
+ }
+
+ if (request->selectionOptions & IMAP_LIST_SELECT_SUBSCRIBED)
+ request->returnOptions |= IMAP_LIST_RETURN_SUBSCRIBED;
+ return STATUS_CONTINUE;
+}
+
+static BOOL
+FolderMatchesBaseSelection(const FolderInformation *folder,
+ unsigned int selectionOptions)
+{
+ if (selectionOptions & IMAP_LIST_SELECT_REMOTE) return FALSE;
+ if ((selectionOptions & IMAP_LIST_SELECT_SUBSCRIBED) &&
+ (folder->flags & STORE_COLLECTION_FLAG_NON_SUBSCRIBED)) return FALSE;
+ if ((selectionOptions & IMAP_LIST_SELECT_SPECIAL_USE) &&
+ !(folder->flags & STORE_COLLECTION_FLAG_SPECIAL_USE_MASK)) return FALSE;
+ return TRUE;
+}
+
+static BOOL
+FolderHasMatchingDescendant(ImapSession *session, FolderInformation *folder,
+ unsigned int selectionOptions)
+{
+ unsigned long i;
+ size_t length = strlen(folder->name.utf7);
+
+ for (i = (unsigned long)(folder - session->folder.list) + 1;
+ i < session->folder.count; i++) {
+ FolderInformation *candidate = &session->folder.list[i];
+ if (strncmp(candidate->name.utf7, folder->name.utf7, length) != 0 ||
+ candidate->name.utf7[length] != '/') break;
+ if (FolderMatchesBaseSelection(candidate, selectionOptions)) return TRUE;
+ }
+ return FALSE;
+}
+
+static BOOL
+FolderMatchesPatterns(const ExtendedListRequest *request, const char *name)
+{
+ size_t i;
+ for (i = 0; i < request->patternCount; i++) {
+ if (IMAPListPatternMatches(request->patterns[i], name)) return TRUE;
+ }
+ return FALSE;
+}
+
+static long
+SendExtendedFolderInfo(ImapSession *session, FolderInformation *folder,
+ unsigned int returnOptions)
+{
+ char attributes[192];
+ char specialUse[96];
+ size_t length = 0;
+ size_t specialUseLength;
+#define APPEND_LIST_ATTRIBUTE(value) do { \
+ const char *attribute_ = (value); \
+ size_t attribute_length_ = strlen(attribute_); \
+ if (length + (length ? 1 : 0) + attribute_length_ >= sizeof(attributes)) \
+ return STATUS_ABORT; \
+ if (length) attributes[length++] = ' '; \
+ memcpy(attributes + length, attribute_, attribute_length_ + 1); \
+ length += attribute_length_; \
+ } while (0)
+
+ attributes[0] = '\0';
+ if (folder->flags & STORE_COLLECTION_FLAG_HIERARCHY_ONLY)
+ APPEND_LIST_ATTRIBUTE("\\NoSelect");
+ if ((returnOptions & IMAP_LIST_RETURN_SUBSCRIBED) &&
+ !(folder->flags & STORE_COLLECTION_FLAG_NON_SUBSCRIBED))
+ APPEND_LIST_ATTRIBUTE("\\Subscribed");
+ if (returnOptions & IMAP_LIST_RETURN_CHILDREN) {
+ APPEND_LIST_ATTRIBUTE(FolderHasSubFolders(session, folder) ?
+ "\\HasChildren" : "\\HasNoChildren");
+ }
+ specialUseLength = IMAPSpecialUseFormat((uint32_t)folder->flags,
+ specialUse, sizeof(specialUse));
+ if (specialUseLength) APPEND_LIST_ATTRIBUTE(specialUse);
+
+#undef APPEND_LIST_ATTRIBUTE
+ if (ConnWriteF(session->client.conn, "* LIST (%s) \"/\" \"%s\"\r\n",
+ attributes, folder->name.utf7) != -1)
+ return STATUS_CONTINUE;
+ return STATUS_ABORT;
+}
+
+static long
+SendExtendedList(ImapSession *session, const ExtendedListRequest *request)
+{
+ unsigned long i;
+
+ /* Preserve the base LIST delimiter query used by old clients. */
+ if (request->patternCount == 1 && request->patterns[0][0] == '\0' &&
+ request->selectionOptions == 0) {
+ FolderInformation delimiter;
+ memset(&delimiter, 0, sizeof(delimiter));
+ delimiter.name.utf7 = "";
+ delimiter.flags = STORE_COLLECTION_FLAG_HIERARCHY_ONLY;
+ return SendFolderInfo(session->client.conn, "LIST", &delimiter);
+ }
+
+ for (i = 0; i < session->folder.count; i++) {
+ FolderInformation *folder = &session->folder.list[i];
+ BOOL selected;
+
+ if (!FolderMatchesPatterns(request, folder->name.utf7)) continue;
+ selected = FolderMatchesBaseSelection(folder,
+ request->selectionOptions);
+ if (!selected &&
+ (request->selectionOptions & IMAP_LIST_SELECT_RECURSIVE)) {
+ selected = FolderHasMatchingDescendant(session, folder,
+ request->selectionOptions);
+ }
+ if (!selected) continue;
+ if (SendExtendedFolderInfo(session, folder, request->returnOptions) !=
+ STATUS_CONTINUE) return STATUS_ABORT;
+ }
+ return STATUS_CONTINUE;
+}
+
__inline static int
SendMatchingFolders(ImapSession *session, const char *command, char *pattern, const BOOL skipUnsubscribed, const BOOL skipSubFolders)
@@ -1958,14 +2200,13 @@ ImapCommandList(void *param)
ImapSession *session = (ImapSession *)param;
long ccode;
- char pattern[STORE_MAX_COLLECTION * 2];
- unsigned long patternLen = 0;
+ ExtendedListRequest request;
if ((ccode = CheckState(session, STATE_AUTH)) == STATUS_CONTINUE) {
if ((ccode = EventsSend(session, STORE_EVENT_ALL)) == STATUS_CONTINUE) {
- if ((ccode = GetSearchPattern(session, session->command.buffer + strlen("LIST"), pattern, sizeof(pattern), &patternLen)) == STATUS_CONTINUE) {
+ if ((ccode = ParseExtendedListRequest(session, session->command.buffer + strlen("LIST"), &request)) == STATUS_CONTINUE) {
if ((ccode = FolderListLoad(session)) == STATUS_CONTINUE) {
- if ((ccode = FolderSend(session, "LIST", pattern, patternLen, FALSE)) == STATUS_CONTINUE) {
+ if ((ccode = SendExtendedList(session, &request)) == STATUS_CONTINUE) {
return(SendOk(session, "LIST"));
}
}
diff --git a/src/agents/imap/list-extended.c b/src/agents/imap/list-extended.c
new file mode 100644
index 0000000..d23dd04
--- /dev/null
+++ b/src/agents/imap/list-extended.c
@@ -0,0 +1,185 @@
+/****************************************************************************
+ *
+ * Copyright (c) 2001 Novell, Inc. All Rights Reserved.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of version 2 of the GNU General Public License
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, contact Novell, Inc.
+ *
+ * To contact Novell about this file by physical or electronic mail, you
+ * may find current contact information at www.novell.com.
+ *
+ ****************************************************************************/
+
+#include "list-extended.h"
+
+#include
+#include
+#include
+
+typedef struct {
+ const char *name;
+ unsigned int value;
+} ListOption;
+
+static int
+TokenEquals(const char *start, size_t length, const char *expected)
+{
+ size_t i;
+ if (strlen(expected) != length) return 0;
+ for (i = 0; i < length; i++) {
+ if (tolower((unsigned char)start[i]) !=
+ tolower((unsigned char)expected[i])) return 0;
+ }
+ return 1;
+}
+
+static int
+ParseOptions(const char *text, const ListOption *known, size_t known_count,
+ unsigned int *options)
+{
+ unsigned int result = 0;
+
+ if (!text || !options) return 0;
+ for (;;) {
+ const char *start;
+ size_t i;
+
+ while (*text && isspace((unsigned char)*text)) text++;
+ if (!*text) {
+ *options = result;
+ return 1;
+ }
+ start = text;
+ while (*text && !isspace((unsigned char)*text)) text++;
+ for (i = 0; i < known_count; i++) {
+ if (TokenEquals(start, (size_t)(text - start), known[i].name)) {
+ if (result & known[i].value) return 0;
+ result |= known[i].value;
+ break;
+ }
+ }
+ if (i == known_count) return 0;
+ }
+}
+
+int
+IMAPListParseSelectionOptions(const char *text, unsigned int *options)
+{
+ static const ListOption Known[] = {
+ { "SUBSCRIBED", IMAP_LIST_SELECT_SUBSCRIBED },
+ { "REMOTE", IMAP_LIST_SELECT_REMOTE },
+ { "RECURSIVEMATCH", IMAP_LIST_SELECT_RECURSIVE },
+ { "SPECIAL-USE", IMAP_LIST_SELECT_SPECIAL_USE }
+ };
+ unsigned int parsed;
+
+ if (!ParseOptions(text, Known, sizeof(Known) / sizeof(Known[0]), &parsed))
+ return 0;
+ if ((parsed & IMAP_LIST_SELECT_RECURSIVE) &&
+ !(parsed & IMAP_LIST_SELECT_SUBSCRIBED)) return 0;
+ *options = parsed;
+ return 1;
+}
+
+int
+IMAPListParseReturnOptions(const char *text, unsigned int *options)
+{
+ static const ListOption Known[] = {
+ { "SUBSCRIBED", IMAP_LIST_RETURN_SUBSCRIBED },
+ { "CHILDREN", IMAP_LIST_RETURN_CHILDREN },
+ { "SPECIAL-USE", IMAP_LIST_RETURN_SPECIAL_USE }
+ };
+ return ParseOptions(text, Known, sizeof(Known) / sizeof(Known[0]), options);
+}
+
+int
+IMAPListPatternMatches(const char *pattern, const char *mailbox)
+{
+ size_t pattern_length;
+ size_t mailbox_length;
+ unsigned char *previous;
+ unsigned char *current;
+ size_t i;
+ size_t j;
+ int result;
+
+ if (!pattern || !mailbox) return 0;
+ pattern_length = strlen(pattern);
+ mailbox_length = strlen(mailbox);
+ previous = calloc(mailbox_length + 1, 1);
+ current = calloc(mailbox_length + 1, 1);
+ if (!previous || !current) {
+ free(previous);
+ free(current);
+ return 0;
+ }
+ previous[0] = 1;
+
+ for (i = 1; i <= pattern_length; i++) {
+ unsigned char *swap;
+ memset(current, 0, mailbox_length + 1);
+ if (pattern[i - 1] == '*') current[0] = previous[0];
+ for (j = 1; j <= mailbox_length; j++) {
+ if (pattern[i - 1] == '*') {
+ current[j] = previous[j] || current[j - 1];
+ } else if (pattern[i - 1] == '%') {
+ current[j] = previous[j] ||
+ (mailbox[j - 1] != '/' && current[j - 1]);
+ } else {
+ current[j] = previous[j - 1] &&
+ pattern[i - 1] == mailbox[j - 1];
+ }
+ }
+ swap = previous;
+ previous = current;
+ current = swap;
+ }
+ result = previous[mailbox_length] != 0;
+ free(previous);
+ free(current);
+ return result;
+}
+
+size_t
+IMAPListSplitPatterns(char *text, char **patterns, size_t capacity)
+{
+ size_t count = 0;
+
+ if (!text || !patterns || capacity == 0) return 0;
+ while (*text) {
+ char *start;
+ char *write;
+
+ while (*text && isspace((unsigned char)*text)) text++;
+ if (!*text) break;
+ if (count == capacity) return 0;
+
+ if (*text == '"') {
+ text++;
+ start = write = text;
+ while (*text && *text != '"') {
+ if (*text == '\\' && text[1]) text++;
+ *write++ = *text++;
+ }
+ if (*text != '"') return 0;
+ text++;
+ *write = '\0';
+ if (*text && !isspace((unsigned char)*text)) return 0;
+ } else {
+ start = text;
+ while (*text && !isspace((unsigned char)*text)) text++;
+ if (*text) *text++ = '\0';
+ }
+ patterns[count++] = start;
+ }
+ return count;
+}
diff --git a/src/agents/imap/list-extended.h b/src/agents/imap/list-extended.h
new file mode 100644
index 0000000..cf64584
--- /dev/null
+++ b/src/agents/imap/list-extended.h
@@ -0,0 +1,41 @@
+/****************************************************************************
+ *
+ * Copyright (c) 2001 Novell, Inc. All Rights Reserved.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of version 2 of the GNU General Public License
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, contact Novell, Inc.
+ *
+ * To contact Novell about this file by physical or electronic mail, you
+ * may find current contact information at www.novell.com.
+ *
+ ****************************************************************************/
+
+#ifndef BONGO_IMAP_LIST_EXTENDED_H
+#define BONGO_IMAP_LIST_EXTENDED_H
+
+#include
+
+#define IMAP_LIST_SELECT_SUBSCRIBED (1u << 0)
+#define IMAP_LIST_SELECT_REMOTE (1u << 1)
+#define IMAP_LIST_SELECT_RECURSIVE (1u << 2)
+#define IMAP_LIST_SELECT_SPECIAL_USE (1u << 3)
+
+#define IMAP_LIST_RETURN_SUBSCRIBED (1u << 0)
+#define IMAP_LIST_RETURN_CHILDREN (1u << 1)
+#define IMAP_LIST_RETURN_SPECIAL_USE (1u << 2)
+
+int IMAPListParseSelectionOptions(const char *text, unsigned int *options);
+int IMAPListParseReturnOptions(const char *text, unsigned int *options);
+int IMAPListPatternMatches(const char *pattern, const char *mailbox);
+size_t IMAPListSplitPatterns(char *text, char **patterns, size_t capacity);
+
+#endif
diff --git a/src/agents/imap/tests/capabilities-test.c b/src/agents/imap/tests/capabilities-test.c
index 51b0e96..493b1ce 100644
--- a/src/agents/imap/tests/capabilities-test.c
+++ b/src/agents/imap/tests/capabilities-test.c
@@ -39,6 +39,7 @@ main(void)
assert(strstr(buffer, "MOVE"));
assert(strstr(buffer, "SPECIAL-USE"));
assert(strstr(buffer, "CREATE-SPECIAL-USE"));
+ assert(strstr(buffer, "LIST-EXTENDED"));
assert(strstr(buffer, "XSENDER"));
assert(strstr(buffer, "LOGINDISABLED"));
assert(!strstr(buffer, "STARTTLS"));
diff --git a/src/agents/imap/tests/list-extended-test.c b/src/agents/imap/tests/list-extended-test.c
new file mode 100644
index 0000000..8a065b4
--- /dev/null
+++ b/src/agents/imap/tests/list-extended-test.c
@@ -0,0 +1,58 @@
+/****************************************************************************
+ *
+ * Copyright (c) 2001 Novell, Inc. All Rights Reserved.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of version 2 of the GNU General Public License
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, contact Novell, Inc.
+ *
+ * To contact Novell about this file by physical or electronic mail, you
+ * may find current contact information at www.novell.com.
+ *
+ ****************************************************************************/
+
+#include "list-extended.h"
+
+#include
+#include
+
+int
+main(void)
+{
+ unsigned int options;
+ char group[] = "\"INBOX/%\" Archive/* Plain";
+ char *patterns[4];
+ size_t count;
+
+ assert(IMAPListParseSelectionOptions("SUBSCRIBED RECURSIVEMATCH", &options));
+ assert(options == (IMAP_LIST_SELECT_SUBSCRIBED |
+ IMAP_LIST_SELECT_RECURSIVE));
+ assert(IMAPListParseSelectionOptions("SPECIAL-USE", &options));
+ assert(options == IMAP_LIST_SELECT_SPECIAL_USE);
+ assert(!IMAPListParseSelectionOptions("RECURSIVEMATCH", &options));
+ assert(!IMAPListParseSelectionOptions("UNKNOWN", &options));
+ assert(IMAPListParseReturnOptions("CHILDREN SUBSCRIBED", &options));
+ assert(options == (IMAP_LIST_RETURN_CHILDREN |
+ IMAP_LIST_RETURN_SUBSCRIBED));
+
+ assert(IMAPListPatternMatches("*", "Archive/2026/July"));
+ assert(IMAPListPatternMatches("Archive/%", "Archive/2026"));
+ assert(!IMAPListPatternMatches("Archive/%", "Archive/2026/July"));
+ assert(IMAPListPatternMatches("A*e/%", "Archive/2026"));
+ assert(!IMAPListPatternMatches("INBOX", "INBOX/Child"));
+
+ count = IMAPListSplitPatterns(group, patterns, 4);
+ assert(count == 3);
+ assert(strcmp(patterns[0], "INBOX/%") == 0);
+ assert(strcmp(patterns[1], "Archive/*") == 0);
+ assert(strcmp(patterns[2], "Plain") == 0);
+ return 0;
+}