Make IMAP recent claims atomic
Debian Trixie package bundle / packages (push) Failing after 17m29s

This commit is contained in:
Mario Fetka
2026-07-30 09:12:45 +02:00
parent 7ed7ebbb04
commit 77dbf528c4
8 changed files with 224 additions and 67 deletions
+2
View File
@@ -164,6 +164,8 @@ int NMAPSetDecimalProperty(Connection *conn, uint64_t guid, const char *property
int NMAPReadHexadecimalPropertyResponse(Connection *conn, const char *propertyName, unsigned long *propertyValue);
int NMAPGetHexadecimalProperty(Connection *conn, uint64_t guid, const char *propertyName, unsigned long *propertyValue);
int NMAPSetHexadecimalProperty(Connection *conn, uint64_t guid, const char *propertyName, unsigned long propertyValue);
int NMAPClaimImapRecent(Connection *conn, uint64_t guid, uint32_t requestedEnd,
uint32_t *previousEnd, uint32_t *storedEnd);
int NMAPCreateCollection(Connection *conn,
const char *collectionName,
+23 -24
View File
@@ -547,45 +547,44 @@ SendRememberedEvents(ImapSession *session, unsigned long typesAllowed)
if ((events->remembered & STORE_EVENT_NEW) && (typesAllowed & STORE_EVENT_NEW)) {
uint32_t recentUid;
BOOL recentLocked = !selectedFolder->readOnly;
uint32_t requestedEnd = 0;
unsigned long eventIndex;
sequenceChanged = TRUE;
/*
* Refresh the persistent threshold before loading new messages. The
* first read-write session to process the event keeps its local
* \Recent flags and advances the shared threshold; later sessions
* therefore cannot report the same messages as recent.
* Claim only the UIDs present in this reconciled event batch. A
* delivery committed after that snapshot remains available for the
* next event batch instead of being silently claimed here.
*/
if (recentLocked) FolderRecentLock();
ccode = FolderGetRecentUid(storeConn, selectedFolder->info,
&recentUid);
if (ccode != STATUS_CONTINUE) {
if (recentLocked) FolderRecentUnlock();
return ccode;
for (eventIndex = 0; eventIndex < events->newCount; eventIndex++) {
if (events->new[eventIndex].uid >= requestedEnd)
requestedEnd = events->new[eventIndex].uid + 1;
}
if (selectedFolder->readOnly) {
ccode = FolderGetRecentUid(storeConn, selectedFolder->info,
&recentUid);
if (ccode != STATUS_CONTINUE)
return ccode;
selectedFolder->info->uidRecent = recentUid;
selectedFolder->info->uidRecentEnd = requestedEnd;
} else {
ccode = FolderClaimRecentUids(storeConn, selectedFolder->info,
requestedEnd);
if (ccode != STATUS_CONTINUE)
return ccode;
}
selectedFolder->info->uidRecent = recentUid;
ccode = MessageListAddNewMessages(storeConn, selectedFolder,
events->new, events->newCount);
if (ccode != STATUS_CONTINUE) {
if (recentLocked) FolderRecentUnlock();
if (ccode != STATUS_CONTINUE)
return ccode;
}
/*
* A newly delivered message can already carry keywords. Announce
* the expanded mailbox FLAGS set before any later FETCH can expose
* such a keyword to this selected client.
*/
ccode = SendMailboxFlags(clientConn, selectedFolder);
if (ccode != STATUS_CONTINUE) {
if (recentLocked) FolderRecentUnlock();
if (ccode != STATUS_CONTINUE)
return ccode;
}
if (!selectedFolder->readOnly) {
ccode = FolderSetRecentUid(storeConn, selectedFolder->info);
FolderRecentUnlock();
recentLocked = FALSE;
if (ccode != STATUS_CONTINUE) return ccode;
}
events->newCount = 0;
events->remembered &= ~STORE_EVENT_NEW;
}
+26 -38
View File
@@ -321,30 +321,30 @@ FolderListShutdown(ImapSession *session)
}
long
FolderSetRecentUid(Connection *storeConn, FolderInformation *folder)
FolderClaimRecentUids(Connection *storeConn, FolderInformation *folder,
uint32_t requestedEnd)
{
long ccode;
uint32_t previousEnd;
uint32_t storedEnd;
/* Set the new recent uid equal to the uidNext */
ccode = NMAPSetHexadecimalProperty(storeConn, folder->guid, "imap.recentuid", folder->uidNext);
if (ccode == 1000) {
ccode = NMAPClaimImapRecent(storeConn, folder->guid, requestedEnd,
&previousEnd, &storedEnd);
if (ccode == 1000) {
if (storedEnd < previousEnd || storedEnd < requestedEnd)
return STATUS_NMAP_PROTOCOL_ERROR;
folder->uidRecent = previousEnd;
/*
* If another session already advanced beyond requestedEnd, this
* session owns no part of that interval.
*/
folder->uidRecentEnd =
previousEnd < requestedEnd ? requestedEnd : previousEnd;
return(STATUS_CONTINUE);
}
return(CheckForNMAPCommError(ccode));
}
void
FolderRecentLock(void)
{
XplMutexLock(Imap.recentLock);
}
void
FolderRecentUnlock(void)
{
XplMutexUnlock(Imap.recentLock);
}
__inline static long
FolderWatchAdd(Connection *storeConn, FolderInformation *folder, const char *eventString)
{
@@ -515,9 +515,17 @@ FolderOpen(Connection *storeConn, OpenedFolder *openFolder, FolderInformation *f
long ccode;
uint32_t highestUid = 0;
ccode = FolderGetRecentUid(storeConn, folder, &folder->uidRecent);
ccode = FolderGetHighestUid(storeConn, folder, &highestUid);
if (ccode == STATUS_CONTINUE) {
ccode = FolderGetHighestUid(storeConn, folder, &highestUid);
folder->uidNext = highestUid + 1;
if (readOnly) {
ccode = FolderGetRecentUid(storeConn, folder,
&folder->uidRecent);
folder->uidRecentEnd = folder->uidNext;
} else {
ccode = FolderClaimRecentUids(storeConn, folder,
folder->uidNext);
}
if (ccode == STATUS_CONTINUE) {
openFolder->info = folder;
ccode = MessageListLoad(storeConn, openFolder);
@@ -557,31 +565,13 @@ FolderSelect(ImapSession *session, char *folderName, BOOL readOnly)
if ((ccode = FolderGetByName(session, folderName, &folder)) == STATUS_CONTINUE) {
if ((ccode = FolderWatchAdd(session->store.conn, folder, "FLAGS MODIFIED NEW DELETED")) == STATUS_CONTINUE) {
if (!readOnly) FolderRecentLock();
ccode = FolderOpen(session->store.conn,
&session->folder.selected, folder, readOnly);
if (ccode == STATUS_CONTINUE) {
/*
* A read-write SELECT claims the current \Recent set for this
* session. Keep the session-local flags loaded by FolderOpen,
* but advance the persistent threshold immediately so STATUS
* and other sessions cannot claim the same messages.
*/
if (!readOnly &&
(ccode = FolderSetRecentUid(session->store.conn, folder)) !=
STATUS_CONTINUE) {
FolderRecentUnlock();
FolderClose(&session->folder.selected);
FolderWatchRemove(session->store.conn, folder);
EventsFree(&session->folder.selected.events);
return ccode;
}
if (!readOnly) FolderRecentUnlock();
session->client.state = STATE_SELECTED;
ImapSearchResultsReset(session);
return(STATUS_CONTINUE);
}
if (!readOnly) FolderRecentUnlock();
FolderWatchRemove(session->store.conn, folder);
EventsFree(&(session->folder.selected.events));
}
@@ -4486,7 +4476,6 @@ InitializeImapGlobals()
/* Initialize the Busy List and Semaphore */
Imap.list_Busy = NULL;
XplOpenLocalSemaphore(Imap.sem_Busy, 1);
XplMutexInit(Imap.recentLock);
/* Global allocations */
BongoKeywordIndexCreateFromTable(Imap.command.index, ImapProtocolCommands, .name, TRUE);
@@ -4524,7 +4513,6 @@ FreeImapGlobals()
CommandFetchCleanup();
FreeReturnValueIndex(Imap.command.returnValueIndex);
BongoKeywordIndexFree(Imap.command.index);
XplMutexDestroy(Imap.recentLock);
return(TRUE);
}
+3 -4
View File
@@ -265,6 +265,7 @@ typedef struct {
uint64_t guid; /* folder id */
uint32_t uidNext; /* the next expected uid */
uint32_t uidRecent; /* the first recent uid */
uint32_t uidRecentEnd; /* exclusive end of recent claim */
struct {
char *utf7; /* utf7 encoding of the folder name */
char *utf8; /* utf8 encoding of the folder name */
@@ -368,7 +369,8 @@ typedef struct {
long FolderGetRecentUid(Connection *storeConn, FolderInformation *folder,
uint32_t *recentUid);
long FolderSetRecentUid(Connection *storeConn, FolderInformation *folder);
long FolderClaimRecentUids(Connection *storeConn, FolderInformation *folder,
uint32_t requestedEnd);
typedef struct {
struct {
@@ -442,7 +444,6 @@ typedef struct {
BongoList *list_Busy; /* Singly linked list of sessions that we should update every 10 seconds */
XplSemaphore sem_Busy; /* Semaphore protecting the busy list */
XplMutex recentLock; /* serializes the shared \Recent claim */
void *logHandle;
XplAtomic exiting;
@@ -464,8 +465,6 @@ long GetMessageRange(MessageInformation *message, unsigned long messageCount, ch
long FolderOpen(Connection *storeConn, OpenedFolder *openFolder, FolderInformation *folder, BOOL readOnly);
long FolderListLoad(ImapSession *session);
void FolderRecentLock(void);
void FolderRecentUnlock(void);
long FolderListInitialize(ImapSession *session);
long MessageListLoad(Connection *conn, OpenedFolder *selected);
+2 -1
View File
@@ -819,7 +819,8 @@ MessageListAddMessage(OpenedFolder *folder, char *response,
keywords && keywords[0] ? MemStrdup(keywords) : NULL;
if (keywords && keywords[0] && !currentMessage->keywords)
return STATUS_MEMORY_ERROR;
if (currentMessage->uid >= folder->info->uidRecent) {
if (currentMessage->uid >= folder->info->uidRecent &&
currentMessage->uid < folder->info->uidRecentEnd) {
currentMessage->flags |= STORE_MSG_FLAG_RECENT;
folder->recentCount++;
}
+138
View File
@@ -142,6 +142,8 @@ StoreSetupCommands()
BongoHashtablePutNoReplace(CommandTable, "RENAME", (void *) STORE_COMMAND_RENAME) ||
BongoHashtablePutNoReplace(CommandTable, "REMOVE", (void *) STORE_COMMAND_REMOVE) ||
BongoHashtablePutNoReplace(CommandTable, "SEARCH", (void *) STORE_COMMAND_SEARCH) ||
BongoHashtablePutNoReplace(CommandTable, "IMAPRECENT",
(void *) STORE_COMMAND_IMAPRECENT) ||
BongoHashtablePutNoReplace(CommandTable, "WATCH", (void *) STORE_COMMAND_WATCH) ||
BongoHashtablePutNoReplace(CommandTable, "REPAIR", (void *) STORE_COMMAND_REPAIR) ||
@@ -846,6 +848,28 @@ StoreCommandLoop(StoreClient *client)
props, int3);
break;
case STORE_COMMAND_IMAPRECENT:
/*
* IMAPRECENT <collection> <exclusive-end-uid>
*
* Atomically claim a half-open UID interval for IMAP4rev1's
* session-specific \Recent flag.
*/
{
uint32_t requestedEnd;
if (TOKEN_OK == (ccode = RequireStore(client)) &&
TOKEN_OK == (ccode = CheckTokC(client, n, 3, 3)) &&
TOKEN_OK == (ccode =
ParseCollection(client, tokens[1], &object)) &&
TOKEN_OK == (ccode =
ParseHexU32(client, tokens[2], &requestedEnd))) {
ccode = StoreCommandIMAPRECENT(client, &object,
requestedEnd);
}
}
break;
case STORE_COMMAND_LINK:
/* LINK <document> <document>
* abuses collection - first argument should be another document*/
@@ -2805,6 +2829,120 @@ StoreCommandPROPSET(StoreClient *client,
return ccode;
}
CCode
StoreCommandIMAPRECENT(StoreClient *client, StoreObject *collection,
uint32_t requestedEnd)
{
static const char propertyName[] = "imap.recentuid";
MsgSQLStatement selectStmt;
MsgSQLStatement deleteStmt;
MsgSQLStatement insertStmt;
BOOL selectPrepared = FALSE;
BOOL deletePrepared = FALSE;
BOOL insertPrepared = FALSE;
uint32_t previousEnd = 0;
uint32_t storedEnd;
char value[16];
char *end = NULL;
unsigned long parsedEnd;
int result;
int auth;
CHECK_NOT_READONLY(client)
auth = StoreObjectCheckAuthorization(
client, collection, STORE_PRIV_READ_PROPS | STORE_PRIV_WRITE_PROPS);
if (auth)
return ConnWriteStr(client->conn, MSG4240NOPERMISSION);
if (!LogicalLockGain(client, collection, LLOCK_READWRITE,
"StoreCommandIMAPRECENT"))
return ConnWriteStr(client->conn, MSG4120BOXLOCKED);
memset(&selectStmt, 0, sizeof(selectStmt));
memset(&deleteStmt, 0, sizeof(deleteStmt));
memset(&insertStmt, 0, sizeof(insertStmt));
if (MsgSQLBeginTransaction(client->storedb))
goto abort;
if (!MsgSQLPrepare(
client->storedb,
"SELECT value FROM properties "
"WHERE guid = ?1 AND name = ?2 LIMIT 1;",
&selectStmt))
goto abort;
selectPrepared = TRUE;
MsgSQLBindInt64(&selectStmt, 1, collection->guid);
MsgSQLBindString(&selectStmt, 2, propertyName, FALSE);
result = MsgSQLResults(client->storedb, &selectStmt);
if (result < 0)
goto abort;
if (result > 0) {
if (MsgSQLResultText(&selectStmt, 0, value, sizeof(value)) < 0)
goto abort;
errno = 0;
parsedEnd = strtoul(value, &end, 16);
if (errno == ERANGE || !end || *end || parsedEnd > UINT32_MAX)
goto abort;
previousEnd = (uint32_t)parsedEnd;
}
MsgSQLFinalize(&selectStmt);
selectPrepared = FALSE;
storedEnd = requestedEnd > previousEnd ? requestedEnd : previousEnd;
if (storedEnd != previousEnd) {
if (!MsgSQLPrepare(
client->storedb,
"DELETE FROM properties WHERE guid = ?1 AND name = ?2;",
&deleteStmt))
goto abort;
deletePrepared = TRUE;
MsgSQLBindInt64(&deleteStmt, 1, collection->guid);
MsgSQLBindString(&deleteStmt, 2, propertyName, FALSE);
if (MsgSQLExecute(client->storedb, &deleteStmt) != 0)
goto abort;
MsgSQLFinalize(&deleteStmt);
deletePrepared = FALSE;
snprintf(value, sizeof(value), "%x", storedEnd);
if (!MsgSQLPrepare(
client->storedb,
"INSERT INTO properties (guid, intprop, name, value) "
"VALUES (?1, NULL, ?2, ?3);",
&insertStmt))
goto abort;
insertPrepared = TRUE;
MsgSQLBindInt64(&insertStmt, 1, collection->guid);
MsgSQLBindString(&insertStmt, 2, propertyName, FALSE);
MsgSQLBindString(&insertStmt, 3, value, FALSE);
if (MsgSQLExecute(client->storedb, &insertStmt) != 0)
goto abort;
MsgSQLFinalize(&insertStmt);
insertPrepared = FALSE;
}
if (MsgSQLCommitTransaction(client->storedb))
goto abort;
LogicalLockRelease(client, collection, LLOCK_READWRITE,
"StoreCommandIMAPRECENT");
return ConnWriteF(client->conn, "1000 OK %x %x\r\n",
previousEnd, storedEnd);
abort:
if (selectPrepared)
MsgSQLFinalize(&selectStmt);
if (deletePrepared)
MsgSQLFinalize(&deleteStmt);
if (insertPrepared)
MsgSQLFinalize(&insertStmt);
MsgSQLAbortTransaction(client->storedb);
LogicalLockRelease(client, collection, LLOCK_READWRITE,
"StoreCommandIMAPRECENT");
return ConnWriteStr(client->conn, MSG5005DBLIBERR);
}
/* returns: -1, causing storecommandloop to exit */
CCode
+4
View File
@@ -54,6 +54,7 @@ typedef enum {
STORE_COMMAND_REPAIR,
STORE_COMMAND_SEARCH,
STORE_COMMAND_STATUS,
STORE_COMMAND_IMAPRECENT,
STORE_COMMAND_WATCH,
/* document commands */
@@ -232,6 +233,9 @@ CCode StoreCommandPROPGET(StoreClient *client, StoreObject *object,
CCode StoreCommandPROPSET(StoreClient *client, StoreObject *object,
StorePropInfo *prop, int size);
CCode StoreCommandIMAPRECENT(StoreClient *client, StoreObject *collection,
uint32_t requestedEnd);
CCode StoreCommandQUIT(StoreClient *client);
CCode StoreCommandREAD(StoreClient *client, StoreObject *object,
+26
View File
@@ -1020,6 +1020,32 @@ NMAPSetHexadecimalProperty(Connection *conn, uint64_t guid, const char *property
return(-1);
}
int
NMAPClaimImapRecent(Connection *conn, uint64_t guid, uint32_t requestedEnd,
uint32_t *previousEnd, uint32_t *storedEnd)
{
char response[64];
unsigned int previous;
unsigned int stored;
char trailing;
int ccode;
if (!previousEnd || !storedEnd)
return -1;
ccode = NMAPRunCommandF(conn, response, sizeof(response),
"IMAPRECENT " STORE_GUID_FMT " %x\r\n",
guid, requestedEnd);
if (ccode != 1000)
return ccode;
if (sscanf(response, "OK %x %x %c", &previous, &stored, &trailing) != 2)
return -1;
*previousEnd = (uint32_t)previous;
*storedEnd = (uint32_t)stored;
return 1000;
}
BongoCalObject *
NMAPGetEvents(Connection *conn, const char *calendar, BongoCalTime start, BongoCalTime end)
{