@remit/imap-worker 0.0.1 → 0.0.3
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
CHANGED
|
@@ -1,50 +1,60 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
2
|
+
"name": "@remit/imap-worker",
|
|
3
|
+
"version": "0.0.3",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./src/index.ts",
|
|
9
|
+
"default": "./src/index.ts"
|
|
10
|
+
},
|
|
11
|
+
"./scheduler": {
|
|
12
|
+
"types": "./src/scheduler/handler.ts",
|
|
13
|
+
"default": "./src/scheduler/handler.ts"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"bin": {
|
|
17
|
+
"remit-worker": "./dist/cli.js"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"bundle": "esbuild src/index.ts --sourcemap --bundle --platform=node --format=esm --outfile=dist/index.js",
|
|
21
|
+
"cli": "node --env-file=../../localhost-dev-aws.env src/cli.ts",
|
|
22
|
+
"test:typecheck": "tsgo --noEmit",
|
|
23
|
+
"test:run": "node --env-file=../../localhost-test-unit.env --test 'src/**/*.test.ts'",
|
|
24
|
+
"test": "npm run test:typecheck && npm run test:run"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@aws-sdk/client-sqs": "*",
|
|
28
|
+
"@aws-sdk/client-ssm": "*",
|
|
29
|
+
"@aws-sdk/core": "*",
|
|
30
|
+
"@remit/storage-service": "*",
|
|
31
|
+
"@types/aws-lambda": "*",
|
|
32
|
+
"@types/nodemailer": "*",
|
|
33
|
+
"aws-sdk-client-mock": "*",
|
|
34
|
+
"esbuild": "^0.27.7",
|
|
35
|
+
"expect-env": "*",
|
|
36
|
+
"p-map": "*"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@aws-sdk/client-s3": "^3.1055.0",
|
|
40
|
+
"nodemailer": "^9.0.3",
|
|
41
|
+
"@remit/data-ports": "*",
|
|
42
|
+
"@remit/logger-lambda": "*",
|
|
43
|
+
"@remit/mailbox-service": "*",
|
|
44
|
+
"@remit/search-index-worker": "*",
|
|
45
|
+
"@remit/sqs-client": "*",
|
|
46
|
+
"@remit/backend": "*",
|
|
47
|
+
"@remit/domain-enums": "*",
|
|
48
|
+
"@remit/mail-oauth-service": "*",
|
|
49
|
+
"@remit/secrets-service": "*"
|
|
50
|
+
},
|
|
51
|
+
"license": "MIT",
|
|
52
|
+
"publishConfig": {
|
|
53
|
+
"access": "public"
|
|
54
|
+
},
|
|
55
|
+
"repository": {
|
|
56
|
+
"type": "git",
|
|
57
|
+
"url": "git+https://github.com/remit-mail/remit.git",
|
|
58
|
+
"directory": "packages/imap-worker"
|
|
59
|
+
}
|
|
50
60
|
}
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import assert from "node:assert";
|
|
2
|
-
import { afterEach, describe, mock, test } from "node:test";
|
|
2
|
+
import { afterEach, before, describe, mock, test } from "node:test";
|
|
3
3
|
import { SendMessageCommand, SQSClient } from "@aws-sdk/client-sqs";
|
|
4
4
|
import { GetParameterCommand, SSMClient } from "@aws-sdk/client-ssm";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
getClient,
|
|
7
|
+
type RemitClient,
|
|
8
|
+
setClient,
|
|
9
|
+
} from "@remit/backend/client";
|
|
6
10
|
import type { AccountItem, MailboxItem } from "@remit/data-ports";
|
|
7
11
|
import type { Logger } from "@remit/logger-lambda";
|
|
8
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
BodySyncService,
|
|
14
|
+
type SyncedMessage,
|
|
15
|
+
} from "@remit/mailbox-service";
|
|
9
16
|
import { mockClient } from "aws-sdk-client-mock";
|
|
10
17
|
import { resetBodySyncGateCache } from "../body-sync-gate.js";
|
|
11
18
|
import { __warmPoolSizeForTest } from "../connection-scope.js";
|
|
@@ -221,6 +228,28 @@ describe("syncMessageBody — DLQ propagation (integrated, #1270)", () => {
|
|
|
221
228
|
const cappedMailbox = (): MailboxItem =>
|
|
222
229
|
({ fullPath: "INBOX" }) as unknown as MailboxItem;
|
|
223
230
|
|
|
231
|
+
// On the DynamoDB path (main repo, DATA_BACKEND unset) the client is injected
|
|
232
|
+
// by the composition root; register a client whose repositories these tests
|
|
233
|
+
// then mock. On the relational path (reader, DATA_BACKEND=postgres) getClient
|
|
234
|
+
// builds its own client and this injection is inert.
|
|
235
|
+
before(() => {
|
|
236
|
+
setClient({
|
|
237
|
+
account: { get: async () => cappedAccount() },
|
|
238
|
+
mailbox: { get: async () => cappedMailbox() },
|
|
239
|
+
mailboxSpecialUse: {},
|
|
240
|
+
message: {},
|
|
241
|
+
threadMessage: {},
|
|
242
|
+
address: {},
|
|
243
|
+
envelope: {},
|
|
244
|
+
placementMove: {},
|
|
245
|
+
filter: {},
|
|
246
|
+
filterAnchor: {},
|
|
247
|
+
messageLabel: {},
|
|
248
|
+
storage: {},
|
|
249
|
+
secrets: { decrypt: async () => "fake-password" },
|
|
250
|
+
} as unknown as RemitClient);
|
|
251
|
+
});
|
|
252
|
+
|
|
224
253
|
afterEach(() => {
|
|
225
254
|
mock.restoreAll();
|
|
226
255
|
mockClient(SSMClient).reset();
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { describe, it } from "node:test";
|
|
3
3
|
import type { SendMessageCommand, SQSClient } from "@aws-sdk/client-sqs";
|
|
4
|
-
import type {
|
|
4
|
+
import type {
|
|
5
|
+
AccountItem,
|
|
6
|
+
AccountSchedulerPage,
|
|
7
|
+
} from "@remit/data-ports";
|
|
5
8
|
import type { Logger } from "@remit/logger-lambda";
|
|
6
9
|
import { runSchedulerTick } from "./run-tick.js";
|
|
7
10
|
|
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A SYNC_MESSAGES trigger can outlive its account: deletion does not purge the
|
|
3
|
-
* already-queued triggers in lockstep, so a trigger can reference an account
|
|
4
|
-
* whose DDB row is gone. The lookup then raises NotFoundError. That error can
|
|
5
|
-
* never succeed on retry, so it would retry to maxReceiveCount and poison the
|
|
6
|
-
* messages DLQ forever (issue #911).
|
|
7
|
-
*
|
|
8
|
-
* Contract under test:
|
|
9
|
-
* - missing account (get throws NotFoundError) -> acked + WARN, never thrown
|
|
10
|
-
* - soft-deleted account (deletedAt set) -> acked, never thrown
|
|
11
|
-
* - healthy account -> proceeds past the gate (the
|
|
12
|
-
* handler does real work and fails without a live IMAP server) — proving a
|
|
13
|
-
* healthy trigger is NOT silently dropped
|
|
14
|
-
* - transient error (a non-NotFound error) -> propagates, so SQS retries
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import assert from "node:assert/strict";
|
|
18
|
-
import { afterEach, describe, it, mock } from "node:test";
|
|
19
|
-
import {
|
|
20
|
-
_resetForTest,
|
|
21
|
-
_setClientForTest,
|
|
22
|
-
type RemitClient,
|
|
23
|
-
} from "@remit/backend/client";
|
|
24
|
-
import type { AccountItem } from "@remit/data-ports";
|
|
25
|
-
import type { Logger } from "@remit/logger-lambda";
|
|
26
|
-
import type { SyncMessagesEvent } from "../events.js";
|
|
27
|
-
import { syncMessages } from "./sync-messages.js";
|
|
28
|
-
|
|
29
|
-
const notFoundError = (message: string): Error => {
|
|
30
|
-
const error = new Error(message);
|
|
31
|
-
error.name = "NotFoundError";
|
|
32
|
-
return error;
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
const setAccountGet = (get: () => Promise<AccountItem>): void => {
|
|
36
|
-
_setClientForTest({ account: { get } } as unknown as RemitClient);
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
interface WarnRecord {
|
|
40
|
-
fields: Record<string, unknown>;
|
|
41
|
-
message: string;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const recordingLogger = (warns: WarnRecord[]): Logger => {
|
|
45
|
-
const noop = () => {};
|
|
46
|
-
const log = {
|
|
47
|
-
info: noop,
|
|
48
|
-
warn: (fields: Record<string, unknown>, message: string) => {
|
|
49
|
-
warns.push({ fields, message });
|
|
50
|
-
},
|
|
51
|
-
error: noop,
|
|
52
|
-
debug: noop,
|
|
53
|
-
fatal: noop,
|
|
54
|
-
trace: noop,
|
|
55
|
-
child: () => log,
|
|
56
|
-
} as unknown as Logger;
|
|
57
|
-
return log;
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
const healthyAccount = (): AccountItem =>
|
|
61
|
-
({
|
|
62
|
-
accountId: "acct-live",
|
|
63
|
-
accountConfigId: "acfg-live",
|
|
64
|
-
connectionState: "authenticated",
|
|
65
|
-
username: "alice@imap.example.com",
|
|
66
|
-
imapHost: "imap.example.com",
|
|
67
|
-
imapPort: 993,
|
|
68
|
-
imapTls: true,
|
|
69
|
-
}) as unknown as AccountItem;
|
|
70
|
-
|
|
71
|
-
const deletedAccount = (): AccountItem =>
|
|
72
|
-
({
|
|
73
|
-
...healthyAccount(),
|
|
74
|
-
accountId: "acct-deleted",
|
|
75
|
-
deletedAt: Date.now(),
|
|
76
|
-
}) as unknown as AccountItem;
|
|
77
|
-
|
|
78
|
-
const event = (accountId: string): SyncMessagesEvent =>
|
|
79
|
-
({
|
|
80
|
-
type: "SYNC_MESSAGES",
|
|
81
|
-
accountId,
|
|
82
|
-
mailboxId: "mbox-1",
|
|
83
|
-
eventId: "evt-1",
|
|
84
|
-
timestamp: 0,
|
|
85
|
-
}) as SyncMessagesEvent;
|
|
86
|
-
|
|
87
|
-
afterEach(() => {
|
|
88
|
-
mock.restoreAll();
|
|
89
|
-
_resetForTest();
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
describe("syncMessages deleted/missing account drop (#911)", () => {
|
|
93
|
-
it("acks and warns when the account no longer exists", async () => {
|
|
94
|
-
setAccountGet(async () => {
|
|
95
|
-
throw notFoundError("Account not found: acct-gone");
|
|
96
|
-
});
|
|
97
|
-
const warns: WarnRecord[] = [];
|
|
98
|
-
|
|
99
|
-
await assert.doesNotReject(() =>
|
|
100
|
-
syncMessages(event("acct-gone"), recordingLogger(warns)),
|
|
101
|
-
);
|
|
102
|
-
|
|
103
|
-
const warn = warns.find((w) =>
|
|
104
|
-
w.message.includes("account no longer exists"),
|
|
105
|
-
);
|
|
106
|
-
assert.ok(warn, "expected a WARN for the missing account");
|
|
107
|
-
assert.equal(warn.fields.accountId, "acct-gone");
|
|
108
|
-
assert.equal(warn.fields.mailboxId, "mbox-1");
|
|
109
|
-
assert.equal(warn.fields.eventId, "evt-1");
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it("acks a soft-deleted account without throwing", async () => {
|
|
113
|
-
setAccountGet(async () => deletedAccount());
|
|
114
|
-
|
|
115
|
-
await assert.doesNotReject(() =>
|
|
116
|
-
syncMessages(event("acct-deleted"), recordingLogger([])),
|
|
117
|
-
);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it("proceeds past the gate for a healthy account (not silently dropped)", async () => {
|
|
121
|
-
setAccountGet(async () => healthyAccount());
|
|
122
|
-
|
|
123
|
-
// No live IMAP server, so a healthy account must reach real work and fail
|
|
124
|
-
// there — the not-found gate must not swallow it as a clean ack.
|
|
125
|
-
await assert.rejects(() =>
|
|
126
|
-
syncMessages(event("acct-live"), recordingLogger([])),
|
|
127
|
-
);
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it("propagates a transient error so SQS retries", async () => {
|
|
131
|
-
const transient = new Error("ProvisionedThroughputExceededException");
|
|
132
|
-
setAccountGet(async () => {
|
|
133
|
-
throw transient;
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
await assert.rejects(
|
|
137
|
-
() => syncMessages(event("acct-live"), recordingLogger([])),
|
|
138
|
-
transient,
|
|
139
|
-
);
|
|
140
|
-
});
|
|
141
|
-
});
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Handler-level guard: an account whose IMAP host is a reserved, never-resolvable
|
|
3
|
-
* placeholder name (RFC 2606 — .invalid/.example) must be skipped cleanly.
|
|
4
|
-
* No connection attempt, no thrown error, so SQS acks the event instead of
|
|
5
|
-
* retrying it into the mailboxes DLQ forever (issue #835).
|
|
6
|
-
*
|
|
7
|
-
* The proof is structural: the only AccountService method the handler may touch
|
|
8
|
-
* is `get`. Any of the post-connect writes (`markAuthenticated`, `update`) firing
|
|
9
|
-
* would mean we passed the skip gate and tried to connect — so those are stubbed
|
|
10
|
-
* to throw, and the test asserts the handler still resolves cleanly.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import assert from "node:assert/strict";
|
|
14
|
-
import { afterEach, describe, it, mock } from "node:test";
|
|
15
|
-
import {
|
|
16
|
-
_resetForTest,
|
|
17
|
-
_setClientForTest,
|
|
18
|
-
type RemitClient,
|
|
19
|
-
} from "@remit/backend/client";
|
|
20
|
-
import type { AccountItem } from "@remit/data-ports";
|
|
21
|
-
import type { Logger } from "@remit/logger-lambda";
|
|
22
|
-
import type { SyncMailboxesEvent, SyncMessagesEvent } from "../events.js";
|
|
23
|
-
import { syncMailboxes } from "./sync-mailboxes.js";
|
|
24
|
-
import { syncMessages } from "./sync-messages.js";
|
|
25
|
-
|
|
26
|
-
const silentLogger = (() => {
|
|
27
|
-
const noop = () => {};
|
|
28
|
-
const log = {
|
|
29
|
-
info: noop,
|
|
30
|
-
warn: noop,
|
|
31
|
-
error: noop,
|
|
32
|
-
debug: noop,
|
|
33
|
-
fatal: noop,
|
|
34
|
-
trace: noop,
|
|
35
|
-
child: () => log,
|
|
36
|
-
} as unknown as Logger;
|
|
37
|
-
return log;
|
|
38
|
-
})();
|
|
39
|
-
|
|
40
|
-
const reservedAccount = (): AccountItem =>
|
|
41
|
-
({
|
|
42
|
-
accountId: "acct-reserved",
|
|
43
|
-
accountConfigId: "acfg-reserved",
|
|
44
|
-
connectionState: "authenticated",
|
|
45
|
-
username: "alice@imap.invalid",
|
|
46
|
-
imapHost: "imap.invalid",
|
|
47
|
-
imapPort: 993,
|
|
48
|
-
imapTls: true,
|
|
49
|
-
}) as unknown as AccountItem;
|
|
50
|
-
|
|
51
|
-
const failIfReached = () => {
|
|
52
|
-
throw new Error("post-skip-gate account write must not run");
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
const setClientWithAccountGet = (get: ReturnType<typeof mock.fn>): void => {
|
|
56
|
-
_setClientForTest({
|
|
57
|
-
account: {
|
|
58
|
-
get,
|
|
59
|
-
markAuthenticated: failIfReached,
|
|
60
|
-
update: failIfReached,
|
|
61
|
-
},
|
|
62
|
-
} as unknown as RemitClient);
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
afterEach(() => {
|
|
66
|
-
mock.restoreAll();
|
|
67
|
-
_resetForTest();
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
describe("reserved-host skip gate", () => {
|
|
71
|
-
it("syncMailboxes skips a reserved host cleanly — no connect, no throw", async () => {
|
|
72
|
-
const get = mock.fn(async () => reservedAccount());
|
|
73
|
-
setClientWithAccountGet(get);
|
|
74
|
-
|
|
75
|
-
const event = {
|
|
76
|
-
type: "SYNC_MAILBOXES",
|
|
77
|
-
accountId: "acct-reserved",
|
|
78
|
-
} as unknown as SyncMailboxesEvent;
|
|
79
|
-
|
|
80
|
-
await assert.doesNotReject(() => syncMailboxes(event, silentLogger));
|
|
81
|
-
assert.equal(get.mock.callCount(), 1);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it("syncMessages skips a reserved host cleanly — no connect, no throw", async () => {
|
|
85
|
-
const get = mock.fn(async () => reservedAccount());
|
|
86
|
-
setClientWithAccountGet(get);
|
|
87
|
-
|
|
88
|
-
const event = {
|
|
89
|
-
type: "SYNC_MESSAGES",
|
|
90
|
-
accountId: "acct-reserved",
|
|
91
|
-
mailboxId: "mbox-1",
|
|
92
|
-
} as unknown as SyncMessagesEvent;
|
|
93
|
-
|
|
94
|
-
await assert.doesNotReject(() => syncMessages(event, silentLogger));
|
|
95
|
-
assert.equal(get.mock.callCount(), 1);
|
|
96
|
-
});
|
|
97
|
-
});
|