@remit/mailbox-service 0.0.111 → 0.0.113

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.
Files changed (298) hide show
  1. package/dist/account-credentials.d.ts +83 -0
  2. package/dist/account-credentials.js +104 -0
  3. package/dist/attribute-mapper.d.ts +36 -0
  4. package/dist/attribute-mapper.js +119 -0
  5. package/dist/body-parse.d.ts +28 -0
  6. package/{src/body-parse.ts → dist/body-parse.js} +18 -26
  7. package/dist/body-part-mapper.d.ts +92 -0
  8. package/dist/body-part-mapper.js +301 -0
  9. package/dist/body-sync-queue.d.ts +59 -0
  10. package/dist/body-sync-queue.js +56 -0
  11. package/dist/body-sync-terminal.d.ts +58 -0
  12. package/dist/body-sync-terminal.js +95 -0
  13. package/dist/body-sync.d.ts +351 -0
  14. package/dist/body-sync.js +1204 -0
  15. package/dist/calendar-parts.d.ts +23 -0
  16. package/dist/calendar-parts.js +18 -0
  17. package/dist/classification-backfill.d.ts +91 -0
  18. package/dist/classification-backfill.js +180 -0
  19. package/dist/classify-message.d.ts +31 -0
  20. package/dist/classify-message.js +89 -0
  21. package/dist/connection-factory.d.ts +54 -0
  22. package/dist/connection-factory.js +54 -0
  23. package/dist/connection-test.d.ts +41 -0
  24. package/dist/connection-test.js +152 -0
  25. package/dist/external-move.d.ts +94 -0
  26. package/dist/external-move.js +173 -0
  27. package/dist/filter-operations.d.ts +42 -0
  28. package/dist/filter-operations.js +211 -0
  29. package/dist/filters/filter-config.d.ts +14 -0
  30. package/dist/filters/filter-config.js +10 -0
  31. package/{src/filters/list-id.ts → dist/filters/list-id.d.ts} +2 -16
  32. package/dist/filters/list-id.js +26 -0
  33. package/dist/filters/match.d.ts +69 -0
  34. package/dist/filters/match.js +165 -0
  35. package/dist/filters/pipeline.d.ts +64 -0
  36. package/dist/filters/pipeline.js +100 -0
  37. package/{src/flag-push-terminal.ts → dist/flag-push-terminal.d.ts} +19 -77
  38. package/dist/flag-push-terminal.js +63 -0
  39. package/dist/flag-push.d.ts +69 -0
  40. package/dist/flag-push.js +111 -0
  41. package/dist/flag-queue.d.ts +175 -0
  42. package/dist/flag-queue.js +291 -0
  43. package/dist/folder-operations.d.ts +39 -0
  44. package/dist/folder-operations.js +112 -0
  45. package/dist/folder-placement.d.ts +13 -0
  46. package/dist/folder-placement.js +23 -0
  47. package/dist/heuristics/authenticationResults.d.ts +10 -0
  48. package/dist/heuristics/authenticationResults.js +187 -0
  49. package/dist/heuristics/classifyByHeaders.d.ts +82 -0
  50. package/dist/heuristics/classifyByHeaders.js +365 -0
  51. package/dist/heuristics/classifyPlacement.d.ts +67 -0
  52. package/dist/heuristics/classifyPlacement.js +153 -0
  53. package/dist/heuristics/machineSenders.d.ts +21 -0
  54. package/{src/heuristics/machineSenders.ts → dist/heuristics/machineSenders.js} +34 -40
  55. package/dist/heuristics/senderMismatch.d.ts +81 -0
  56. package/dist/heuristics/senderMismatch.js +192 -0
  57. package/dist/heuristics/socialDomains.d.ts +14 -0
  58. package/{src/heuristics/socialDomains.ts → dist/heuristics/socialDomains.js} +9 -9
  59. package/dist/heuristics/transactionalDomains.d.ts +18 -0
  60. package/{src/heuristics/transactionalDomains.ts → dist/heuristics/transactionalDomains.js} +8 -8
  61. package/dist/imapflow-connection.d.ts +377 -0
  62. package/dist/imapflow-connection.js +1147 -0
  63. package/dist/index.d.ts +63 -0
  64. package/dist/index.js +65 -0
  65. package/dist/list-id-backfill.d.ts +83 -0
  66. package/dist/list-id-backfill.js +132 -0
  67. package/dist/mailbox-cursor-rebuild.d.ts +74 -0
  68. package/dist/mailbox-cursor-rebuild.js +71 -0
  69. package/{src/mailbox-cursor.ts → dist/mailbox-cursor.d.ts} +20 -98
  70. package/dist/mailbox-cursor.js +102 -0
  71. package/dist/mailbox-intent.d.ts +41 -0
  72. package/{src/mailbox-intent.ts → dist/mailbox-intent.js} +39 -63
  73. package/dist/mailbox-management.d.ts +240 -0
  74. package/dist/mailbox-management.js +576 -0
  75. package/dist/mailbox-presence.d.ts +52 -0
  76. package/{src/mailbox-presence.ts → dist/mailbox-presence.js} +19 -30
  77. package/dist/mailbox-queue.d.ts +126 -0
  78. package/dist/mailbox-queue.js +214 -0
  79. package/dist/mailbox-sync.d.ts +93 -0
  80. package/dist/mailbox-sync.js +490 -0
  81. package/dist/mailbox-upstream.d.ts +38 -0
  82. package/{src/mailbox-upstream.ts → dist/mailbox-upstream.js} +37 -47
  83. package/dist/message-category.d.ts +51 -0
  84. package/{src/message-category.ts → dist/message-category.js} +28 -71
  85. package/dist/message-move.d.ts +285 -0
  86. package/dist/message-move.js +858 -0
  87. package/dist/message-parser.d.ts +14 -0
  88. package/dist/message-parser.js +17 -0
  89. package/{src/message-presence.ts → dist/message-presence.d.ts} +1 -13
  90. package/dist/message-presence.js +29 -0
  91. package/dist/message-read.d.ts +51 -0
  92. package/dist/message-read.js +31 -0
  93. package/dist/message-sync.d.ts +345 -0
  94. package/dist/message-sync.js +1358 -0
  95. package/dist/mime-walker.d.ts +102 -0
  96. package/dist/mime-walker.js +198 -0
  97. package/dist/organize-job.d.ts +20 -0
  98. package/dist/organize-job.js +37 -0
  99. package/dist/outbox-attachment-content.d.ts +37 -0
  100. package/dist/outbox-attachment-content.js +50 -0
  101. package/dist/outbox-attachment-filename.d.ts +19 -0
  102. package/dist/outbox-attachment-filename.js +64 -0
  103. package/dist/outbox-attachment.d.ts +148 -0
  104. package/dist/outbox-attachment.js +273 -0
  105. package/dist/outbox-queue.d.ts +73 -0
  106. package/dist/outbox-queue.js +284 -0
  107. package/{src/outbox-status.ts → dist/outbox-status.d.ts} +1 -6
  108. package/dist/outbox-status.js +18 -0
  109. package/dist/ownership.d.ts +24 -0
  110. package/dist/ownership.js +83 -0
  111. package/dist/placement-move-terminal.d.ts +67 -0
  112. package/dist/placement-move-terminal.js +88 -0
  113. package/dist/placement-move.d.ts +83 -0
  114. package/dist/placement-move.js +216 -0
  115. package/{src/placement-settled.ts → dist/placement-settled.d.ts} +19 -50
  116. package/dist/placement-settled.js +75 -0
  117. package/dist/quarantine.d.ts +95 -0
  118. package/dist/quarantine.js +131 -0
  119. package/dist/refusal.d.ts +31 -0
  120. package/dist/refusal.js +3 -0
  121. package/{src/restore-source-placement.ts → dist/restore-source-placement.d.ts} +29 -101
  122. package/dist/restore-source-placement.js +79 -0
  123. package/dist/search-text-backfill.d.ts +15 -0
  124. package/dist/search-text-backfill.js +33 -0
  125. package/dist/snippet.d.ts +61 -0
  126. package/dist/snippet.js +129 -0
  127. package/dist/spam-report.d.ts +86 -0
  128. package/dist/spam-report.js +241 -0
  129. package/dist/sync-warning.d.ts +21 -0
  130. package/dist/sync-warning.js +26 -0
  131. package/dist/sync-watermarks.d.ts +138 -0
  132. package/dist/sync-watermarks.js +205 -0
  133. package/dist/test-helpers/fake-imap.d.ts +151 -0
  134. package/dist/test-helpers/fake-imap.js +508 -0
  135. package/dist/test-helpers/folder-roles.d.ts +34 -0
  136. package/dist/test-helpers/folder-roles.js +47 -0
  137. package/dist/test-helpers/mime-tree-generator.d.ts +82 -0
  138. package/dist/test-helpers/mime-tree-generator.js +329 -0
  139. package/dist/test-helpers/test-store.d.ts +69 -0
  140. package/dist/test-helpers/test-store.js +193 -0
  141. package/dist/text/normalizer.d.ts +20 -0
  142. package/dist/text/normalizer.js +102 -0
  143. package/dist/thread-message-rows.d.ts +69 -0
  144. package/dist/thread-message-rows.js +40 -0
  145. package/dist/thread-root.d.ts +7 -0
  146. package/dist/thread-root.js +25 -0
  147. package/dist/types.d.ts +343 -0
  148. package/dist/types.js +19 -0
  149. package/package.json +86 -73
  150. package/src/account-credentials.ts +0 -210
  151. package/src/adversarial-mime.e2e.test.ts +0 -307
  152. package/src/attribute-mapper.test.ts +0 -104
  153. package/src/attribute-mapper.ts +0 -148
  154. package/src/body-parse.test.ts +0 -55
  155. package/src/body-part-mapper.property.test.ts +0 -354
  156. package/src/body-part-mapper.ts +0 -360
  157. package/src/body-sync-auto-read-writeonce.test.ts +0 -219
  158. package/src/body-sync-calendar-suggestion.test.ts +0 -433
  159. package/src/body-sync-category-denormalize.test.ts +0 -352
  160. package/src/body-sync-category-override.test.ts +0 -501
  161. package/src/body-sync-category-writeonce.test.ts +0 -234
  162. package/src/body-sync-filter.test.ts +0 -237
  163. package/src/body-sync-message-gone.test.ts +0 -139
  164. package/src/body-sync-placement-flags.test.ts +0 -347
  165. package/src/body-sync-placement-writeonce.test.ts +0 -337
  166. package/src/body-sync-quarantine.test.ts +0 -276
  167. package/src/body-sync-queue.test.ts +0 -124
  168. package/src/body-sync-queue.ts +0 -131
  169. package/src/body-sync-resync.test.ts +0 -605
  170. package/src/body-sync-terminal.test.ts +0 -310
  171. package/src/body-sync-terminal.ts +0 -176
  172. package/src/body-sync-unclassifiable-once.test.ts +0 -260
  173. package/src/body-sync-unsubscribed-auto-read.test.ts +0 -176
  174. package/src/body-sync.ts +0 -1970
  175. package/src/calendar-parts.ts +0 -41
  176. package/src/classification-backfill.test.ts +0 -615
  177. package/src/classification-backfill.ts +0 -363
  178. package/src/classify-message.test.ts +0 -205
  179. package/src/classify-message.ts +0 -116
  180. package/src/connection-factory.ts +0 -102
  181. package/src/connection-test.ts +0 -208
  182. package/src/credentials.test.ts +0 -181
  183. package/src/external-move.test.ts +0 -369
  184. package/src/external-move.ts +0 -272
  185. package/src/filters/anchor-drift.ts +0 -55
  186. package/src/filters/filter-config.test.ts +0 -229
  187. package/src/filters/filter-config.ts +0 -69
  188. package/src/filters/list-id.test.ts +0 -50
  189. package/src/filters/match.test.ts +0 -429
  190. package/src/filters/match.ts +0 -274
  191. package/src/filters/message-embedder.test.ts +0 -39
  192. package/src/filters/message-embedder.ts +0 -33
  193. package/src/filters/pipeline.test.ts +0 -330
  194. package/src/filters/pipeline.ts +0 -284
  195. package/src/flag-push-terminal.test.ts +0 -209
  196. package/src/flag-push.test.ts +0 -408
  197. package/src/flag-push.ts +0 -198
  198. package/src/flag-queue-stars.test.ts +0 -171
  199. package/src/flag-queue.ts +0 -509
  200. package/src/folder-placement.ts +0 -56
  201. package/src/heuristics/authenticationResults.test.ts +0 -189
  202. package/src/heuristics/authenticationResults.ts +0 -212
  203. package/src/heuristics/classifyByHeaders.test.ts +0 -889
  204. package/src/heuristics/classifyByHeaders.ts +0 -467
  205. package/src/heuristics/classifyPlacement.test.ts +0 -366
  206. package/src/heuristics/classifyPlacement.ts +0 -211
  207. package/src/heuristics/classifyRealMail.test.ts +0 -242
  208. package/src/heuristics/machineSenders.test.ts +0 -101
  209. package/src/heuristics/senderMismatch.test.ts +0 -346
  210. package/src/heuristics/senderMismatch.ts +0 -259
  211. package/src/imapflow-connect-retry.test.ts +0 -134
  212. package/src/imapflow-connection.e2e.test.ts +0 -335
  213. package/src/imapflow-connection.integ.test.ts +0 -316
  214. package/src/imapflow-connection.test.ts +0 -469
  215. package/src/imapflow-connection.ts +0 -1475
  216. package/src/index.ts +0 -350
  217. package/src/list-id-backfill.test.ts +0 -573
  218. package/src/list-id-backfill.ts +0 -274
  219. package/src/mailbox-cursor-rebuild.test.ts +0 -171
  220. package/src/mailbox-cursor-rebuild.ts +0 -146
  221. package/src/mailbox-cursor.test.ts +0 -249
  222. package/src/mailbox-delete-intent.test.ts +0 -246
  223. package/src/mailbox-management.test.ts +0 -356
  224. package/src/mailbox-management.ts +0 -839
  225. package/src/mailbox-presence.test.ts +0 -72
  226. package/src/mailbox-queue.ts +0 -415
  227. package/src/mailbox-rename-intent.test.ts +0 -389
  228. package/src/mailbox-rename-subtree.test.ts +0 -664
  229. package/src/mailbox-sync.test.ts +0 -848
  230. package/src/mailbox-sync.ts +0 -724
  231. package/src/message-delete-junk-reconcile.test.ts +0 -117
  232. package/src/message-delete-no-trash.test.ts +0 -245
  233. package/src/message-delete-unsettled-placement.test.ts +0 -396
  234. package/src/message-move-copy.test.ts +0 -562
  235. package/src/message-move-empty-trash.test.ts +0 -326
  236. package/src/message-move-same-mailbox.test.ts +0 -139
  237. package/src/message-move-unsettled-placement.test.ts +0 -378
  238. package/src/message-move.ts +0 -1348
  239. package/src/message-parser.ts +0 -26
  240. package/src/message-presence.test.ts +0 -50
  241. package/src/message-sync-address-spoof.test.ts +0 -293
  242. package/src/message-sync-category.test.ts +0 -142
  243. package/src/message-sync-changedsince.test.ts +0 -840
  244. package/src/message-sync-flags.test.ts +0 -158
  245. package/src/message-sync-junk-harvest.test.ts +0 -244
  246. package/src/message-sync-quarantine.test.ts +0 -416
  247. package/src/message-sync-repoint.test.ts +0 -419
  248. package/src/message-sync.ts +0 -1963
  249. package/src/mime-walker.test.ts +0 -311
  250. package/src/mime-walker.ts +0 -305
  251. package/src/outbox-attachment-content.test.ts +0 -153
  252. package/src/outbox-attachment-content.ts +0 -104
  253. package/src/outbox-attachment-filename.test.ts +0 -132
  254. package/src/outbox-attachment-filename.ts +0 -76
  255. package/src/outbox-attachment.test.ts +0 -738
  256. package/src/outbox-attachment.ts +0 -536
  257. package/src/outbox-failed-recovery.test.ts +0 -370
  258. package/src/outbox-queue-settled-row.test.ts +0 -140
  259. package/src/outbox-queue.test.ts +0 -291
  260. package/src/outbox-queue.ts +0 -508
  261. package/src/pass-through-unit-of-work.ts +0 -17
  262. package/src/placement-move-terminal.test.ts +0 -300
  263. package/src/placement-move-terminal.ts +0 -148
  264. package/src/placement-move-unsettled.test.ts +0 -275
  265. package/src/placement-move.ts +0 -336
  266. package/src/placement-settled.test.ts +0 -147
  267. package/src/placement-transition-race.test.ts +0 -482
  268. package/src/quarantine.test.ts +0 -191
  269. package/src/quarantine.ts +0 -228
  270. package/src/snippet.test.ts +0 -213
  271. package/src/snippet.ts +0 -152
  272. package/src/spam-report.test.ts +0 -801
  273. package/src/spam-report.ts +0 -357
  274. package/src/stale-message-reconcile.test.ts +0 -82
  275. package/src/stale-message-reconcile.ts +0 -55
  276. package/src/sync-watermarks.test.ts +0 -409
  277. package/src/sync-watermarks.ts +0 -319
  278. package/src/system-flag-wire-format.test.ts +0 -77
  279. package/src/test-helpers/folder-roles.ts +0 -66
  280. package/src/test-helpers/isolated-mailbox.ts +0 -44
  281. package/src/test-helpers/mailfuzz-connection.ts +0 -27
  282. package/src/test-helpers/mime-tree-generator.ts +0 -461
  283. package/src/text/normalizer.test.ts +0 -223
  284. package/src/text/normalizer.ts +0 -140
  285. package/src/thread-message-rows.ts +0 -77
  286. package/src/types/mailparser-augment.d.ts +0 -15
  287. package/src/types/natural-porter-stemmer.d.ts +0 -50
  288. package/src/types/stopword.d.ts +0 -69
  289. package/src/types.ts +0 -371
  290. package/test/fixtures/mime-adversarial-e2e/01-octet-stream-pdf.eml +0 -21
  291. package/test/fixtures/mime-adversarial-e2e/02-nested-related-cid-image.eml +0 -31
  292. package/test/fixtures/mime-adversarial-e2e/03-calendar-invite.eml +0 -33
  293. package/test/fixtures/mime-adversarial-e2e/04-forwarded-rfc822-with-attachments.eml +0 -42
  294. package/test/fixtures/mime-adversarial-e2e/05-tnef-winmail.eml +0 -25
  295. package/test/fixtures/mime-adversarial-e2e/06-8bit-non-ascii.eml +0 -15
  296. package/test/fixtures/mime-adversarial-e2e/07-quoted-printable-soft-breaks.eml +0 -17
  297. package/test/fixtures/mime-adversarial-e2e/08-alternative-empty-text.eml +0 -18
  298. package/tsconfig.json +0 -8
@@ -1,840 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { describe, it } from "node:test";
3
- import type {
4
- IAddressRepository,
5
- IEnvelopeRepository,
6
- IMailboxRepository,
7
- IMessageFlagPushRepository,
8
- IMessageFlagRepository,
9
- IMessageRepository,
10
- IThreadMessageRepository,
11
- IUnitOfWork,
12
- MailboxItem,
13
- ThreadMessageItem,
14
- UpdateMailboxInput,
15
- UpdateThreadMessageInput,
16
- } from "@remit/data-ports";
17
- import {
18
- MailboxCursorState,
19
- MessageKeywordFlag,
20
- MessageSystemFlag,
21
- } from "@remit/domain-enums";
22
- import type { ManagedConnectionFactory } from "./connection-factory.js";
23
- import type { FlagPushService } from "./flag-push.js";
24
- import { FlagQueueService } from "./flag-queue.js";
25
- import { MessageSyncService } from "./message-sync.js";
26
- import { noFolderRoles } from "./test-helpers/folder-roles.js";
27
- import type { IImapConnection, ImapMessage } from "./types.js";
28
-
29
- const ACCOUNT_ID = "acc-1";
30
- const ACCOUNT_CONFIG_ID = "cfg-1";
31
- const MAILBOX_ID = "mbx-1";
32
-
33
- const mailbox = (over: Partial<MailboxItem> = {}): MailboxItem =>
34
- ({
35
- mailboxId: MAILBOX_ID,
36
- accountId: ACCOUNT_ID,
37
- fullPath: "INBOX",
38
- uidValidity: 100,
39
- lastSyncUid: 1,
40
- highWaterMarkUid: 20,
41
- highestModseq: "500",
42
- cursorState: MailboxCursorState.normal,
43
- ...over,
44
- }) as MailboxItem;
45
-
46
- const serverMessage = (over: Partial<ImapMessage> = {}): ImapMessage => ({
47
- uid: 21,
48
- seq: 21,
49
- flags: [],
50
- internalDate: new Date("2026-01-01T00:00:00Z"),
51
- size: 42,
52
- modseq: "510",
53
- envelope: {
54
- date: "Thu, 01 Jan 2026 00:00:00 +0000",
55
- subject: "Hello",
56
- from: [{ mailbox: "sender", host: "example.com" }],
57
- sender: [],
58
- replyTo: [],
59
- to: [{ mailbox: "me", host: "example.com" }],
60
- cc: [],
61
- bcc: [],
62
- inReplyTo: "",
63
- messageId: "<a@example.com>",
64
- },
65
- ...over,
66
- });
67
-
68
- const storedRow = (over: Partial<ThreadMessageItem> = {}): ThreadMessageItem =>
69
- ({
70
- threadMessageId: "tm-1",
71
- messageId: "msg-1",
72
- accountConfigId: ACCOUNT_CONFIG_ID,
73
- mailboxId: MAILBOX_ID,
74
- threadId: "thr-1",
75
- uid: 21,
76
- sentDate: 1_767_225_600_000,
77
- internalDate: 1_767_225_600_000,
78
- isRead: false,
79
- isDeleted: false,
80
- hasStars: false,
81
- hasAttachment: false,
82
- ...over,
83
- }) as ThreadMessageItem;
84
-
85
- interface HarnessOptions {
86
- mailbox: MailboxItem;
87
- supportsCondstore: boolean;
88
- /** UIDVALIDITY the server serves, when it differs from the stored one. */
89
- servedUidValidity?: number;
90
- serverModseq?: string;
91
- changed?: ImapMessage[];
92
- allUids?: number[];
93
- enumerated?: ImapMessage[];
94
- /** Stored rows, keyed by the messageId the sync derives. */
95
- storedRows?: ThreadMessageItem[];
96
- /** Flag names with an outbound push still owed to IMAP. */
97
- pendingFlags?: Set<string>;
98
- /** Flag names already on the canonical MessageFlag record. */
99
- storedFlags?: string[];
100
- /** Envelope snapshots the cursor rebuild sees on the server. */
101
- snapshots?: Array<{ uid: number; messageId: string; internalDate: Date }>;
102
- /** UIDs whose save throws, to exercise the failure clamps. */
103
- failUids?: Set<number>;
104
- }
105
-
106
- interface Harness {
107
- service: MessageSyncService;
108
- mailboxUpdates: UpdateMailboxInput[];
109
- threadUpdates: Array<{
110
- threadMessageId: string;
111
- input: UpdateThreadMessageInput;
112
- }>;
113
- created: string[];
114
- calls: { search: number; fetchMessages: number; changedSince: bigint[] };
115
- /** Alarm-shaped ERROR logs the round emitted. */
116
- errors: Array<Record<string, unknown>>;
117
- /** The canonical flag record after the round. */
118
- flagStore: Set<string>;
119
- messageFlagService: IMessageFlagRepository;
120
- /** Method names invoked on the top-level address repository. */
121
- addressCalls: string[];
122
- }
123
-
124
- const buildHarness = (options: HarnessOptions): Harness => {
125
- const mailboxUpdates: UpdateMailboxInput[] = [];
126
- const threadUpdates: Array<{
127
- threadMessageId: string;
128
- input: UpdateThreadMessageInput;
129
- }> = [];
130
- const created: string[] = [];
131
- const calls = { search: 0, fetchMessages: 0, changedSince: [] as bigint[] };
132
- const errors: Array<Record<string, unknown>> = [];
133
- const logger = {
134
- info: () => {},
135
- warn: () => {},
136
- error: (obj: Record<string, unknown>) => {
137
- errors.push(obj);
138
- },
139
- };
140
- const rows = options.storedRows ?? [];
141
- const uidValidity = options.servedUidValidity ?? options.mailbox.uidValidity;
142
-
143
- const connection = {
144
- openBox: async () => ({ uidvalidity: uidValidity, uidnext: 99 }),
145
- getMailboxStatus: async () => ({
146
- messages: 10,
147
- recent: 0,
148
- unseen: 1,
149
- uidNext: 99,
150
- uidValidity,
151
- highestModseq: options.serverModseq ?? "600",
152
- deletedCount: 0,
153
- }),
154
- supportsCondstore: () => options.supportsCondstore,
155
- fetchMessagesChangedSince: async (since: bigint) => {
156
- calls.changedSince.push(since);
157
- return options.changed ?? [];
158
- },
159
- search: async () => {
160
- calls.search++;
161
- return options.allUids ?? [];
162
- },
163
- fetchMessages: async () => {
164
- calls.fetchMessages++;
165
- return options.enumerated ?? [];
166
- },
167
- fetchEnvelopeSnapshots: async () => options.snapshots ?? [],
168
- } as unknown as IImapConnection;
169
-
170
- const connectionFactory = {
171
- getConnection: () => connection,
172
- close: async () => {},
173
- } as ManagedConnectionFactory;
174
-
175
- const mailboxService = {
176
- get: async () => options.mailbox,
177
- update: async (_a: string, _m: string, input: UpdateMailboxInput) => {
178
- mailboxUpdates.push(input);
179
- return options.mailbox;
180
- },
181
- } as unknown as IMailboxRepository;
182
-
183
- // The sync derives its own messageId from the server envelope, so the fake
184
- // answers by UID — the one field a test can state up front.
185
- const threadMessageService = {
186
- findByMessageId: async () => rows[0] ?? null,
187
- findAllByMessageId: async () => rows,
188
- listByMailbox: async () => ({ items: rows, continuationToken: undefined }),
189
- update: async (
190
- _cfg: string,
191
- threadMessageId: string,
192
- input: UpdateThreadMessageInput,
193
- ) => {
194
- threadUpdates.push({ threadMessageId, input });
195
- return storedRow();
196
- },
197
- create: async (input: { messageId: string }) => {
198
- created.push(input.messageId);
199
- return storedRow();
200
- },
201
- } as unknown as IThreadMessageRepository;
202
-
203
- const unitOfWork: IUnitOfWork = {
204
- transaction: (fn) =>
205
- fn({
206
- message: {
207
- upsertWithStatus: async (input: {
208
- mailboxId: string;
209
- uid: number;
210
- }) => {
211
- if (options.failUids?.has(input.uid)) {
212
- throw new Error(`save failed for uid ${input.uid}`);
213
- }
214
- return { item: { mailboxId: input.mailboxId }, created: true };
215
- },
216
- updateUid: async () => undefined,
217
- } as unknown as IMessageRepository,
218
- envelope: {
219
- upsertEnvelope: async () => undefined,
220
- upsertBodyParts: async () => undefined,
221
- } as unknown as IEnvelopeRepository,
222
- address: {
223
- upsertCorrespondentAddress: async () => undefined,
224
- upsertEnvelopeAddress: async () => undefined,
225
- } as unknown as IAddressRepository,
226
- threadMessage: threadMessageService,
227
- }),
228
- };
229
-
230
- const flagPushMarkerService = {
231
- find: async (_messageId: string, flagName: string) =>
232
- options.pendingFlags?.has(flagName)
233
- ? { messageId: _messageId, flagName }
234
- : null,
235
- } as unknown as IMessageFlagPushRepository;
236
-
237
- // The canonical flag record, as a set of flag names held per message.
238
- const flagStore = new Set<string>(options.storedFlags ?? []);
239
- const messageFlagService = {
240
- hasFlag: async (_messageId: string, flagName: string) =>
241
- flagStore.has(flagName),
242
- addFlag: async (_messageId: string, flagName: string) => {
243
- flagStore.add(flagName);
244
- return { flagName };
245
- },
246
- removeFlag: async (_messageId: string, flagName: string) => {
247
- flagStore.delete(flagName);
248
- },
249
- } as unknown as IMessageFlagRepository;
250
-
251
- // The existing-message path (applyChange -> applyServerFlags) never
252
- // touches this repository. Recording every call, rather than leaving the
253
- // stub as `{}`, is what makes "$Junk stays message state" an assertion
254
- // instead of an assumption a future change could silently break.
255
- const addressCalls: string[] = [];
256
- const addressRepository = new Proxy(
257
- {},
258
- {
259
- get:
260
- (_target, prop: string) =>
261
- (..._args: unknown[]) => {
262
- addressCalls.push(prop);
263
- throw new Error(`unexpected IAddressRepository.${prop} call`);
264
- },
265
- },
266
- ) as IAddressRepository;
267
-
268
- // The rows sync reads back before it saves, to ask whether a message sighted
269
- // here has left the folder its row points at (#1146). This harness stores
270
- // none, so nothing is contested and the probe issues no IMAP.
271
- const messageRepository = {
272
- get: async () => [],
273
- } as unknown as IMessageRepository;
274
-
275
- const service = new MessageSyncService(
276
- connectionFactory,
277
- mailboxService,
278
- noFolderRoles,
279
- messageRepository,
280
- {} as IEnvelopeRepository,
281
- addressRepository,
282
- threadMessageService,
283
- logger,
284
- unitOfWork,
285
- flagPushMarkerService,
286
- messageFlagService,
287
- );
288
-
289
- return {
290
- service,
291
- mailboxUpdates,
292
- threadUpdates,
293
- created,
294
- calls,
295
- errors,
296
- flagStore,
297
- messageFlagService,
298
- addressCalls,
299
- };
300
- };
301
-
302
- const syncOnce = (harness: Harness, batchSize = 50) =>
303
- harness.service.syncMessages(
304
- MAILBOX_ID,
305
- ACCOUNT_ID,
306
- ACCOUNT_CONFIG_ID,
307
- batchSize,
308
- );
309
-
310
- describe("MessageSyncService CHANGEDSINCE path", () => {
311
- it("picks up a read-state change made on another client without enumerating the folder", async () => {
312
- const harness = buildHarness({
313
- mailbox: mailbox(),
314
- supportsCondstore: true,
315
- changed: [serverMessage({ flags: ["\\Seen"] })],
316
- storedRows: [storedRow({ isRead: false })],
317
- });
318
-
319
- await syncOnce(harness);
320
-
321
- assert.deepEqual(harness.calls.changedSince, [500n]);
322
- assert.equal(harness.calls.search, 0);
323
- assert.equal(harness.calls.fetchMessages, 0);
324
- assert.deepEqual(harness.threadUpdates, [
325
- { threadMessageId: "tm-1", input: { isRead: true } },
326
- ]);
327
- });
328
-
329
- it("picks up a star set on another client, colour following the boolean", async () => {
330
- const harness = buildHarness({
331
- mailbox: mailbox(),
332
- supportsCondstore: true,
333
- changed: [serverMessage({ flags: ["\\Flagged"] })],
334
- storedRows: [storedRow({ hasStars: false })],
335
- });
336
-
337
- await syncOnce(harness);
338
-
339
- // #58: `hasStars` is the boolean of record and `star` its colour; the
340
- // two may never disagree.
341
- assert.deepEqual(harness.threadUpdates, [
342
- {
343
- threadMessageId: "tm-1",
344
- input: { hasStars: true, star: "yellow" },
345
- },
346
- ]);
347
- });
348
-
349
- it("keeps a colour the user chose when the server re-flags", async () => {
350
- const harness = buildHarness({
351
- mailbox: mailbox(),
352
- supportsCondstore: true,
353
- changed: [serverMessage({ flags: ["\\Flagged"] })],
354
- storedRows: [storedRow({ hasStars: false, star: "purple" })],
355
- });
356
-
357
- await syncOnce(harness);
358
-
359
- assert.deepEqual(harness.threadUpdates, [
360
- { threadMessageId: "tm-1", input: { hasStars: true } },
361
- ]);
362
- });
363
-
364
- it("writes nothing when the server flags already match the stored row", async () => {
365
- const harness = buildHarness({
366
- mailbox: mailbox(),
367
- supportsCondstore: true,
368
- changed: [serverMessage({ flags: ["\\Seen"] })],
369
- storedRows: [storedRow({ isRead: true })],
370
- });
371
-
372
- await syncOnce(harness);
373
-
374
- assert.deepEqual(harness.threadUpdates, []);
375
- });
376
-
377
- it("leaves a field alone while its local flip is still owed to IMAP", async () => {
378
- const harness = buildHarness({
379
- mailbox: mailbox(),
380
- supportsCondstore: true,
381
- changed: [serverMessage({ flags: [] })],
382
- storedRows: [storedRow({ isRead: true })],
383
- pendingFlags: new Set([MessageSystemFlag.Seen]),
384
- });
385
-
386
- await syncOnce(harness);
387
-
388
- assert.deepEqual(harness.threadUpdates, []);
389
- });
390
-
391
- it("still discovers a new message and reports it for body sync", async () => {
392
- const harness = buildHarness({
393
- mailbox: mailbox(),
394
- supportsCondstore: true,
395
- changed: [serverMessage({ uid: 31, modseq: "515" })],
396
- storedRows: [],
397
- });
398
-
399
- const result = await syncOnce(harness);
400
-
401
- assert.equal(harness.calls.search, 0);
402
- assert.equal(result.syncedCount, 1);
403
- assert.deepEqual(
404
- result.syncedMessages.map((m) => m.uid),
405
- [31],
406
- );
407
- assert.equal(harness.created.length, 1);
408
- });
409
-
410
- it("advances the watermark to the server value once the round is applied", async () => {
411
- const harness = buildHarness({
412
- mailbox: mailbox(),
413
- supportsCondstore: true,
414
- serverModseq: "600",
415
- changed: [serverMessage({ uid: 31, modseq: "515" })],
416
- storedRows: [],
417
- });
418
-
419
- const result = await syncOnce(harness);
420
-
421
- assert.equal(result.hasMore, false);
422
- assert.equal(harness.mailboxUpdates[0].highestModseq, "600");
423
- assert.equal(harness.mailboxUpdates[0].highWaterMarkUid, 31);
424
- });
425
-
426
- it("advances only over the batch it applied and asks to be resumed", async () => {
427
- const harness = buildHarness({
428
- mailbox: mailbox(),
429
- supportsCondstore: true,
430
- serverModseq: "600",
431
- changed: [
432
- serverMessage({ uid: 31, modseq: "515" }),
433
- serverMessage({ uid: 32, modseq: "525" }),
434
- ],
435
- storedRows: [],
436
- });
437
-
438
- const result = await syncOnce(harness, 1);
439
-
440
- assert.equal(result.hasMore, true);
441
- assert.equal(result.remainingCount, 1);
442
- assert.equal(harness.mailboxUpdates[0].highestModseq, "515");
443
- });
444
-
445
- it("splits a bulk change across rounds without losing its tail", async () => {
446
- // One STORE marked 60 messages read, so all 60 carry mod-sequence 900.
447
- const bulk = Array.from({ length: 60 }, (_, i) =>
448
- serverMessage({ uid: 100 + i, modseq: "900" }),
449
- );
450
- const harness = buildHarness({
451
- mailbox: mailbox(),
452
- supportsCondstore: true,
453
- serverModseq: "900",
454
- changed: bulk,
455
- storedRows: [],
456
- });
457
-
458
- const first = await syncOnce(harness, 50);
459
-
460
- // The round is bounded, and the cursor records how far into the group it
461
- // got rather than claiming the whole of mod-sequence 900.
462
- assert.equal(harness.created.length, 50);
463
- assert.equal(first.hasMore, true);
464
- assert.equal(harness.mailboxUpdates[0].highestModseq, "900:149");
465
-
466
- // The next round asks from just below the recorded group, so the server
467
- // serves that group again and the applied half is skipped by identity.
468
- const resumed = buildHarness({
469
- mailbox: mailbox({ highestModseq: "900:149" }),
470
- supportsCondstore: true,
471
- serverModseq: "900",
472
- changed: bulk,
473
- storedRows: [],
474
- });
475
-
476
- const second = await syncOnce(resumed, 50);
477
-
478
- assert.deepEqual(resumed.calls.changedSince, [899n]);
479
- assert.equal(resumed.created.length, 10);
480
- assert.equal(second.hasMore, false);
481
- assert.equal(resumed.mailboxUpdates[0].highestModseq, "900");
482
- });
483
-
484
- it("applies every change when the group it was resuming has vanished", async () => {
485
- // The rest of group 900 was expunged, or modified again onto 950. The
486
- // resumed round must treat none of what it is served as already applied.
487
- const harness = buildHarness({
488
- mailbox: mailbox({ highestModseq: "900:149" }),
489
- supportsCondstore: true,
490
- serverModseq: "950",
491
- changed: [
492
- serverMessage({ uid: 10, modseq: "950" }),
493
- serverMessage({ uid: 20, modseq: "950" }),
494
- serverMessage({ uid: 600, modseq: "950" }),
495
- ],
496
- storedRows: [],
497
- });
498
-
499
- await syncOnce(harness);
500
-
501
- assert.equal(harness.created.length, 3);
502
- assert.equal(harness.mailboxUpdates[0].highestModseq, "950");
503
- });
504
-
505
- it("keeps the cursor below a group it only partly applied", async () => {
506
- const bulk = [
507
- serverMessage({ uid: 99, modseq: "800" }),
508
- ...Array.from({ length: 60 }, (_, i) =>
509
- serverMessage({ uid: 100 + i, modseq: "900" }),
510
- ),
511
- ];
512
- const harness = buildHarness({
513
- mailbox: mailbox(),
514
- supportsCondstore: true,
515
- serverModseq: "900",
516
- changed: bulk,
517
- storedRows: [],
518
- });
519
-
520
- const result = await syncOnce(harness, 50);
521
-
522
- assert.equal(result.hasMore, true);
523
- // Group 800 complete, 49 into group 900 — never a bare "900".
524
- assert.equal(harness.mailboxUpdates[0].highestModseq, "900:148");
525
- });
526
-
527
- it("reports a stalled cursor when a round moves nothing", async () => {
528
- const harness = buildHarness({
529
- mailbox: mailbox(),
530
- supportsCondstore: true,
531
- serverModseq: "900",
532
- changed: [serverMessage({ uid: 31, modseq: "515" })],
533
- storedRows: [],
534
- failUids: new Set([31]),
535
- });
536
-
537
- const result = await syncOnce(harness);
538
-
539
- assert.equal(result.cursorStalled, true);
540
- assert.equal(harness.mailboxUpdates[0].highestModseq, "500");
541
- assert.equal(
542
- harness.errors.filter((e) => e.alert === "message_sync_cursor_stalled")
543
- .length,
544
- 1,
545
- );
546
- });
547
-
548
- it("does not report a stall when the cursor moved", async () => {
549
- const harness = buildHarness({
550
- mailbox: mailbox(),
551
- supportsCondstore: true,
552
- changed: [serverMessage({ uid: 31, modseq: "515" })],
553
- storedRows: [],
554
- });
555
-
556
- const result = await syncOnce(harness);
557
-
558
- assert.equal(result.cursorStalled, false);
559
- assert.deepEqual(harness.errors, []);
560
- });
561
-
562
- it("writes the canonical flag record, not only the list projection", async () => {
563
- const harness = buildHarness({
564
- mailbox: mailbox(),
565
- supportsCondstore: true,
566
- changed: [serverMessage({ flags: ["\\Seen"] })],
567
- storedRows: [storedRow({ isRead: false })],
568
- storedFlags: [],
569
- });
570
-
571
- await syncOnce(harness);
572
-
573
- assert.deepEqual([...harness.flagStore], [MessageSystemFlag.Seen]);
574
- });
575
-
576
- it("clears the canonical flag and the star colour when the server unstars", async () => {
577
- const harness = buildHarness({
578
- mailbox: mailbox(),
579
- supportsCondstore: true,
580
- changed: [serverMessage({ flags: [] })],
581
- storedRows: [storedRow({ hasStars: true })],
582
- storedFlags: [MessageSystemFlag.Flagged],
583
- });
584
-
585
- await syncOnce(harness);
586
-
587
- assert.deepEqual([...harness.flagStore], []);
588
- assert.deepEqual(harness.threadUpdates, [
589
- {
590
- threadMessageId: "tm-1",
591
- input: { hasStars: false, star: "none" },
592
- },
593
- ]);
594
- });
595
-
596
- it("leaves the user's next flip a real flip, not a redundant no-op", async () => {
597
- // Another client marks the message read; the reader picks it up.
598
- const harness = buildHarness({
599
- mailbox: mailbox(),
600
- supportsCondstore: true,
601
- changed: [serverMessage({ flags: ["\\Seen"] })],
602
- storedRows: [storedRow({ isRead: false })],
603
- storedFlags: [],
604
- });
605
-
606
- await syncOnce(harness);
607
-
608
- // The user now clicks "mark unread". FlagQueueService decides whether
609
- // that is a real change by reading the canonical record.
610
- const pushes: Array<{ flagName: string; operation: string }> = [];
611
- const flagQueue = new FlagQueueService({
612
- messageFlagService: harness.messageFlagService,
613
- messageService: {
614
- get: async () => ({ mailboxId: MAILBOX_ID }),
615
- } as unknown as IMessageRepository,
616
- threadMessageService: {
617
- findAllByMessageId: async () => [storedRow({ isRead: true })],
618
- update: async () => storedRow(),
619
- } as unknown as IThreadMessageRepository,
620
- flagPushService: {
621
- flip: async (params: { flagName: string; operation: string }) => {
622
- pushes.push({
623
- flagName: params.flagName,
624
- operation: params.operation,
625
- });
626
- },
627
- } as unknown as FlagPushService,
628
- });
629
-
630
- await flagQueue.markAsUnread(ACCOUNT_CONFIG_ID, "msg-1", ACCOUNT_ID);
631
-
632
- assert.deepEqual(pushes, [
633
- { flagName: MessageSystemFlag.Seen, operation: "remove" },
634
- ]);
635
- assert.deepEqual([...harness.flagStore], []);
636
- });
637
- });
638
-
639
- // $Junk/$NotJunk mirroring (report-spam epic, message-state slice). This
640
- // mailbox is never remit-only — Apple Mail or another IMAP client is
641
- // routinely connected too — so a keyword another client set is message
642
- // state only. It must never become a standing rule about the sender.
643
- describe("MessageSyncService mirrors $Junk/$NotJunk keywords", () => {
644
- it("mirrors a server-side $Junk into the canonical flag record", async () => {
645
- const harness = buildHarness({
646
- mailbox: mailbox(),
647
- supportsCondstore: true,
648
- changed: [serverMessage({ flags: [MessageKeywordFlag.Junk] })],
649
- storedRows: [storedRow()],
650
- storedFlags: [],
651
- });
652
-
653
- await syncOnce(harness);
654
-
655
- assert.ok(harness.flagStore.has(MessageKeywordFlag.Junk));
656
- });
657
-
658
- it("mirrors a server-side $NotJunk into the canonical flag record", async () => {
659
- const harness = buildHarness({
660
- mailbox: mailbox(),
661
- supportsCondstore: true,
662
- changed: [serverMessage({ flags: [MessageKeywordFlag.NotJunk] })],
663
- storedRows: [storedRow()],
664
- storedFlags: [],
665
- });
666
-
667
- await syncOnce(harness);
668
-
669
- assert.ok(harness.flagStore.has(MessageKeywordFlag.NotJunk));
670
- });
671
-
672
- it("leaves $Junk alone while its local flip is still owed to IMAP", async () => {
673
- const harness = buildHarness({
674
- mailbox: mailbox(),
675
- supportsCondstore: true,
676
- // Server has not seen the local flip yet.
677
- changed: [serverMessage({ flags: [] })],
678
- storedRows: [storedRow()],
679
- storedFlags: [MessageKeywordFlag.Junk],
680
- pendingFlags: new Set([MessageKeywordFlag.Junk]),
681
- });
682
-
683
- await syncOnce(harness);
684
-
685
- assert.ok(harness.flagStore.has(MessageKeywordFlag.Junk));
686
- });
687
-
688
- it("mirroring $Junk leaves the sender's Address record completely untouched", async () => {
689
- const harness = buildHarness({
690
- mailbox: mailbox(),
691
- supportsCondstore: true,
692
- changed: [serverMessage({ flags: [MessageKeywordFlag.Junk] })],
693
- storedRows: [storedRow()],
694
- storedFlags: [],
695
- });
696
-
697
- await syncOnce(harness);
698
-
699
- assert.ok(harness.flagStore.has(MessageKeywordFlag.Junk));
700
- assert.deepEqual(harness.addressCalls, []);
701
- });
702
- });
703
-
704
- describe("MessageSyncService without CONDSTORE", () => {
705
- it("falls back to full enumeration and never issues CHANGEDSINCE", async () => {
706
- const harness = buildHarness({
707
- mailbox: mailbox(),
708
- supportsCondstore: false,
709
- allUids: [],
710
- serverModseq: "0",
711
- });
712
-
713
- await syncOnce(harness);
714
-
715
- assert.deepEqual(harness.calls.changedSince, []);
716
- assert.equal(harness.calls.search, 1);
717
- assert.equal(harness.mailboxUpdates[0].highestModseq, "0");
718
- });
719
-
720
- it("enumerates while no mod-sequence watermark has been seeded yet", async () => {
721
- const harness = buildHarness({
722
- mailbox: mailbox({ highestModseq: "0" }),
723
- supportsCondstore: true,
724
- allUids: [],
725
- });
726
-
727
- await syncOnce(harness);
728
-
729
- assert.deepEqual(harness.calls.changedSince, []);
730
- assert.equal(harness.calls.search, 1);
731
- });
732
-
733
- it("seeds the mod-sequence watermark once enumeration finds nothing left", async () => {
734
- const harness = buildHarness({
735
- mailbox: mailbox({ highestModseq: "0" }),
736
- supportsCondstore: true,
737
- allUids: [],
738
- serverModseq: "700",
739
- });
740
-
741
- await syncOnce(harness);
742
-
743
- assert.equal(harness.mailboxUpdates[0].highestModseq, "700");
744
- });
745
-
746
- it("withholds the seed while enumeration still has work left", async () => {
747
- const harness = buildHarness({
748
- mailbox: mailbox({ highestModseq: "0" }),
749
- supportsCondstore: true,
750
- allUids: [21, 22],
751
- enumerated: [serverMessage({ uid: 21 }), serverMessage({ uid: 22 })],
752
- serverModseq: "700",
753
- storedRows: [],
754
- });
755
-
756
- await syncOnce(harness, 1);
757
-
758
- assert.equal(harness.mailboxUpdates[0].highestModseq, undefined);
759
- });
760
- });
761
-
762
- describe("MessageSyncService UIDVALIDITY reseed", () => {
763
- it("trips the cursor instead of trusting the stored mod-sequence", async () => {
764
- const harness = buildHarness({
765
- mailbox: mailbox({ uidValidity: 100 }),
766
- servedUidValidity: 777,
767
- supportsCondstore: true,
768
- changed: [serverMessage()],
769
- });
770
-
771
- const result = await syncOnce(harness);
772
-
773
- assert.deepEqual(result.syncedMessages, []);
774
- assert.deepEqual(harness.calls.changedSince, []);
775
- assert.deepEqual(harness.mailboxUpdates, [
776
- { cursorState: MailboxCursorState.cursor_invalid },
777
- ]);
778
- });
779
-
780
- it("withholds the seed when the rebuild lost a message to a failed save", async () => {
781
- const harness = buildHarness({
782
- mailbox: mailbox({
783
- cursorState: MailboxCursorState.cursor_invalid,
784
- highestModseq: "500",
785
- }),
786
- servedUidValidity: 777,
787
- supportsCondstore: true,
788
- serverModseq: "12",
789
- allUids: [30, 31],
790
- snapshots: [
791
- { uid: 30, messageId: "<a@example.com>", internalDate: new Date(0) },
792
- { uid: 31, messageId: "<b@example.com>", internalDate: new Date(0) },
793
- ],
794
- enumerated: [serverMessage({ uid: 30 }), serverMessage({ uid: 31 })],
795
- failUids: new Set([31]),
796
- storedRows: [],
797
- });
798
-
799
- await syncOnce(harness);
800
-
801
- const final = harness.mailboxUpdates[1];
802
- // Neither the stale value (meaningless on the new axis) nor the new one
803
- // (already above the message that failed).
804
- assert.equal(final.highestModseq, "0");
805
- // The forward watermark stops below the failure, so uid 31 is enumerated
806
- // again next round.
807
- assert.equal(final.highWaterMarkUid, 30);
808
- });
809
-
810
- it("reseeds the watermark from the new axis when the rebuild completes", async () => {
811
- const harness = buildHarness({
812
- mailbox: mailbox({
813
- cursorState: MailboxCursorState.cursor_invalid,
814
- highestModseq: "500",
815
- }),
816
- servedUidValidity: 777,
817
- supportsCondstore: true,
818
- serverModseq: "12",
819
- storedRows: [],
820
- });
821
-
822
- await syncOnce(harness);
823
-
824
- assert.deepEqual(harness.calls.changedSince, []);
825
- assert.deepEqual(harness.mailboxUpdates, [
826
- { cursorState: MailboxCursorState.rebuilding },
827
- {
828
- cursorState: MailboxCursorState.normal,
829
- uidValidity: 777,
830
- highWaterMarkUid: 0,
831
- lastSyncUid: 0,
832
- highestModseq: "12",
833
- lastMessageSyncAt: harness.mailboxUpdates[1]?.lastMessageSyncAt,
834
- messageCount: 10,
835
- unseenCount: 1,
836
- deletedCount: 0,
837
- },
838
- ]);
839
- });
840
- });