@remit/mailbox-service 0.0.26 → 0.0.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remit/mailbox-service",
3
- "version": "0.0.26",
3
+ "version": "0.0.27",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -4,7 +4,11 @@ import type {
4
4
  IMailboxRepository,
5
5
  IMailboxSpecialUseRepository,
6
6
  } from "@remit/data-ports";
7
- import { MailboxCursorState, MailboxSpecialUse } from "@remit/domain-enums";
7
+ import {
8
+ MailboxCursorState,
9
+ MailboxSpecialUse,
10
+ MailboxSyncStatus,
11
+ } from "@remit/domain-enums";
8
12
  import { parseImapAttributes } from "./attribute-mapper.js";
9
13
  import { MailboxSyncService } from "./mailbox-sync.js";
10
14
  import type { IImapConnection, ImapNamespaces } from "./types.js";
@@ -180,3 +184,128 @@ describe("MailboxSyncService.syncMailboxes — UIDVALIDITY cursor detection (#12
180
184
  assert.equal("cursorState" in (uidValidityUpdate ?? {}), false);
181
185
  });
182
186
  });
187
+
188
+ describe("MailboxSyncService.syncMailboxes — reconcile does not delete pending folders (#290)", () => {
189
+ const namespaces: ImapNamespaces = {
190
+ personal: [{ prefix: "", delimiter: "/" }],
191
+ other: [],
192
+ shared: [],
193
+ };
194
+
195
+ // The server lists only INBOX: neither user folder is on it yet.
196
+ const serverConnection = (): IImapConnection =>
197
+ ({
198
+ getNamespaces: async () => namespaces,
199
+ listMailboxes: async () => [
200
+ {
201
+ fullPath: "INBOX",
202
+ name: "INBOX",
203
+ delimiter: "/",
204
+ attributes: [],
205
+ parentPath: null,
206
+ },
207
+ ],
208
+ getMailboxStatus: async () => ({
209
+ messages: 0,
210
+ recent: 0,
211
+ unseen: 0,
212
+ uidNext: 1,
213
+ uidValidity: 1,
214
+ highestModseq: "0",
215
+ deletedCount: 0,
216
+ }),
217
+ }) as unknown as IImapConnection;
218
+
219
+ const buildServices = (
220
+ existing: Array<{
221
+ mailboxId: string;
222
+ fullPath: string;
223
+ syncStatus?: string;
224
+ }>,
225
+ ) => {
226
+ const deleted: string[] = [];
227
+ const mailboxService = {
228
+ listByAccount: async () => ({
229
+ items: existing.map((m) => ({
230
+ mailboxId: m.mailboxId,
231
+ fullPath: m.fullPath,
232
+ uidNext: 1,
233
+ uidValidity: 1,
234
+ messageCount: 0,
235
+ unseenCount: 0,
236
+ deletedCount: 0,
237
+ highestModseq: "0",
238
+ specialUse: undefined,
239
+ syncStatus: m.syncStatus,
240
+ })),
241
+ continuationToken: undefined,
242
+ }),
243
+ update: async () => ({}),
244
+ delete: async (_accountId: string, mailboxId: string) => {
245
+ deleted.push(mailboxId);
246
+ },
247
+ create: async () => ({}),
248
+ } as unknown as IMailboxRepository;
249
+
250
+ const specialUseService = {
251
+ listByMailboxId: async () => [],
252
+ deleteByMailboxId: async () => undefined,
253
+ createMany: async () => undefined,
254
+ } as unknown as IMailboxSpecialUseRepository;
255
+
256
+ return { mailboxService, specialUseService, deleted };
257
+ };
258
+
259
+ it("keeps a pending folder the server has not listed yet", async () => {
260
+ // The folder was just created locally; MAILBOX_CREATE has not reached the
261
+ // server, so the LIST omits it. Deleting the row here races the create and
262
+ // wedges the account's mailbox-sync FIFO group for a full visibility window.
263
+ const { mailboxService, specialUseService, deleted } = buildServices([
264
+ {
265
+ mailboxId: "inbox",
266
+ fullPath: "INBOX",
267
+ syncStatus: MailboxSyncStatus.synced,
268
+ },
269
+ {
270
+ mailboxId: "pending-folder",
271
+ fullPath: "New Folder",
272
+ syncStatus: MailboxSyncStatus.pending,
273
+ },
274
+ ]);
275
+ const service = new MailboxSyncService(mailboxService, specialUseService);
276
+
277
+ const result = await service.syncMailboxes(
278
+ { accountId: "acc-1" },
279
+ serverConnection(),
280
+ );
281
+
282
+ assert.deepEqual(deleted, []);
283
+ assert.equal(result.deleted, 0);
284
+ });
285
+
286
+ it("still deletes a synced folder that has left the server", async () => {
287
+ // A folder that was confirmed on the server and is now gone is a genuine
288
+ // server-side deletion; the reconcile must still reap it.
289
+ const { mailboxService, specialUseService, deleted } = buildServices([
290
+ {
291
+ mailboxId: "inbox",
292
+ fullPath: "INBOX",
293
+ syncStatus: MailboxSyncStatus.synced,
294
+ },
295
+ {
296
+ mailboxId: "synced-gone",
297
+ fullPath: "Old Folder",
298
+ syncStatus: MailboxSyncStatus.synced,
299
+ },
300
+ ]);
301
+ const service = new MailboxSyncService(mailboxService, specialUseService);
302
+
303
+ const result = await service.syncMailboxes(
304
+ { accountId: "acc-1" },
305
+ serverConnection(),
306
+ );
307
+
308
+ assert.deepEqual(deleted, ["synced-gone"]);
309
+ assert.equal(result.deleted, 1);
310
+ });
311
+ });
@@ -13,6 +13,7 @@ import type {
13
13
  import {
14
14
  MailboxCursorState,
15
15
  MailboxSpecialUse,
16
+ MailboxSyncStatus,
16
17
  NamespaceType,
17
18
  } from "@remit/domain-enums";
18
19
  import pMap from "p-map";
@@ -198,13 +199,21 @@ export class MailboxSyncService {
198
199
 
199
200
  // Handle deleted mailboxes (exist in DB but not on server)
200
201
  for (const existing of existingMailboxes) {
201
- if (!seenPaths.has(existing.fullPath)) {
202
- await this.mailboxService.delete(account.accountId, existing.mailboxId);
203
- console.info(
204
- `Deleted mailbox: ${existing.mailboxId} (${existing.fullPath})`,
205
- );
206
- result.deleted++;
207
- }
202
+ if (seenPaths.has(existing.fullPath)) continue;
203
+ // A `pending` row is a folder the user just created (or renamed) whose
204
+ // MAILBOX_CREATE/RENAME has not yet reached the server, so its absence
205
+ // from the LIST is expected, not a server-side deletion. Deleting it
206
+ // races the create: the row vanishes, then MAILBOX_CREATE fails with
207
+ // NotFoundError trying to mark it synced, and — sharing this account's
208
+ // mailboxes FIFO group — that un-acked failure stalls every later
209
+ // mailbox sync for the queue's whole visibility window (#290). Leave
210
+ // pending rows to the create/rename flow that owns them.
211
+ if (existing.syncStatus === MailboxSyncStatus.pending) continue;
212
+ await this.mailboxService.delete(account.accountId, existing.mailboxId);
213
+ console.info(
214
+ `Deleted mailbox: ${existing.mailboxId} (${existing.fullPath})`,
215
+ );
216
+ result.deleted++;
208
217
  }
209
218
 
210
219
  return result;