@remit/mailbox-service 0.0.110 → 0.0.112

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 (123) hide show
  1. package/package.json +21 -6
  2. package/src/body-part-mapper.ts +51 -1
  3. package/src/body-sync-queue.test.ts +39 -99
  4. package/src/body-sync-terminal.test.ts +115 -277
  5. package/src/body-sync-terminal.ts +10 -12
  6. package/src/body-sync.test.ts +925 -0
  7. package/src/body-sync.ts +226 -200
  8. package/src/classification-backfill.test.ts +197 -527
  9. package/src/classify-message.test.ts +11 -74
  10. package/src/external-move.test.ts +102 -209
  11. package/src/external-move.ts +13 -0
  12. package/src/filter-operations.test.ts +319 -0
  13. package/src/filter-operations.ts +377 -0
  14. package/src/filter-update.test.ts +313 -0
  15. package/src/filters/filter-config.test.ts +55 -197
  16. package/src/filters/filter-config.ts +4 -39
  17. package/src/filters/match.test.ts +1 -38
  18. package/src/filters/match.ts +3 -52
  19. package/src/filters/pipeline.test.ts +158 -289
  20. package/src/filters/pipeline.ts +56 -170
  21. package/src/flag-push-terminal.ts +9 -11
  22. package/src/flag-push.test.ts +76 -372
  23. package/src/flag-queue.test.ts +197 -0
  24. package/src/folder-operations.test.ts +273 -0
  25. package/src/folder-operations.ts +217 -0
  26. package/src/forwarded-message.test.ts +122 -0
  27. package/src/imapflow-connect-retry.test.ts +76 -113
  28. package/src/imapflow-connection.test.ts +34 -0
  29. package/src/imapflow-connection.ts +19 -15
  30. package/src/index.ts +59 -12
  31. package/src/list-id-backfill.test.ts +133 -499
  32. package/src/mailbox-cursor.test.ts +81 -189
  33. package/src/mailbox-delete-intent.test.ts +78 -205
  34. package/src/mailbox-intent.ts +5 -3
  35. package/src/mailbox-management.test.ts +107 -220
  36. package/src/mailbox-management.ts +1 -2
  37. package/src/mailbox-presence.test.ts +36 -34
  38. package/src/mailbox-queue.ts +2 -20
  39. package/src/mailbox-rename-intent.test.ts +156 -325
  40. package/src/mailbox-rename-subtree.test.ts +205 -582
  41. package/src/mailbox-sync.test.ts +182 -744
  42. package/src/message-move.test.ts +726 -0
  43. package/src/message-move.ts +227 -57
  44. package/src/message-read.ts +87 -0
  45. package/src/message-save-retry.test.ts +90 -0
  46. package/src/message-sync-flags.test.ts +10 -1
  47. package/src/message-sync.test.ts +762 -0
  48. package/src/message-sync.ts +409 -239
  49. package/src/mime-walker.ts +16 -15
  50. package/src/organize-job.ts +93 -0
  51. package/src/outbox-attachment.test.ts +97 -209
  52. package/src/outbox-failed-recovery.test.ts +198 -269
  53. package/src/outbox-queue.test.ts +24 -0
  54. package/src/outbox-queue.ts +25 -8
  55. package/src/ownership.ts +140 -0
  56. package/src/placement-move-terminal.ts +9 -12
  57. package/src/placement-move.ts +33 -6
  58. package/src/placement-settled.ts +28 -4
  59. package/src/placement.test.ts +404 -0
  60. package/src/quarantine.test.ts +90 -148
  61. package/src/quarantine.ts +3 -2
  62. package/src/refusal.ts +29 -0
  63. package/src/restore-source-placement.ts +0 -4
  64. package/src/spam-report.test.ts +238 -710
  65. package/src/sync-warning.test.ts +213 -0
  66. package/src/sync-warning.ts +56 -0
  67. package/src/test-helpers/fake-imap.ts +690 -0
  68. package/src/test-helpers/test-queue.ts +56 -0
  69. package/src/test-helpers/test-store.ts +299 -0
  70. package/src/thread-root.test.ts +29 -0
  71. package/src/thread-root.ts +51 -0
  72. package/src/types/mailparser-augment.d.ts +8 -0
  73. package/src/types.ts +1 -1
  74. package/src/adversarial-mime.e2e.test.ts +0 -307
  75. package/src/body-sync-auto-read-writeonce.test.ts +0 -219
  76. package/src/body-sync-calendar-suggestion.test.ts +0 -433
  77. package/src/body-sync-category-denormalize.test.ts +0 -352
  78. package/src/body-sync-category-override.test.ts +0 -501
  79. package/src/body-sync-category-writeonce.test.ts +0 -234
  80. package/src/body-sync-filter.test.ts +0 -237
  81. package/src/body-sync-placement-flags.test.ts +0 -347
  82. package/src/body-sync-placement-writeonce.test.ts +0 -337
  83. package/src/body-sync-quarantine.test.ts +0 -276
  84. package/src/body-sync-resync.test.ts +0 -605
  85. package/src/body-sync-unclassifiable-once.test.ts +0 -260
  86. package/src/body-sync-unsubscribed-auto-read.test.ts +0 -176
  87. package/src/filters/anchor-drift.ts +0 -55
  88. package/src/filters/message-embedder.test.ts +0 -39
  89. package/src/filters/message-embedder.ts +0 -33
  90. package/src/flag-push-terminal.test.ts +0 -209
  91. package/src/flag-queue-stars.test.ts +0 -171
  92. package/src/imapflow-connection.e2e.test.ts +0 -335
  93. package/src/imapflow-connection.integ.test.ts +0 -316
  94. package/src/message-delete-junk-reconcile.test.ts +0 -117
  95. package/src/message-delete-no-trash.test.ts +0 -245
  96. package/src/message-delete-unsettled-placement.test.ts +0 -396
  97. package/src/message-move-copy.test.ts +0 -562
  98. package/src/message-move-empty-trash.test.ts +0 -326
  99. package/src/message-move-same-mailbox.test.ts +0 -139
  100. package/src/message-move-unsettled-placement.test.ts +0 -378
  101. package/src/message-sync-address-spoof.test.ts +0 -293
  102. package/src/message-sync-category.test.ts +0 -142
  103. package/src/message-sync-changedsince.test.ts +0 -840
  104. package/src/message-sync-junk-harvest.test.ts +0 -244
  105. package/src/message-sync-quarantine.test.ts +0 -416
  106. package/src/message-sync-repoint.test.ts +0 -419
  107. package/src/outbox-queue-settled-row.test.ts +0 -140
  108. package/src/pass-through-unit-of-work.ts +0 -17
  109. package/src/placement-move-terminal.test.ts +0 -300
  110. package/src/placement-move-unsettled.test.ts +0 -275
  111. package/src/placement-transition-race.test.ts +0 -482
  112. package/src/stale-message-reconcile.test.ts +0 -82
  113. package/src/stale-message-reconcile.ts +0 -55
  114. package/src/test-helpers/isolated-mailbox.ts +0 -44
  115. package/src/test-helpers/mailfuzz-connection.ts +0 -27
  116. package/test/fixtures/mime-adversarial-e2e/01-octet-stream-pdf.eml +0 -21
  117. package/test/fixtures/mime-adversarial-e2e/02-nested-related-cid-image.eml +0 -31
  118. package/test/fixtures/mime-adversarial-e2e/03-calendar-invite.eml +0 -33
  119. package/test/fixtures/mime-adversarial-e2e/04-forwarded-rfc822-with-attachments.eml +0 -42
  120. package/test/fixtures/mime-adversarial-e2e/05-tnef-winmail.eml +0 -25
  121. package/test/fixtures/mime-adversarial-e2e/06-8bit-non-ascii.eml +0 -15
  122. package/test/fixtures/mime-adversarial-e2e/07-quoted-printable-soft-breaks.eml +0 -17
  123. package/test/fixtures/mime-adversarial-e2e/08-alternative-empty-text.eml +0 -18
@@ -1,117 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { describe, it } from "node:test";
3
- import type {
4
- IAddressRepository,
5
- IMailboxRepository,
6
- IMailboxSpecialUseRepository,
7
- IMessageRepository,
8
- IThreadMessageRepository,
9
- } from "@remit/data-ports";
10
- import { type MessageMoveConfig, MessageMoveService } from "./message-move.js";
11
- import { NO_JUNK_ROLES, trashRole } from "./test-helpers/folder-roles.js";
12
-
13
- const ACCOUNT = "acc-1";
14
- const ACCOUNT_CONFIG = "cfg-1";
15
- const INBOX = "mbx-inbox";
16
- const TRASH = "mbx-trash";
17
- const MESSAGE_ID = "msg-1";
18
-
19
- const buildWorld = (trashExists: boolean) => {
20
- const reconciled: string[] = [];
21
- const message = {
22
- messageId: MESSAGE_ID,
23
- mailboxId: INBOX,
24
- uid: 337,
25
- status: "active",
26
- syncStatus: "synced",
27
- };
28
-
29
- const messageService = {
30
- get: async () => [message],
31
- update: async (_id: string, patch: Record<string, unknown>) =>
32
- Object.assign(message, patch),
33
- transitionPlacement: async (
34
- _id: string,
35
- _expected: Record<string, unknown>,
36
- patch: Record<string, unknown>,
37
- ) => Object.assign(message, patch),
38
- } as unknown as IMessageRepository;
39
-
40
- const threadMessageService = {
41
- findAllByMessageId: async () => [],
42
- getByMessageId: async () => ({
43
- accountConfigId: ACCOUNT_CONFIG,
44
- threadMessageId: "tm-1",
45
- messageId: MESSAGE_ID,
46
- mailboxId: INBOX,
47
- }),
48
- update: async () => {},
49
- delete: async () => {},
50
- } as unknown as IThreadMessageRepository;
51
-
52
- const mailboxService = {
53
- get: async () => [
54
- { mailboxId: INBOX, fullPath: "INBOX", accountId: ACCOUNT },
55
- ],
56
- } as unknown as IMailboxRepository;
57
-
58
- const trash = trashExists ? { mailboxId: TRASH, fullPath: "Trash" } : null;
59
-
60
- const mailboxSpecialUseService = {
61
- findTrashMailbox: async () => trash,
62
- resolveTrashRole: async () => trashRole(trash),
63
- resolveJunkRolesForConfig: async () => NO_JUNK_ROLES,
64
- } as unknown as IMailboxSpecialUseRepository;
65
-
66
- const addressService = {
67
- reconcileJunkOnlyForMessage: async (messageId: string) => {
68
- reconciled.push(`${messageId}@${message.mailboxId}`);
69
- },
70
- } as unknown as IAddressRepository;
71
-
72
- const config: MessageMoveConfig = {
73
- messageService,
74
- mailboxService,
75
- mailboxSpecialUseService,
76
- threadMessageService,
77
- addressService,
78
- sqsQueueUrl: "http://localhost:9324/000000000000/remit-messages.fifo",
79
- };
80
-
81
- const service = new MessageMoveService(config);
82
- (
83
- service as unknown as { enqueueEventsBatch: () => Promise<void> }
84
- ).enqueueEventsBatch = async () => {};
85
-
86
- return { service, reconciled };
87
- };
88
-
89
- describe("deleting a message re-asks what its senders stand on", () => {
90
- it("re-asks when the delete moves the message to Trash", async () => {
91
- const { service, reconciled } = buildWorld(true);
92
-
93
- await service.deleteMessages(ACCOUNT_CONFIG, [MESSAGE_ID], ACCOUNT);
94
-
95
- assert.deepEqual(reconciled, [`${MESSAGE_ID}@${TRASH}`]);
96
- });
97
-
98
- it("asks nothing when the delete is permanent", async () => {
99
- const { service, reconciled } = buildWorld(true);
100
-
101
- await service.deleteMessages(ACCOUNT_CONFIG, [MESSAGE_ID], ACCOUNT, {
102
- permanent: true,
103
- });
104
-
105
- assert.deepEqual(reconciled, []);
106
- });
107
-
108
- it("asks nothing when the account has no Trash folder, because the delete is refused", async () => {
109
- const { service, reconciled } = buildWorld(false);
110
-
111
- await assert.rejects(() =>
112
- service.deleteMessages(ACCOUNT_CONFIG, [MESSAGE_ID], ACCOUNT),
113
- );
114
-
115
- assert.deepEqual(reconciled, []);
116
- });
117
- });
@@ -1,245 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { describe, it } from "node:test";
3
- import type {
4
- IAddressRepository,
5
- IMailboxRepository,
6
- IMailboxSpecialUseRepository,
7
- IMessageRepository,
8
- IThreadMessageRepository,
9
- } from "@remit/data-ports";
10
- import type { RoleResolution } from "@remit/data-ports/folder-role";
11
- import {
12
- type MessageMoveConfig,
13
- MessageMoveService,
14
- NoTrashMailboxError,
15
- StaleTrashAppointmentError,
16
- UnconfirmedTrashMailboxError,
17
- } from "./message-move.js";
18
- import { NO_JUNK_ROLES } from "./test-helpers/folder-roles.js";
19
-
20
- const ACCOUNT = "acc-1";
21
- const ACCOUNT_CONFIG = "cfg-1";
22
- const INBOX = "mbx-inbox";
23
- const TRASH = "mbx-trash";
24
- const MESSAGE_ID = "msg-1";
25
-
26
- type TrashMailbox = { mailboxId: string; fullPath: string };
27
-
28
- interface EnqueuedEvent {
29
- operation: string;
30
- destinationMailboxPath?: string;
31
- }
32
-
33
- const buildWorld = (
34
- trashResolution: RoleResolution<TrashMailbox>,
35
- startingMailboxId = INBOX,
36
- ) => {
37
- const patches: Array<Record<string, unknown>> = [];
38
- const events: EnqueuedEvent[] = [];
39
- const threadMessageDeletes: string[] = [];
40
-
41
- const message = {
42
- messageId: MESSAGE_ID,
43
- mailboxId: startingMailboxId,
44
- uid: 337,
45
- status: "active",
46
- syncStatus: "synced",
47
- };
48
-
49
- const messageService = {
50
- get: async () => [message],
51
- update: async (_id: string, patch: Record<string, unknown>) => {
52
- patches.push(patch);
53
- return Object.assign(message, patch);
54
- },
55
- transitionPlacement: async (
56
- _id: string,
57
- _expected: Record<string, unknown>,
58
- patch: Record<string, unknown>,
59
- ) => {
60
- patches.push(patch);
61
- return Object.assign(message, patch);
62
- },
63
- } as unknown as IMessageRepository;
64
-
65
- const threadMessageService = {
66
- findAllByMessageId: async () => [],
67
- getByMessageId: async () => ({
68
- accountConfigId: ACCOUNT_CONFIG,
69
- threadMessageId: "tm-1",
70
- messageId: MESSAGE_ID,
71
- mailboxId: startingMailboxId,
72
- }),
73
- update: async () => {},
74
- delete: async (_cfg: string, id: string) => {
75
- threadMessageDeletes.push(id);
76
- },
77
- } as unknown as IThreadMessageRepository;
78
-
79
- const mailboxService = {
80
- get: async () => [
81
- { mailboxId: INBOX, fullPath: "INBOX", accountId: ACCOUNT },
82
- { mailboxId: TRASH, fullPath: "INBOX/Trash", accountId: ACCOUNT },
83
- ],
84
- } as unknown as IMailboxRepository;
85
-
86
- const mailboxSpecialUseService = {
87
- resolveTrashRole: async () => trashResolution,
88
- resolveJunkRolesForConfig: async () => NO_JUNK_ROLES,
89
- } as unknown as IMailboxSpecialUseRepository;
90
-
91
- const addressService = {
92
- reconcileJunkOnlyForMessage: async () => {},
93
- } as unknown as IAddressRepository;
94
-
95
- const config: MessageMoveConfig = {
96
- messageService,
97
- mailboxService,
98
- mailboxSpecialUseService,
99
- threadMessageService,
100
- addressService,
101
- sqsQueueUrl: "http://localhost:9324/000000000000/remit-messages.fifo",
102
- };
103
-
104
- const service = new MessageMoveService(config);
105
- (
106
- service as unknown as {
107
- enqueueEventsBatch: (batch: EnqueuedEvent[]) => Promise<void>;
108
- }
109
- ).enqueueEventsBatch = async (batch) => {
110
- events.push(...batch);
111
- };
112
-
113
- return { service, patches, events, message };
114
- };
115
-
116
- const flaggedTrash: RoleResolution<TrashMailbox> = {
117
- kind: "flagged",
118
- mailbox: { mailboxId: TRASH, fullPath: "INBOX/Trash" },
119
- };
120
-
121
- describe("delete only expunges when it was asked to", () => {
122
- it("moves to a Trash the account resolves under a nested path", async () => {
123
- const { service, events, message } = buildWorld(flaggedTrash);
124
-
125
- await service.deleteMessages(ACCOUNT_CONFIG, [MESSAGE_ID], ACCOUNT);
126
-
127
- assert.deepEqual(
128
- events.map((event) => event.operation),
129
- ["move_to_trash"],
130
- );
131
- assert.equal(events[0].destinationMailboxPath, "INBOX/Trash");
132
- assert.equal(message.mailboxId, TRASH);
133
- });
134
-
135
- it("refuses, and touches nothing, when no Trash resolves", async () => {
136
- const { service, patches, events, message } = buildWorld({ kind: "none" });
137
-
138
- await assert.rejects(
139
- () => service.deleteMessages(ACCOUNT_CONFIG, [MESSAGE_ID], ACCOUNT),
140
- (error: unknown) =>
141
- error instanceof NoTrashMailboxError &&
142
- error.statusCode === 409 &&
143
- error.publicApiError?.details?.reason === "none" &&
144
- error.message.includes("no Trash folder"),
145
- );
146
-
147
- assert.deepEqual(events, []);
148
- assert.deepEqual(patches, []);
149
- assert.equal(message.mailboxId, INBOX);
150
- assert.equal(message.status, "active");
151
- });
152
-
153
- it("refuses a stale appointment rather than filing into the fallback", async () => {
154
- // #887 Done item 2: the user appointed a folder another client has since
155
- // deleted. Silently filing 200 messages into the flagged folder overrides
156
- // a choice they made and cannot see was lost.
157
- const { service, patches, events, message } = buildWorld({
158
- kind: "appointment_stale",
159
- appointedMailboxId: "mbx-appointed-and-gone",
160
- fallback: flaggedTrash,
161
- });
162
-
163
- await assert.rejects(
164
- () => service.deleteMessages(ACCOUNT_CONFIG, [MESSAGE_ID], ACCOUNT),
165
- (error: unknown) =>
166
- error instanceof StaleTrashAppointmentError &&
167
- error.statusCode === 409 &&
168
- error.publicApiError?.details?.reason === "stale" &&
169
- error.publicApiError?.details?.accountId === ACCOUNT,
170
- );
171
-
172
- assert.deepEqual(events, []);
173
- assert.deepEqual(patches, []);
174
- assert.equal(message.mailboxId, INBOX);
175
- assert.equal(message.status, "active");
176
- });
177
-
178
- it("files into a Trash that resolves by name alone", async () => {
179
- // A name guess is enough to move mail somewhere retrievable; only the
180
- // expunge demands more (D4).
181
- const { service, events, message } = buildWorld({
182
- kind: "proposed",
183
- mailbox: { mailboxId: TRASH, fullPath: "Trash" },
184
- });
185
-
186
- await service.deleteMessages(ACCOUNT_CONFIG, [MESSAGE_ID], ACCOUNT);
187
-
188
- assert.deepEqual(
189
- events.map((event) => event.operation),
190
- ["move_to_trash"],
191
- );
192
- assert.equal(message.mailboxId, TRASH);
193
- });
194
-
195
- it("expunges only when the caller asked for a permanent delete", async () => {
196
- // fc685509: an explicit permanent delete never resolves Trash at all, so
197
- // an account with none can still empty a message it selected.
198
- const { service, events } = buildWorld({ kind: "none" });
199
-
200
- await service.deleteMessages(ACCOUNT_CONFIG, [MESSAGE_ID], ACCOUNT, {
201
- permanent: true,
202
- });
203
-
204
- assert.deepEqual(
205
- events.map((event) => event.operation),
206
- ["permanent_delete"],
207
- );
208
- });
209
-
210
- it("refuses to expunge a message already inside a Trash that only resolves by name", async () => {
211
- // #876: a message already sitting in the guessed folder used to fall
212
- // straight through to an expunge, on the same name guess Empty Trash
213
- // refuses to act on. Deleting it is exactly as unrecoverable, so it
214
- // demands the same confirmed evidence — nobody, neither the user nor the
215
- // server, ever said this folder is Trash.
216
- const { service, patches, events, message } = buildWorld(
217
- { kind: "proposed", mailbox: { mailboxId: TRASH, fullPath: "Trash" } },
218
- TRASH,
219
- );
220
-
221
- await assert.rejects(
222
- () => service.deleteMessages(ACCOUNT_CONFIG, [MESSAGE_ID], ACCOUNT),
223
- (error: unknown) =>
224
- error instanceof UnconfirmedTrashMailboxError &&
225
- error.statusCode === 409 &&
226
- error.publicApiError?.details?.reason === "unconfirmed" &&
227
- error.publicApiError?.details?.accountId === ACCOUNT,
228
- );
229
-
230
- assert.deepEqual(events, []);
231
- assert.deepEqual(patches, []);
232
- assert.equal(message.mailboxId, TRASH);
233
- });
234
-
235
- it("expunges a message already inside a confirmed Trash", async () => {
236
- const { service, events } = buildWorld(flaggedTrash, TRASH);
237
-
238
- await service.deleteMessages(ACCOUNT_CONFIG, [MESSAGE_ID], ACCOUNT);
239
-
240
- assert.deepEqual(
241
- events.map((event) => event.operation),
242
- ["permanent_delete"],
243
- );
244
- });
245
- });