@remit/imap-worker 0.0.89 → 0.0.91

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 (171) hide show
  1. package/dist/account-check.d.ts +27 -0
  2. package/dist/account-check.js +70 -0
  3. package/dist/body-sync-gate.d.ts +12 -0
  4. package/dist/body-sync-gate.js +53 -0
  5. package/dist/cli.d.ts +2 -0
  6. package/dist/cli.js +193 -0
  7. package/dist/config-env.d.ts +5 -0
  8. package/dist/config-env.js +37 -0
  9. package/dist/connection-pool.d.ts +22 -0
  10. package/dist/connection-pool.js +97 -0
  11. package/dist/connection-scope.d.ts +8 -0
  12. package/dist/connection-scope.js +10 -0
  13. package/dist/context.d.ts +46 -0
  14. package/dist/context.js +15 -0
  15. package/dist/e2e-processor-shim.d.ts +2 -0
  16. package/dist/e2e-processor-shim.js +181 -0
  17. package/dist/emit.d.ts +15 -0
  18. package/dist/emit.js +76 -0
  19. package/dist/events.d.ts +203 -0
  20. package/dist/events.js +1 -0
  21. package/dist/failure-kind.d.ts +10 -0
  22. package/{src/failure-kind.ts → dist/failure-kind.js} +6 -6
  23. package/dist/handlers/append-sent-message.d.ts +3 -0
  24. package/dist/handlers/append-sent-message.js +152 -0
  25. package/dist/handlers/delete-account-objects.d.ts +3 -0
  26. package/dist/handlers/delete-account-objects.js +35 -0
  27. package/dist/handlers/empty-trash.d.ts +14 -0
  28. package/dist/handlers/empty-trash.js +217 -0
  29. package/dist/handlers/flag-push.d.ts +38 -0
  30. package/dist/handlers/flag-push.js +195 -0
  31. package/dist/handlers/imported-folder.d.ts +4 -0
  32. package/dist/handlers/imported-folder.js +23 -0
  33. package/dist/handlers/mailbox-management.d.ts +6 -0
  34. package/dist/handlers/mailbox-management.js +294 -0
  35. package/dist/handlers/mailbox-sync-order.d.ts +21 -0
  36. package/dist/handlers/mailbox-sync-order.js +50 -0
  37. package/dist/handlers/message-copy.d.ts +40 -0
  38. package/dist/handlers/message-copy.js +371 -0
  39. package/dist/handlers/message-delete-terminal.d.ts +60 -0
  40. package/dist/handlers/message-delete-terminal.js +77 -0
  41. package/dist/handlers/message-delete.d.ts +57 -0
  42. package/dist/handlers/message-delete.js +520 -0
  43. package/{src/handlers/message-move-terminal.ts → dist/handlers/message-move-terminal.d.ts} +18 -105
  44. package/dist/handlers/message-move-terminal.js +94 -0
  45. package/dist/handlers/message-move.d.ts +146 -0
  46. package/dist/handlers/message-move.js +490 -0
  47. package/dist/handlers/placement-move-push.d.ts +62 -0
  48. package/dist/handlers/placement-move-push.js +307 -0
  49. package/dist/handlers/probe-message-id.d.ts +2 -0
  50. package/dist/handlers/probe-message-id.js +6 -0
  51. package/dist/handlers/sync-mailboxes.d.ts +45 -0
  52. package/dist/handlers/sync-mailboxes.js +233 -0
  53. package/dist/handlers/sync-message-body.d.ts +23 -0
  54. package/dist/handlers/sync-message-body.js +182 -0
  55. package/dist/handlers/sync-messages.d.ts +29 -0
  56. package/dist/handlers/sync-messages.js +285 -0
  57. package/dist/index.d.ts +9 -0
  58. package/dist/index.js +67 -0
  59. package/dist/lambda-context.d.ts +8 -0
  60. package/dist/lambda-context.js +33 -0
  61. package/dist/mailbox-row.d.ts +9 -0
  62. package/dist/mailbox-row.js +6 -0
  63. package/dist/poller.d.ts +1 -0
  64. package/dist/poller.js +51 -0
  65. package/dist/process.d.ts +6 -0
  66. package/dist/process.js +3 -0
  67. package/dist/processor.d.ts +3 -0
  68. package/dist/processor.js +44 -0
  69. package/dist/queues.d.ts +10 -0
  70. package/dist/queues.js +1 -0
  71. package/dist/s3-account-objects.d.ts +3 -0
  72. package/dist/s3-account-objects.js +26 -0
  73. package/dist/scheduler/attachment-sweep.d.ts +11 -0
  74. package/dist/scheduler/attachment-sweep.js +23 -0
  75. package/dist/scheduler/calendar-subscriptions.d.ts +26 -0
  76. package/dist/scheduler/calendar-subscriptions.js +35 -0
  77. package/dist/scheduler/config.d.ts +27 -0
  78. package/{src/scheduler/config.ts → dist/scheduler/config.js} +7 -26
  79. package/{src/scheduler/decide-due.ts → dist/scheduler/decide-due.d.ts} +3 -12
  80. package/dist/scheduler/decide-due.js +17 -0
  81. package/dist/scheduler/handler.d.ts +21 -0
  82. package/{src/scheduler/handler.ts → dist/scheduler/handler.js} +24 -36
  83. package/{src/scheduler/loop.ts → dist/scheduler/loop.d.ts} +7 -23
  84. package/dist/scheduler/loop.js +8 -0
  85. package/dist/scheduler/run-tick.d.ts +62 -0
  86. package/dist/scheduler/run-tick.js +164 -0
  87. package/dist/scheduler/runner.d.ts +2 -0
  88. package/{src/scheduler/runner.ts → dist/scheduler/runner.js} +58 -84
  89. package/dist/scheduler/stranded-placement-redrive.d.ts +16 -0
  90. package/dist/scheduler/stranded-placement-redrive.js +172 -0
  91. package/dist/sqs-queues.d.ts +4 -0
  92. package/dist/sqs-queues.js +22 -0
  93. package/dist/with-oauth-lifecycle-deps.d.ts +5 -0
  94. package/dist/with-oauth-lifecycle-deps.js +17 -0
  95. package/dist/with-oauth-lifecycle.d.ts +56 -0
  96. package/dist/with-oauth-lifecycle.js +92 -0
  97. package/{src/worker-version.ts → dist/worker-version.d.ts} +1 -2
  98. package/dist/worker-version.js +12 -0
  99. package/package.json +75 -69
  100. package/src/account-check.test.ts +0 -149
  101. package/src/account-check.ts +0 -101
  102. package/src/body-sync-gate.test.ts +0 -185
  103. package/src/body-sync-gate.ts +0 -86
  104. package/src/cli.ts +0 -224
  105. package/src/connection-scope.test.ts +0 -266
  106. package/src/connection-scope.ts +0 -314
  107. package/src/credential-less-sync-fence.test.ts +0 -152
  108. package/src/e2e-processor-shim.ts +0 -232
  109. package/src/emit.test.ts +0 -127
  110. package/src/emit.ts +0 -117
  111. package/src/events.ts +0 -252
  112. package/src/failure-kind.test.ts +0 -42
  113. package/src/handlers/append-sent-message.sqlite.test.ts +0 -176
  114. package/src/handlers/append-sent-message.ts +0 -264
  115. package/src/handlers/delete-account-objects.test.ts +0 -128
  116. package/src/handlers/delete-account-objects.ts +0 -94
  117. package/src/handlers/empty-trash.sqlite.test.ts +0 -209
  118. package/src/handlers/empty-trash.ts +0 -352
  119. package/src/handlers/flag-push.ts +0 -285
  120. package/src/handlers/imported-folder.test.ts +0 -63
  121. package/src/handlers/imported-folder.ts +0 -38
  122. package/src/handlers/mailbox-management.sqlite.test.ts +0 -265
  123. package/src/handlers/mailbox-management.ts +0 -486
  124. package/src/handlers/mailbox-sync-order.test.ts +0 -93
  125. package/src/handlers/mailbox-sync-order.ts +0 -65
  126. package/src/handlers/message-copy.sqlite.test.ts +0 -298
  127. package/src/handlers/message-copy.ts +0 -566
  128. package/src/handlers/message-delete-terminal.ts +0 -147
  129. package/src/handlers/message-delete.sqlite.test.ts +0 -291
  130. package/src/handlers/message-delete.ts +0 -885
  131. package/src/handlers/message-move.sqlite.test.ts +0 -271
  132. package/src/handlers/message-move.test.ts +0 -216
  133. package/src/handlers/message-move.ts +0 -752
  134. package/src/handlers/placement-move-push.test.ts +0 -137
  135. package/src/handlers/placement-move-push.ts +0 -475
  136. package/src/handlers/placement-push.sqlite.test.ts +0 -298
  137. package/src/handlers/probe-message-id.sqlite.test.ts +0 -192
  138. package/src/handlers/probe-message-id.ts +0 -15
  139. package/src/handlers/sync-mailboxes.test.ts +0 -183
  140. package/src/handlers/sync-mailboxes.ts +0 -421
  141. package/src/handlers/sync-message-body.sqlite.test.ts +0 -314
  142. package/src/handlers/sync-message-body.test.ts +0 -150
  143. package/src/handlers/sync-message-body.ts +0 -365
  144. package/src/handlers/sync-messages.sqlite.test.ts +0 -183
  145. package/src/handlers/sync-messages.ts +0 -525
  146. package/src/index.test.ts +0 -22
  147. package/src/index.ts +0 -86
  148. package/src/mail-sync-selection.sqlite.test.ts +0 -234
  149. package/src/mailbox-row.ts +0 -20
  150. package/src/pending-sync.sqlite.test.ts +0 -166
  151. package/src/poller.ts +0 -55
  152. package/src/processor.ts +0 -60
  153. package/src/queues.ts +0 -49
  154. package/src/scheduler/attachment-sweep.sqlite.test.ts +0 -75
  155. package/src/scheduler/attachment-sweep.ts +0 -46
  156. package/src/scheduler/calendar-subscriptions.sqlite.test.ts +0 -147
  157. package/src/scheduler/calendar-subscriptions.ts +0 -103
  158. package/src/scheduler/config.test.ts +0 -40
  159. package/src/scheduler/decide-due.test.ts +0 -44
  160. package/src/scheduler/half-deleted-accounts.sqlite.test.ts +0 -196
  161. package/src/scheduler/loop.test.ts +0 -143
  162. package/src/scheduler/run-tick.sqlite.test.ts +0 -191
  163. package/src/scheduler/run-tick.ts +0 -281
  164. package/src/scheduler/stranded-placement-redrive.ts +0 -265
  165. package/src/stranded-placement.sqlite.test.ts +0 -794
  166. package/src/test-helpers/test-world.ts +0 -427
  167. package/src/thread-join.sqlite.test.ts +0 -106
  168. package/src/with-oauth-lifecycle-deps.sqlite.test.ts +0 -269
  169. package/src/with-oauth-lifecycle-deps.ts +0 -71
  170. package/src/with-oauth-lifecycle.ts +0 -158
  171. package/tsconfig.json +0 -8
@@ -0,0 +1,92 @@
1
+ /**
2
+ * with-oauth-lifecycle.ts
3
+ *
4
+ * Centralizes the OAuth reauth/ACK contract shared by every IMAP handler.
5
+ *
6
+ * The contract (see issue #472):
7
+ * - If the account already requires reauth, skip all IMAP traffic entirely.
8
+ * - Credential resolution AND the per-handler work both run inside one
9
+ * try/catch so that a revoked OAuth token is caught regardless of where it
10
+ * surfaces (token mint vs. first IMAP command).
11
+ * - An account storing no credential at all is terminal, not transient: the
12
+ * account is flipped to the state that describes it and the event is ACKed.
13
+ * Retrying instead fenced the account's FIFO group for the message's whole
14
+ * visibility timeout, blocking the sync that follows the credential the
15
+ * user was still typing (issue #1120).
16
+ * - On a terminal auth failure (RefreshTokenError reauth-required, or
17
+ * MailConnectionError auth), flip the account to reauth_required and return
18
+ * WITHOUT rethrowing — this ACKs the SQS message so it is not retried.
19
+ * An IMAP refusal is stored with it, because not every refusal a mail server
20
+ * hands back is one re-authenticating clears: "SmtpClientAuthentication is
21
+ * disabled" needs a tenant setting changed, and an account card that only
22
+ * ever says "Re-authentication required" sends the user around a loop.
23
+ * - On transient / config / network errors, rethrow so SQS retries with
24
+ * backoff (let-it-crash).
25
+ *
26
+ * Tokens must NEVER appear in logs — only accountId / errorKind / errorCode.
27
+ */
28
+ import { AccountAuthType, ConnectionState } from "@remit/domain-enums";
29
+ import { RefreshTokenError } from "@remit/mail-oauth-service";
30
+ import { resolveConnectionCredentials, } from "@remit/mailbox-service/account-credentials";
31
+ import { MailConnectionError, } from "@remit/mailbox-service/types";
32
+ import { isAccountReauthRequired } from "./account-check.js";
33
+ /**
34
+ * Run `work` for an account under the shared OAuth reauth/ACK contract.
35
+ *
36
+ * `work` receives the resolved MailCredentials. Both credential resolution and
37
+ * `work` run inside the same try/catch.
38
+ */
39
+ export const withOAuthLifecycle = async (deps, account, log, work) => {
40
+ // Skip all IMAP traffic for accounts that already require reauth.
41
+ if (isAccountReauthRequired(account, log)) {
42
+ return;
43
+ }
44
+ const resolve = deps.resolveCredentials ?? resolveConnectionCredentials;
45
+ try {
46
+ const resolution = await resolve(account, deps);
47
+ if (resolution.status === "missing") {
48
+ log.warn({
49
+ accountId: account.accountId,
50
+ reason: resolution.reason,
51
+ connectionState: resolution.terminalState,
52
+ }, "Account stores no credential; acking the event without IMAP traffic");
53
+ await deps.updateConnectionState(account.accountId, resolution.terminalState);
54
+ return; // ACK — no retry can produce a credential
55
+ }
56
+ await work(resolution.credentials);
57
+ }
58
+ catch (err) {
59
+ // Terminal OAuth failure: token revoked / consent withdrawn.
60
+ if (err instanceof RefreshTokenError) {
61
+ if (err.error.kind === "reauth-required") {
62
+ log.warn({
63
+ accountId: account.accountId,
64
+ errorKind: err.error.kind,
65
+ errorCode: err.error.code,
66
+ }, "OAuth token revoked; marking account reauth_required");
67
+ await deps.updateConnectionState(account.accountId, ConnectionState.ReauthRequired);
68
+ return; // ACK — do not retry
69
+ }
70
+ // transient or config: let-it-crash (SQS retry / DLQ)
71
+ throw err;
72
+ }
73
+ // Terminal auth failure at the IMAP layer (bad credentials / expired token).
74
+ // Only OAuth accounts can recover via the re-auth flow; password accounts
75
+ // have no such path so we rethrow to preserve pre-PR batch-item-failure
76
+ // behaviour instead of permanently fencing the account.
77
+ if (err instanceof MailConnectionError && err.kind === "auth") {
78
+ if (account.authType !== AccountAuthType.OauthMicrosoft) {
79
+ throw err;
80
+ }
81
+ log.warn({
82
+ accountId: account.accountId,
83
+ errorKind: err.kind,
84
+ error: err.message,
85
+ }, "IMAP auth rejected; marking account reauth_required");
86
+ await deps.updateConnectionState(account.accountId, ConnectionState.ReauthRequired, err.message);
87
+ return; // ACK — do not retry
88
+ }
89
+ // Transient / network / unexpected: rethrow so SQS retries with backoff.
90
+ throw err;
91
+ }
92
+ };
@@ -9,5 +9,4 @@
9
9
  * CI-provided SHA, then to `dev`, so a local run is labelled honestly rather
10
10
  * than claiming a release it is not.
11
11
  */
12
- export const workerVersion = (): string =>
13
- process.env.REMIT_WORKER_SHA || process.env.GITHUB_SHA || "dev";
12
+ export declare const workerVersion: () => string;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The build of this worker, stamped onto a quarantine record so a maintainer
3
+ * reading a filed report knows which commit failed to read the message.
4
+ *
5
+ * Deliberately not the client's `APP_SHA`: that is the web build, and pointing
6
+ * a maintainer at it for a backend parse failure sends them to the wrong
7
+ * commit. The container bundle replaces this read at build time (see
8
+ * `npm-scripts/docker-bundle.mjs`); outside an image build it falls back to the
9
+ * CI-provided SHA, then to `dev`, so a local run is labelled honestly rather
10
+ * than claiming a release it is not.
11
+ */
12
+ export const workerVersion = () => process.env.REMIT_WORKER_SHA || process.env.GITHUB_SHA || "dev";
package/package.json CHANGED
@@ -1,71 +1,77 @@
1
1
  {
2
- "name": "@remit/imap-worker",
3
- "version": "0.0.89",
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
- "./mailbox-management": {
16
- "types": "./src/handlers/mailbox-management.ts",
17
- "default": "./src/handlers/mailbox-management.ts"
18
- },
19
- "./test-helpers/test-world": {
20
- "types": "./src/test-helpers/test-world.ts",
21
- "default": "./src/test-helpers/test-world.ts"
22
- }
23
- },
24
- "bin": {
25
- "remit-worker": "./dist/cli.js"
26
- },
27
- "scripts": {
28
- "bundle": "esbuild src/index.ts --sourcemap --bundle --platform=node --format=esm --outfile=dist/index.js",
29
- "cli": "node --env-file=../../localhost-dev-aws.env src/cli.ts",
30
- "test:typecheck": "tsgo --noEmit",
31
- "test:run": "node $NODE_TEST_FLAGS --env-file=../../localhost-test-unit.env --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'",
32
- "test": "npm run test:typecheck && npm run test:run",
33
- "dev": "node --import tsx src/e2e-processor-shim.ts"
34
- },
35
- "devDependencies": {
36
- "@remit/drizzle-service": "*",
37
- "@remit/queue-sidecar": "*",
38
- "@remit/storage-service": "*",
39
- "aws-sdk-client-mock": "*",
40
- "esbuild": "^0.28.1"
41
- },
42
- "dependencies": {
43
- "@remit/logger": "*",
44
- "@aws-sdk/client-s3": "^3.1055.0",
45
- "@remit/config-transfer": "*",
46
- "@remit/data-ports": "*",
47
- "@remit/smtp-service": "*",
48
- "@remit/logger-lambda": "*",
49
- "@remit/mailbox-service": "*",
50
- "@remit/sqs-client": "*",
51
- "@remit/backend": "*",
52
- "@remit/calendar-service": "*",
53
- "@remit/domain-enums": "*",
54
- "@remit/mail-oauth-service": "*",
55
- "@remit/secrets-service": "*",
56
- "@aws-sdk/client-sqs": "^3.1090.0",
57
- "@aws-sdk/client-ssm": "^3.1090.0",
58
- "expect-env": "^1.0.1",
59
- "p-map": "^2.1.0",
60
- "@types/aws-lambda": "^8.10.162"
61
- },
62
- "license": "MIT",
63
- "publishConfig": {
64
- "access": "public"
65
- },
66
- "repository": {
67
- "type": "git",
68
- "url": "git+https://github.com/remit-mail/reader.git",
69
- "directory": "packages/imap-worker"
70
- }
2
+ "name": "@remit/imap-worker",
3
+ "version": "0.0.91",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./dist/index.d.ts",
9
+ "default": "./dist/index.js"
10
+ },
11
+ "./process": {
12
+ "types": "./dist/process.d.ts",
13
+ "default": "./dist/process.js"
14
+ },
15
+ "./scheduler": {
16
+ "types": "./dist/scheduler/handler.d.ts",
17
+ "default": "./dist/scheduler/handler.js"
18
+ },
19
+ "./mailbox-management": {
20
+ "types": "./dist/handlers/mailbox-management.d.ts",
21
+ "default": "./dist/handlers/mailbox-management.js"
22
+ }
23
+ },
24
+ "bin": {
25
+ "remit-worker": "./dist/cli.js"
26
+ },
27
+ "scripts": {
28
+ "prepack": "node ../../npm-scripts/pack-dist.mjs prepack",
29
+ "postpack": "node ../../npm-scripts/pack-dist.mjs postpack",
30
+ "bundle": "esbuild src/index.ts --sourcemap --bundle --platform=node --format=esm --outfile=dist/index.js",
31
+ "cli": "node --env-file=../../localhost-dev-aws.env src/cli.ts",
32
+ "test:typecheck": "tsgo --noEmit",
33
+ "test:run": "node $NODE_TEST_FLAGS --env-file=../../localhost-test-unit.env --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'",
34
+ "test": "npm run test:typecheck && npm run test:run",
35
+ "dev": "node --import tsx src/e2e-processor-shim.ts"
36
+ },
37
+ "devDependencies": {
38
+ "@remit/drizzle-service": "*",
39
+ "@remit/queue-sidecar": "*",
40
+ "@remit/storage-service": "*",
41
+ "aws-sdk-client-mock": "*",
42
+ "esbuild": "^0.28.1"
43
+ },
44
+ "dependencies": {
45
+ "@remit/logger": "*",
46
+ "@aws-sdk/client-s3": "^3.1055.0",
47
+ "@remit/config-transfer": "*",
48
+ "@remit/data-ports": "*",
49
+ "@remit/smtp-service": "*",
50
+ "@remit/logger-lambda": "*",
51
+ "@remit/mailbox-service": "*",
52
+ "@remit/sqs-client": "*",
53
+ "@remit/backend": "*",
54
+ "@remit/calendar-service": "*",
55
+ "@remit/domain-enums": "*",
56
+ "@remit/mail-oauth-service": "*",
57
+ "@remit/secrets-service": "*",
58
+ "@aws-sdk/client-sqs": "^3.1090.0",
59
+ "@aws-sdk/client-ssm": "^3.1090.0",
60
+ "expect-env": "^1.0.1",
61
+ "p-map": "^2.1.0",
62
+ "@types/aws-lambda": "^8.10.162"
63
+ },
64
+ "license": "MIT",
65
+ "publishConfig": {
66
+ "access": "public"
67
+ },
68
+ "repository": {
69
+ "type": "git",
70
+ "url": "git+https://github.com/remit-mail/reader.git",
71
+ "directory": "packages/imap-worker"
72
+ },
73
+ "types": "dist/index.d.ts",
74
+ "files": [
75
+ "dist"
76
+ ]
71
77
  }
@@ -1,149 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { describe, it } from "node:test";
3
- import type { AccountItem } from "@remit/data-ports";
4
- import {
5
- isAccountDeleted,
6
- isAccountReauthRequired,
7
- isMailSyncDisabled,
8
- isReservedHost,
9
- isUnsyncableHost,
10
- } from "./account-check.js";
11
-
12
- const log = {
13
- info: () => {},
14
- warn: () => {},
15
- error: () => {},
16
- debug: () => {},
17
- } as unknown as Parameters<typeof isAccountDeleted>[1];
18
-
19
- const buildAccount = (overrides: Partial<AccountItem> = {}): AccountItem =>
20
- ({
21
- accountId: "acct-1",
22
- connectionState: "authenticated",
23
- ...overrides,
24
- }) as unknown as AccountItem;
25
-
26
- describe("isAccountDeleted", () => {
27
- it("returns false when deletedAt is absent", () => {
28
- assert.equal(isAccountDeleted(buildAccount(), log), false);
29
- });
30
-
31
- it("returns true when deletedAt is set", () => {
32
- assert.equal(
33
- isAccountDeleted(buildAccount({ deletedAt: Date.now() }), log),
34
- true,
35
- );
36
- });
37
- });
38
-
39
- describe("isReservedHost", () => {
40
- const reserved = [
41
- "invalid",
42
- "example",
43
- "imap.invalid",
44
- "foo.invalid",
45
- "server.invalid",
46
- "SERVER.INVALID",
47
- "mail.example",
48
- "foo.example",
49
- " imap.invalid ",
50
- ];
51
- for (const host of reserved) {
52
- it(`returns true for reserved host ${JSON.stringify(host)}`, () => {
53
- assert.equal(isReservedHost(host), true);
54
- });
55
- }
56
-
57
- // localhost / .localhost / .test are legitimately used by local & e2e envs,
58
- // so they must NOT be skipped (issue #835 follow-up).
59
- const real = [
60
- "imap.hostnet.nl",
61
- "imap.gmail.com",
62
- "localhost",
63
- "foo.localhost",
64
- "foo.test",
65
- "notlocalhost",
66
- "invalid.com",
67
- "example.com",
68
- "test.org",
69
- ];
70
- for (const host of real) {
71
- it(`returns false for real host ${JSON.stringify(host)}`, () => {
72
- assert.equal(isReservedHost(host), false);
73
- });
74
- }
75
- });
76
-
77
- describe("isUnsyncableHost", () => {
78
- it("returns false for a resolvable host", () => {
79
- assert.equal(
80
- isUnsyncableHost(buildAccount({ imapHost: "imap.gmail.com" }), log),
81
- false,
82
- );
83
- });
84
-
85
- it("returns true for a reserved host", () => {
86
- assert.equal(
87
- isUnsyncableHost(buildAccount({ imapHost: "imap.invalid" }), log),
88
- true,
89
- );
90
- });
91
- });
92
-
93
- describe("isAccountReauthRequired", () => {
94
- it("returns false for authenticated accounts", () => {
95
- assert.equal(
96
- isAccountReauthRequired(
97
- buildAccount({ connectionState: "authenticated" }),
98
- log,
99
- ),
100
- false,
101
- );
102
- });
103
-
104
- it("returns false for not_authenticated accounts", () => {
105
- assert.equal(
106
- isAccountReauthRequired(
107
- buildAccount({ connectionState: "not_authenticated" }),
108
- log,
109
- ),
110
- false,
111
- );
112
- });
113
-
114
- it("returns true when connectionState is reauth_required", () => {
115
- assert.equal(
116
- isAccountReauthRequired(
117
- buildAccount({ connectionState: "reauth_required" }),
118
- log,
119
- ),
120
- true,
121
- );
122
- });
123
- });
124
-
125
- describe("isMailSyncDisabled", () => {
126
- it("returns false for a mail-only account", () => {
127
- assert.equal(
128
- isMailSyncDisabled(buildAccount({ syncedServices: ["Mail"] }), log),
129
- false,
130
- );
131
- });
132
-
133
- it("returns false for an account syncing mail and calendar", () => {
134
- assert.equal(
135
- isMailSyncDisabled(
136
- buildAccount({ syncedServices: ["Mail", "Calendar"] }),
137
- log,
138
- ),
139
- false,
140
- );
141
- });
142
-
143
- it("returns true for a calendar-only account", () => {
144
- assert.equal(
145
- isMailSyncDisabled(buildAccount({ syncedServices: ["Calendar"] }), log),
146
- true,
147
- );
148
- });
149
- });
@@ -1,101 +0,0 @@
1
- import type { AccountItem } from "@remit/data-ports";
2
- import { AccountService } from "@remit/domain-enums";
3
- import type { Logger } from "@remit/logger";
4
-
5
- /**
6
- * RFC 2606 reserved placeholder namespaces that are guaranteed never to resolve
7
- * and are not used by any local/dev/e2e environment: `.invalid` (the actual
8
- * smoke-test placeholder) and `.example`. An account pointed at one of these can
9
- * never connect, so syncing it would retry and dead-letter forever — we skip it.
10
- *
11
- * `localhost`/`.localhost` and `.test` are deliberately NOT skipped: the e2e and
12
- * mailfuzz suites run their IMAP server on `localhost`, and `.test` is commonly
13
- * used for local testing.
14
- */
15
- const RESERVED_NAMES = ["invalid", "example"] as const;
16
- const RESERVED_HOST_SUFFIXES = RESERVED_NAMES.map((name) => `.${name}`);
17
-
18
- /**
19
- * True when `host` is a reserved placeholder name that can never resolve.
20
- *
21
- * Matches the bare `invalid`/`example` and any subdomain of those namespaces
22
- * (`mail.example`, `foo.invalid`, …). Suffix matching is anchored on the dot so
23
- * real hosts like `invalid.com` or `imap.gmail.com` are NOT treated as reserved.
24
- */
25
- export const isReservedHost = (host: string): boolean => {
26
- const normalized = host.trim().toLowerCase();
27
- if (RESERVED_NAMES.includes(normalized as (typeof RESERVED_NAMES)[number])) {
28
- return true;
29
- }
30
- return RESERVED_HOST_SUFFIXES.some((suffix) => normalized.endsWith(suffix));
31
- };
32
-
33
- /**
34
- * Check if an account's IMAP host can never resolve (reserved TLD).
35
- * Returns true if the account should be skipped cleanly — no connection
36
- * attempt, no thrown error, so the event is acked rather than dead-lettered.
37
- */
38
- export const isUnsyncableHost = (
39
- account: AccountItem,
40
- log: Logger,
41
- ): boolean => {
42
- if (isReservedHost(account.imapHost)) {
43
- log.warn(
44
- { accountId: account.accountId, imapHost: account.imapHost },
45
- "Skipping account: IMAP host is a reserved, never-resolvable name",
46
- );
47
- return true;
48
- }
49
- return false;
50
- };
51
-
52
- /**
53
- * Check if an account is deleted (tombstone pattern).
54
- * Returns true if the account should be skipped.
55
- */
56
- export const isAccountDeleted = (
57
- account: AccountItem,
58
- log: Logger,
59
- ): boolean => {
60
- if (account.deletedAt) {
61
- log.info(
62
- { accountId: account.accountId, deletedAt: account.deletedAt },
63
- "Skipping deleted account",
64
- );
65
- return true;
66
- }
67
- return false;
68
- };
69
-
70
- /**
71
- * Check if an account requires re-authentication (e.g. OAuth token revoked).
72
- * Returns true if the account should be skipped until the user re-auths.
73
- */
74
- export const isAccountReauthRequired = (
75
- account: AccountItem,
76
- log: Logger,
77
- ): boolean => {
78
- if (account.connectionState === "reauth_required") {
79
- log.info(
80
- {
81
- accountId: account.accountId,
82
- connectionState: account.connectionState,
83
- },
84
- "Skipping account: reauth required",
85
- );
86
- return true;
87
- }
88
- return false;
89
- };
90
-
91
- export const isMailSyncDisabled = (
92
- account: AccountItem,
93
- log: Logger,
94
- ): boolean => {
95
- if (account.syncedServices.includes(AccountService.Mail)) return false;
96
- log.info(
97
- { accountId: account.accountId, syncedServices: account.syncedServices },
98
- "Skipping account: mail sync is off",
99
- );
100
- return true;
101
- };