@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
@@ -1,357 +0,0 @@
1
- import type {
2
- IAccountRepository,
3
- IAddressRepository,
4
- IMailboxSpecialUseRepository,
5
- IMessageRepository,
6
- } from "@remit/data-ports";
7
- import { deriveAddressId } from "@remit/data-ports/id";
8
- import { AddressRole, MessageKeywordFlag } from "@remit/domain-enums";
9
- import type { FlagPushService } from "./flag-push.js";
10
- import type { MessageMoveService } from "./message-move.js";
11
- import {
12
- isPlacementUnsettled,
13
- waitForPlacementToSettle,
14
- } from "./placement-settled.js";
15
-
16
- export interface SpamReportLogger {
17
- info(obj: Record<string, unknown>, msg: string): void;
18
- error(obj: Record<string, unknown>, msg: string): void;
19
- }
20
-
21
- /**
22
- * The one designed, user-facing outcome of `notSpam`'s R2 wait timing out —
23
- * distinct from any other failure so a caller surfacing failures to the user
24
- * (the bulk handlers in packages/backend) can allowlist this specific,
25
- * pre-written message instead of relaying an arbitrary thrown error's text.
26
- */
27
- export class MoveNotSettledError extends Error {
28
- constructor(messageId: string) {
29
- super(
30
- `Message ${messageId}'s move to Junk has not settled yet; try again in a moment.`,
31
- );
32
- this.name = "MoveNotSettledError";
33
- }
34
- }
35
-
36
- /**
37
- * The account advertises no Junk folder and has none under any conventional
38
- * name, so there is nowhere to file the report. A designed, user-facing
39
- * outcome — the user has to create the folder, and no amount of retrying
40
- * changes that — so it carries its own text through the bulk handler's
41
- * allowlist rather than being flattened to the generic retry copy.
42
- */
43
- export class NoJunkMailboxError extends Error {
44
- constructor() {
45
- super(
46
- "This account has no Junk folder. Create one named Junk or Spam in your mail provider, then report this message again.",
47
- );
48
- this.name = "NoJunkMailboxError";
49
- }
50
- }
51
-
52
- const noopLogger: SpamReportLogger = {
53
- info: () => {},
54
- error: () => {},
55
- };
56
-
57
- const DEFAULT_MOVE_SETTLE_TIMEOUT_MS = 5_000;
58
- const DEFAULT_MOVE_SETTLE_POLL_MS = 250;
59
-
60
- export interface SpamReportConfig {
61
- messageService: IMessageRepository;
62
- addressService: IAddressRepository;
63
- accountService: IAccountRepository;
64
- mailboxSpecialUseService: IMailboxSpecialUseRepository;
65
- messageMoveService: MessageMoveService;
66
- flagPushService: FlagPushService;
67
- logger?: SpamReportLogger;
68
- /** How long `notSpam` waits for a still-in-flight move before giving up. */
69
- moveSettleTimeoutMs?: number;
70
- moveSettlePollMs?: number;
71
- }
72
-
73
- export interface SpamReportParams {
74
- accountConfigId: string;
75
- accountId: string;
76
- messageId: string;
77
- setBy?: string;
78
- }
79
-
80
- export class SpamReportService {
81
- private messageService: IMessageRepository;
82
- private addressService: IAddressRepository;
83
- private accountService: IAccountRepository;
84
- private mailboxSpecialUseService: IMailboxSpecialUseRepository;
85
- private messageMoveService: MessageMoveService;
86
- private flagPushService: FlagPushService;
87
- private log: SpamReportLogger;
88
- private moveSettleTimeoutMs: number;
89
- private moveSettlePollMs: number;
90
-
91
- constructor(config: SpamReportConfig) {
92
- this.messageService = config.messageService;
93
- this.addressService = config.addressService;
94
- this.accountService = config.accountService;
95
- this.mailboxSpecialUseService = config.mailboxSpecialUseService;
96
- this.messageMoveService = config.messageMoveService;
97
- this.flagPushService = config.flagPushService;
98
- this.log = config.logger ?? noopLogger;
99
- this.moveSettleTimeoutMs =
100
- config.moveSettleTimeoutMs ?? DEFAULT_MOVE_SETTLE_TIMEOUT_MS;
101
- this.moveSettlePollMs =
102
- config.moveSettlePollMs ?? DEFAULT_MOVE_SETTLE_POLL_MS;
103
- }
104
-
105
- /**
106
- * The sender to act on. `addressId` is derived rather than read off the
107
- * envelope row so it always names the row `ensureSenderAddress` writes and
108
- * the harvester writes — one derivation, one row, whatever the envelope
109
- * happens to carry.
110
- */
111
- private resolveFromAddress = async (
112
- accountConfigId: string,
113
- messageId: string,
114
- ): Promise<{
115
- addressId: string;
116
- normalizedEmail: string;
117
- displayName: string;
118
- }> => {
119
- const description = await this.messageService.describe(messageId);
120
- const from = description.envelopeAddress.find(
121
- (a) => a.addressRole === AddressRole.From,
122
- );
123
- if (!from) {
124
- throw new Error(`Message ${messageId} has no From address to act on`);
125
- }
126
- return {
127
- addressId: deriveAddressId(accountConfigId, from.normalizedEmail),
128
- normalizedEmail: from.normalizedEmail,
129
- displayName: from.displayName ?? "",
130
- };
131
- };
132
-
133
- /**
134
- * The list form answers `[]` for a row that is not there, where the single
135
- * form throws — absence is a case both callers handle, not a fault.
136
- */
137
- private senderIsKnown = async (
138
- accountConfigId: string,
139
- addressId: string,
140
- ): Promise<boolean> => {
141
- const [known] = await this.addressService.getAddress(accountConfigId, [
142
- addressId,
143
- ]);
144
- return known !== undefined;
145
- };
146
-
147
- /**
148
- * Blocking a sender owns the row it writes to. Harvesting is what ordinarily
149
- * creates it, but a message whose harvest never landed — or whose row was
150
- * taken by a cascade — still has a sender the user is entitled to block, and
151
- * a missing row used to abort report-spam before the message ever reached
152
- * Junk. The id is the harvester's own derivation, so this converges on the
153
- * one row rather than minting a parallel one.
154
- *
155
- * A row that already exists is left exactly as it stands. Reporting spam is
156
- * evidence about the sender, never about the account's standing record of
157
- * them: an upsert here would carry whatever the upsert path does on
158
- * conflict — today a display name chosen by the spammer, and any flag a
159
- * later sighting-clearing rule hangs off it — into an operation that means
160
- * the opposite.
161
- */
162
- private ensureSenderAddress = async (
163
- accountConfigId: string,
164
- from: { addressId: string; normalizedEmail: string; displayName: string },
165
- ): Promise<void> => {
166
- const separator = from.normalizedEmail.lastIndexOf("@");
167
- const localPart = from.normalizedEmail.slice(0, separator);
168
- const domain = from.normalizedEmail.slice(separator + 1);
169
- if (separator < 0 || localPart.length === 0 || domain.length === 0) {
170
- throw new Error(
171
- `Sender address ${from.normalizedEmail} is not a usable email address`,
172
- );
173
- }
174
- if (await this.senderIsKnown(accountConfigId, from.addressId)) return;
175
- await this.addressService.upsertAddress({
176
- addressId: from.addressId,
177
- accountConfigId,
178
- localPart,
179
- domain,
180
- normalizedEmail: from.normalizedEmail,
181
- normalizedCompound:
182
- `${from.displayName.toLowerCase()} ${from.normalizedEmail}`.trim(),
183
- displayName: from.displayName,
184
- });
185
- };
186
-
187
- reportSpam = async (params: SpamReportParams): Promise<void> => {
188
- const { accountConfigId, accountId, messageId, setBy } = params;
189
- const now = Date.now();
190
-
191
- // Resolved before anything is written: with nowhere to file the report,
192
- // the operation cannot happen, and a blocked sender plus a `spamReport`
193
- // stamp on a message still sitting in the inbox is a report that half
194
- // happened. Fail here and the press changed nothing.
195
- const junkMailbox =
196
- await this.mailboxSpecialUseService.findJunkMailbox(accountId);
197
- if (!junkMailbox) {
198
- this.log.error({ accountId, messageId }, "No Junk mailbox for account");
199
- throw new NoJunkMailboxError();
200
- }
201
-
202
- const from = await this.resolveFromAddress(accountConfigId, messageId);
203
- const account = await this.accountService.get(accountId);
204
- const isOwnAddress =
205
- from.normalizedEmail.toLowerCase() === account.email.toLowerCase();
206
-
207
- if (!isOwnAddress) {
208
- await this.ensureSenderAddress(accountConfigId, from);
209
- await this.addressService.mergeFlags(accountConfigId, from.addressId, {
210
- blocked: { value: true, setAt: now, setBy },
211
- });
212
- }
213
-
214
- // Captured before this call writes anything: `before.spamReport` tells
215
- // apart the two reasons the message can already be in Junk. If it is
216
- // already set, a PREVIOUS report-spam press put it there and owns
217
- // `originalMailboxId` — that value is exactly what Undo needs and must
218
- // survive a repeated press. Only when it is absent is the message in
219
- // Junk for some unrelated reason (a provider filter, a manual move),
220
- // making any `originalMailboxId` on the row stale.
221
- const before = await this.messageService.get(messageId);
222
- const hadSpamReportAlready = Boolean(before.spamReport);
223
-
224
- await this.messageService.update(messageId, {
225
- spamReport: { reportedAt: now },
226
- });
227
-
228
- const alreadyInJunk = before.mailboxId === junkMailbox.mailboxId;
229
-
230
- await this.messageMoveService.moveMessage(
231
- accountConfigId,
232
- messageId,
233
- junkMailbox.mailboxId,
234
- accountId,
235
- );
236
-
237
- if (alreadyInJunk && before.originalMailboxId && !hadSpamReportAlready) {
238
- // moveMessage's same-mailbox guard no-opped — it never touches
239
- // originalMailboxId, so a stale value left by some earlier, unrelated
240
- // move would otherwise survive and send a later notSpam to the wrong
241
- // folder. This report-spam action established no move of its own.
242
- //
243
- // A transition off the row this call read (imap-mutations R3): the pair
244
- // being cleared belongs to a placement, and a lane that has claimed the
245
- // row since keeps its own — its settle manages the pair.
246
- await this.messageService.transitionPlacement(
247
- messageId,
248
- {
249
- status: before.status,
250
- mailboxId: before.mailboxId,
251
- uid: before.uid,
252
- },
253
- { originalMailboxId: null, originalUid: null },
254
- );
255
- }
256
-
257
- await this.flagPushService.flip({
258
- accountId,
259
- accountConfigId,
260
- messageId,
261
- mailboxId: junkMailbox.mailboxId,
262
- flagName: MessageKeywordFlag.Junk,
263
- operation: "add",
264
- });
265
-
266
- this.log.info(
267
- {
268
- accountId,
269
- messageId,
270
- addressId: from.addressId,
271
- junkMailboxId: junkMailbox.mailboxId,
272
- skippedBlock: isOwnAddress,
273
- },
274
- "Reported message as spam",
275
- );
276
- };
277
-
278
- notSpam = async (params: SpamReportParams): Promise<void> => {
279
- const { accountConfigId, accountId, messageId } = params;
280
-
281
- // Unblocking a sender the account has no row for is already true, and
282
- // minting one here would put an address nothing ever harvested into the
283
- // address book — the withdrawal of a judgement is not a sighting.
284
- const from = await this.resolveFromAddress(accountConfigId, messageId);
285
- if (await this.senderIsKnown(accountConfigId, from.addressId)) {
286
- await this.addressService.mergeFlags(accountConfigId, from.addressId, {
287
- blocked: null,
288
- });
289
- }
290
-
291
- const message = await this.messageService.get(messageId);
292
-
293
- if (message.originalMailboxId) {
294
- // Only wait on the move's settlement when there is actually a
295
- // dependent write to make (the restore below) — a move in flight for
296
- // an unrelated reason is not this operation's concern.
297
- const settled = await waitForPlacementToSettle(
298
- this.messageService,
299
- messageId,
300
- {
301
- timeoutMs: this.moveSettleTimeoutMs,
302
- pollMs: this.moveSettlePollMs,
303
- },
304
- );
305
- if (isPlacementUnsettled(settled)) {
306
- throw new MoveNotSettledError(messageId);
307
- }
308
-
309
- await this.messageMoveService.restoreMessage(
310
- accountConfigId,
311
- messageId,
312
- accountId,
313
- );
314
-
315
- // Re-read, then clear against what was read. The restore just wrote a
316
- // placement, so the row here is not the one above; predicating on it is
317
- // what keeps this from erasing a pair some other lane is relying on
318
- // (imap-mutations R3).
319
- const restored = await this.messageService.get(messageId);
320
- await this.messageService.transitionPlacement(
321
- messageId,
322
- {
323
- status: restored.status,
324
- mailboxId: restored.mailboxId,
325
- uid: restored.uid,
326
- },
327
- { originalMailboxId: null, originalUid: null },
328
- );
329
- }
330
-
331
- const current = await this.messageService.get(messageId);
332
-
333
- await this.flagPushService.flip({
334
- accountId,
335
- accountConfigId,
336
- messageId,
337
- mailboxId: current.mailboxId,
338
- flagName: MessageKeywordFlag.Junk,
339
- operation: "remove",
340
- });
341
- await this.flagPushService.flip({
342
- accountId,
343
- accountConfigId,
344
- messageId,
345
- mailboxId: current.mailboxId,
346
- flagName: MessageKeywordFlag.NotJunk,
347
- operation: "add",
348
- });
349
-
350
- await this.messageService.clearSpamReport(messageId);
351
-
352
- this.log.info(
353
- { accountId, messageId, addressId: from.addressId },
354
- "Undid spam report",
355
- );
356
- };
357
- }
@@ -1,82 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { describe, it } from "node:test";
3
- import type {
4
- IMessageRepository,
5
- IThreadMessageRepository,
6
- } from "@remit/data-ports";
7
- import {
8
- reconcileStaleMessage,
9
- type StaleMessageReconcileDeps,
10
- } from "./stale-message-reconcile.js";
11
-
12
- describe("reconcileStaleMessage", () => {
13
- it("deletes the Message row and every ThreadMessage copy that points at it", async () => {
14
- const deletedMessages: string[] = [];
15
- const deletedThreadMessageKeys: Array<{
16
- accountConfigId: string;
17
- threadMessageId: string;
18
- }> = [];
19
-
20
- const deps: StaleMessageReconcileDeps = {
21
- messageService: {
22
- delete: async (messageId: string) => {
23
- deletedMessages.push(messageId);
24
- },
25
- } as unknown as Pick<IMessageRepository, "delete">,
26
- threadMessageService: {
27
- findAllByMessageId: async (
28
- accountConfigId: string,
29
- messageId: string,
30
- ) => [
31
- { accountConfigId, threadMessageId: `tm-inbox-${messageId}` },
32
- { accountConfigId, threadMessageId: `tm-label-${messageId}` },
33
- ],
34
- deleteMany: async (
35
- keys: Array<{ accountConfigId: string; threadMessageId: string }>,
36
- ) => {
37
- deletedThreadMessageKeys.push(...keys);
38
- },
39
- } as unknown as Pick<
40
- IThreadMessageRepository,
41
- "findAllByMessageId" | "deleteMany"
42
- >,
43
- };
44
-
45
- const result = await reconcileStaleMessage(deps, "cfg-1", "msg-1");
46
-
47
- assert.equal(result.threadMessagesDeleted, 2);
48
- assert.deepEqual(deletedMessages, ["msg-1"]);
49
- assert.deepEqual(deletedThreadMessageKeys, [
50
- { accountConfigId: "cfg-1", threadMessageId: "tm-inbox-msg-1" },
51
- { accountConfigId: "cfg-1", threadMessageId: "tm-label-msg-1" },
52
- ]);
53
- });
54
-
55
- it("skips the batch delete (never calls deleteMany with an empty array) when no copies exist", async () => {
56
- const deletedMessages: string[] = [];
57
- let deleteManyCalls = 0;
58
-
59
- const deps: StaleMessageReconcileDeps = {
60
- messageService: {
61
- delete: async (messageId: string) => {
62
- deletedMessages.push(messageId);
63
- },
64
- } as unknown as Pick<IMessageRepository, "delete">,
65
- threadMessageService: {
66
- findAllByMessageId: async () => [],
67
- deleteMany: async () => {
68
- deleteManyCalls++;
69
- },
70
- } as unknown as Pick<
71
- IThreadMessageRepository,
72
- "findAllByMessageId" | "deleteMany"
73
- >,
74
- };
75
-
76
- const result = await reconcileStaleMessage(deps, "cfg-1", "msg-solo");
77
-
78
- assert.equal(result.threadMessagesDeleted, 0);
79
- assert.deepEqual(deletedMessages, ["msg-solo"]);
80
- assert.equal(deleteManyCalls, 0);
81
- });
82
- });
@@ -1,55 +0,0 @@
1
- import type {
2
- IMessageRepository,
3
- IThreadMessageRepository,
4
- } from "@remit/data-ports";
5
-
6
- export interface StaleMessageReconcileDeps {
7
- messageService: Pick<IMessageRepository, "delete">;
8
- threadMessageService: Pick<
9
- IThreadMessageRepository,
10
- "findAllByMessageId" | "deleteMany"
11
- >;
12
- }
13
-
14
- export interface StaleMessageReconcileResult {
15
- threadMessagesDeleted: number;
16
- }
17
-
18
- /**
19
- * Delete a Message row (and every ThreadMessage copy that points at it) once
20
- * the caller has confirmed the message no longer exists on the IMAP server.
21
- *
22
- * A row outliving an upstream expunge (or a UIDVALIDITY change — #1272) is
23
- * the EXPECTED terminal outcome for a body that can never be fetched (issue
24
- * #1270 / epic #1281 invariant 3): expunges are routine, so cleaning up here
25
- * is normal operation, not an incident. Deleting the row makes the existing
26
- * missing-row 404 path (`MessageService.get` / `describe`) authoritative
27
- * again — nothing renders the row, nothing re-arms a sync for it.
28
- *
29
- * Exported standalone, not inlined in the body-sync retry path, so #1272's
30
- * UIDVALIDITY cursor rebuild can call the same reconciliation when it finds
31
- * rows whose UIDs no longer resolve after a cursor rebuild.
32
- */
33
- export const reconcileStaleMessage = async (
34
- deps: StaleMessageReconcileDeps,
35
- accountConfigId: string,
36
- messageId: string,
37
- ): Promise<StaleMessageReconcileResult> => {
38
- const threadMessages = await deps.threadMessageService.findAllByMessageId(
39
- accountConfigId,
40
- messageId,
41
- );
42
-
43
- if (threadMessages.length > 0) {
44
- await deps.threadMessageService.deleteMany(
45
- threadMessages.map((row) => ({
46
- accountConfigId: row.accountConfigId,
47
- threadMessageId: row.threadMessageId,
48
- })),
49
- );
50
- }
51
-
52
- await deps.messageService.delete(messageId);
53
-
54
- return { threadMessagesDeleted: threadMessages.length };
55
- };