@remit/mailbox-service 0.0.111 → 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.
- package/package.json +21 -6
- package/src/body-part-mapper.ts +51 -1
- package/src/body-sync-queue.test.ts +39 -99
- package/src/body-sync-terminal.test.ts +115 -277
- package/src/body-sync-terminal.ts +10 -12
- package/src/body-sync.test.ts +925 -0
- package/src/body-sync.ts +156 -235
- package/src/classification-backfill.test.ts +197 -527
- package/src/classify-message.test.ts +11 -74
- package/src/external-move.test.ts +102 -209
- package/src/external-move.ts +13 -0
- package/src/filter-operations.test.ts +319 -0
- package/src/filter-operations.ts +377 -0
- package/src/filter-update.test.ts +313 -0
- package/src/filters/filter-config.test.ts +55 -197
- package/src/filters/filter-config.ts +4 -39
- package/src/filters/match.test.ts +1 -38
- package/src/filters/match.ts +3 -52
- package/src/filters/pipeline.test.ts +158 -289
- package/src/filters/pipeline.ts +56 -170
- package/src/flag-push-terminal.ts +9 -11
- package/src/flag-push.test.ts +76 -372
- package/src/flag-queue.test.ts +197 -0
- package/src/folder-operations.test.ts +273 -0
- package/src/folder-operations.ts +217 -0
- package/src/forwarded-message.test.ts +122 -0
- package/src/imapflow-connect-retry.test.ts +76 -113
- package/src/imapflow-connection.test.ts +34 -0
- package/src/imapflow-connection.ts +5 -4
- package/src/index.ts +59 -12
- package/src/list-id-backfill.test.ts +133 -499
- package/src/mailbox-cursor.test.ts +81 -189
- package/src/mailbox-delete-intent.test.ts +78 -205
- package/src/mailbox-intent.ts +5 -3
- package/src/mailbox-management.test.ts +107 -220
- package/src/mailbox-management.ts +1 -2
- package/src/mailbox-presence.test.ts +36 -34
- package/src/mailbox-queue.ts +2 -20
- package/src/mailbox-rename-intent.test.ts +156 -325
- package/src/mailbox-rename-subtree.test.ts +205 -582
- package/src/mailbox-sync.test.ts +182 -744
- package/src/message-move.test.ts +726 -0
- package/src/message-move.ts +227 -57
- package/src/message-read.ts +87 -0
- package/src/message-save-retry.test.ts +90 -0
- package/src/message-sync-flags.test.ts +10 -1
- package/src/message-sync.test.ts +762 -0
- package/src/message-sync.ts +409 -239
- package/src/mime-walker.ts +16 -15
- package/src/organize-job.ts +93 -0
- package/src/outbox-attachment.test.ts +97 -209
- package/src/outbox-failed-recovery.test.ts +198 -269
- package/src/outbox-queue.test.ts +24 -0
- package/src/outbox-queue.ts +25 -8
- package/src/ownership.ts +140 -0
- package/src/placement-move-terminal.ts +9 -12
- package/src/placement-move.ts +33 -6
- package/src/placement-settled.ts +28 -4
- package/src/placement.test.ts +404 -0
- package/src/quarantine.test.ts +90 -148
- package/src/quarantine.ts +3 -2
- package/src/refusal.ts +29 -0
- package/src/restore-source-placement.ts +0 -4
- package/src/spam-report.test.ts +238 -710
- package/src/sync-warning.test.ts +213 -0
- package/src/sync-warning.ts +56 -0
- package/src/test-helpers/fake-imap.ts +690 -0
- package/src/test-helpers/test-queue.ts +56 -0
- package/src/test-helpers/test-store.ts +299 -0
- package/src/thread-root.test.ts +29 -0
- package/src/thread-root.ts +51 -0
- package/src/types/mailparser-augment.d.ts +8 -0
- package/src/adversarial-mime.e2e.test.ts +0 -307
- package/src/body-sync-auto-read-writeonce.test.ts +0 -219
- package/src/body-sync-calendar-suggestion.test.ts +0 -433
- package/src/body-sync-category-denormalize.test.ts +0 -352
- package/src/body-sync-category-override.test.ts +0 -501
- package/src/body-sync-category-writeonce.test.ts +0 -234
- package/src/body-sync-filter.test.ts +0 -237
- package/src/body-sync-message-gone.test.ts +0 -139
- package/src/body-sync-placement-flags.test.ts +0 -347
- package/src/body-sync-placement-writeonce.test.ts +0 -337
- package/src/body-sync-quarantine.test.ts +0 -276
- package/src/body-sync-resync.test.ts +0 -605
- package/src/body-sync-unclassifiable-once.test.ts +0 -260
- package/src/body-sync-unsubscribed-auto-read.test.ts +0 -176
- package/src/filters/anchor-drift.ts +0 -55
- package/src/filters/message-embedder.test.ts +0 -39
- package/src/filters/message-embedder.ts +0 -33
- package/src/flag-push-terminal.test.ts +0 -209
- package/src/flag-queue-stars.test.ts +0 -171
- package/src/imapflow-connection.e2e.test.ts +0 -335
- package/src/imapflow-connection.integ.test.ts +0 -316
- package/src/message-delete-junk-reconcile.test.ts +0 -117
- package/src/message-delete-no-trash.test.ts +0 -245
- package/src/message-delete-unsettled-placement.test.ts +0 -396
- package/src/message-move-copy.test.ts +0 -562
- package/src/message-move-empty-trash.test.ts +0 -326
- package/src/message-move-same-mailbox.test.ts +0 -139
- package/src/message-move-unsettled-placement.test.ts +0 -378
- package/src/message-sync-address-spoof.test.ts +0 -293
- package/src/message-sync-category.test.ts +0 -142
- package/src/message-sync-changedsince.test.ts +0 -840
- package/src/message-sync-junk-harvest.test.ts +0 -244
- package/src/message-sync-quarantine.test.ts +0 -416
- package/src/message-sync-repoint.test.ts +0 -419
- package/src/outbox-queue-settled-row.test.ts +0 -140
- package/src/pass-through-unit-of-work.ts +0 -17
- package/src/placement-move-terminal.test.ts +0 -300
- package/src/placement-move-unsettled.test.ts +0 -275
- package/src/placement-transition-race.test.ts +0 -482
- package/src/stale-message-reconcile.test.ts +0 -82
- package/src/stale-message-reconcile.ts +0 -55
- package/src/test-helpers/isolated-mailbox.ts +0 -44
- package/src/test-helpers/mailfuzz-connection.ts +0 -27
- package/test/fixtures/mime-adversarial-e2e/01-octet-stream-pdf.eml +0 -21
- package/test/fixtures/mime-adversarial-e2e/02-nested-related-cid-image.eml +0 -31
- package/test/fixtures/mime-adversarial-e2e/03-calendar-invite.eml +0 -33
- package/test/fixtures/mime-adversarial-e2e/04-forwarded-rfc822-with-attachments.eml +0 -42
- package/test/fixtures/mime-adversarial-e2e/05-tnef-winmail.eml +0 -25
- package/test/fixtures/mime-adversarial-e2e/06-8bit-non-ascii.eml +0 -15
- package/test/fixtures/mime-adversarial-e2e/07-quoted-printable-soft-breaks.eml +0 -17
- package/test/fixtures/mime-adversarial-e2e/08-alternative-empty-text.eml +0 -18
package/src/mime-walker.ts
CHANGED
|
@@ -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**:
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
*
|
|
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 (
|
|
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
|
-
|
|
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
|
+
};
|