diff --git a/include/bongostore.h b/include/bongostore.h index 7b3f67e..7446770 100644 --- a/include/bongostore.h +++ b/include/bongostore.h @@ -71,6 +71,24 @@ typedef enum { /* DOCTYPE_COLLECTION flags: */ #define STORE_COLLECTION_FLAG_HIERARCHY_ONLY (1 << 0) #define STORE_COLLECTION_FLAG_NON_SUBSCRIBED (1 << 1) +#define STORE_COLLECTION_FLAG_SPECIAL_ALL (1 << 2) +#define STORE_COLLECTION_FLAG_SPECIAL_ARCHIVE (1 << 3) +#define STORE_COLLECTION_FLAG_SPECIAL_DRAFTS (1 << 4) +#define STORE_COLLECTION_FLAG_SPECIAL_FLAGGED (1 << 5) +#define STORE_COLLECTION_FLAG_SPECIAL_JUNK (1 << 6) +#define STORE_COLLECTION_FLAG_SPECIAL_SENT (1 << 7) +#define STORE_COLLECTION_FLAG_SPECIAL_TRASH (1 << 8) + +#define STORE_COLLECTION_FLAG_SPECIAL_USE_MASK \ + (STORE_COLLECTION_FLAG_SPECIAL_ALL | STORE_COLLECTION_FLAG_SPECIAL_ARCHIVE | \ + STORE_COLLECTION_FLAG_SPECIAL_DRAFTS | STORE_COLLECTION_FLAG_SPECIAL_FLAGGED | \ + STORE_COLLECTION_FLAG_SPECIAL_JUNK | STORE_COLLECTION_FLAG_SPECIAL_SENT | \ + STORE_COLLECTION_FLAG_SPECIAL_TRASH) + +/* Stable GUIDs used by both the Store and protocol agents. */ +#define STORE_MAIL_INBOX_GUID 6 +#define STORE_MAIL_DRAFTS_GUID 7 +#define STORE_MAIL_ARCHIVES_GUID 8 /** end of document flags **/ diff --git a/src/agents/imap/CMakeLists.txt b/src/agents/imap/CMakeLists.txt index 879a795..91890fc 100644 --- a/src/agents/imap/CMakeLists.txt +++ b/src/agents/imap/CMakeLists.txt @@ -3,6 +3,7 @@ add_executable(bongoimap capabilities.c idle.c uidplus.c + special-use.c copy.c event.c fetch.c @@ -53,4 +54,13 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR} ) add_test(NAME imap-uidplus COMMAND imap-uidplus-test) + + add_executable(imap-special-use-test + tests/special-use-test.c + special-use.c + ) + target_include_directories(imap-special-use-test PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ) + add_test(NAME imap-special-use COMMAND imap-special-use-test) endif() diff --git a/src/agents/imap/capabilities.c b/src/agents/imap/capabilities.c index d2e4585..2133de9 100644 --- a/src/agents/imap/capabilities.c +++ b/src/agents/imap/capabilities.c @@ -55,7 +55,8 @@ IMAPFormatCapabilities(char *buffer, size_t buffer_size, buffer[0] = '\0'; if (!Append(buffer, buffer_size, &length, - "* CAPABILITY IMAP4rev1 IMAP4 NAMESPACE IDLE UIDPLUS MOVE XSENDER") || + "* CAPABILITY IMAP4rev1 IMAP4 NAMESPACE IDLE UIDPLUS MOVE " + "SPECIAL-USE CREATE-SPECIAL-USE 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 b0145e8..8d98db7 100644 --- a/src/agents/imap/imapd.c +++ b/src/agents/imap/imapd.c @@ -46,6 +46,7 @@ #include "capabilities.h" #include "idle.h" #include "uidplus.h" +#include "special-use.h" ImapGlobal Imap; @@ -528,6 +529,14 @@ ParseCollectionLine(char *buffer, FolderInformation *folder) if ((ptr > start) && (*ptr == ' ')) { folder->flags = atol(start); + /* Stores created before SPECIAL-USE support did not have + * role bits. The stable default-folder GUIDs preserve + * those roles across renames without a schema migration. */ + if (folder->guid == STORE_MAIL_DRAFTS_GUID) { + folder->flags |= STORE_COLLECTION_FLAG_SPECIAL_DRAFTS; + } else if (folder->guid == STORE_MAIL_ARCHIVES_GUID) { + folder->flags |= STORE_COLLECTION_FLAG_SPECIAL_ARCHIVE; + } ptr++; len = strlen(ptr); @@ -1157,6 +1166,7 @@ ImapCommandCreate(void *param) char *pathUtf8; int len; FolderInformation *folder; + uint32_t specialUseFlags; if ((ccode = CheckState(session, STATE_AUTH)) != STATUS_CONTINUE) { return(SendError(session->client.conn, session->command.tag, "CREATE", ccode)); @@ -1171,6 +1181,12 @@ ImapCommandCreate(void *param) return(SendError(session->client.conn, session->command.tag, "CREATE", ccode)); } + if (!IMAPSpecialUseParseCreateOptions(ptr, &specialUseFlags)) { + FreePathArgument(&path); + return(SendError(session->client.conn, session->command.tag, "CREATE", + STATUS_INVALID_ARGUMENT)); + } + /* prohibition of naming a folder the same name as the userid; don't know why (rprice) */ if (XplStrCaseCmp(path.name, session->user.name) == 0) { FreePathArgument(&path); @@ -1212,12 +1228,14 @@ ImapCommandCreate(void *param) } /* this folder is just hierarchy. Make it a real folder */ - if (NMAPSendCommandF(session->store.conn, "FLAG /mail/%s -%u\r\n", pathUtf8, STORE_COLLECTION_FLAG_HIERARCHY_ONLY) == -1) { + folder->flags &= ~(STORE_COLLECTION_FLAG_HIERARCHY_ONLY | + STORE_COLLECTION_FLAG_SPECIAL_USE_MASK); + folder->flags |= specialUseFlags; + if (NMAPSendCommandF(session->store.conn, "FLAG /mail/%s %lu\r\n", + pathUtf8, folder->flags) == -1) { MemFree(pathUtf8); return(STATUS_NMAP_COMM_ERROR); } - - folder->flags &= ~(STORE_COLLECTION_FLAG_HIERARCHY_ONLY); MemFree(pathUtf8); ccode = NMAPReadResponse(session->store.conn, NULL, 0, 0); @@ -1234,7 +1252,15 @@ ImapCommandCreate(void *param) return(SendError(session->client.conn, session->command.tag, "CREATE", ccode)); } - if (NMAPSendCommandF(session->store.conn, "CREATE \"/mail/%s\"\r\n", pathUtf8) == -1) { + if (specialUseFlags != 0) { + ccode = NMAPSendCommandF(session->store.conn, + "CREATE \"/mail/%s\" 0 %u\r\n", + pathUtf8, (unsigned int)specialUseFlags); + } else { + ccode = NMAPSendCommandF(session->store.conn, + "CREATE \"/mail/%s\"\r\n", pathUtf8); + } + if (ccode == -1) { MemFree(pathUtf8); return(SendError(session->client.conn, session->command.tag, "CREATE", STATUS_NMAP_COMM_ERROR)); } @@ -1746,7 +1772,26 @@ ImapCommandUnsubscribe(void *param) __inline static long SendFolderInfo(Connection *conn, const char *command, FolderInformation *folder) { - if (ConnWriteF(conn, "* %s (%s) \"/\" \"%s\"\r\n", command, ((folder->flags & STORE_COLLECTION_FLAG_HIERARCHY_ONLY) == 0) ? "" : "\\NoSelect", folder->name.utf7) != -1) { + char attributes[128]; + char specialUse[96]; + size_t length = 0; + size_t specialUseLength; + + attributes[0] = '\0'; + if (folder->flags & STORE_COLLECTION_FLAG_HIERARCHY_ONLY) { + memcpy(attributes, "\\NoSelect", sizeof("\\NoSelect")); + length = strlen(attributes); + } + specialUseLength = IMAPSpecialUseFormat((uint32_t)folder->flags, + specialUse, sizeof(specialUse)); + if (specialUseLength != 0 && + length + (length ? 1 : 0) + specialUseLength < sizeof(attributes)) { + if (length) attributes[length++] = ' '; + memcpy(attributes + length, specialUse, specialUseLength + 1); + } + + if (ConnWriteF(conn, "* %s (%s) \"/\" \"%s\"\r\n", command, + attributes, folder->name.utf7) != -1) { return(STATUS_CONTINUE); } return(STATUS_ABORT); diff --git a/src/agents/imap/special-use.c b/src/agents/imap/special-use.c new file mode 100644 index 0000000..efbef7c --- /dev/null +++ b/src/agents/imap/special-use.c @@ -0,0 +1,129 @@ +/**************************************************************************** + * + * 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 "special-use.h" + +#include + +#include +#include +#include + +typedef struct { + const char *name; + uint32_t flag; +} SpecialUseAttribute; + +static const SpecialUseAttribute Attributes[] = { + { "\\All", STORE_COLLECTION_FLAG_SPECIAL_ALL }, + { "\\Archive", STORE_COLLECTION_FLAG_SPECIAL_ARCHIVE }, + { "\\Drafts", STORE_COLLECTION_FLAG_SPECIAL_DRAFTS }, + { "\\Flagged", STORE_COLLECTION_FLAG_SPECIAL_FLAGGED }, + { "\\Junk", STORE_COLLECTION_FLAG_SPECIAL_JUNK }, + { "\\Sent", STORE_COLLECTION_FLAG_SPECIAL_SENT }, + { "\\Trash", STORE_COLLECTION_FLAG_SPECIAL_TRASH } +}; + +static const char * +SkipSpace(const char *text) +{ + while (*text && isspace((unsigned char)*text)) text++; + return text; +} + +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; +} + +int +IMAPSpecialUseParseCreateOptions(const char *text, uint32_t *flags) +{ + const char *start; + size_t i; + uint32_t result = 0; + + if (!text || !flags) return 0; + text = SkipSpace(text); + if (!*text) { + *flags = 0; + return 1; + } + + start = text; + while (*text && !isspace((unsigned char)*text) && *text != '(') text++; + if (!TokenEquals(start, (size_t)(text - start), "USE")) return 0; + text = SkipSpace(text); + if (*text++ != '(') return 0; + + for (;;) { + text = SkipSpace(text); + if (*text == ')') { + text = SkipSpace(text + 1); + if (*text || result == 0) return 0; + *flags = result; + return 1; + } + if (!*text) return 0; + + start = text; + while (*text && !isspace((unsigned char)*text) && *text != ')') text++; + for (i = 0; i < sizeof(Attributes) / sizeof(Attributes[0]); i++) { + if (TokenEquals(start, (size_t)(text - start), Attributes[i].name)) { + if (result & Attributes[i].flag) return 0; + result |= Attributes[i].flag; + break; + } + } + if (i == sizeof(Attributes) / sizeof(Attributes[0])) return 0; + } +} + +size_t +IMAPSpecialUseFormat(uint32_t flags, char *buffer, size_t buffer_size) +{ + size_t i; + size_t length = 0; + + if (!buffer || buffer_size == 0) return 0; + buffer[0] = '\0'; + + for (i = 0; i < sizeof(Attributes) / sizeof(Attributes[0]); i++) { + int written; + if (!(flags & Attributes[i].flag)) continue; + written = snprintf(buffer + length, buffer_size - length, "%s%s", + length ? " " : "", Attributes[i].name); + if (written < 0 || (size_t)written >= buffer_size - length) { + buffer[0] = '\0'; + return 0; + } + length += (size_t)written; + } + return length; +} diff --git a/src/agents/imap/special-use.h b/src/agents/imap/special-use.h new file mode 100644 index 0000000..68d3255 --- /dev/null +++ b/src/agents/imap/special-use.h @@ -0,0 +1,31 @@ +/**************************************************************************** + * + * 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_SPECIAL_USE_H +#define BONGO_IMAP_SPECIAL_USE_H + +#include +#include + +int IMAPSpecialUseParseCreateOptions(const char *text, uint32_t *flags); +size_t IMAPSpecialUseFormat(uint32_t flags, char *buffer, size_t buffer_size); + +#endif diff --git a/src/agents/imap/tests/capabilities-test.c b/src/agents/imap/tests/capabilities-test.c index 71cad4d..51b0e96 100644 --- a/src/agents/imap/tests/capabilities-test.c +++ b/src/agents/imap/tests/capabilities-test.c @@ -37,6 +37,8 @@ main(void) assert(strstr(buffer, "IDLE")); assert(strstr(buffer, "UIDPLUS")); assert(strstr(buffer, "MOVE")); + assert(strstr(buffer, "SPECIAL-USE")); + assert(strstr(buffer, "CREATE-SPECIAL-USE")); assert(strstr(buffer, "XSENDER")); assert(strstr(buffer, "LOGINDISABLED")); assert(!strstr(buffer, "STARTTLS")); diff --git a/src/agents/imap/tests/special-use-test.c b/src/agents/imap/tests/special-use-test.c new file mode 100644 index 0000000..019381e --- /dev/null +++ b/src/agents/imap/tests/special-use-test.c @@ -0,0 +1,54 @@ +/**************************************************************************** + * + * 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 "special-use.h" + +#include + +#include +#include + +int +main(void) +{ + uint32_t flags; + char buffer[96]; + + assert(IMAPSpecialUseParseCreateOptions("", &flags)); + assert(flags == 0); + assert(IMAPSpecialUseParseCreateOptions(" USE (\\Sent \\Archive) ", &flags)); + assert(flags == (STORE_COLLECTION_FLAG_SPECIAL_SENT | + STORE_COLLECTION_FLAG_SPECIAL_ARCHIVE)); + assert(!IMAPSpecialUseParseCreateOptions("USE ()", &flags)); + assert(!IMAPSpecialUseParseCreateOptions("USE (\\Sent \\Sent)", &flags)); + assert(!IMAPSpecialUseParseCreateOptions("USE (\\Unknown)", &flags)); + assert(!IMAPSpecialUseParseCreateOptions("OTHER (\\Sent)", &flags)); + + flags = STORE_COLLECTION_FLAG_SPECIAL_DRAFTS | + STORE_COLLECTION_FLAG_SPECIAL_TRASH; + assert(IMAPSpecialUseFormat(flags, buffer, sizeof(buffer)) > 0); + assert(strcmp(buffer, "\\Drafts \\Trash") == 0); + assert(IMAPSpecialUseFormat(0, buffer, sizeof(buffer)) == 0); + assert(buffer[0] == '\0'); + assert(IMAPSpecialUseFormat(flags, buffer, 4) == 0); + assert(buffer[0] == '\0'); + return 0; +} diff --git a/src/agents/store/command.c b/src/agents/store/command.c index 7ae6b46..0f9c9c0 100644 --- a/src/agents/store/command.c +++ b/src/agents/store/command.c @@ -646,14 +646,17 @@ StoreCommandLoop(StoreClient *client) break; case STORE_COMMAND_CREATE: - /* CREATE [] */ + /* CREATE [ []] */ guid = 0; + ulong = 0; if (TOKEN_OK == (ccode = RequireStore(client)) && - TOKEN_OK == (ccode = CheckTokC(client, n, 2, 3)) && - (n <= 2 || TOKEN_OK == (ccode = ParseGUID(client, tokens[2], &guid)))) + TOKEN_OK == (ccode = CheckTokC(client, n, 2, 4)) && + (n <= 2 || (n == 4 && strcmp(tokens[2], "0") == 0) || + TOKEN_OK == (ccode = ParseGUID(client, tokens[2], &guid))) && + (n <= 3 || TOKEN_OK == (ccode = ParseUnsignedLong(client, tokens[3], &ulong)))) { - ccode = StoreCommandCREATE(client, tokens[1], guid); + ccode = StoreCommandCREATE(client, tokens[1], guid, (uint32_t)ulong); } break; @@ -1686,7 +1689,7 @@ finish: // FIXME: doesn't detect all error conditions // [LOCKING] Create(X) => RwLock(Parent(X)) CCode -StoreCommandCREATE(StoreClient *client, char *name, uint64_t guid) +StoreCommandCREATE(StoreClient *client, char *name, uint64_t guid, uint32_t flags) { StoreObject object; StoreObject container; @@ -1725,6 +1728,7 @@ StoreCommandCREATE(StoreClient *client, char *name, uint64_t guid) memset(&object, 0, sizeof(StoreObject)); object.type = STORE_DOCTYPE_FOLDER; object.guid = guid; + object.flags = flags; strncpy(object.filename, name, MAX_FILE_NAME); ret = StoreObjectCreate(client, &object); diff --git a/src/agents/store/command.h b/src/agents/store/command.h index 1632326..aa3867a 100644 --- a/src/agents/store/command.h +++ b/src/agents/store/command.h @@ -158,7 +158,8 @@ CCode StoreCommandCOOKIELIST(StoreClient *client); CCode StoreCommandCOPY(StoreClient *client, StoreObject *object, StoreObject *collection); -CCode StoreCommandCREATE(StoreClient *client, char *collection, uint64_t guid); +CCode StoreCommandCREATE(StoreClient *client, char *collection, uint64_t guid, + uint32_t flags); CCode StoreCommandDELETE(StoreClient *client, StoreObject *object); diff --git a/src/agents/store/object-model.c b/src/agents/store/object-model.c index 426b2a0..2c28629 100644 --- a/src/agents/store/object-model.c +++ b/src/agents/store/object-model.c @@ -86,11 +86,14 @@ StoreObjectDBCreate(StoreClient *client) collection.guid = STORE_MAIL_DRAFTS_GUID; snprintf(collection.filename, MAX_FILE_NAME, "/mail/drafts"); + collection.flags = STORE_COLLECTION_FLAG_SPECIAL_DRAFTS; StoreObjectCreate(client, &collection); collection.guid = STORE_MAIL_ARCHIVES_GUID; snprintf(collection.filename, MAX_FILE_NAME, "/mail/archives"); + collection.flags = STORE_COLLECTION_FLAG_SPECIAL_ARCHIVE; StoreObjectCreate(client, &collection); + collection.flags = 0; collection.guid = STORE_ADDRESSBOOK_PERSONAL_GUID; snprintf(collection.filename, MAX_FILE_NAME, "/addressbook/personal"); diff --git a/src/agents/store/stored.h b/src/agents/store/stored.h index 01fe14f..7df002e 100644 --- a/src/agents/store/stored.h +++ b/src/agents/store/stored.h @@ -453,9 +453,6 @@ void LogicalLockDestroy(); #define STORE_CALENDARS_GUID 3 #define STORE_ADDRESBOOK_GUID 4 #define STORE_MAIL_GUID 5 -#define STORE_MAIL_INBOX_GUID 6 -#define STORE_MAIL_DRAFTS_GUID 7 -#define STORE_MAIL_ARCHIVES_GUID 8 #define STORE_ADDRESSBOOK_PERSONAL_GUID 9 #define STORE_ADDRESSBOOK_COLLECTED_GUID 10 #define STORE_PREFERENCES_GUID 11