@remit/mailbox-service 0.0.111 → 0.0.113

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 (298) hide show
  1. package/dist/account-credentials.d.ts +83 -0
  2. package/dist/account-credentials.js +104 -0
  3. package/dist/attribute-mapper.d.ts +36 -0
  4. package/dist/attribute-mapper.js +119 -0
  5. package/dist/body-parse.d.ts +28 -0
  6. package/{src/body-parse.ts → dist/body-parse.js} +18 -26
  7. package/dist/body-part-mapper.d.ts +92 -0
  8. package/dist/body-part-mapper.js +301 -0
  9. package/dist/body-sync-queue.d.ts +59 -0
  10. package/dist/body-sync-queue.js +56 -0
  11. package/dist/body-sync-terminal.d.ts +58 -0
  12. package/dist/body-sync-terminal.js +95 -0
  13. package/dist/body-sync.d.ts +351 -0
  14. package/dist/body-sync.js +1204 -0
  15. package/dist/calendar-parts.d.ts +23 -0
  16. package/dist/calendar-parts.js +18 -0
  17. package/dist/classification-backfill.d.ts +91 -0
  18. package/dist/classification-backfill.js +180 -0
  19. package/dist/classify-message.d.ts +31 -0
  20. package/dist/classify-message.js +89 -0
  21. package/dist/connection-factory.d.ts +54 -0
  22. package/dist/connection-factory.js +54 -0
  23. package/dist/connection-test.d.ts +41 -0
  24. package/dist/connection-test.js +152 -0
  25. package/dist/external-move.d.ts +94 -0
  26. package/dist/external-move.js +173 -0
  27. package/dist/filter-operations.d.ts +42 -0
  28. package/dist/filter-operations.js +211 -0
  29. package/dist/filters/filter-config.d.ts +14 -0
  30. package/dist/filters/filter-config.js +10 -0
  31. package/{src/filters/list-id.ts → dist/filters/list-id.d.ts} +2 -16
  32. package/dist/filters/list-id.js +26 -0
  33. package/dist/filters/match.d.ts +69 -0
  34. package/dist/filters/match.js +165 -0
  35. package/dist/filters/pipeline.d.ts +64 -0
  36. package/dist/filters/pipeline.js +100 -0
  37. package/{src/flag-push-terminal.ts → dist/flag-push-terminal.d.ts} +19 -77
  38. package/dist/flag-push-terminal.js +63 -0
  39. package/dist/flag-push.d.ts +69 -0
  40. package/dist/flag-push.js +111 -0
  41. package/dist/flag-queue.d.ts +175 -0
  42. package/dist/flag-queue.js +291 -0
  43. package/dist/folder-operations.d.ts +39 -0
  44. package/dist/folder-operations.js +112 -0
  45. package/dist/folder-placement.d.ts +13 -0
  46. package/dist/folder-placement.js +23 -0
  47. package/dist/heuristics/authenticationResults.d.ts +10 -0
  48. package/dist/heuristics/authenticationResults.js +187 -0
  49. package/dist/heuristics/classifyByHeaders.d.ts +82 -0
  50. package/dist/heuristics/classifyByHeaders.js +365 -0
  51. package/dist/heuristics/classifyPlacement.d.ts +67 -0
  52. package/dist/heuristics/classifyPlacement.js +153 -0
  53. package/dist/heuristics/machineSenders.d.ts +21 -0
  54. package/{src/heuristics/machineSenders.ts → dist/heuristics/machineSenders.js} +34 -40
  55. package/dist/heuristics/senderMismatch.d.ts +81 -0
  56. package/dist/heuristics/senderMismatch.js +192 -0
  57. package/dist/heuristics/socialDomains.d.ts +14 -0
  58. package/{src/heuristics/socialDomains.ts → dist/heuristics/socialDomains.js} +9 -9
  59. package/dist/heuristics/transactionalDomains.d.ts +18 -0
  60. package/{src/heuristics/transactionalDomains.ts → dist/heuristics/transactionalDomains.js} +8 -8
  61. package/dist/imapflow-connection.d.ts +377 -0
  62. package/dist/imapflow-connection.js +1147 -0
  63. package/dist/index.d.ts +63 -0
  64. package/dist/index.js +65 -0
  65. package/dist/list-id-backfill.d.ts +83 -0
  66. package/dist/list-id-backfill.js +132 -0
  67. package/dist/mailbox-cursor-rebuild.d.ts +74 -0
  68. package/dist/mailbox-cursor-rebuild.js +71 -0
  69. package/{src/mailbox-cursor.ts → dist/mailbox-cursor.d.ts} +20 -98
  70. package/dist/mailbox-cursor.js +102 -0
  71. package/dist/mailbox-intent.d.ts +41 -0
  72. package/{src/mailbox-intent.ts → dist/mailbox-intent.js} +39 -63
  73. package/dist/mailbox-management.d.ts +240 -0
  74. package/dist/mailbox-management.js +576 -0
  75. package/dist/mailbox-presence.d.ts +52 -0
  76. package/{src/mailbox-presence.ts → dist/mailbox-presence.js} +19 -30
  77. package/dist/mailbox-queue.d.ts +126 -0
  78. package/dist/mailbox-queue.js +214 -0
  79. package/dist/mailbox-sync.d.ts +93 -0
  80. package/dist/mailbox-sync.js +490 -0
  81. package/dist/mailbox-upstream.d.ts +38 -0
  82. package/{src/mailbox-upstream.ts → dist/mailbox-upstream.js} +37 -47
  83. package/dist/message-category.d.ts +51 -0
  84. package/{src/message-category.ts → dist/message-category.js} +28 -71
  85. package/dist/message-move.d.ts +285 -0
  86. package/dist/message-move.js +858 -0
  87. package/dist/message-parser.d.ts +14 -0
  88. package/dist/message-parser.js +17 -0
  89. package/{src/message-presence.ts → dist/message-presence.d.ts} +1 -13
  90. package/dist/message-presence.js +29 -0
  91. package/dist/message-read.d.ts +51 -0
  92. package/dist/message-read.js +31 -0
  93. package/dist/message-sync.d.ts +345 -0
  94. package/dist/message-sync.js +1358 -0
  95. package/dist/mime-walker.d.ts +102 -0
  96. package/dist/mime-walker.js +198 -0
  97. package/dist/organize-job.d.ts +20 -0
  98. package/dist/organize-job.js +37 -0
  99. package/dist/outbox-attachment-content.d.ts +37 -0
  100. package/dist/outbox-attachment-content.js +50 -0
  101. package/dist/outbox-attachment-filename.d.ts +19 -0
  102. package/dist/outbox-attachment-filename.js +64 -0
  103. package/dist/outbox-attachment.d.ts +148 -0
  104. package/dist/outbox-attachment.js +273 -0
  105. package/dist/outbox-queue.d.ts +73 -0
  106. package/dist/outbox-queue.js +284 -0
  107. package/{src/outbox-status.ts → dist/outbox-status.d.ts} +1 -6
  108. package/dist/outbox-status.js +18 -0
  109. package/dist/ownership.d.ts +24 -0
  110. package/dist/ownership.js +83 -0
  111. package/dist/placement-move-terminal.d.ts +67 -0
  112. package/dist/placement-move-terminal.js +88 -0
  113. package/dist/placement-move.d.ts +83 -0
  114. package/dist/placement-move.js +216 -0
  115. package/{src/placement-settled.ts → dist/placement-settled.d.ts} +19 -50
  116. package/dist/placement-settled.js +75 -0
  117. package/dist/quarantine.d.ts +95 -0
  118. package/dist/quarantine.js +131 -0
  119. package/dist/refusal.d.ts +31 -0
  120. package/dist/refusal.js +3 -0
  121. package/{src/restore-source-placement.ts → dist/restore-source-placement.d.ts} +29 -101
  122. package/dist/restore-source-placement.js +79 -0
  123. package/dist/search-text-backfill.d.ts +15 -0
  124. package/dist/search-text-backfill.js +33 -0
  125. package/dist/snippet.d.ts +61 -0
  126. package/dist/snippet.js +129 -0
  127. package/dist/spam-report.d.ts +86 -0
  128. package/dist/spam-report.js +241 -0
  129. package/dist/sync-warning.d.ts +21 -0
  130. package/dist/sync-warning.js +26 -0
  131. package/dist/sync-watermarks.d.ts +138 -0
  132. package/dist/sync-watermarks.js +205 -0
  133. package/dist/test-helpers/fake-imap.d.ts +151 -0
  134. package/dist/test-helpers/fake-imap.js +508 -0
  135. package/dist/test-helpers/folder-roles.d.ts +34 -0
  136. package/dist/test-helpers/folder-roles.js +47 -0
  137. package/dist/test-helpers/mime-tree-generator.d.ts +82 -0
  138. package/dist/test-helpers/mime-tree-generator.js +329 -0
  139. package/dist/test-helpers/test-store.d.ts +69 -0
  140. package/dist/test-helpers/test-store.js +193 -0
  141. package/dist/text/normalizer.d.ts +20 -0
  142. package/dist/text/normalizer.js +102 -0
  143. package/dist/thread-message-rows.d.ts +69 -0
  144. package/dist/thread-message-rows.js +40 -0
  145. package/dist/thread-root.d.ts +7 -0
  146. package/dist/thread-root.js +25 -0
  147. package/dist/types.d.ts +343 -0
  148. package/dist/types.js +19 -0
  149. package/package.json +86 -73
  150. package/src/account-credentials.ts +0 -210
  151. package/src/adversarial-mime.e2e.test.ts +0 -307
  152. package/src/attribute-mapper.test.ts +0 -104
  153. package/src/attribute-mapper.ts +0 -148
  154. package/src/body-parse.test.ts +0 -55
  155. package/src/body-part-mapper.property.test.ts +0 -354
  156. package/src/body-part-mapper.ts +0 -360
  157. package/src/body-sync-auto-read-writeonce.test.ts +0 -219
  158. package/src/body-sync-calendar-suggestion.test.ts +0 -433
  159. package/src/body-sync-category-denormalize.test.ts +0 -352
  160. package/src/body-sync-category-override.test.ts +0 -501
  161. package/src/body-sync-category-writeonce.test.ts +0 -234
  162. package/src/body-sync-filter.test.ts +0 -237
  163. package/src/body-sync-message-gone.test.ts +0 -139
  164. package/src/body-sync-placement-flags.test.ts +0 -347
  165. package/src/body-sync-placement-writeonce.test.ts +0 -337
  166. package/src/body-sync-quarantine.test.ts +0 -276
  167. package/src/body-sync-queue.test.ts +0 -124
  168. package/src/body-sync-queue.ts +0 -131
  169. package/src/body-sync-resync.test.ts +0 -605
  170. package/src/body-sync-terminal.test.ts +0 -310
  171. package/src/body-sync-terminal.ts +0 -176
  172. package/src/body-sync-unclassifiable-once.test.ts +0 -260
  173. package/src/body-sync-unsubscribed-auto-read.test.ts +0 -176
  174. package/src/body-sync.ts +0 -1970
  175. package/src/calendar-parts.ts +0 -41
  176. package/src/classification-backfill.test.ts +0 -615
  177. package/src/classification-backfill.ts +0 -363
  178. package/src/classify-message.test.ts +0 -205
  179. package/src/classify-message.ts +0 -116
  180. package/src/connection-factory.ts +0 -102
  181. package/src/connection-test.ts +0 -208
  182. package/src/credentials.test.ts +0 -181
  183. package/src/external-move.test.ts +0 -369
  184. package/src/external-move.ts +0 -272
  185. package/src/filters/anchor-drift.ts +0 -55
  186. package/src/filters/filter-config.test.ts +0 -229
  187. package/src/filters/filter-config.ts +0 -69
  188. package/src/filters/list-id.test.ts +0 -50
  189. package/src/filters/match.test.ts +0 -429
  190. package/src/filters/match.ts +0 -274
  191. package/src/filters/message-embedder.test.ts +0 -39
  192. package/src/filters/message-embedder.ts +0 -33
  193. package/src/filters/pipeline.test.ts +0 -330
  194. package/src/filters/pipeline.ts +0 -284
  195. package/src/flag-push-terminal.test.ts +0 -209
  196. package/src/flag-push.test.ts +0 -408
  197. package/src/flag-push.ts +0 -198
  198. package/src/flag-queue-stars.test.ts +0 -171
  199. package/src/flag-queue.ts +0 -509
  200. package/src/folder-placement.ts +0 -56
  201. package/src/heuristics/authenticationResults.test.ts +0 -189
  202. package/src/heuristics/authenticationResults.ts +0 -212
  203. package/src/heuristics/classifyByHeaders.test.ts +0 -889
  204. package/src/heuristics/classifyByHeaders.ts +0 -467
  205. package/src/heuristics/classifyPlacement.test.ts +0 -366
  206. package/src/heuristics/classifyPlacement.ts +0 -211
  207. package/src/heuristics/classifyRealMail.test.ts +0 -242
  208. package/src/heuristics/machineSenders.test.ts +0 -101
  209. package/src/heuristics/senderMismatch.test.ts +0 -346
  210. package/src/heuristics/senderMismatch.ts +0 -259
  211. package/src/imapflow-connect-retry.test.ts +0 -134
  212. package/src/imapflow-connection.e2e.test.ts +0 -335
  213. package/src/imapflow-connection.integ.test.ts +0 -316
  214. package/src/imapflow-connection.test.ts +0 -469
  215. package/src/imapflow-connection.ts +0 -1475
  216. package/src/index.ts +0 -350
  217. package/src/list-id-backfill.test.ts +0 -573
  218. package/src/list-id-backfill.ts +0 -274
  219. package/src/mailbox-cursor-rebuild.test.ts +0 -171
  220. package/src/mailbox-cursor-rebuild.ts +0 -146
  221. package/src/mailbox-cursor.test.ts +0 -249
  222. package/src/mailbox-delete-intent.test.ts +0 -246
  223. package/src/mailbox-management.test.ts +0 -356
  224. package/src/mailbox-management.ts +0 -839
  225. package/src/mailbox-presence.test.ts +0 -72
  226. package/src/mailbox-queue.ts +0 -415
  227. package/src/mailbox-rename-intent.test.ts +0 -389
  228. package/src/mailbox-rename-subtree.test.ts +0 -664
  229. package/src/mailbox-sync.test.ts +0 -848
  230. package/src/mailbox-sync.ts +0 -724
  231. package/src/message-delete-junk-reconcile.test.ts +0 -117
  232. package/src/message-delete-no-trash.test.ts +0 -245
  233. package/src/message-delete-unsettled-placement.test.ts +0 -396
  234. package/src/message-move-copy.test.ts +0 -562
  235. package/src/message-move-empty-trash.test.ts +0 -326
  236. package/src/message-move-same-mailbox.test.ts +0 -139
  237. package/src/message-move-unsettled-placement.test.ts +0 -378
  238. package/src/message-move.ts +0 -1348
  239. package/src/message-parser.ts +0 -26
  240. package/src/message-presence.test.ts +0 -50
  241. package/src/message-sync-address-spoof.test.ts +0 -293
  242. package/src/message-sync-category.test.ts +0 -142
  243. package/src/message-sync-changedsince.test.ts +0 -840
  244. package/src/message-sync-flags.test.ts +0 -158
  245. package/src/message-sync-junk-harvest.test.ts +0 -244
  246. package/src/message-sync-quarantine.test.ts +0 -416
  247. package/src/message-sync-repoint.test.ts +0 -419
  248. package/src/message-sync.ts +0 -1963
  249. package/src/mime-walker.test.ts +0 -311
  250. package/src/mime-walker.ts +0 -305
  251. package/src/outbox-attachment-content.test.ts +0 -153
  252. package/src/outbox-attachment-content.ts +0 -104
  253. package/src/outbox-attachment-filename.test.ts +0 -132
  254. package/src/outbox-attachment-filename.ts +0 -76
  255. package/src/outbox-attachment.test.ts +0 -738
  256. package/src/outbox-attachment.ts +0 -536
  257. package/src/outbox-failed-recovery.test.ts +0 -370
  258. package/src/outbox-queue-settled-row.test.ts +0 -140
  259. package/src/outbox-queue.test.ts +0 -291
  260. package/src/outbox-queue.ts +0 -508
  261. package/src/pass-through-unit-of-work.ts +0 -17
  262. package/src/placement-move-terminal.test.ts +0 -300
  263. package/src/placement-move-terminal.ts +0 -148
  264. package/src/placement-move-unsettled.test.ts +0 -275
  265. package/src/placement-move.ts +0 -336
  266. package/src/placement-settled.test.ts +0 -147
  267. package/src/placement-transition-race.test.ts +0 -482
  268. package/src/quarantine.test.ts +0 -191
  269. package/src/quarantine.ts +0 -228
  270. package/src/snippet.test.ts +0 -213
  271. package/src/snippet.ts +0 -152
  272. package/src/spam-report.test.ts +0 -801
  273. package/src/spam-report.ts +0 -357
  274. package/src/stale-message-reconcile.test.ts +0 -82
  275. package/src/stale-message-reconcile.ts +0 -55
  276. package/src/sync-watermarks.test.ts +0 -409
  277. package/src/sync-watermarks.ts +0 -319
  278. package/src/system-flag-wire-format.test.ts +0 -77
  279. package/src/test-helpers/folder-roles.ts +0 -66
  280. package/src/test-helpers/isolated-mailbox.ts +0 -44
  281. package/src/test-helpers/mailfuzz-connection.ts +0 -27
  282. package/src/test-helpers/mime-tree-generator.ts +0 -461
  283. package/src/text/normalizer.test.ts +0 -223
  284. package/src/text/normalizer.ts +0 -140
  285. package/src/thread-message-rows.ts +0 -77
  286. package/src/types/mailparser-augment.d.ts +0 -15
  287. package/src/types/natural-porter-stemmer.d.ts +0 -50
  288. package/src/types/stopword.d.ts +0 -69
  289. package/src/types.ts +0 -371
  290. package/test/fixtures/mime-adversarial-e2e/01-octet-stream-pdf.eml +0 -21
  291. package/test/fixtures/mime-adversarial-e2e/02-nested-related-cid-image.eml +0 -31
  292. package/test/fixtures/mime-adversarial-e2e/03-calendar-invite.eml +0 -33
  293. package/test/fixtures/mime-adversarial-e2e/04-forwarded-rfc822-with-attachments.eml +0 -42
  294. package/test/fixtures/mime-adversarial-e2e/05-tnef-winmail.eml +0 -25
  295. package/test/fixtures/mime-adversarial-e2e/06-8bit-non-ascii.eml +0 -15
  296. package/test/fixtures/mime-adversarial-e2e/07-quoted-printable-soft-breaks.eml +0 -17
  297. package/test/fixtures/mime-adversarial-e2e/08-alternative-empty-text.eml +0 -18
  298. package/tsconfig.json +0 -8
package/package.json CHANGED
@@ -1,75 +1,88 @@
1
1
  {
2
- "name": "@remit/mailbox-service",
3
- "version": "0.0.111",
4
- "type": "module",
5
- "main": "src/index.ts",
6
- "types": "src/index.ts",
7
- "exports": {
8
- ".": {
9
- "types": "./src/index.ts",
10
- "default": "./src/index.ts"
11
- },
12
- "./filter-config": {
13
- "types": "./src/filters/filter-config.ts",
14
- "default": "./src/filters/filter-config.ts"
15
- },
16
- "./body-sync-queue": {
17
- "types": "./src/body-sync-queue.ts",
18
- "default": "./src/body-sync-queue.ts"
19
- },
20
- "./imapflow-connection": {
21
- "types": "./src/imapflow-connection.ts",
22
- "default": "./src/imapflow-connection.ts"
23
- },
24
- "./account-credentials": {
25
- "types": "./src/account-credentials.ts",
26
- "default": "./src/account-credentials.ts"
27
- },
28
- "./outbox-attachment-content": {
29
- "types": "./src/outbox-attachment-content.ts",
30
- "default": "./src/outbox-attachment-content.ts"
31
- },
32
- "./types": {
33
- "types": "./src/types.ts",
34
- "default": "./src/types.ts"
35
- }
36
- },
37
- "scripts": {
38
- "test:typecheck": "tsgo --noEmit",
39
- "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
- "test": "npm run test:typecheck && npm run test:run"
43
- },
44
- "dependencies": {
45
- "imapflow": "^1.3.3",
46
- "mailparser": "^3.9.14",
47
- "nodemailer": "^9.0.3",
48
- "p-map": "^7.0.4",
49
- "tldts": "^7.4.9",
50
- "@remit/data-ports": "*",
51
- "@remit/domain-enums": "*",
52
- "@remit/mail-oauth-service": "*",
53
- "@remit/search-service": "*",
54
- "@remit/secrets-service": "*",
55
- "@remit/sqs-client": "*",
56
- "@remit/storage-service": "*",
57
- "@aws-sdk/client-sqs": "*",
58
- "franc": "*",
59
- "natural": "^8.1.1",
60
- "stopword": "*",
61
- "@types/mailparser": "*",
62
- "@types/nodemailer": "*",
63
- "@types/stopword": "^2.0.3",
64
- "@remit/calendar-service": "*"
65
- },
66
- "license": "MIT",
67
- "publishConfig": {
68
- "access": "public"
69
- },
70
- "repository": {
71
- "type": "git",
72
- "url": "git+https://github.com/remit-mail/reader.git",
73
- "directory": "packages/mailbox-service"
74
- }
2
+ "name": "@remit/mailbox-service",
3
+ "version": "0.0.113",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ },
12
+ "./filter-config": {
13
+ "types": "./dist/filters/filter-config.d.ts",
14
+ "default": "./dist/filters/filter-config.js"
15
+ },
16
+ "./body-sync-queue": {
17
+ "types": "./dist/body-sync-queue.d.ts",
18
+ "default": "./dist/body-sync-queue.js"
19
+ },
20
+ "./imapflow-connection": {
21
+ "types": "./dist/imapflow-connection.d.ts",
22
+ "default": "./dist/imapflow-connection.js"
23
+ },
24
+ "./account-credentials": {
25
+ "types": "./dist/account-credentials.d.ts",
26
+ "default": "./dist/account-credentials.js"
27
+ },
28
+ "./outbox-attachment-content": {
29
+ "types": "./dist/outbox-attachment-content.d.ts",
30
+ "default": "./dist/outbox-attachment-content.js"
31
+ },
32
+ "./types": {
33
+ "types": "./dist/types.d.ts",
34
+ "default": "./dist/types.js"
35
+ },
36
+ "./test-helpers/fake-imap": {
37
+ "types": "./dist/test-helpers/fake-imap.d.ts",
38
+ "default": "./dist/test-helpers/fake-imap.js"
39
+ },
40
+ "./test-helpers/test-store": {
41
+ "types": "./dist/test-helpers/test-store.d.ts",
42
+ "default": "./dist/test-helpers/test-store.js"
43
+ }
44
+ },
45
+ "scripts": {
46
+ "prepack": "node ../../npm-scripts/pack-dist.mjs prepack",
47
+ "postpack": "node ../../npm-scripts/pack-dist.mjs postpack",
48
+ "test:typecheck": "tsgo --noEmit",
49
+ "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'",
50
+ "test": "npm run test:typecheck && npm run test:run"
51
+ },
52
+ "devDependencies": {
53
+ "@remit/drizzle-service": "*",
54
+ "@types/better-sqlite3": "^7.6.13",
55
+ "better-sqlite3": "^12.11.1"
56
+ },
57
+ "dependencies": {
58
+ "imapflow": "^1.3.3",
59
+ "mailparser": "3.12.2",
60
+ "nodemailer": "^10.0.6",
61
+ "p-map": "^7.0.4",
62
+ "tldts": "^7.4.9",
63
+ "@remit/data-ports": "*",
64
+ "@remit/domain-enums": "*",
65
+ "@remit/mail-oauth-service": "*",
66
+ "@remit/secrets-service": "*",
67
+ "@remit/storage-service": "*",
68
+ "franc": "^6.2.0",
69
+ "natural": "^8.1.1",
70
+ "stopword": "^3.1.5",
71
+ "@types/mailparser": "^3.4.6",
72
+ "@types/nodemailer": "^8.0.1",
73
+ "@types/stopword": "^2.0.3",
74
+ "@remit/calendar-service": "*"
75
+ },
76
+ "license": "MIT",
77
+ "publishConfig": {
78
+ "access": "public"
79
+ },
80
+ "repository": {
81
+ "type": "git",
82
+ "url": "git+https://github.com/remit-mail/reader.git",
83
+ "directory": "packages/mailbox-service"
84
+ },
85
+ "files": [
86
+ "dist"
87
+ ]
75
88
  }
@@ -1,210 +0,0 @@
1
- /**
2
- * account-credentials.ts
3
- *
4
- * Single place in the codebase that branches on account.authType.
5
- * All sync handlers (IMAP, SMTP) must call resolveConnectionCredentials
6
- * rather than duplicating credential logic.
7
- *
8
- * Grep-verifiable constraint: `account.authType` must only appear here.
9
- */
10
-
11
- import type { AccountItem } from "@remit/data-ports";
12
- import { AccountAuthType, ConnectionState } from "@remit/domain-enums";
13
- import type { MailOAuthService } from "@remit/mail-oauth-service";
14
- import { RefreshTokenError } from "@remit/mail-oauth-service";
15
- import {
16
- deserializeEncryptedPayload,
17
- type SecretsService,
18
- serializeEncryptedPayload,
19
- } from "@remit/secrets-service";
20
- import type { MailCredentials } from "./types.js";
21
-
22
- /** ConnectionState is a const object (not a TS enum); this is its value type. */
23
- export type ConnectionStateValue =
24
- (typeof ConnectionState)[keyof typeof ConnectionState];
25
-
26
- /**
27
- * The fields that say whether an account holds a credential at all. Narrower
28
- * than AccountItem so a caller holding only an account summary can ask.
29
- */
30
- export type StoredCredentialFields = Partial<
31
- Pick<AccountItem, "authType" | "passwordHash" | "oauthRefreshTokenHash">
32
- >;
33
-
34
- /**
35
- * No credential of the kind this account's authType needs is stored. An
36
- * imported account holds none by design until its owner types one, so this is
37
- * a normal state of the world rather than a fault: no retry can change it, and
38
- * it is distinct from a credential the mail server rejected.
39
- *
40
- * `terminalState` is the connectionState that describes the account while it
41
- * stays this way, decided here so nothing downstream branches on authType.
42
- */
43
- export interface MissingCredential {
44
- readonly status: "missing";
45
- readonly terminalState: ConnectionStateValue;
46
- readonly reason: string;
47
- }
48
-
49
- export type CredentialResolution =
50
- | { readonly status: "resolved"; readonly credentials: MailCredentials }
51
- | MissingCredential;
52
-
53
- type StoredCredential =
54
- | { readonly status: "password"; readonly passwordHash: string }
55
- | { readonly status: "oauthMicrosoft"; readonly refreshTokenHash: string }
56
- | MissingCredential;
57
-
58
- const readStoredCredential = (
59
- account: StoredCredentialFields,
60
- ): StoredCredential => {
61
- const authType = account.authType ?? AccountAuthType.Password;
62
-
63
- if (authType === AccountAuthType.OauthMicrosoft) {
64
- if (!account.oauthRefreshTokenHash) {
65
- return {
66
- status: "missing",
67
- terminalState: ConnectionState.ReauthRequired,
68
- reason: "authType=oauthMicrosoft but no oauthRefreshTokenHash",
69
- };
70
- }
71
- return {
72
- status: "oauthMicrosoft",
73
- refreshTokenHash: account.oauthRefreshTokenHash,
74
- };
75
- }
76
-
77
- if (!account.passwordHash) {
78
- return {
79
- status: "missing",
80
- terminalState: ConnectionState.CredentialsMissing,
81
- reason: "authType=password but no passwordHash",
82
- };
83
- }
84
- return { status: "password", passwordHash: account.passwordHash };
85
- };
86
-
87
- /**
88
- * Whether the account holds the credential its authType requires.
89
- *
90
- * Keyed on the stored credential, never on `connectionState`: PATCH
91
- * /accounts/{id} writes a password without clearing `credentials_missing` —
92
- * only a successful connect clears it, via markAuthenticated — so a
93
- * state-based check would go on skipping an account that can sync perfectly
94
- * well.
95
- */
96
- export const hasStoredCredential = (account: StoredCredentialFields): boolean =>
97
- readStoredCredential(account).status !== "missing";
98
-
99
- export interface AccountCredentialsDeps {
100
- secrets: Pick<SecretsService, "decrypt" | "encrypt">;
101
- tokenService: Pick<MailOAuthService, "getAccessToken">;
102
- /**
103
- * Called when the OAuth provider rotated the refresh token.
104
- * Must be awaited BEFORE the credentials are returned so the new token
105
- * is persisted before any IMAP command is issued.
106
- */
107
- persistRotatedToken: (
108
- accountId: string,
109
- encryptedHash: string,
110
- updatedAt: number,
111
- ) => Promise<void>;
112
- }
113
-
114
- /**
115
- * Resolve IMAP credentials for an account.
116
- *
117
- * - Password accounts: decrypt passwordHash → return password credential.
118
- * - OAuth accounts: decrypt oauthRefreshTokenHash → mint access token via
119
- * tokenService. If the provider rotated the refresh token, persist the
120
- * new token via `deps.persistRotatedToken` BEFORE returning credentials.
121
- *
122
- * Returns `{ status: "missing" }` when the account stores no credential —
123
- * an expected outcome every caller settles terminally, never retries.
124
- *
125
- * Throws `RefreshTokenError` on OAuth failures; callers should handle:
126
- * - `kind === "reauth-required"` → set connectionState to reauth_required, ACK
127
- * - `kind === "transient"` → rethrow (SQS retry)
128
- * - `kind === "config"` → rethrow (SQS retry / alert)
129
- */
130
- export const resolveConnectionCredentials = async (
131
- account: AccountItem,
132
- deps: AccountCredentialsDeps,
133
- ): Promise<CredentialResolution> => {
134
- const stored = readStoredCredential(account);
135
-
136
- if (stored.status === "missing") {
137
- return stored;
138
- }
139
-
140
- if (stored.status === "oauthMicrosoft") {
141
- return {
142
- status: "resolved",
143
- credentials: await resolveOauthCredentials(
144
- account.accountId,
145
- stored.refreshTokenHash,
146
- deps,
147
- ),
148
- };
149
- }
150
-
151
- return {
152
- status: "resolved",
153
- credentials: await resolvePasswordCredentials(
154
- stored.passwordHash,
155
- deps.secrets,
156
- ),
157
- };
158
- };
159
-
160
- const resolvePasswordCredentials = async (
161
- passwordHash: string,
162
- secrets: Pick<SecretsService, "decrypt">,
163
- ): Promise<MailCredentials> => {
164
- const password = await secrets.decrypt(
165
- deserializeEncryptedPayload(JSON.parse(passwordHash)),
166
- );
167
- return { kind: "password", password };
168
- };
169
-
170
- const resolveOauthCredentials = async (
171
- accountId: string,
172
- refreshTokenHash: string,
173
- deps: AccountCredentialsDeps,
174
- ): Promise<MailCredentials> => {
175
- const refreshToken = await deps.secrets.decrypt(
176
- deserializeEncryptedPayload(JSON.parse(refreshTokenHash)),
177
- );
178
-
179
- // getAccessToken throws RefreshTokenError on failure
180
- const tokenSet = await deps.tokenService.getAccessToken(
181
- accountId,
182
- refreshToken,
183
- );
184
-
185
- // If the provider rotated the refresh token, persist it BEFORE returning.
186
- // This guarantees the new token is stored even if a subsequent error occurs.
187
- if (tokenSet.refreshToken) {
188
- const encryptedPayload = await deps.secrets.encrypt(tokenSet.refreshToken);
189
- const encryptedHash = JSON.stringify(
190
- serializeEncryptedPayload(encryptedPayload),
191
- );
192
- await deps.persistRotatedToken(accountId, encryptedHash, Date.now());
193
- }
194
-
195
- return { kind: "accessToken", accessToken: tokenSet.accessToken };
196
- };
197
-
198
- /**
199
- * Build the encrypted hash string for an OAuth refresh token.
200
- * Helper used when first storing an OAuth token (e.g. after OAuth callback).
201
- */
202
- export const encryptRefreshToken = async (
203
- refreshToken: string,
204
- secrets: Pick<SecretsService, "encrypt">,
205
- ): Promise<string> => {
206
- const payload = await secrets.encrypt(refreshToken);
207
- return JSON.stringify(serializeEncryptedPayload(payload));
208
- };
209
-
210
- export { ConnectionState, RefreshTokenError };
@@ -1,307 +0,0 @@
1
- /**
2
- * E2E adversarial MIME corpus (#402).
3
- *
4
- * Each .eml fixture under `test/fixtures/mime-adversarial-e2e/` is APPENDed to
5
- * the mailfuzz Dovecot INBOX, fetched back via IMAP, then parsed end-to-end
6
- * with mailparser. The suite asserts the two behaviours called out in #402:
7
- *
8
- * 1. body renders — `parsed.text` or `parsed.html` is non-empty
9
- * 2. attachments are listable — when the fixture declares attachments, every
10
- * expected filename appears in `parsed.attachments`
11
- * with non-zero content length
12
- *
13
- * The shapes here are content-driven adversarial cases that real-world senders
14
- * actually emit (PDF as application/octet-stream, TNEF, nested multipart/related,
15
- * forwarded message/rfc822, calendar invites, quoted-printable soft breaks,
16
- * 8-bit UTF-8 bodies, empty text/plain alternatives) — the same class of shape
17
- * that caused the body-sync regression in #394.
18
- *
19
- * Mapper unit-level defence lives in `body-part-mapper.property.test.ts`; this
20
- * suite is the integration-layer belt-and-braces against the same class.
21
- */
22
-
23
- import assert from "node:assert/strict";
24
- import { readdirSync, readFileSync } from "node:fs";
25
- import { dirname, join, resolve } from "node:path";
26
- import { after, before, describe, test } from "node:test";
27
- import { fileURLToPath } from "node:url";
28
- import { type ParsedMail, simpleParser } from "mailparser";
29
- import { uniqueMailboxName } from "./test-helpers/isolated-mailbox.js";
30
- import {
31
- createMailfuzzConnection,
32
- withMailfuzzConnection,
33
- } from "./test-helpers/mailfuzz-connection.js";
34
-
35
- const __filename = fileURLToPath(import.meta.url);
36
- const __dirname = dirname(__filename);
37
- const FIXTURES_DIR = resolve(
38
- __dirname,
39
- "../test/fixtures/mime-adversarial-e2e",
40
- );
41
-
42
- interface AttachmentExpectation {
43
- filename: string;
44
- contentType?: string;
45
- }
46
-
47
- interface FixtureExpectation {
48
- /** Subject must round-trip — proves the message we fetched is the one we appended. */
49
- subjectIncludes: string;
50
- /** At least one of `text` or `html` must be non-empty after parsing. */
51
- bodyRenders: true;
52
- /** Attachments expected after parsing. Empty array means "no attachments". */
53
- attachments: AttachmentExpectation[];
54
- /** Optional substring that must appear in parsed.text or parsed.html. */
55
- bodyContains?: string;
56
- /**
57
- * Skip the BODYSTRUCTURE re-fetch for this fixture.
58
- * Set when the fixture shape is synthetic and doesn't produce a meaningful
59
- * BODYSTRUCTURE response (e.g. bare TNEF blob — see fixture 05 and #408).
60
- */
61
- skipBodyStructure?: true;
62
- }
63
-
64
- /**
65
- * Expectations indexed by .eml basename. Each fixture must have one entry.
66
- *
67
- * Filename is the contract — match it exactly to the file on disk.
68
- */
69
- const EXPECTATIONS: Record<string, FixtureExpectation> = {
70
- "01-octet-stream-pdf.eml": {
71
- subjectIncludes: "Adversarial fixture 01",
72
- bodyRenders: true,
73
- attachments: [{ filename: "contract.pdf" }],
74
- bodyContains: "contract PDF",
75
- },
76
- "02-nested-related-cid-image.eml": {
77
- subjectIncludes: "Adversarial fixture 02",
78
- bodyRenders: true,
79
- attachments: [
80
- { filename: "logo.png", contentType: "image/png" },
81
- { filename: "invoice.pdf" },
82
- ],
83
- bodyContains: "Inline logo",
84
- },
85
- "03-calendar-invite.eml": {
86
- subjectIncludes: "Adversarial fixture 03",
87
- bodyRenders: true,
88
- attachments: [{ filename: "invite.ics" }],
89
- bodyContains: "Meeting invite",
90
- },
91
- "04-forwarded-rfc822-with-attachments.eml": {
92
- subjectIncludes: "Adversarial fixture 04",
93
- bodyRenders: true,
94
- // mailparser surfaces the forwarded rfc822 as an attachment whose name
95
- // derives from the Content-Disposition filename. The inner pdf is not
96
- // surfaced as a top-level attachment because it lives inside the nested
97
- // rfc822 — listing the rfc822 itself is the integration-layer contract.
98
- attachments: [{ filename: "forwarded.eml" }],
99
- bodyContains: "Forwarding",
100
- },
101
- "05-tnef-winmail.eml": {
102
- subjectIncludes: "Adversarial fixture 05",
103
- bodyRenders: true,
104
- attachments: [{ filename: "winmail.dat" }],
105
- bodyContains: "TNEF",
106
- // skipBodyStructure: The TNEF fixture uses a synthetic magic-byte stub
107
- // rather than a real-world winmail.dat capture, so the BODYSTRUCTURE
108
- // returned by Dovecot for this fixture is not meaningful. The re-fetch
109
- // is skipped here to avoid asserting on a shape that will change once
110
- // the fixture is replaced with a real capture. See #408.
111
- skipBodyStructure: true,
112
- },
113
- "06-8bit-non-ascii.eml": {
114
- subjectIncludes: "Café façade",
115
- bodyRenders: true,
116
- attachments: [],
117
- bodyContains: "café",
118
- },
119
- "07-quoted-printable-soft-breaks.eml": {
120
- subjectIncludes: "Adversarial fixture 07",
121
- bodyRenders: true,
122
- attachments: [],
123
- // "single" is split across a soft line break in the encoded source;
124
- // only an actually-decoded quoted-printable reassembles it.
125
- bodyContains: "single logical line",
126
- },
127
- "08-alternative-empty-text.eml": {
128
- subjectIncludes: "Adversarial fixture 08",
129
- bodyRenders: true,
130
- attachments: [],
131
- // text/plain is empty; rendering must fall back to html.
132
- bodyContains: "intentionally empty",
133
- },
134
- };
135
-
136
- const listEmlFixtures = (): string[] =>
137
- readdirSync(FIXTURES_DIR)
138
- .filter((name) => name.endsWith(".eml"))
139
- .sort();
140
-
141
- const loadFixtureRaw = (name: string): string => {
142
- const path = join(FIXTURES_DIR, name);
143
- // Read as utf8 — fixture 06 includes raw 8-bit UTF-8 bytes which IMAP
144
- // servers accept on APPEND as long as they're valid UTF-8 octets.
145
- return readFileSync(path, "utf8");
146
- };
147
-
148
- const bodyText = (parsed: ParsedMail): string => {
149
- const text = parsed.text ?? "";
150
- const html = typeof parsed.html === "string" ? parsed.html : "";
151
- return `${text}\n${html}`;
152
- };
153
-
154
- describe("Adversarial MIME corpus (Dovecot) — #402", {
155
- skip: !process.env.RUN_E2E_TESTS,
156
- }, () => {
157
- // Own mailbox instead of the shared INBOX so appending fixtures never
158
- // collides with another e2e file's count/flag assertions (#508).
159
- const inbox = uniqueMailboxName("E2E_adversarial");
160
-
161
- before(async () => {
162
- await withMailfuzzConnection(async (connection) => {
163
- await connection.createMailbox(inbox);
164
- });
165
- });
166
-
167
- after(async () => {
168
- const connection = createMailfuzzConnection();
169
- await connection.connect();
170
- await connection.deleteMailbox(inbox).catch(() => {});
171
- if (connection.isConnected) {
172
- await connection.disconnect();
173
- }
174
- });
175
-
176
- // Sanity: every .eml on disk has an expectation entry, and every
177
- // expectation entry points at an existing file. Catches a fixture being
178
- // added without an assertion (or vice versa).
179
- test("every fixture has an expectation entry", () => {
180
- const onDisk = new Set(listEmlFixtures());
181
- const expected = new Set(Object.keys(EXPECTATIONS));
182
- for (const name of onDisk) {
183
- assert.ok(
184
- expected.has(name),
185
- `Fixture ${name} has no expectation entry in EXPECTATIONS`,
186
- );
187
- }
188
- for (const name of expected) {
189
- assert.ok(
190
- onDisk.has(name),
191
- `Expectation ${name} has no .eml file on disk`,
192
- );
193
- }
194
- // Pin the count: #402 calls for 8 adversarial shapes.
195
- assert.equal(
196
- onDisk.size,
197
- 8,
198
- "Expected exactly 8 adversarial MIME fixtures",
199
- );
200
- });
201
-
202
- for (const name of Object.keys(EXPECTATIONS).sort()) {
203
- const expectation = EXPECTATIONS[name];
204
- if (!expectation) continue;
205
-
206
- test(`fixture ${name} round-trips through IMAP and parses`, async () => {
207
- const raw = loadFixtureRaw(name);
208
-
209
- await withMailfuzzConnection(async (connection) => {
210
- // APPEND the fixture into this file's own mailbox. Mailfuzz uses
211
- // Dovecot which returns a proper UID on APPEND (mokapi does not —
212
- // see outbox-roundtrip.e2e.test.ts and the RFC 025 notes).
213
- const appendResult = await connection.append(inbox, raw);
214
- assert.ok(
215
- appendResult.uid > 0,
216
- `APPEND should return a UID for ${name}`,
217
- );
218
-
219
- await connection.openBox(inbox, true);
220
-
221
- // Fetch the message we just appended back. fetchMessageBody
222
- // returns the raw RFC822 source; mailparser handles the rest
223
- // (multipart walking, base64/qp decoding, charset folding).
224
- const rfc822 = await connection.fetchMessageBody(appendResult.uid);
225
- assert.ok(
226
- rfc822 && rfc822.length > 0,
227
- `fetchMessageBody should return bytes for ${name}`,
228
- );
229
-
230
- // BODYSTRUCTURE re-fetch: exercises the IMAP mapper path at the
231
- // integration boundary (the exact regression class from #394).
232
- // Skipped for fixtures with synthetic shapes that don't produce
233
- // meaningful BODYSTRUCTURE (see skipBodyStructure flag + #408).
234
- // The guard in fetchMessages skips rows with undefined uid/internalDate
235
- // so back-to-back FETCH is safe on Dovecot (see imapflow-connection.ts).
236
- if (!expectation.skipBodyStructure) {
237
- const refetched = await connection.fetchMessages([appendResult.uid]);
238
- assert.ok(
239
- refetched.length > 0,
240
- `BODYSTRUCTURE re-fetch should return a message for ${name}`,
241
- );
242
- assert.ok(
243
- refetched[0]?.bodyStructure != null,
244
- `BODYSTRUCTURE should be present for ${name}`,
245
- );
246
- }
247
-
248
- const parsed = await simpleParser(rfc822);
249
-
250
- // Subject round-trips (proves we fetched the right message and
251
- // any encoded-word headers decoded correctly).
252
- assert.ok(
253
- parsed.subject,
254
- `Parsed subject should be present for ${name}`,
255
- );
256
- assert.ok(
257
- parsed.subject?.includes(expectation.subjectIncludes),
258
- `Subject "${parsed.subject}" should include "${expectation.subjectIncludes}" for ${name}`,
259
- );
260
-
261
- // Body renders — at least one of text/html is non-empty. This
262
- // is the headline "body renders" assertion from #402.
263
- const text = parsed.text ?? "";
264
- const html = typeof parsed.html === "string" ? parsed.html : "";
265
- assert.ok(
266
- text.trim().length > 0 || html.trim().length > 0,
267
- `Body should render (text or html non-empty) for ${name}`,
268
- );
269
-
270
- if (expectation.bodyContains) {
271
- const haystack = bodyText(parsed).toLowerCase();
272
- const needle = expectation.bodyContains.toLowerCase();
273
- assert.ok(
274
- haystack.includes(needle),
275
- `Body should contain "${expectation.bodyContains}" for ${name}`,
276
- );
277
- }
278
-
279
- // Attachments are listable — every expected attachment appears
280
- // in parsed.attachments with a matching filename and non-zero
281
- // content length. This is the "attachments listable" half of
282
- // the #402 contract.
283
- const attachments = parsed.attachments ?? [];
284
- for (const expectedAtt of expectation.attachments) {
285
- const found = attachments.find(
286
- (a) => a.filename === expectedAtt.filename,
287
- );
288
- assert.ok(
289
- found,
290
- `Attachment "${expectedAtt.filename}" should be listable for ${name}; got [${attachments.map((a) => a.filename).join(", ")}]`,
291
- );
292
- assert.ok(
293
- (found.size ?? 0) > 0,
294
- `Attachment "${expectedAtt.filename}" should have non-zero size for ${name}`,
295
- );
296
- if (expectedAtt.contentType) {
297
- assert.equal(
298
- found.contentType,
299
- expectedAtt.contentType,
300
- `Attachment "${expectedAtt.filename}" should have contentType "${expectedAtt.contentType}" for ${name}`,
301
- );
302
- }
303
- }
304
- });
305
- });
306
- }
307
- });