@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.
- 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 +226 -200
- 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 +19 -15
- 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/types.ts +1 -1
- 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-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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remit/mailbox-service",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.112",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -32,25 +32,40 @@
|
|
|
32
32
|
"./types": {
|
|
33
33
|
"types": "./src/types.ts",
|
|
34
34
|
"default": "./src/types.ts"
|
|
35
|
+
},
|
|
36
|
+
"./test-helpers/fake-imap": {
|
|
37
|
+
"types": "./src/test-helpers/fake-imap.ts",
|
|
38
|
+
"default": "./src/test-helpers/fake-imap.ts"
|
|
39
|
+
},
|
|
40
|
+
"./test-helpers/test-queue": {
|
|
41
|
+
"types": "./src/test-helpers/test-queue.ts",
|
|
42
|
+
"default": "./src/test-helpers/test-queue.ts"
|
|
43
|
+
},
|
|
44
|
+
"./test-helpers/test-store": {
|
|
45
|
+
"types": "./src/test-helpers/test-store.ts",
|
|
46
|
+
"default": "./src/test-helpers/test-store.ts"
|
|
35
47
|
}
|
|
36
48
|
},
|
|
37
49
|
"scripts": {
|
|
38
50
|
"test:typecheck": "tsgo --noEmit",
|
|
39
51
|
"test:run": "node $NODE_TEST_FLAGS --env-file=../../localhost-test-unit.env --import tsx --experimental-test-coverage --test-coverage-include='src/**' --test-coverage-exclude='src/**/*.test.ts' --test-coverage-exclude='src/**/*.test.tsx' --test-coverage-lines=86 --test 'src/**/*.test.ts'",
|
|
40
|
-
"test:integ": "RUN_INTEG_TESTS=1 node --env-file=../../localhost-test-unit.env --import tsx --test 'src/**/*.integ.test.ts'",
|
|
41
|
-
"test:e2e": "RUN_E2E_TESTS=1 node --env-file=../../localhost-test-e2e.env --import tsx --test --test-concurrency=1 'src/**/*.e2e.test.ts'",
|
|
42
52
|
"test": "npm run test:typecheck && npm run test:run"
|
|
43
53
|
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@remit/drizzle-service": "*",
|
|
56
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
57
|
+
"aws-sdk-client-mock": "*",
|
|
58
|
+
"better-sqlite3": "^12.11.1"
|
|
59
|
+
},
|
|
44
60
|
"dependencies": {
|
|
45
61
|
"imapflow": "^1.3.3",
|
|
46
|
-
"mailparser": "
|
|
47
|
-
"nodemailer": "^
|
|
62
|
+
"mailparser": "3.12.2",
|
|
63
|
+
"nodemailer": "^10.0.6",
|
|
48
64
|
"p-map": "^7.0.4",
|
|
49
65
|
"tldts": "^7.4.9",
|
|
50
66
|
"@remit/data-ports": "*",
|
|
51
67
|
"@remit/domain-enums": "*",
|
|
52
68
|
"@remit/mail-oauth-service": "*",
|
|
53
|
-
"@remit/search-service": "*",
|
|
54
69
|
"@remit/secrets-service": "*",
|
|
55
70
|
"@remit/sqs-client": "*",
|
|
56
71
|
"@remit/storage-service": "*",
|
package/src/body-part-mapper.ts
CHANGED
|
@@ -46,7 +46,12 @@
|
|
|
46
46
|
|
|
47
47
|
import { Buffer } from "node:buffer";
|
|
48
48
|
import type { BodyPartItem } from "@remit/data-ports";
|
|
49
|
-
import
|
|
49
|
+
import {
|
|
50
|
+
encapsulatedParts,
|
|
51
|
+
outermostMessages,
|
|
52
|
+
ownParts,
|
|
53
|
+
} from "@remit/data-ports/message-parts";
|
|
54
|
+
import { type Attachment, type ParsedMail, simpleParser } from "mailparser";
|
|
50
55
|
|
|
51
56
|
export interface BodyPartContentPair {
|
|
52
57
|
partPath: string;
|
|
@@ -358,3 +363,48 @@ export const mapBodyPartsToContent = (
|
|
|
358
363
|
|
|
359
364
|
return pairs;
|
|
360
365
|
};
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* The parse every part's bytes are paired against. A message/rfc822 part stays
|
|
369
|
+
* one leaf holding the raw forwarded message whatever its disposition, so the
|
|
370
|
+
* forwarded message's own parts come only from parsing it in turn.
|
|
371
|
+
*/
|
|
372
|
+
export const parsePartSource = (source: Buffer): Promise<ParsedMail> =>
|
|
373
|
+
simpleParser(source, { ignoreEmbedded: true });
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* `mapBodyPartsToContent`, extended to forwarded messages. mailparser hands a
|
|
377
|
+
* message/rfc822 part over as one attachment holding the raw forwarded
|
|
378
|
+
* message, so the parts inside it have no bytes in `parsed`. Each forwarded
|
|
379
|
+
* message is parsed on its own and its parts are paired against that parse,
|
|
380
|
+
* with their paths rebased so `<messagePath>.0` is the forwarded message's
|
|
381
|
+
* root — the shape `walkMimeStructure` stores.
|
|
382
|
+
*/
|
|
383
|
+
export const mapMessagePartsToContent = async (
|
|
384
|
+
bodyParts: readonly MapperInput[],
|
|
385
|
+
parsed: ParsedMail,
|
|
386
|
+
context: MapperContext = {},
|
|
387
|
+
): Promise<BodyPartContentPair[]> => {
|
|
388
|
+
const messages = outermostMessages(bodyParts);
|
|
389
|
+
const pairs = mapBodyPartsToContent(ownParts(bodyParts), parsed, context);
|
|
390
|
+
|
|
391
|
+
for (const message of messages) {
|
|
392
|
+
const raw = pairs.find((pair) => pair.partPath === message.partPath);
|
|
393
|
+
const inner = encapsulatedParts(bodyParts, message.partPath);
|
|
394
|
+
if (!raw || inner.length === 0) continue;
|
|
395
|
+
const prefix = `${message.partPath}.`;
|
|
396
|
+
const nested = await mapMessagePartsToContent(
|
|
397
|
+
inner.map((part) => ({
|
|
398
|
+
...part,
|
|
399
|
+
partPath: part.partPath.slice(prefix.length),
|
|
400
|
+
})),
|
|
401
|
+
await parsePartSource(raw.content),
|
|
402
|
+
context,
|
|
403
|
+
);
|
|
404
|
+
for (const pair of nested) {
|
|
405
|
+
pairs.push({ ...pair, partPath: `${prefix}${pair.partPath}` });
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return pairs;
|
|
410
|
+
};
|
|
@@ -1,123 +1,63 @@
|
|
|
1
1
|
import assert from "node:assert";
|
|
2
|
-
import { describe, it
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
BodySyncQueueService,
|
|
6
|
-
} from "./body-sync-queue.js";
|
|
2
|
+
import { afterEach, beforeEach, describe, it } from "node:test";
|
|
3
|
+
import { BodySyncQueueService } from "./body-sync-queue.js";
|
|
4
|
+
import { captureQueue, type QueueCapture } from "./test-helpers/test-store.js";
|
|
7
5
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
MessageBody: string;
|
|
11
|
-
MessageGroupId?: string;
|
|
12
|
-
MessageDeduplicationId?: string;
|
|
13
|
-
}
|
|
6
|
+
let capture: QueueCapture;
|
|
7
|
+
let errors: Array<[Record<string, unknown>, string]>;
|
|
14
8
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
capture = captureQueue();
|
|
11
|
+
errors = [];
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterEach(() => capture.restore());
|
|
15
|
+
|
|
16
|
+
const request = (uid?: number) =>
|
|
17
|
+
new BodySyncQueueService({
|
|
18
|
+
sqsQueueUrl: "http://localhost:9324/000/remit-body",
|
|
19
|
+
logger: {
|
|
20
|
+
info: () => {},
|
|
21
|
+
error: (obj, msg) => {
|
|
22
|
+
errors.push([obj, msg]);
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
}).requestBodySync({
|
|
26
|
+
accountId: "acc-1",
|
|
27
|
+
mailboxId: "mbx-1",
|
|
28
|
+
messageId: "msg-1",
|
|
29
|
+
...(uid === undefined ? {} : { uid }),
|
|
34
30
|
});
|
|
35
|
-
// @ts-expect-error - inject mock SQS client for the test
|
|
36
|
-
service.sqs = { send };
|
|
37
|
-
return { service, send, sent };
|
|
38
|
-
};
|
|
39
31
|
|
|
40
32
|
describe("BodySyncQueueService.requestBodySync", () => {
|
|
41
|
-
it("
|
|
42
|
-
|
|
33
|
+
it("asks for a forced re-sync of the message and its uid on a standard queue", async () => {
|
|
34
|
+
await request(42);
|
|
43
35
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
mailboxId: "mbx-1",
|
|
47
|
-
messageId: "msg-1",
|
|
48
|
-
uid: 42,
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
assert.equal(send.mock.calls.length, 1);
|
|
52
|
-
const event = JSON.parse(sent[0].MessageBody);
|
|
36
|
+
const [input] = capture.inputs();
|
|
37
|
+
const event = JSON.parse(input.MessageBody ?? "{}");
|
|
53
38
|
assert.equal(event.type, "SYNC_MESSAGE_BODY");
|
|
54
|
-
assert.equal(event.accountId, "acc-1");
|
|
55
|
-
assert.equal(event.mailboxId, "mbx-1");
|
|
56
39
|
assert.deepEqual(event.messageIds, ["msg-1"]);
|
|
57
40
|
assert.deepEqual(event.messages, [{ messageId: "msg-1", uid: 42 }]);
|
|
58
|
-
// The worker's skip guard must be bypassed for this message — a read
|
|
59
|
-
// miss already proved bodyStorageKey is stale.
|
|
60
41
|
assert.equal(event.force, true);
|
|
61
|
-
|
|
62
|
-
assert.equal(sent[0].MessageGroupId, undefined);
|
|
42
|
+
assert.equal(input.MessageGroupId, undefined);
|
|
63
43
|
});
|
|
64
44
|
|
|
65
45
|
it("omits the uid pair when no uid is known", async () => {
|
|
66
|
-
|
|
46
|
+
await request();
|
|
67
47
|
|
|
68
|
-
|
|
69
|
-
accountId: "acc-1",
|
|
70
|
-
mailboxId: "mbx-1",
|
|
71
|
-
messageId: "msg-1",
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
const event = JSON.parse(sent[0].MessageBody);
|
|
75
|
-
assert.deepEqual(event.messageIds, ["msg-1"]);
|
|
76
|
-
assert.equal(event.messages, undefined);
|
|
48
|
+
assert.equal(capture.sent[0].body.messages, undefined);
|
|
77
49
|
});
|
|
78
50
|
|
|
79
|
-
it("never rejects when the queue
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
throw new Error("queue down");
|
|
83
|
-
},
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
await assert.doesNotReject(() =>
|
|
87
|
-
service.requestBodySync({
|
|
88
|
-
accountId: "acc-1",
|
|
89
|
-
mailboxId: "mbx-1",
|
|
90
|
-
messageId: "msg-1",
|
|
91
|
-
}),
|
|
92
|
-
);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it("logs an alertable body_sync_cue_enqueue_failed error instead of rejecting — the swallow is not silent operationally, only silent to the caller (RFC 033 D5)", async () => {
|
|
96
|
-
const errorCalls: Array<[Record<string, unknown>, string]> = [];
|
|
97
|
-
const logger: BodySyncQueueLogger = {
|
|
98
|
-
info: () => {},
|
|
99
|
-
error: (obj, msg) => {
|
|
100
|
-
errorCalls.push([obj, msg]);
|
|
101
|
-
},
|
|
102
|
-
};
|
|
103
|
-
const { service } = buildService({
|
|
104
|
-
send: async () => {
|
|
105
|
-
throw new Error("queue down");
|
|
106
|
-
},
|
|
107
|
-
logger,
|
|
51
|
+
it("never rejects when the queue is down, and raises an alertable error", async () => {
|
|
52
|
+
capture.answer(async () => {
|
|
53
|
+
throw new Error("queue down");
|
|
108
54
|
});
|
|
109
55
|
|
|
110
|
-
await
|
|
111
|
-
accountId: "acc-1",
|
|
112
|
-
mailboxId: "mbx-1",
|
|
113
|
-
messageId: "msg-1",
|
|
114
|
-
});
|
|
56
|
+
await assert.doesNotReject(request());
|
|
115
57
|
|
|
116
|
-
assert.equal(
|
|
117
|
-
const [fields, message] =
|
|
58
|
+
assert.equal(errors.length, 1);
|
|
59
|
+
const [fields, message] = errors[0];
|
|
118
60
|
assert.equal(fields.alert, "body_sync_cue_enqueue_failed");
|
|
119
|
-
assert.equal(fields.accountId, "acc-1");
|
|
120
|
-
assert.equal(fields.mailboxId, "mbx-1");
|
|
121
61
|
assert.equal(fields.messageId, "msg-1");
|
|
122
62
|
assert.match(message, /read path returned a retryable 202/);
|
|
123
63
|
});
|