@remit/imap-worker 0.0.90 → 0.0.92

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 (176) 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 +22 -0
  76. package/dist/scheduler/calendar-subscriptions.js +35 -0
  77. package/dist/scheduler/config.d.ts +29 -0
  78. package/{src/scheduler/config.ts → dist/scheduler/config.js} +9 -28
  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/dist/scheduler/handler.js +50 -0
  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 +66 -0
  86. package/dist/scheduler/run-tick.js +164 -0
  87. package/dist/scheduler/runner.d.ts +2 -0
  88. package/dist/scheduler/runner.js +102 -0
  89. package/dist/scheduler/stranded-placement-redrive.d.ts +16 -0
  90. package/dist/scheduler/stranded-placement-redrive.js +172 -0
  91. package/dist/scheduler/tick.d.ts +5 -0
  92. package/dist/scheduler/tick.js +5 -0
  93. package/dist/sqs-queues.d.ts +4 -0
  94. package/dist/sqs-queues.js +22 -0
  95. package/dist/with-oauth-lifecycle-deps.d.ts +5 -0
  96. package/dist/with-oauth-lifecycle-deps.js +17 -0
  97. package/dist/with-oauth-lifecycle.d.ts +56 -0
  98. package/dist/with-oauth-lifecycle.js +92 -0
  99. package/{src/worker-version.ts → dist/worker-version.d.ts} +1 -2
  100. package/dist/worker-version.js +12 -0
  101. package/package.json +79 -69
  102. package/src/account-check.test.ts +0 -149
  103. package/src/account-check.ts +0 -101
  104. package/src/body-sync-gate.test.ts +0 -185
  105. package/src/body-sync-gate.ts +0 -86
  106. package/src/cli.ts +0 -218
  107. package/src/connection-scope.test.ts +0 -266
  108. package/src/connection-scope.ts +0 -314
  109. package/src/credential-less-sync-fence.test.ts +0 -152
  110. package/src/e2e-processor-shim.ts +0 -232
  111. package/src/emit.test.ts +0 -145
  112. package/src/emit.ts +0 -115
  113. package/src/events.ts +0 -252
  114. package/src/failure-kind.test.ts +0 -42
  115. package/src/handlers/append-sent-message.sqlite.test.ts +0 -176
  116. package/src/handlers/append-sent-message.ts +0 -264
  117. package/src/handlers/delete-account-objects.test.ts +0 -126
  118. package/src/handlers/delete-account-objects.ts +0 -96
  119. package/src/handlers/empty-trash.sqlite.test.ts +0 -209
  120. package/src/handlers/empty-trash.ts +0 -352
  121. package/src/handlers/flag-push.ts +0 -285
  122. package/src/handlers/imported-folder.test.ts +0 -63
  123. package/src/handlers/imported-folder.ts +0 -38
  124. package/src/handlers/mailbox-management.sqlite.test.ts +0 -265
  125. package/src/handlers/mailbox-management.ts +0 -486
  126. package/src/handlers/mailbox-sync-order.test.ts +0 -93
  127. package/src/handlers/mailbox-sync-order.ts +0 -65
  128. package/src/handlers/message-copy.sqlite.test.ts +0 -298
  129. package/src/handlers/message-copy.ts +0 -569
  130. package/src/handlers/message-delete-terminal.ts +0 -147
  131. package/src/handlers/message-delete.sqlite.test.ts +0 -291
  132. package/src/handlers/message-delete.ts +0 -888
  133. package/src/handlers/message-move.sqlite.test.ts +0 -271
  134. package/src/handlers/message-move.test.ts +0 -216
  135. package/src/handlers/message-move.ts +0 -755
  136. package/src/handlers/placement-move-push.test.ts +0 -137
  137. package/src/handlers/placement-move-push.ts +0 -478
  138. package/src/handlers/placement-push.sqlite.test.ts +0 -299
  139. package/src/handlers/probe-message-id.sqlite.test.ts +0 -192
  140. package/src/handlers/probe-message-id.ts +0 -15
  141. package/src/handlers/sync-mailboxes.test.ts +0 -183
  142. package/src/handlers/sync-mailboxes.ts +0 -425
  143. package/src/handlers/sync-message-body.sqlite.test.ts +0 -314
  144. package/src/handlers/sync-message-body.test.ts +0 -150
  145. package/src/handlers/sync-message-body.ts +0 -366
  146. package/src/handlers/sync-messages.sqlite.test.ts +0 -195
  147. package/src/handlers/sync-messages.ts +0 -537
  148. package/src/index.test.ts +0 -22
  149. package/src/index.ts +0 -82
  150. package/src/mail-sync-selection.sqlite.test.ts +0 -227
  151. package/src/mailbox-row.ts +0 -20
  152. package/src/pending-sync.sqlite.test.ts +0 -165
  153. package/src/poller.ts +0 -55
  154. package/src/processor.ts +0 -62
  155. package/src/queues.ts +0 -40
  156. package/src/scheduler/attachment-sweep.sqlite.test.ts +0 -74
  157. package/src/scheduler/attachment-sweep.ts +0 -46
  158. package/src/scheduler/calendar-subscriptions.sqlite.test.ts +0 -147
  159. package/src/scheduler/calendar-subscriptions.ts +0 -103
  160. package/src/scheduler/config.test.ts +0 -40
  161. package/src/scheduler/decide-due.test.ts +0 -44
  162. package/src/scheduler/half-deleted-accounts.sqlite.test.ts +0 -196
  163. package/src/scheduler/handler.ts +0 -69
  164. package/src/scheduler/loop.test.ts +0 -143
  165. package/src/scheduler/run-tick.sqlite.test.ts +0 -191
  166. package/src/scheduler/run-tick.ts +0 -281
  167. package/src/scheduler/runner.ts +0 -125
  168. package/src/scheduler/stranded-placement-redrive.ts +0 -265
  169. package/src/stranded-placement.sqlite.test.ts +0 -799
  170. package/src/test-helpers/test-world.ts +0 -425
  171. package/src/test-helpers/worker-queues.ts +0 -11
  172. package/src/thread-join.sqlite.test.ts +0 -106
  173. package/src/with-oauth-lifecycle-deps.sqlite.test.ts +0 -269
  174. package/src/with-oauth-lifecycle-deps.ts +0 -71
  175. package/src/with-oauth-lifecycle.ts +0 -158
  176. 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,81 @@
1
1
  {
2
- "name": "@remit/imap-worker",
3
- "version": "0.0.90",
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.92",
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
+ "./scheduler-tick": {
20
+ "types": "./dist/scheduler/tick.d.ts",
21
+ "default": "./dist/scheduler/tick.js"
22
+ },
23
+ "./mailbox-management": {
24
+ "types": "./dist/handlers/mailbox-management.d.ts",
25
+ "default": "./dist/handlers/mailbox-management.js"
26
+ }
27
+ },
28
+ "bin": {
29
+ "remit-worker": "./dist/cli.js"
30
+ },
31
+ "scripts": {
32
+ "prepack": "node ../../npm-scripts/pack-dist.mjs prepack",
33
+ "postpack": "node ../../npm-scripts/pack-dist.mjs postpack",
34
+ "bundle": "esbuild src/index.ts --sourcemap --bundle --platform=node --format=esm --outfile=dist/index.js",
35
+ "cli": "node --env-file=../../localhost-dev-aws.env src/cli.ts",
36
+ "test:typecheck": "tsgo --noEmit",
37
+ "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'",
38
+ "test": "npm run test:typecheck && npm run test:run",
39
+ "dev": "node --import tsx src/e2e-processor-shim.ts"
40
+ },
41
+ "devDependencies": {
42
+ "@remit/drizzle-service": "*",
43
+ "@remit/queue-sidecar": "*",
44
+ "@remit/storage-service": "*",
45
+ "aws-sdk-client-mock": "*",
46
+ "esbuild": "^0.28.1"
47
+ },
48
+ "dependencies": {
49
+ "@remit/logger": "*",
50
+ "@aws-sdk/client-s3": "^3.1055.0",
51
+ "@remit/config-transfer": "*",
52
+ "@remit/data-ports": "*",
53
+ "@remit/smtp-service": "*",
54
+ "@remit/logger-lambda": "*",
55
+ "@remit/mailbox-service": "*",
56
+ "@remit/sqs-client": "*",
57
+ "@remit/backend": "*",
58
+ "@remit/calendar-service": "*",
59
+ "@remit/domain-enums": "*",
60
+ "@remit/mail-oauth-service": "*",
61
+ "@remit/secrets-service": "*",
62
+ "@aws-sdk/client-sqs": "^3.1090.0",
63
+ "@aws-sdk/client-ssm": "^3.1090.0",
64
+ "expect-env": "^1.0.1",
65
+ "p-map": "^2.1.0",
66
+ "@types/aws-lambda": "^8.10.162"
67
+ },
68
+ "license": "MIT",
69
+ "publishConfig": {
70
+ "access": "public"
71
+ },
72
+ "repository": {
73
+ "type": "git",
74
+ "url": "git+https://github.com/remit-mail/reader.git",
75
+ "directory": "packages/imap-worker"
76
+ },
77
+ "types": "dist/index.d.ts",
78
+ "files": [
79
+ "dist"
80
+ ]
71
81
  }
@@ -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
- };