@remit/mailbox-service 0.0.35 → 0.0.36
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 +1 -1
- package/src/mailbox-sync.test.ts +54 -11
- package/src/mailbox-sync.ts +74 -16
package/package.json
CHANGED
package/src/mailbox-sync.test.ts
CHANGED
|
@@ -11,7 +11,10 @@ import {
|
|
|
11
11
|
MailboxSyncStatus,
|
|
12
12
|
} from "@remit/domain-enums";
|
|
13
13
|
import { parseImapAttributes } from "./attribute-mapper.js";
|
|
14
|
-
import { MailboxSyncService } from "./mailbox-sync.js";
|
|
14
|
+
import { type MailboxSyncLogger, MailboxSyncService } from "./mailbox-sync.js";
|
|
15
|
+
|
|
16
|
+
const silentLogger: MailboxSyncLogger = { info: () => {}, debug: () => {} };
|
|
17
|
+
|
|
15
18
|
import type { IImapConnection, ImapNamespaces } from "./types.js";
|
|
16
19
|
|
|
17
20
|
describe("parseImapAttributes – locale invariance (#194)", () => {
|
|
@@ -121,7 +124,11 @@ describe("MailboxSyncService.syncMailboxes — UIDVALIDITY cursor detection (#12
|
|
|
121
124
|
|
|
122
125
|
it("trips cursor_invalid when the STATUS sweep observes a changed UIDVALIDITY", async () => {
|
|
123
126
|
const { mailboxService, specialUseService, updateCalls } = buildServices(1);
|
|
124
|
-
const service = new MailboxSyncService(
|
|
127
|
+
const service = new MailboxSyncService(
|
|
128
|
+
mailboxService,
|
|
129
|
+
specialUseService,
|
|
130
|
+
silentLogger,
|
|
131
|
+
);
|
|
125
132
|
const connection = buildConnection(2);
|
|
126
133
|
|
|
127
134
|
await service.syncMailboxes({ accountId: "acc-1" }, connection);
|
|
@@ -136,7 +143,11 @@ describe("MailboxSyncService.syncMailboxes — UIDVALIDITY cursor detection (#12
|
|
|
136
143
|
|
|
137
144
|
it("does not write anything when UIDVALIDITY (and everything else) is unchanged", async () => {
|
|
138
145
|
const { mailboxService, specialUseService, updateCalls } = buildServices(1);
|
|
139
|
-
const service = new MailboxSyncService(
|
|
146
|
+
const service = new MailboxSyncService(
|
|
147
|
+
mailboxService,
|
|
148
|
+
specialUseService,
|
|
149
|
+
silentLogger,
|
|
150
|
+
);
|
|
140
151
|
const connection = buildConnection(1);
|
|
141
152
|
|
|
142
153
|
await service.syncMailboxes({ accountId: "acc-1" }, connection);
|
|
@@ -150,7 +161,11 @@ describe("MailboxSyncService.syncMailboxes — UIDVALIDITY cursor detection (#12
|
|
|
150
161
|
// the server's current value would step it over every change message sync
|
|
151
162
|
// had not yet applied.
|
|
152
163
|
const { mailboxService, specialUseService, updateCalls } = buildServices(1);
|
|
153
|
-
const service = new MailboxSyncService(
|
|
164
|
+
const service = new MailboxSyncService(
|
|
165
|
+
mailboxService,
|
|
166
|
+
specialUseService,
|
|
167
|
+
silentLogger,
|
|
168
|
+
);
|
|
154
169
|
const connection = buildConnection(2, "99999");
|
|
155
170
|
|
|
156
171
|
await service.syncMailboxes({ accountId: "acc-1" }, connection);
|
|
@@ -162,7 +177,11 @@ describe("MailboxSyncService.syncMailboxes — UIDVALIDITY cursor detection (#12
|
|
|
162
177
|
|
|
163
178
|
it("does not sweep-write a mailbox whose only difference is the server mod-sequence", async () => {
|
|
164
179
|
const { mailboxService, specialUseService, updateCalls } = buildServices(1);
|
|
165
|
-
const service = new MailboxSyncService(
|
|
180
|
+
const service = new MailboxSyncService(
|
|
181
|
+
mailboxService,
|
|
182
|
+
specialUseService,
|
|
183
|
+
silentLogger,
|
|
184
|
+
);
|
|
166
185
|
const connection = buildConnection(1, "4242");
|
|
167
186
|
|
|
168
187
|
await service.syncMailboxes({ accountId: "acc-1" }, connection);
|
|
@@ -175,7 +194,11 @@ describe("MailboxSyncService.syncMailboxes — UIDVALIDITY cursor detection (#12
|
|
|
175
194
|
1,
|
|
176
195
|
MailboxCursorState.cursor_invalid,
|
|
177
196
|
);
|
|
178
|
-
const service = new MailboxSyncService(
|
|
197
|
+
const service = new MailboxSyncService(
|
|
198
|
+
mailboxService,
|
|
199
|
+
specialUseService,
|
|
200
|
+
silentLogger,
|
|
201
|
+
);
|
|
179
202
|
const connection = buildConnection(2);
|
|
180
203
|
|
|
181
204
|
await service.syncMailboxes({ accountId: "acc-1" }, connection);
|
|
@@ -273,7 +296,11 @@ describe("MailboxSyncService.syncMailboxes — reconcile does not delete pending
|
|
|
273
296
|
syncStatus: MailboxSyncStatus.pending,
|
|
274
297
|
},
|
|
275
298
|
]);
|
|
276
|
-
const service = new MailboxSyncService(
|
|
299
|
+
const service = new MailboxSyncService(
|
|
300
|
+
mailboxService,
|
|
301
|
+
specialUseService,
|
|
302
|
+
silentLogger,
|
|
303
|
+
);
|
|
277
304
|
|
|
278
305
|
const result = await service.syncMailboxes(
|
|
279
306
|
{ accountId: "acc-1" },
|
|
@@ -299,7 +326,11 @@ describe("MailboxSyncService.syncMailboxes — reconcile does not delete pending
|
|
|
299
326
|
syncStatus: MailboxSyncStatus.synced,
|
|
300
327
|
},
|
|
301
328
|
]);
|
|
302
|
-
const service = new MailboxSyncService(
|
|
329
|
+
const service = new MailboxSyncService(
|
|
330
|
+
mailboxService,
|
|
331
|
+
specialUseService,
|
|
332
|
+
silentLogger,
|
|
333
|
+
);
|
|
303
334
|
|
|
304
335
|
const result = await service.syncMailboxes(
|
|
305
336
|
{ accountId: "acc-1" },
|
|
@@ -419,7 +450,11 @@ describe("MailboxSyncService.syncMailboxes — a folder leaving mid-sweep (#339)
|
|
|
419
450
|
doomedSyncStatus: syncStatus,
|
|
420
451
|
});
|
|
421
452
|
const { connection, statusPaths } = buildConnection(async () => status());
|
|
422
|
-
const service = new MailboxSyncService(
|
|
453
|
+
const service = new MailboxSyncService(
|
|
454
|
+
mailboxService,
|
|
455
|
+
specialUseService,
|
|
456
|
+
silentLogger,
|
|
457
|
+
);
|
|
423
458
|
|
|
424
459
|
await service.syncMailboxes({ accountId: "acc-1" }, connection);
|
|
425
460
|
|
|
@@ -439,7 +474,11 @@ describe("MailboxSyncService.syncMailboxes — a folder leaving mid-sweep (#339)
|
|
|
439
474
|
if (path === "Doomed") throw new Error("Mailbox doesn't exist: Doomed");
|
|
440
475
|
return status();
|
|
441
476
|
});
|
|
442
|
-
const service = new MailboxSyncService(
|
|
477
|
+
const service = new MailboxSyncService(
|
|
478
|
+
mailboxService,
|
|
479
|
+
specialUseService,
|
|
480
|
+
silentLogger,
|
|
481
|
+
);
|
|
443
482
|
|
|
444
483
|
await assert.doesNotReject(
|
|
445
484
|
service.syncMailboxes({ accountId: "acc-1" }, connection),
|
|
@@ -460,7 +499,11 @@ describe("MailboxSyncService.syncMailboxes — a folder leaving mid-sweep (#339)
|
|
|
460
499
|
if (path === "Doomed") throw new Error("connection reset by peer");
|
|
461
500
|
return status();
|
|
462
501
|
});
|
|
463
|
-
const service = new MailboxSyncService(
|
|
502
|
+
const service = new MailboxSyncService(
|
|
503
|
+
mailboxService,
|
|
504
|
+
specialUseService,
|
|
505
|
+
silentLogger,
|
|
506
|
+
);
|
|
464
507
|
|
|
465
508
|
await assert.rejects(
|
|
466
509
|
service.syncMailboxes({ accountId: "acc-1" }, connection),
|
package/src/mailbox-sync.ts
CHANGED
|
@@ -77,19 +77,45 @@ export interface SyncAccountInfo {
|
|
|
77
77
|
accountId: string;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Logger interface for MailboxSyncService.
|
|
82
|
+
*
|
|
83
|
+
* `info` is reserved here for the three mailbox lifecycle events — created,
|
|
84
|
+
* updated, deleted — and is the argued exception to `plugins/no-logger-info.grit`
|
|
85
|
+
* that the rest of this repo writes as a suppression. (The plugin matches the
|
|
86
|
+
* bare `logger` identifier, so it does not see an injected logger; the reasoning
|
|
87
|
+
* is written out instead of being silently absent.) A folder disappearing from
|
|
88
|
+
* someone's mail is destructive and irreversible from this service's side, and
|
|
89
|
+
* the only record that this process did it is the line it writes. That must not
|
|
90
|
+
* sit below the default threshold.
|
|
91
|
+
*
|
|
92
|
+
* Everything else this service has to say — a special-use flag it synced, a
|
|
93
|
+
* duplicate folder it skipped — is a routine trace and goes to `debug`.
|
|
94
|
+
*/
|
|
95
|
+
export interface MailboxSyncLogger {
|
|
96
|
+
info(obj: Record<string, unknown>, msg: string): void;
|
|
97
|
+
debug(obj: Record<string, unknown>, msg: string): void;
|
|
98
|
+
}
|
|
99
|
+
|
|
80
100
|
/**
|
|
81
101
|
* Service for synchronizing mailbox metadata between IMAP and the data backend.
|
|
82
102
|
*/
|
|
83
103
|
export class MailboxSyncService {
|
|
84
104
|
private mailboxService: IMailboxRepository;
|
|
85
105
|
private specialUseService: IMailboxSpecialUseRepository;
|
|
106
|
+
private log: MailboxSyncLogger;
|
|
86
107
|
|
|
108
|
+
// Required, with no no-op default: the lines below are the only record that a
|
|
109
|
+
// mailbox was created or destroyed, and a default would let the next call site
|
|
110
|
+
// lose them with no error and no failing test.
|
|
87
111
|
constructor(
|
|
88
112
|
mailboxService: IMailboxRepository,
|
|
89
113
|
specialUseService: IMailboxSpecialUseRepository,
|
|
114
|
+
logger: MailboxSyncLogger,
|
|
90
115
|
) {
|
|
91
116
|
this.mailboxService = mailboxService;
|
|
92
117
|
this.specialUseService = specialUseService;
|
|
118
|
+
this.log = logger;
|
|
93
119
|
}
|
|
94
120
|
|
|
95
121
|
/**
|
|
@@ -146,8 +172,13 @@ export class MailboxSyncService {
|
|
|
146
172
|
account.accountId,
|
|
147
173
|
existing.mailboxId,
|
|
148
174
|
);
|
|
149
|
-
|
|
150
|
-
|
|
175
|
+
this.log.info(
|
|
176
|
+
{
|
|
177
|
+
mailboxId: existing.mailboxId,
|
|
178
|
+
fullPath: existing.fullPath,
|
|
179
|
+
reason: "non-selectable",
|
|
180
|
+
},
|
|
181
|
+
"Deleted mailbox",
|
|
151
182
|
);
|
|
152
183
|
result.deleted++;
|
|
153
184
|
}
|
|
@@ -164,8 +195,13 @@ export class MailboxSyncService {
|
|
|
164
195
|
account.accountId,
|
|
165
196
|
existing.mailboxId,
|
|
166
197
|
);
|
|
167
|
-
|
|
168
|
-
|
|
198
|
+
this.log.info(
|
|
199
|
+
{
|
|
200
|
+
mailboxId: existing.mailboxId,
|
|
201
|
+
fullPath: existing.fullPath,
|
|
202
|
+
reason: "duplicate-special-use",
|
|
203
|
+
},
|
|
204
|
+
"Deleted mailbox",
|
|
169
205
|
);
|
|
170
206
|
result.deleted++;
|
|
171
207
|
}
|
|
@@ -238,8 +274,13 @@ export class MailboxSyncService {
|
|
|
238
274
|
// pending rows to the create/rename flow that owns them.
|
|
239
275
|
if (existing.syncStatus === MailboxSyncStatus.pending) continue;
|
|
240
276
|
await this.mailboxService.delete(account.accountId, existing.mailboxId);
|
|
241
|
-
|
|
242
|
-
|
|
277
|
+
this.log.info(
|
|
278
|
+
{
|
|
279
|
+
mailboxId: existing.mailboxId,
|
|
280
|
+
fullPath: existing.fullPath,
|
|
281
|
+
reason: "not-on-server",
|
|
282
|
+
},
|
|
283
|
+
"Deleted mailbox",
|
|
243
284
|
);
|
|
244
285
|
result.deleted++;
|
|
245
286
|
}
|
|
@@ -419,12 +460,18 @@ export class MailboxSyncService {
|
|
|
419
460
|
mailbox.mailboxId,
|
|
420
461
|
parsed.specialUse,
|
|
421
462
|
);
|
|
422
|
-
|
|
423
|
-
|
|
463
|
+
this.log.info(
|
|
464
|
+
{
|
|
465
|
+
mailboxId: mailbox.mailboxId,
|
|
466
|
+
fullPath: mailboxInfo.fullPath,
|
|
467
|
+
specialUse: parsed.specialUse,
|
|
468
|
+
},
|
|
469
|
+
"Created mailbox",
|
|
424
470
|
);
|
|
425
471
|
} else {
|
|
426
|
-
|
|
427
|
-
|
|
472
|
+
this.log.info(
|
|
473
|
+
{ mailboxId: mailbox.mailboxId, fullPath: mailboxInfo.fullPath },
|
|
474
|
+
"Created mailbox",
|
|
428
475
|
);
|
|
429
476
|
}
|
|
430
477
|
|
|
@@ -504,8 +551,13 @@ export class MailboxSyncService {
|
|
|
504
551
|
`specialUse: [${(existing.specialUse ?? []).join(",")}] -> [${parsed.specialUse.join(",")}]`,
|
|
505
552
|
);
|
|
506
553
|
|
|
507
|
-
|
|
508
|
-
|
|
554
|
+
this.log.info(
|
|
555
|
+
{
|
|
556
|
+
mailboxId: existing.mailboxId,
|
|
557
|
+
fullPath: mailboxInfo.fullPath,
|
|
558
|
+
changes,
|
|
559
|
+
},
|
|
560
|
+
"Updating mailbox",
|
|
509
561
|
);
|
|
510
562
|
|
|
511
563
|
// Update mailbox with fresh status. ElectroDB rejects empty sets, so we
|
|
@@ -557,8 +609,9 @@ export class MailboxSyncService {
|
|
|
557
609
|
|
|
558
610
|
if (parsed.specialUse.length > 0) {
|
|
559
611
|
await this.specialUseService.createMany(mailboxId, parsed.specialUse);
|
|
560
|
-
|
|
561
|
-
|
|
612
|
+
this.log.debug(
|
|
613
|
+
{ fullPath: mailboxInfo.fullPath, specialUse: parsed.specialUse },
|
|
614
|
+
"Synced special-use",
|
|
562
615
|
);
|
|
563
616
|
}
|
|
564
617
|
};
|
|
@@ -619,8 +672,13 @@ export class MailboxSyncService {
|
|
|
619
672
|
}
|
|
620
673
|
|
|
621
674
|
// This is a duplicate - another folder has the attribute
|
|
622
|
-
|
|
623
|
-
|
|
675
|
+
this.log.debug(
|
|
676
|
+
{
|
|
677
|
+
fullPath: mailbox.fullPath,
|
|
678
|
+
claimedPath,
|
|
679
|
+
specialUse: expectedSpecialUse,
|
|
680
|
+
},
|
|
681
|
+
"Skipping duplicate folder",
|
|
624
682
|
);
|
|
625
683
|
return true;
|
|
626
684
|
};
|