Skip to content

Commit 2adcde0

Browse files
committed
exmdb: add cgkreset functionality
exchange.sqlite3 stores CNs not only in the PR_CHANGE_KEY property, but also the folders.change_number and messages.change_number SQL column. cgkrepair (as a Gromox/MAPI client) cannot reach that. This makes a server operation necessary. cgkreset is this new function. In contrast to cgkrepair, it does not try to determine whether objects need fixing; it just bumps everything for simplicity of implementation.
1 parent 7d31525 commit 2adcde0

File tree

11 files changed

+232
-6
lines changed

11 files changed

+232
-6
lines changed

doc/gromox-mbop.8

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ store of a domain, leave out the local part, i.e. use
2424
\fB(\fP command1 c1args \fB) (\fP command2 c2args \fB)\fP: command
2525
chaining
2626
.IP \(bu 4
27+
cgkreset: reset synchronization state (PR_CHANGE_KEY, PR_PREDECESSOR_LIST)
28+
.IP \(bu 4
2729
clear\-photo: delete user picture
2830
.IP \(bu 4
2931
clear\-profile: delete user's PHP-MAPI profile
@@ -82,6 +84,22 @@ time around and \(...\) does not cache the input for any subcommands.
8284
.SS Examples
8385
Run two commands for a user: gromox\-mbop \-u [email protected] \\( purge\-softdelete -r /
8486
\\) \\( purge\-datafiles \\)
87+
.SH cgkreset
88+
cgkreset resets Change Numbers on all folder and message objects, PR_CHANGE_KEY
89+
and PR_PREDECESSOR_LIST values. The use cases for cgkreset are:
90+
.IP \(bu 4
91+
when the mailbox has CN corruption and Incremental Change Synchronization (by
92+
e.g. Outlook or grommunio-sync) is hampered (e.g. message flags/color updates
93+
not transferred)
94+
.IP \(bu 4
95+
when the mailbox has CN corruption and gromox-http/emsmdb has thrown the error
96+
"INSERT INTO messages ... UNIQUE constraint failed: messages.change_number"
97+
.PP
98+
After execution, .ost files referencing the reset mailbox should be deleted.
99+
`gromox\-mbop cgkreset` is different from `/usr/libexec/gromox/cgkrepair` in
100+
that cgkreset does not make any attempts to keep synchronization state; it just
101+
resets everything, unconditionally. On the other hand, cgkrepair is unable to
102+
get to all fields that may need resetting.
85103
.SH clear\-photo
86104
The clear\-photo command will delete the user picture. Note that, when there is
87105
no mailbox-level profile picture set, Gromox server processes may serve an

exch/exmdb/db_engine.cpp

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,158 @@ BOOL db_engine_unload_db(const char *path)
285285
return FALSE;
286286
}
287287

288+
/**
289+
* @db: sqlite handle
290+
* @last_cn: counter for most recently assigned CN (GCV)
291+
* @q_list: query for obtaining some objects
292+
* (shall return 2 columns; id and eligibility)
293+
* @q_cn: query for updating per-object CN
294+
* @q_prop: query for updating per-object CK & PCL
295+
*
296+
* Iterate over an object set and assign new Change Numbers, Change Keys and
297+
* Predecessor Change Lists.
298+
*/
299+
static bool cgkreset_3(sqlite3 *db, uint64_t &last_cn, const GUID &store_guid,
300+
const char *q_list, const char *q_cn, const char *q_prop)
301+
{
302+
std::vector<std::pair<uint64_t, uint64_t>> gcv_list;
303+
auto stm = gx_sql_prep(db, q_list);
304+
if (stm == nullptr)
305+
return false;
306+
while (stm.step() == SQLITE_ROW)
307+
gcv_list.emplace_back(stm.col_uint64(0), stm.col_uint64(1));
308+
stm = gx_sql_prep(db, q_cn);
309+
if (stm == nullptr)
310+
return false;
311+
auto stm_prop = gx_sql_prep(db, q_prop);
312+
if (stm_prop == nullptr)
313+
return false;
314+
315+
for (auto [objid, parent_attid] : gcv_list) {
316+
auto next_cn = last_cn + 1;
317+
stm.reset();
318+
stm.bind_int64(1, next_cn);
319+
stm.bind_int64(2, objid);
320+
if (stm.step() != SQLITE_DONE)
321+
return false;
322+
++last_cn;
323+
if (parent_attid != 0)
324+
/* CK/PCL on attachments makes no sense */
325+
continue;
326+
327+
char buf[23];
328+
XID xid{store_guid, rop_util_make_eid_ex(1, last_cn)};
329+
EXT_PUSH ep;
330+
if (!ep.init(&buf[1], sizeof(buf) - 1, 0))
331+
return false;
332+
ep.p_xid(xid);
333+
stm_prop.reset();
334+
stm_prop.bind_blob(1, &buf[1], ep.m_offset);
335+
stm_prop.bind_int64(2, objid);
336+
stm_prop.bind_int64(3, PR_CHANGE_KEY);
337+
if (stm_prop.step() != SQLITE_DONE)
338+
return false;
339+
340+
stm_prop.reset();
341+
buf[0] = 22;
342+
stm_prop.bind_blob(1, buf, 23);
343+
stm_prop.bind_int64(2, objid);
344+
stm_prop.bind_int64(3, PR_PREDECESSOR_CHANGE_LIST);
345+
if (stm_prop.step() != SQLITE_DONE)
346+
return false;
347+
}
348+
return true;
349+
}
350+
351+
static bool cgkreset_2(sqlite3 *db, uint64_t &last_cn, const GUID &store_guid,
352+
unsigned int flags)
353+
{
354+
if (flags & CGKRESET_FOLDERS) {
355+
auto succ = cgkreset_3(db, last_cn, store_guid,
356+
"SELECT folder_id, NULL FROM folders",
357+
"UPDATE folders SET change_number=? WHERE folder_id=?",
358+
"UPDATE folder_properties SET propval=? WHERE folder_id=? AND proptag=?");
359+
if (!succ)
360+
return false;
361+
}
362+
if (flags & CGKRESET_MESSAGES) {
363+
auto succ = cgkreset_3(db, last_cn, store_guid,
364+
"SELECT message_id, parent_attid FROM messages",
365+
"UPDATE messages SET change_number=? WHERE message_id=?",
366+
"UPDATE message_properties SET propval=? WHERE message_id=? AND proptag=?");
367+
if (!succ)
368+
return false;
369+
}
370+
return true;
371+
}
372+
373+
/**
374+
* Obtain essential parameters for a global CN/CK reassignment. In doing so, it
375+
* performs the first sanity check and returns a bumped last_cn if necessary.
376+
*/
377+
static bool cgkreset_load_param(sqlite3 *db, uint64_t &last_cn, GUID &store_guid)
378+
{
379+
auto stm = gx_sql_prep(db, "SELECT config_value FROM configurations WHERE config_id=?");
380+
if (stm == nullptr)
381+
return false;
382+
stm.bind_int64(1, CONFIG_ID_MAILBOX_GUID);
383+
if (stm.step() != SQLITE_ROW)
384+
return false;
385+
store_guid.from_str(stm.col_text(0));
386+
stm.reset();
387+
stm.bind_int64(1, CONFIG_ID_LAST_CHANGE_NUMBER);
388+
if (stm.step() != SQLITE_ROW)
389+
return false;
390+
last_cn = stm.col_uint64(0);
391+
392+
stm = gx_sql_prep(db, "SELECT MAX(change_number) FROM folders");
393+
if (stm == nullptr)
394+
return false;
395+
if (stm.step() == SQLITE_ROW)
396+
last_cn = std::max(last_cn, stm.col_uint64(0));
397+
stm = gx_sql_prep(db, "SELECT MAX(change_number) FROM messages");
398+
if (stm == nullptr)
399+
return false;
400+
if (stm.step() == SQLITE_ROW)
401+
last_cn = std::max(last_cn, stm.col_uint64(0));
402+
return true;
403+
}
404+
405+
static bool cgkreset_save_param(sqlite3 *db, uint64_t last_cn)
406+
{
407+
auto stm = gx_sql_prep(db, "UPDATE configurations SET config_value=? WHERE config_id=?");
408+
if (stm == nullptr)
409+
return false;
410+
stm.bind_int64(1, last_cn);
411+
stm.bind_int64(2, CONFIG_ID_LAST_CHANGE_NUMBER);
412+
return stm.step() == SQLITE_DONE;
413+
}
414+
415+
BOOL db_engine_cgkreset(const char *dir, uint32_t flags)
416+
{
417+
auto db = db_engine_get_db(dir);
418+
if (!db)
419+
return false;
420+
auto xact = gx_sql_begin(db->psqlite, txn_mode::write);
421+
if (!xact)
422+
return false;
423+
uint64_t last_cn = 0;
424+
GUID store_guid;
425+
if (!cgkreset_load_param(db->psqlite, last_cn, store_guid))
426+
return false;
427+
if (flags & CGKRESET_ZERO_LASTCN)
428+
last_cn = 0;
429+
if (flags & (CGKRESET_ZERO_LASTCN | CGKRESET_FOLDERS | CGKRESET_MESSAGES)) {
430+
auto succ = cgkreset_2(db->psqlite, last_cn, store_guid, flags);
431+
if (!succ)
432+
return false;
433+
}
434+
auto succ = cgkreset_save_param(db->psqlite, last_cn);
435+
if (!succ)
436+
return false;
437+
return xact.commit() == SQLITE_OK;
438+
}
439+
288440
dynamic_node::dynamic_node(dynamic_node &&o) noexcept :
289441
folder_id(o.folder_id), search_flags(o.search_flags),
290442
prestriction(o.prestriction), folder_ids(o.folder_ids)

exch/exmdb/db_engine.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@ extern void db_engine_stop();
221221

222222
extern db_conn_ptr db_engine_get_db(const char *dir);
223223
extern BOOL db_engine_vacuum(const char *path);
224+
extern BOOL db_engine_cgkreset(const char *dir, uint32_t flags);
224225
BOOL db_engine_unload_db(const char *path);
225226
extern BOOL db_engine_enqueue_populating_criteria(const char *dir, cpid_t, uint64_t folder_id, BOOL recursive, const RESTRICTION *, const LONGLONG_ARRAY *folder_ids);
226227
extern bool db_engine_check_populating(const char *dir, uint64_t folder_id);

exch/exmdb/names.cpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// SPDX-License-Identifier: AGPL-3.0-or-later, OR GPL-2.0-or-later WITH linking exception
1+
// SPDX-License-Identifier: AGPL-3.0-or-later
22
// SPDX-FileCopyrightText: 2025 grommunio GmbH
33
// This file is part of Gromox.
44
#include <gromox/defs.h>
@@ -154,6 +154,7 @@ static constexpr const char *exmdb_rpc_names[] = {
154154
E(imapfile_read),
155155
E(imapfile_write),
156156
E(imapfile_delete),
157+
E(cgkreset),
157158
};
158159
#undef E
159160

@@ -162,7 +163,7 @@ namespace exmdb {
162163
const char *exmdb_rpc_idtoname(exmdb_callid i)
163164
{
164165
auto j = static_cast<uint8_t>(i);
165-
static_assert(std::size(exmdb_rpc_names) == static_cast<uint8_t>(exmdb_callid::imapfile_delete) + 1);
166+
static_assert(std::size(exmdb_rpc_names) == static_cast<uint8_t>(exmdb_callid::cgkreset) + 1);
166167
auto s = j < std::size(exmdb_rpc_names) ? exmdb_rpc_names[j] : nullptr;
167168
return znul(s);
168169
}

exch/exmdb/store2.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ BOOL exmdb_server::unload_store(const char *dir)
5353
return db_engine_unload_db(dir);
5454
}
5555

56+
BOOL exmdb_server::cgkreset(const char *dir, uint32_t flags)
57+
{
58+
return db_engine_cgkreset(dir, flags);
59+
}
60+
5661
BOOL exmdb_server::notify_new_mail(const char *dir, uint64_t folder_id,
5762
uint64_t message_id)
5863
{

include/gromox/exmdb_idef.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,3 +140,4 @@ EXMIDL(recalc_store_size, (const char *dir, uint32_t flags))
140140
EXMIDL(imapfile_read, (const char *dir, const std::string &type, const std::string &mid, IDLOUT std::string *data))
141141
EXMIDL(imapfile_write, (const char *dir, const std::string &type, const std::string &mid, const std::string &data))
142142
EXMIDL(imapfile_delete, (const char *dir, const std::string &type, const std::string &mid))
143+
EXMIDL(cgkreset, (const char *dir, uint32_t flags))

include/gromox/exmdb_rpc.hpp

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,8 @@ enum class exmdb_callid : uint8_t {
170170
imapfile_read = 0x8e,
171171
imapfile_write = 0x8f,
172172
imapfile_delete = 0x90,
173-
/* update exch/exmdb_provider/names.cpp:exmdb_rpc_idtoname! */
173+
cgkreset = 0x91,
174+
/* update exch/exmdb/names.cpp:exmdb_rpc_idtoname! */
174175
};
175176

176177
struct exreq {
@@ -863,7 +864,19 @@ struct exreq_imapfile_write final : public exreq {
863864
std::string type, mid, data;
864865
};
865866

867+
/**
868+
* FOLDERS: process folders
869+
* MESSAGES: process messages
870+
* ZERO_LASTCN: reset all CNs, start from 0 (implies FOLDERS|MESSAGES)
871+
*/
872+
enum cgkreset_flags {
873+
CGKRESET_FOLDERS = 0x1U,
874+
CGKRESET_MESSAGES = 0x2U,
875+
CGKRESET_ZERO_LASTCN = 0x4U,
876+
};
877+
866878
using exreq_imapfile_delete = exreq_imapfile_read;
879+
using exreq_cgkreset = exreq_recalc_store_size;
867880

868881
struct exresp {
869882
exresp() = default; /* Prevent use of direct-init-list */
@@ -1358,6 +1371,7 @@ using exresp_write_message = exresp_error;
13581371
using exresp_movecopy_folder = exresp_error;
13591372
using exresp_imapfile_write = exresp;
13601373
using exresp_imapfile_delete = exresp;
1374+
using exresp_cgkreset = exresp;
13611375

13621376
struct DB_NOTIFY_DATAGRAM {
13631377
char *dir = nullptr;

lib/exmdb_ext.cpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2409,7 +2409,8 @@ static pack_result exmdb_push(EXT_PUSH &x, const exreq_imapfile_write &d)
24092409
E(write_message_v2) \
24102410
E(imapfile_read) \
24112411
E(imapfile_write) \
2412-
E(imapfile_delete)
2412+
E(imapfile_delete) \
2413+
E(cgkreset)
24132414

24142415
/**
24152416
* This uses *& because we do not know which request type we are going to get
@@ -3744,7 +3745,8 @@ static pack_result exmdb_push(EXT_PUSH &x, const exresp_imapfile_read &d)
37443745
E(autoreply_tsupdate) \
37453746
E(recalc_store_size) \
37463747
E(imapfile_write) \
3747-
E(imapfile_delete)
3748+
E(imapfile_delete) \
3749+
E(cgkreset)
37483750
#define RSP_WITH_ARGS \
37493751
E(get_all_named_propids) \
37503752
E(get_named_propids) \

tools/mbop.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ using LLU = unsigned long long;
1313

1414
static constexpr int EXIT_PARAM = 2;
1515

16+
namespace cgkreset { extern int main(int, char **); }
1617
namespace delmsg { extern int main(int, char **); }
1718
namespace emptyfld { extern int main(int, char **); }
1819
namespace foreach_wrap { extern int main(int, char **); }

tools/mbop_main.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ static constexpr HXoption g_options_table[] = {
7676

7777
void command_overview()
7878
{
79-
fprintf(stderr, "Commands:\n\tclear-photo clear-profile clear-rwz delmsg "
79+
fprintf(stderr, "Commands:\n\tcgkreset clear-photo clear-profile clear-rwz delmsg "
8080
"echo-maildir echo-username "
8181
"emptyfld get-freebusy get-photo get-websettings "
8282
"get-websettings-persistent "
@@ -514,6 +514,8 @@ int cmd_parser(int argc, char **argv)
514514
} else if (strcmp(argv[0], "echo-username") == 0) {
515515
printf("%s\n", g_dstuser.c_str());
516516
return EXIT_SUCCESS;
517+
} else if (strcmp(argv[0], "cgkreset") == 0) {
518+
return cgkreset::main(argc, argv);
517519
}
518520
return simple_rpc::main(argc, argv);
519521
}

0 commit comments

Comments
 (0)