@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
@@ -263,13 +263,13 @@ const toRecord = (
263
263
  * safe default rather than throwing — one malformed part must never drop the
264
264
  * whole message.
265
265
  *
266
- * **Part-path uniqueness**: some IMAP servers (and the `message/rfc822`
267
- * inner-body convention) return child nodes with an empty `part` field.
268
- * Assigning ROOT_PART_PATH to every such node would produce duplicate keys
269
- * and make `upsertBodyParts` write the same body part twice. Non-root nodes
270
- * without a `part` therefore receive a
271
- * synthetic path `<parentPath>.<siblingIndex>` that is stable across
272
- * repeated syncs of the same message.
266
+ * **Part-path uniqueness**: imapflow gives the root of a message/rfc822
267
+ * node's encapsulated message the same `part` as the node itself, and some
268
+ * servers give it none. That root is stored as `<messagePath>.0` — the
269
+ * encapsulated message's own ROOT_PART_PATH — so it can neither overwrite the
270
+ * message/rfc822 row nor collide with its children (`<messagePath>.1`, ...).
271
+ * Any other non-root node without a `part` receives a synthetic path
272
+ * `<parentPath>.<siblingIndex>` that is stable across repeated syncs.
273
273
  */
274
274
  export const walkMimeStructure = (root: MimeNode): BodyPartRecord[] => {
275
275
  const out: BodyPartRecord[] = [];
@@ -277,29 +277,30 @@ export const walkMimeStructure = (root: MimeNode): BodyPartRecord[] => {
277
277
  const visit = (
278
278
  node: MimeNode,
279
279
  parentPath: string | null,
280
+ parentIsMessage: boolean,
280
281
  siblingIndex: number,
281
282
  ) => {
282
283
  let partPath: string;
283
- if (node.part && node.part.length > 0) {
284
+ if (parentIsMessage && parentPath !== null) {
285
+ partPath = `${parentPath}.${ROOT_PART_PATH}`;
286
+ } else if (node.part && node.part.length > 0) {
284
287
  partPath = node.part;
285
288
  } else if (parentPath === null) {
286
289
  // True root of the BODYSTRUCTURE tree.
287
290
  partPath = ROOT_PART_PATH;
288
291
  } else {
289
- // Non-root node without an IMAP part path — synthesise one so
290
- // the body-part keys remain unique. This happens most commonly
291
- // for the body of a message/rfc822 attachment, whose inner
292
- // structure imapflow attaches as a childNode with part="".
293
292
  partPath = `${parentPath}.${siblingIndex}`;
294
293
  }
295
- out.push(toRecord(node, partPath, parentPath));
294
+ const record = toRecord(node, partPath, parentPath);
295
+ out.push(record);
296
+ const isMessage = record.mediaType === MediaType.Message;
296
297
  const children = node.childNodes ?? [];
297
298
  for (let i = 0; i < children.length; i++) {
298
299
  // biome-ignore lint/style/noNonNullAssertion: value is guaranteed by caller contract
299
- visit(children[i]!, partPath, i + 1);
300
+ visit(children[i]!, partPath, isMessage, i + 1);
300
301
  }
301
302
  };
302
303
 
303
- visit(root, null, 0);
304
+ visit(root, null, false, 0);
304
305
  return out;
305
306
  };
@@ -0,0 +1,93 @@
1
+ import { SendMessageCommand, type SQSClient } from "@aws-sdk/client-sqs";
2
+ import type {
3
+ CreateOrganizeJobRequestInput,
4
+ IOrganizeJobRequestRepository,
5
+ OrganizeJobRequestItem,
6
+ } from "@remit/data-ports";
7
+ import { FilterMatchOperator } from "@remit/domain-enums";
8
+ import { createQueueProducer } from "@remit/sqs-client/producer";
9
+ import { NO_ACTION } from "./filters/match.js";
10
+
11
+ const ORGANIZE_JOB_TTL_SECONDS = 7 * 24 * 60 * 60;
12
+
13
+ export interface OrganizeJobOwner {
14
+ accountConfigId: string;
15
+ userId: string;
16
+ }
17
+
18
+ export type OrganizeJobPredicate = Pick<
19
+ CreateOrganizeJobRequestInput,
20
+ "matchOperator" | "literalClauses" | "actionLabelId" | "actionMailboxId"
21
+ >;
22
+
23
+ export type OrganizeJobRequest = OrganizeJobOwner &
24
+ ({ predicate: OrganizeJobPredicate } | { filterId: string });
25
+
26
+ export interface OrganizeJobLogger {
27
+ info(obj: Record<string, unknown>, msg: string): void;
28
+ }
29
+
30
+ export interface OrganizeJobDeps {
31
+ organizeJobRequest: Pick<IOrganizeJobRequestRepository, "create">;
32
+ sqsQueueUrl: string;
33
+ logger: OrganizeJobLogger;
34
+ }
35
+
36
+ const producers = new Map<string, SQSClient>();
37
+
38
+ const producerFor = (queueUrl: string): SQSClient => {
39
+ const existing = producers.get(queueUrl);
40
+ if (existing) return existing;
41
+ const producer = createQueueProducer({ queueUrl });
42
+ producers.set(queueUrl, producer);
43
+ return producer;
44
+ };
45
+
46
+ const jobColumns = (
47
+ request: OrganizeJobRequest,
48
+ ): Omit<CreateOrganizeJobRequestInput, keyof OrganizeJobOwner | "ttl"> => {
49
+ if ("filterId" in request) {
50
+ return {
51
+ filterId: request.filterId,
52
+ matchOperator: FilterMatchOperator.And,
53
+ literalClauses: [],
54
+ actionLabelId: NO_ACTION,
55
+ actionMailboxId: NO_ACTION,
56
+ };
57
+ }
58
+ return { filterId: NO_ACTION, ...request.predicate };
59
+ };
60
+
61
+ // A filter job names only the filter; the worker reads it when it runs.
62
+ export const queueOrganizeJob = async (
63
+ deps: OrganizeJobDeps,
64
+ request: OrganizeJobRequest,
65
+ ): Promise<OrganizeJobRequestItem> => {
66
+ const job = await deps.organizeJobRequest.create({
67
+ accountConfigId: request.accountConfigId,
68
+ userId: request.userId,
69
+ ...jobColumns(request),
70
+ ttl: Math.floor(Date.now() / 1000) + ORGANIZE_JOB_TTL_SECONDS,
71
+ });
72
+
73
+ await producerFor(deps.sqsQueueUrl).send(
74
+ new SendMessageCommand({
75
+ QueueUrl: deps.sqsQueueUrl,
76
+ MessageBody: JSON.stringify({
77
+ type: "OrganizeJob",
78
+ accountConfigId: job.accountConfigId,
79
+ organizeJobId: job.organizeJobId,
80
+ }),
81
+ }),
82
+ );
83
+
84
+ deps.logger.info(
85
+ {
86
+ accountConfigId: job.accountConfigId,
87
+ organizeJobId: job.organizeJobId,
88
+ filterId: job.filterId,
89
+ },
90
+ "Organize back-apply job initiated",
91
+ );
92
+ return job;
93
+ };