@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
- "name": "@remit/imap-worker",
3
- "version": "0.0.1",
4
- "type": "module",
5
- "main": "dist/index.js",
6
- "bin": {
7
- "remit-worker": "./dist/cli.js"
8
- },
9
- "scripts": {
10
- "bundle": "esbuild src/index.ts --sourcemap --bundle --platform=node --format=esm --outfile=dist/index.js",
11
- "cli": "node --env-file=../../localhost-dev-aws.env src/cli.ts",
12
- "test:typecheck": "tsgo --noEmit",
13
- "test:run": "node --env-file=../../localhost-test-unit.env --test 'src/**/*.test.ts'",
14
- "test": "npm run test:typecheck && npm run test:run"
15
- },
16
- "devDependencies": {
17
- "@aws-sdk/client-sqs": "*",
18
- "@aws-sdk/client-ssm": "*",
19
- "@aws-sdk/core": "*",
20
- "@remit/backend": "*",
21
- "@remit/data-ports": "*",
22
- "@remit/domain-enums": "*",
23
- "@remit/logger-lambda": "*",
24
- "@remit/mail-oauth-service": "*",
25
- "@remit/mailbox-service": "*",
26
- "@remit/search-index-worker": "*",
27
- "@remit/secrets-service": "*",
28
- "@remit/sqs-client": "*",
29
- "@remit/storage-service": "*",
30
- "@types/aws-lambda": "*",
31
- "@types/nodemailer": "*",
32
- "aws-sdk-client-mock": "*",
33
- "esbuild": "^0.27.7",
34
- "expect-env": "*",
35
- "p-map": "*"
36
- },
37
- "dependencies": {
38
- "@aws-sdk/client-s3": "^3.1055.0",
39
- "nodemailer": "^9.0.3"
40
- },
41
- "license": "MIT",
42
- "publishConfig": {
43
- "access": "public"
44
- },
45
- "repository": {
46
- "type": "git",
47
- "url": "git+https://github.com/remit-mail/remit.git",
48
- "directory": "packages/imap-worker"
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 { getClient } from "@remit/backend/client";
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 { BodySyncService, type SyncedMessage } from "@remit/mailbox-service";
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 { AccountItem, AccountSchedulerPage } from "@remit/data-ports";
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
- });