@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,378 +0,0 @@
1
- /**
2
- * Issue #665. While a placement move is in flight the Message row names the
3
- * destination folder but still carries the SOURCE folder's uid, so a user move
4
- * that binds the pair as it stands sends the worker to `openBox(destination)`
5
- * and `moveMessages([uid])` — the destination folder's own message at that uid,
6
- * carried off to wherever the user pointed. The reachable press is Undo on the
7
- * auto-moved badge, inside the seconds before the placement push confirms.
8
- */
9
-
10
- import assert from "node:assert/strict";
11
- import { describe, it } from "node:test";
12
- import type {
13
- IAddressRepository,
14
- IMailboxRepository,
15
- IMailboxSpecialUseRepository,
16
- IMessageRepository,
17
- IThreadMessageRepository,
18
- } from "@remit/data-ports";
19
- import { MessagePlacementUnsettledError } from "@remit/data-ports/errors";
20
- import { MessageMoveService } from "./message-move.js";
21
- import { NO_JUNK_ROLES } from "./test-helpers/folder-roles.js";
22
-
23
- const ACCOUNT = "acc-1";
24
- const ACCOUNT_CONFIG = "cfg-1";
25
- const INBOX = "mbx-inbox";
26
- const ARCHIVE = "mbx-archive";
27
-
28
- const MOVING_ID = "msg-moving";
29
- const SETTLED_ID = "msg-settled";
30
- const STRANDED_ID = "msg-stranded";
31
- const DELETING_ID = "msg-deleting";
32
- const COPY_ID = "msg-copy";
33
-
34
- const INBOX_UID = 42;
35
- const ARCHIVE_UID = 907;
36
- const SETTLED_UID = 11;
37
-
38
- interface CapturedEvent {
39
- type: string;
40
- messageId?: string;
41
- uid?: number;
42
- sourceMailboxPath?: string;
43
- destinationMailboxPath?: string;
44
- }
45
-
46
- const mailboxes = [
47
- { mailboxId: INBOX, fullPath: "INBOX", accountId: ACCOUNT },
48
- { mailboxId: ARCHIVE, fullPath: "Archive", accountId: ACCOUNT },
49
- ];
50
-
51
- /** A row mid-move: Archive is already written, the uid is still INBOX's. */
52
- const movingRow = () => ({
53
- messageId: MOVING_ID,
54
- mailboxId: ARCHIVE,
55
- uid: INBOX_UID,
56
- status: "moving",
57
- syncStatus: "pending",
58
- originalMailboxId: INBOX,
59
- originalUid: INBOX_UID,
60
- });
61
-
62
- /**
63
- * A row a mutation gave up on. R3 defines the give-up as a hand-back: the row
64
- * goes back on the placement the mail server confirmed, and only then carries
65
- * `abandoned`. The pair is honest, so nothing downstream refuses it — the
66
- * failure is a statement the reading pane makes, never a gate.
67
- */
68
- const strandedRow = () => ({
69
- messageId: STRANDED_ID,
70
- mailboxId: INBOX,
71
- uid: INBOX_UID,
72
- status: "active",
73
- syncStatus: "abandoned",
74
- abandonedMutation: "move",
75
- originalMailboxId: INBOX,
76
- });
77
-
78
- /**
79
- * The same shape a delete leaves: Trash is already written, the uid is still
80
- * INBOX's, and `deleting` is what says so. R3 calls this in flight exactly as
81
- * `moving` is — reading only `moving` let a move enqueued behind a delete bind
82
- * this pair and be dropped when the delete settled.
83
- */
84
- const deletingRow = () => ({
85
- messageId: DELETING_ID,
86
- mailboxId: ARCHIVE,
87
- uid: INBOX_UID,
88
- status: "deleting",
89
- syncStatus: "pending",
90
- originalMailboxId: INBOX,
91
- originalUid: INBOX_UID,
92
- });
93
-
94
- /** An ordinary settled row. */
95
- const settledRow = () => ({
96
- messageId: SETTLED_ID,
97
- mailboxId: INBOX,
98
- uid: SETTLED_UID,
99
- status: "active",
100
- syncStatus: "synced",
101
- });
102
-
103
- /**
104
- * A freshly copied row: `moving` until COPYUID lands, with no server-side uid
105
- * at all and no move behind it. Its uid is not ready; it does not name anyone.
106
- */
107
- const freshCopyRow = () => ({
108
- messageId: COPY_ID,
109
- mailboxId: ARCHIVE,
110
- uid: 0,
111
- status: "moving",
112
- syncStatus: "pending",
113
- });
114
-
115
- const buildWorld = (seed: Array<Record<string, unknown>>) => {
116
- const patches: Array<{ messageId: string; patch: Record<string, unknown> }> =
117
- [];
118
- const events: CapturedEvent[] = [];
119
- const rows = new Map<string, Record<string, unknown>>(
120
- seed.map((row): [string, Record<string, unknown>] => [
121
- String(row.messageId),
122
- row,
123
- ]),
124
- );
125
-
126
- /** What `updateUid` writes when the placement move confirms. */
127
- const settle = (messageId: string) => {
128
- Object.assign(rows.get(messageId) ?? {}, {
129
- uid: ARCHIVE_UID,
130
- status: "active",
131
- syncStatus: "synced",
132
- });
133
- };
134
-
135
- const messageService = {
136
- get: async (ids: string | string[]) =>
137
- Array.isArray(ids)
138
- ? ids.flatMap((id) => {
139
- const row = rows.get(id);
140
- return row ? [row] : [];
141
- })
142
- : rows.get(ids),
143
- update: async (id: string, patch: Record<string, unknown>) => {
144
- patches.push({ messageId: id, patch });
145
- return Object.assign(rows.get(id) ?? {}, patch);
146
- },
147
- transitionPlacement: async (
148
- id: string,
149
- _expected: Record<string, unknown>,
150
- patch: Record<string, unknown>,
151
- ) => {
152
- patches.push({ messageId: id, patch });
153
- return Object.assign(rows.get(id) ?? {}, patch);
154
- },
155
- } as unknown as IMessageRepository;
156
-
157
- const threadMessageService = {
158
- getByMessageId: async (_cfg: string, messageId: string) => ({
159
- accountConfigId: ACCOUNT_CONFIG,
160
- threadMessageId: `tm-${messageId}`,
161
- messageId,
162
- threadId: "thr-1",
163
- mailboxId: rows.get(messageId)?.mailboxId,
164
- sentDate: 1,
165
- isRead: false,
166
- isDeleted: false,
167
- hasStars: false,
168
- hasAttachment: false,
169
- }),
170
- update: async () => {},
171
- } as unknown as IThreadMessageRepository;
172
-
173
- const mailboxService = {
174
- get: async (_accountId: string, ids: string | string[]) =>
175
- Array.isArray(ids)
176
- ? mailboxes.filter((mailbox) => ids.includes(mailbox.mailboxId))
177
- : mailboxes.find((mailbox) => mailbox.mailboxId === ids),
178
- } as unknown as IMailboxRepository;
179
-
180
- const mailboxSpecialUseService = {
181
- findTrashMailbox: async () => null,
182
- resolveJunkRolesForConfig: async () => NO_JUNK_ROLES,
183
- } as unknown as IMailboxSpecialUseRepository;
184
-
185
- const addressService = {
186
- reconcileJunkOnlyForMessage: async () => {},
187
- } as unknown as IAddressRepository;
188
-
189
- const service = new MessageMoveService({
190
- messageService,
191
- mailboxService,
192
- mailboxSpecialUseService,
193
- threadMessageService,
194
- addressService,
195
- sqsQueueUrl: "http://localhost:9324/000000000000/remit-messages.fifo",
196
- moveSettleTimeoutMs: 200,
197
- moveSettlePollMs: 10,
198
- });
199
-
200
- (
201
- service as unknown as { enqueueEvent: (e: CapturedEvent) => Promise<void> }
202
- ).enqueueEvent = async (event) => {
203
- events.push(event);
204
- };
205
-
206
- return { service, patches, events, rows, settle };
207
- };
208
-
209
- describe("a move never binds the folder/uid pair of an in-flight move (#665)", () => {
210
- it("binds the confirmed pair once the placement move settles", async () => {
211
- const { service, events, settle } = buildWorld([movingRow()]);
212
-
213
- setTimeout(() => settle(MOVING_ID), 30);
214
- await service.moveMessage(ACCOUNT_CONFIG, MOVING_ID, INBOX, ACCOUNT);
215
-
216
- assert.equal(events.length, 1);
217
- assert.equal(events[0].type, "MESSAGE_MOVE");
218
- assert.equal(
219
- events[0].sourceMailboxPath,
220
- "Archive",
221
- "the folder the confirmed move left the message in",
222
- );
223
- assert.equal(
224
- events[0].uid,
225
- ARCHIVE_UID,
226
- "and that folder's own uid, never the source folder's",
227
- );
228
- assert.equal(events[0].destinationMailboxPath, "INBOX");
229
- });
230
-
231
- it("refuses the move, and touches nothing, when the placement never settles", async () => {
232
- const { service, patches, events, rows } = buildWorld([movingRow()]);
233
-
234
- await assert.rejects(
235
- () => service.moveMessage(ACCOUNT_CONFIG, MOVING_ID, INBOX, ACCOUNT),
236
- (error: unknown) =>
237
- error instanceof MessagePlacementUnsettledError &&
238
- error.statusCode === 409 &&
239
- error.publicApiError?.code === "message_placement_unsettled" &&
240
- error.publicApiError?.details?.messageId === MOVING_ID &&
241
- error.publicApiError?.details?.accountId === ACCOUNT &&
242
- error.publicApiError?.details?.reason === "in_flight",
243
- );
244
-
245
- assert.deepEqual(events, [], "nothing was enqueued");
246
- assert.deepEqual(patches, [], "and no local write was made");
247
- assert.equal(rows.get(MOVING_ID)?.mailboxId, ARCHIVE);
248
- assert.equal(rows.get(MOVING_ID)?.uid, INBOX_UID);
249
- });
250
-
251
- it("never answers the same-mailbox skip from a folder the message has not reached", async () => {
252
- // The skip compares the row's `mailboxId` against the destination. An
253
- // in-flight move has already written that folder optimistically, so the
254
- // comparison would report the message home and return a silent success
255
- // for a move the server has not made.
256
- const { service, events } = buildWorld([movingRow()]);
257
-
258
- await assert.rejects(
259
- () => service.moveMessage(ACCOUNT_CONFIG, MOVING_ID, ARCHIVE, ACCOUNT),
260
- MessagePlacementUnsettledError,
261
- );
262
-
263
- assert.deepEqual(events, []);
264
- });
265
-
266
- it("moves no part of a batch when one row is in flight", async () => {
267
- // All-or-nothing: the settled row is movable on its own and a per-row
268
- // gate would have moved it before reaching the refusal.
269
- const { service, patches, events } = buildWorld([
270
- settledRow(),
271
- movingRow(),
272
- ]);
273
-
274
- await assert.rejects(
275
- () =>
276
- service.moveMessages(
277
- ACCOUNT_CONFIG,
278
- [SETTLED_ID, MOVING_ID],
279
- ARCHIVE,
280
- ACCOUNT,
281
- ),
282
- MessagePlacementUnsettledError,
283
- );
284
-
285
- assert.deepEqual(
286
- events,
287
- [],
288
- "the settled row was not moved alongside the refusal",
289
- );
290
- assert.deepEqual(patches, []);
291
- });
292
-
293
- it("moves the whole batch once the in-flight row settles", async () => {
294
- const { service, events, settle } = buildWorld([settledRow(), movingRow()]);
295
-
296
- setTimeout(() => settle(MOVING_ID), 30);
297
- await service.moveMessages(
298
- ACCOUNT_CONFIG,
299
- [SETTLED_ID, MOVING_ID],
300
- INBOX,
301
- ACCOUNT,
302
- );
303
-
304
- assert.deepEqual(
305
- events.map((event) => [event.messageId, event.uid]),
306
- [[MOVING_ID, ARCHIVE_UID]],
307
- "the settled row was already in INBOX and skipped; the moved row went with the confirmed uid",
308
- );
309
- });
310
-
311
- it("waits one row's ceiling for a batch, not one per row", async () => {
312
- const rows = [movingRow(), movingRow(), movingRow()].map((row, index) => ({
313
- ...row,
314
- messageId: `${MOVING_ID}-${index}`,
315
- }));
316
- const { service } = buildWorld(rows);
317
-
318
- const startedAt = Date.now();
319
- await assert.rejects(
320
- () =>
321
- service.moveMessages(
322
- ACCOUNT_CONFIG,
323
- rows.map((row) => row.messageId),
324
- INBOX,
325
- ACCOUNT,
326
- ),
327
- MessagePlacementUnsettledError,
328
- );
329
-
330
- assert.ok(
331
- Date.now() - startedAt < 500,
332
- "three rows refused within one 200ms ceiling, not three",
333
- );
334
- });
335
- });
336
-
337
- describe("the move gate refuses only a uid that names somebody else (#665)", () => {
338
- it("moves a freshly copied row rather than waiting on it", async () => {
339
- // A copy is `moving` with no server-side uid yet. That uid is not ready,
340
- // which is not the same as naming another folder's message.
341
- const { service, events } = buildWorld([freshCopyRow()]);
342
-
343
- const startedAt = Date.now();
344
- await service.moveMessage(ACCOUNT_CONFIG, COPY_ID, INBOX, ACCOUNT);
345
-
346
- assert.equal(events.length, 1);
347
- assert.equal(events[0].type, "MESSAGE_MOVE");
348
- assert.ok(Date.now() - startedAt < 100, "and never entered the wait");
349
- });
350
-
351
- it("waits out a delete in flight rather than binding its borrowed uid", async () => {
352
- const { service, events, patches } = buildWorld([deletingRow()]);
353
-
354
- await assert.rejects(
355
- () => service.moveMessage(ACCOUNT_CONFIG, DELETING_ID, INBOX, ACCOUNT),
356
- (error: unknown) => error instanceof MessagePlacementUnsettledError,
357
- );
358
-
359
- assert.deepEqual(events, [], "nothing was enqueued");
360
- assert.deepEqual(patches, [], "and no local write was made");
361
- });
362
-
363
- it("acts on a row a move gave up on, rather than refusing it", async () => {
364
- // A give-up hands the row back to the placement the server confirmed
365
- // before it stops (R3), so the folder and uid name the same message and
366
- // the user's next move is an ordinary one. Refusing it — which is what a
367
- // give-up left `moving` used to earn — made the message unmovable for
368
- // good, with nothing routine to repair it.
369
- const { service, events } = buildWorld([strandedRow()]);
370
-
371
- const startedAt = Date.now();
372
- await service.moveMessage(ACCOUNT_CONFIG, STRANDED_ID, ARCHIVE, ACCOUNT);
373
-
374
- assert.equal(events.length, 1);
375
- assert.equal(events[0].type, "MESSAGE_MOVE");
376
- assert.ok(Date.now() - startedAt < 100, "and never entered the wait");
377
- });
378
- });
@@ -1,293 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { describe, it } from "node:test";
3
- import type {
4
- CreateAddressInput,
5
- CreateEnvelopeAddressInput,
6
- CreateThreadMessageInput,
7
- IAddressRepository,
8
- IEnvelopeRepository,
9
- IMailboxRepository,
10
- IMessageRepository,
11
- IThreadMessageRepository,
12
- MailboxItem,
13
- MessageItem,
14
- ThreadMessageItem,
15
- } from "@remit/data-ports";
16
- import type { ManagedConnectionFactory } from "./connection-factory.js";
17
- import { type AccountFolderRoles, MessageSyncService } from "./message-sync.js";
18
- import { NO_FOLDER_ROLES, noFolderRoles } from "./test-helpers/folder-roles.js";
19
- import type { ImapAddress, ImapEnvelope, ImapMessage } from "./types.js";
20
-
21
- const stub = <T>(): T => ({}) as T;
22
-
23
- const emptyEnvelope: ImapEnvelope = {
24
- date: new Date(0).toISOString(),
25
- messageId: "<root@example.com>",
26
- subject: "Subject",
27
- from: [],
28
- sender: [],
29
- replyTo: [],
30
- to: [],
31
- cc: [],
32
- bcc: [],
33
- inReplyTo: "",
34
- };
35
-
36
- interface Saved {
37
- addresses: CreateAddressInput[];
38
- envelopeAddresses: CreateEnvelopeAddressInput[];
39
- threadMessages: CreateThreadMessageInput[];
40
- }
41
-
42
- const harvest = async (envelope: Partial<ImapEnvelope>): Promise<Saved> => {
43
- const saved: Saved = {
44
- addresses: [],
45
- envelopeAddresses: [],
46
- threadMessages: [],
47
- };
48
-
49
- const messageService = {
50
- upsertWithStatus: async (input: unknown) => ({
51
- item: input as MessageItem,
52
- created: true,
53
- }),
54
- } as unknown as IMessageRepository;
55
-
56
- const threadMessageService = {
57
- create: async (input: CreateThreadMessageInput) => {
58
- saved.threadMessages.push(input);
59
- return input as unknown as ThreadMessageItem;
60
- },
61
- } as unknown as IThreadMessageRepository;
62
-
63
- const envelopeService = {
64
- upsertEnvelope: async () => {},
65
- upsertBodyParts: async () => {},
66
- } as unknown as IEnvelopeRepository;
67
-
68
- const addressService = {
69
- upsertCorrespondentAddress: async (input: CreateAddressInput) => {
70
- saved.addresses.push(input);
71
- },
72
- upsertEnvelopeAddress: async (input: CreateEnvelopeAddressInput) => {
73
- saved.envelopeAddresses.push(input);
74
- },
75
- } as unknown as IAddressRepository;
76
-
77
- const service = new MessageSyncService(
78
- stub<ManagedConnectionFactory>(),
79
- stub<IMailboxRepository>(),
80
- noFolderRoles,
81
- messageService,
82
- envelopeService,
83
- addressService,
84
- threadMessageService,
85
- );
86
-
87
- const msg = {
88
- uid: 42,
89
- seq: 1,
90
- size: 100,
91
- internalDate: new Date(0),
92
- flags: [],
93
- envelope: { ...emptyEnvelope, ...envelope },
94
- } as unknown as ImapMessage;
95
-
96
- await (
97
- service as unknown as {
98
- saveMessage: (
99
- mailbox: MailboxItem,
100
- accountId: string,
101
- accountConfigId: string,
102
- msg: ImapMessage,
103
- roles: AccountFolderRoles,
104
- ) => Promise<unknown>;
105
- }
106
- ).saveMessage(
107
- { mailboxId: "mbx-1", fullPath: "INBOX" } as MailboxItem,
108
- "acct-1",
109
- "cfg-1",
110
- msg,
111
- NO_FOLDER_ROLES,
112
- );
113
-
114
- return saved;
115
- };
116
-
117
- const spoof: ImapAddress = {
118
- name: "matthijs@ischen.nl",
119
- mailbox: "aramirez",
120
- host: "secresaludguaviare.gov.co",
121
- };
122
-
123
- const onlyAddress = async (
124
- envelope: Partial<ImapEnvelope>,
125
- ): Promise<CreateAddressInput> => {
126
- const { addresses } = await harvest(envelope);
127
- assert.equal(addresses.length, 1);
128
- const [input] = addresses;
129
- assert.ok(input);
130
- return input;
131
- };
132
-
133
- describe("harvesting an envelope display name", () => {
134
- it("drops a name that is an address the row does not own", async () => {
135
- const input = await onlyAddress({ from: [spoof] });
136
-
137
- assert.equal(input.displayName, "");
138
- assert.equal(input.normalizedEmail, "aramirez@secresaludguaviare.gov.co");
139
- assert.equal(
140
- input.normalizedCompound,
141
- "aramirez@secresaludguaviare.gov.co",
142
- );
143
- });
144
-
145
- it("keeps what a name says besides the address it claims", async () => {
146
- const input = await onlyAddress({
147
- from: [{ ...spoof, name: "Matthijs <matthijs@ischen.nl>" }],
148
- });
149
-
150
- assert.equal(input.displayName, "Matthijs");
151
- assert.equal(
152
- input.normalizedCompound,
153
- "matthijs aramirez@secresaludguaviare.gov.co",
154
- );
155
- });
156
-
157
- it("keeps a name a sender wrote its own support address into", async () => {
158
- const input = await onlyAddress({
159
- from: [
160
- {
161
- name: "Support (support@acme.com)",
162
- mailbox: "noreply",
163
- host: "acme.com",
164
- },
165
- ],
166
- });
167
-
168
- assert.equal(input.displayName, "Support");
169
- });
170
-
171
- it("keeps a name that is the address it labels", async () => {
172
- const input = await onlyAddress({
173
- from: [
174
- {
175
- name: "ING@ing-nl-mailing.nl",
176
- mailbox: "ing",
177
- host: "ing-nl-mailing.nl",
178
- },
179
- ],
180
- });
181
-
182
- assert.equal(input.displayName, "ING@ing-nl-mailing.nl");
183
- assert.equal(
184
- input.normalizedCompound,
185
- "ing@ing-nl-mailing.nl ing@ing-nl-mailing.nl",
186
- );
187
- });
188
-
189
- it("keeps a non-ASCII name that is the address it labels", async () => {
190
- const input = await onlyAddress({
191
- from: [
192
- { name: "Özcan@example.com", mailbox: "Özcan", host: "example.com" },
193
- ],
194
- });
195
-
196
- assert.equal(input.displayName, "Özcan@example.com");
197
- });
198
-
199
- it("keeps an ordinary human name", async () => {
200
- const input = await onlyAddress({
201
- from: [
202
- { name: "Matthijs van Henten", mailbox: "matthijs", host: "ischen.nl" },
203
- ],
204
- });
205
-
206
- assert.equal(input.displayName, "Matthijs van Henten");
207
- assert.equal(
208
- input.normalizedCompound,
209
- "matthijs van henten matthijs@ischen.nl",
210
- );
211
- });
212
-
213
- it("drops the name on every harvested envelope role", async () => {
214
- const { addresses } = await harvest({
215
- from: [spoof],
216
- sender: [spoof],
217
- replyTo: [spoof],
218
- to: [spoof],
219
- cc: [spoof],
220
- bcc: [spoof],
221
- });
222
-
223
- assert.equal(addresses.length, 6);
224
- for (const input of addresses) {
225
- assert.equal(input.displayName, "");
226
- }
227
- });
228
-
229
- it("still harvests the address from every envelope role", async () => {
230
- const named = (mailbox: string): ImapAddress[] => [
231
- { name: "Someone", mailbox, host: "example.com" },
232
- ];
233
- const { addresses } = await harvest({
234
- from: named("from"),
235
- sender: named("sender"),
236
- replyTo: named("reply-to"),
237
- to: named("to"),
238
- cc: named("cc"),
239
- bcc: named("bcc"),
240
- });
241
-
242
- assert.deepEqual(
243
- addresses.map((input) => input.normalizedEmail),
244
- [
245
- "from@example.com",
246
- "sender@example.com",
247
- "reply-to@example.com",
248
- "to@example.com",
249
- "cc@example.com",
250
- "bcc@example.com",
251
- ],
252
- );
253
- for (const input of addresses) {
254
- assert.equal(input.displayName, "Someone");
255
- }
256
- });
257
-
258
- it("drops the name on the envelope the message header renders", async () => {
259
- const { envelopeAddresses } = await harvest({ from: [spoof] });
260
-
261
- assert.equal(envelopeAddresses.length, 1);
262
- assert.equal(envelopeAddresses[0]?.displayName, "");
263
- });
264
-
265
- it("keeps the sender label's text and drops only the address", async () => {
266
- const { threadMessages } = await harvest({
267
- from: [{ ...spoof, name: "Support <matthijs@ischen.nl>" }],
268
- });
269
-
270
- assert.equal(threadMessages[0]?.fromName, "Support");
271
- });
272
-
273
- it("drops the sender label the message list shows", async () => {
274
- const { threadMessages } = await harvest({ from: [spoof] });
275
-
276
- assert.equal(threadMessages.length, 1);
277
- assert.equal(threadMessages[0]?.fromName, undefined);
278
- assert.equal(
279
- threadMessages[0]?.fromEmail,
280
- "aramirez@secresaludguaviare.gov.co",
281
- );
282
- });
283
-
284
- it("keeps the sender label when it is an ordinary name", async () => {
285
- const { threadMessages } = await harvest({
286
- from: [
287
- { name: "Alejandro Ramirez", mailbox: "aramirez", host: "example.gov" },
288
- ],
289
- });
290
-
291
- assert.equal(threadMessages[0]?.fromName, "Alejandro Ramirez");
292
- });
293
- });