@remit/mailbox-service 0.0.112 → 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 (270) 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} +16 -72
  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} +38 -64
  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} +13 -68
  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 -97
  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 -88
  150. package/src/account-credentials.ts +0 -210
  151. package/src/attribute-mapper.test.ts +0 -104
  152. package/src/attribute-mapper.ts +0 -148
  153. package/src/body-parse.test.ts +0 -55
  154. package/src/body-part-mapper.property.test.ts +0 -354
  155. package/src/body-part-mapper.ts +0 -410
  156. package/src/body-sync-queue.test.ts +0 -64
  157. package/src/body-sync-queue.ts +0 -131
  158. package/src/body-sync-terminal.test.ts +0 -148
  159. package/src/body-sync-terminal.ts +0 -174
  160. package/src/body-sync.test.ts +0 -925
  161. package/src/body-sync.ts +0 -1891
  162. package/src/calendar-parts.ts +0 -41
  163. package/src/classification-backfill.test.ts +0 -285
  164. package/src/classification-backfill.ts +0 -363
  165. package/src/classify-message.test.ts +0 -142
  166. package/src/classify-message.ts +0 -116
  167. package/src/connection-factory.ts +0 -102
  168. package/src/connection-test.ts +0 -208
  169. package/src/credentials.test.ts +0 -181
  170. package/src/external-move.test.ts +0 -262
  171. package/src/external-move.ts +0 -285
  172. package/src/filter-operations.test.ts +0 -319
  173. package/src/filter-operations.ts +0 -377
  174. package/src/filter-update.test.ts +0 -313
  175. package/src/filters/filter-config.test.ts +0 -87
  176. package/src/filters/filter-config.ts +0 -34
  177. package/src/filters/list-id.test.ts +0 -50
  178. package/src/filters/match.test.ts +0 -392
  179. package/src/filters/match.ts +0 -225
  180. package/src/filters/pipeline.test.ts +0 -199
  181. package/src/filters/pipeline.ts +0 -170
  182. package/src/flag-push.test.ts +0 -112
  183. package/src/flag-push.ts +0 -198
  184. package/src/flag-queue.test.ts +0 -197
  185. package/src/flag-queue.ts +0 -509
  186. package/src/folder-operations.test.ts +0 -273
  187. package/src/folder-operations.ts +0 -217
  188. package/src/folder-placement.ts +0 -56
  189. package/src/forwarded-message.test.ts +0 -122
  190. package/src/heuristics/authenticationResults.test.ts +0 -189
  191. package/src/heuristics/authenticationResults.ts +0 -212
  192. package/src/heuristics/classifyByHeaders.test.ts +0 -889
  193. package/src/heuristics/classifyByHeaders.ts +0 -467
  194. package/src/heuristics/classifyPlacement.test.ts +0 -366
  195. package/src/heuristics/classifyPlacement.ts +0 -211
  196. package/src/heuristics/classifyRealMail.test.ts +0 -242
  197. package/src/heuristics/machineSenders.test.ts +0 -101
  198. package/src/heuristics/senderMismatch.test.ts +0 -346
  199. package/src/heuristics/senderMismatch.ts +0 -259
  200. package/src/imapflow-connect-retry.test.ts +0 -97
  201. package/src/imapflow-connection.test.ts +0 -503
  202. package/src/imapflow-connection.ts +0 -1476
  203. package/src/index.ts +0 -397
  204. package/src/list-id-backfill.test.ts +0 -207
  205. package/src/list-id-backfill.ts +0 -274
  206. package/src/mailbox-cursor-rebuild.test.ts +0 -171
  207. package/src/mailbox-cursor-rebuild.ts +0 -146
  208. package/src/mailbox-cursor.test.ts +0 -141
  209. package/src/mailbox-delete-intent.test.ts +0 -119
  210. package/src/mailbox-management.test.ts +0 -243
  211. package/src/mailbox-management.ts +0 -838
  212. package/src/mailbox-presence.test.ts +0 -74
  213. package/src/mailbox-queue.ts +0 -397
  214. package/src/mailbox-rename-intent.test.ts +0 -220
  215. package/src/mailbox-rename-subtree.test.ts +0 -287
  216. package/src/mailbox-sync.test.ts +0 -286
  217. package/src/mailbox-sync.ts +0 -724
  218. package/src/message-move.test.ts +0 -726
  219. package/src/message-move.ts +0 -1518
  220. package/src/message-parser.ts +0 -26
  221. package/src/message-presence.test.ts +0 -50
  222. package/src/message-read.ts +0 -87
  223. package/src/message-save-retry.test.ts +0 -90
  224. package/src/message-sync-flags.test.ts +0 -167
  225. package/src/message-sync.test.ts +0 -762
  226. package/src/message-sync.ts +0 -2133
  227. package/src/mime-walker.test.ts +0 -311
  228. package/src/mime-walker.ts +0 -306
  229. package/src/organize-job.ts +0 -93
  230. package/src/outbox-attachment-content.test.ts +0 -153
  231. package/src/outbox-attachment-content.ts +0 -104
  232. package/src/outbox-attachment-filename.test.ts +0 -132
  233. package/src/outbox-attachment-filename.ts +0 -76
  234. package/src/outbox-attachment.test.ts +0 -626
  235. package/src/outbox-attachment.ts +0 -536
  236. package/src/outbox-failed-recovery.test.ts +0 -299
  237. package/src/outbox-queue.test.ts +0 -315
  238. package/src/outbox-queue.ts +0 -525
  239. package/src/ownership.ts +0 -140
  240. package/src/placement-move-terminal.ts +0 -145
  241. package/src/placement-move.ts +0 -363
  242. package/src/placement-settled.test.ts +0 -147
  243. package/src/placement.test.ts +0 -404
  244. package/src/quarantine.test.ts +0 -133
  245. package/src/quarantine.ts +0 -229
  246. package/src/refusal.ts +0 -29
  247. package/src/snippet.test.ts +0 -213
  248. package/src/snippet.ts +0 -152
  249. package/src/spam-report.test.ts +0 -329
  250. package/src/spam-report.ts +0 -357
  251. package/src/sync-warning.test.ts +0 -213
  252. package/src/sync-warning.ts +0 -56
  253. package/src/sync-watermarks.test.ts +0 -409
  254. package/src/sync-watermarks.ts +0 -319
  255. package/src/system-flag-wire-format.test.ts +0 -77
  256. package/src/test-helpers/fake-imap.ts +0 -690
  257. package/src/test-helpers/folder-roles.ts +0 -66
  258. package/src/test-helpers/mime-tree-generator.ts +0 -461
  259. package/src/test-helpers/test-queue.ts +0 -56
  260. package/src/test-helpers/test-store.ts +0 -299
  261. package/src/text/normalizer.test.ts +0 -223
  262. package/src/text/normalizer.ts +0 -140
  263. package/src/thread-message-rows.ts +0 -77
  264. package/src/thread-root.test.ts +0 -29
  265. package/src/thread-root.ts +0 -51
  266. package/src/types/mailparser-augment.d.ts +0 -23
  267. package/src/types/natural-porter-stemmer.d.ts +0 -50
  268. package/src/types/stopword.d.ts +0 -69
  269. package/src/types.ts +0 -371
  270. package/tsconfig.json +0 -8
@@ -1,329 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { afterEach, beforeEach, describe, it } from "node:test";
3
- import type { AccountItem, MailboxItem } from "@remit/data-ports";
4
- import { MessagePlacementUnsettledError } from "@remit/data-ports/errors";
5
- import { deriveAddressId } from "@remit/data-ports/id";
6
- import { AddressRole, MessageKeywordFlag } from "@remit/domain-enums";
7
- import { FlagPushService } from "./flag-push.js";
8
- import { MessageMoveService } from "./message-move.js";
9
- import {
10
- MoveNotSettledError,
11
- NoJunkMailboxError,
12
- SpamReportService,
13
- } from "./spam-report.js";
14
- import { eml, FakeImapServer } from "./test-helpers/fake-imap.js";
15
- import {
16
- arriveAt,
17
- captureQueue,
18
- openStore,
19
- type QueueCapture,
20
- type QueuedEvent,
21
- type Store,
22
- seedAccount,
23
- seedMailbox,
24
- seedMessage,
25
- } from "./test-helpers/test-store.js";
26
-
27
- const SENDER = "sender@example.com";
28
-
29
- let store: Store;
30
- let capture: QueueCapture;
31
- let queue: QueuedEvent[];
32
- let imap: FakeImapServer;
33
- let account: AccountItem;
34
- let inbox: MailboxItem;
35
- let junk: MailboxItem | undefined;
36
- let other: MailboxItem;
37
-
38
- beforeEach(async () => {
39
- capture = captureQueue();
40
- queue = capture.sent;
41
- store = await openStore();
42
- imap = new FakeImapServer();
43
- account = await seedAccount(store);
44
- inbox = await seedMailbox(store, account, "INBOX");
45
- other = await seedMailbox(store, account, "Other");
46
- junk = await seedMailbox(store, account, "Junk", { specialUse: "Junk" });
47
- for (const mailbox of await store.mailbox.listAllByAccount(
48
- account.accountId,
49
- )) {
50
- imap.addFolder(mailbox.fullPath);
51
- }
52
- });
53
-
54
- afterEach(async () => {
55
- capture.restore();
56
- await store.close();
57
- });
58
-
59
- const userMoves = () =>
60
- new MessageMoveService({
61
- messageService: store.message,
62
- mailboxService: store.mailbox,
63
- mailboxSpecialUseService: store.mailboxSpecialUse,
64
- threadMessageService: store.threadMessage,
65
- addressService: store.address,
66
- sqsQueueUrl: "http://localhost:9324/000000000000/remit-messages.fifo",
67
- });
68
-
69
- const spam = () =>
70
- new SpamReportService({
71
- messageService: store.message,
72
- addressService: store.address,
73
- accountService: store.account,
74
- mailboxSpecialUseService: store.mailboxSpecialUse,
75
- messageMoveService: new MessageMoveService({
76
- messageService: store.message,
77
- mailboxService: store.mailbox,
78
- mailboxSpecialUseService: store.mailboxSpecialUse,
79
- threadMessageService: store.threadMessage,
80
- addressService: store.address,
81
- sqsQueueUrl: "http://localhost:9324/000000000000/remit-messages.fifo",
82
- moveSettleTimeoutMs: 30,
83
- moveSettlePollMs: 5,
84
- }),
85
- flagPushService: new FlagPushService({
86
- markerService: store.flagPush,
87
- sqsQueueUrl: "https://sqs.test/flags",
88
- }),
89
- moveSettleTimeoutMs: 30,
90
- moveSettlePollMs: 5,
91
- });
92
-
93
- const arrive = (mailbox: MailboxItem = inbox, from = `Sender <${SENDER}>`) =>
94
- arriveAt(store, imap, account, mailbox, eml({ From: from }));
95
-
96
- const params = (messageId: string) => ({
97
- accountConfigId: account.accountConfigId,
98
- accountId: account.accountId,
99
- messageId,
100
- setBy: "user-1",
101
- });
102
-
103
- const report = (messageId: string) => spam().reportSpam(params(messageId));
104
-
105
- const undo = (messageId: string) => spam().notSpam(params(messageId));
106
-
107
- const sender = (email = SENDER) =>
108
- store.address.getAddress(
109
- account.accountConfigId,
110
- deriveAddressId(account.accountConfigId, email),
111
- );
112
-
113
- const settle = async (messageId: string) => {
114
- const row = await store.message.get(messageId);
115
- await store.message.updateUid(messageId, row.uid + 100, row.mailboxId);
116
- };
117
-
118
- const moves = () => queue.filter((event) => event.body.type === "MESSAGE_MOVE");
119
-
120
- const row = (messageId: string) => store.message.get(messageId);
121
-
122
- describe("SpamReportService.reportSpam", () => {
123
- it("blocks the sender, moves the message to Junk and owes the server $Junk", async () => {
124
- const messageId = await arrive();
125
-
126
- await report(messageId);
127
-
128
- const blocked = (await sender()).flags.blocked;
129
- assert.equal(blocked?.value, true);
130
- assert.equal(blocked?.setBy, "user-1");
131
- const moved = await row(messageId);
132
- assert.equal(moved.mailboxId, junk?.mailboxId);
133
- assert.ok((moved.spamReport?.reportedAt ?? 0) > 0);
134
- assert.equal(moves().length, 1);
135
- assert.equal(
136
- (await store.flagPush.find(messageId, MessageKeywordFlag.Junk))
137
- ?.operation,
138
- "add",
139
- );
140
- });
141
-
142
- it("keeps the block when the move fails", async () => {
143
- const messageId = await arrive();
144
- capture.answer(async () => {
145
- throw new Error("SQS unavailable");
146
- });
147
-
148
- await assert.rejects(report(messageId), /SQS unavailable/);
149
-
150
- assert.equal((await sender()).flags.blocked?.value, true);
151
- });
152
-
153
- it("moves once under a double press, and refuses a press inside the first move's flight", async () => {
154
- const messageId = await arrive();
155
- await report(messageId);
156
-
157
- await assert.rejects(
158
- report(messageId),
159
- (error: unknown) =>
160
- error instanceof MessagePlacementUnsettledError &&
161
- error.publicApiError?.details?.reason === "in_flight",
162
- );
163
- await settle(messageId);
164
- await report(messageId);
165
-
166
- assert.equal(moves().length, 1);
167
- });
168
-
169
- it("moves mail forged from the account's own address without blocking the account", async () => {
170
- const messageId = await arrive(inbox, `Me <${account.email}>`);
171
-
172
- await report(messageId);
173
-
174
- assert.equal((await sender(account.email)).flags.blocked, undefined);
175
- assert.equal((await row(messageId)).mailboxId, junk?.mailboxId);
176
- });
177
-
178
- it("blocks a sender that was never harvested into the address book", async () => {
179
- const messageId = await arrive();
180
- await store.address.deleteAddress(
181
- account.accountConfigId,
182
- deriveAddressId(account.accountConfigId, SENDER),
183
- );
184
-
185
- await report(messageId);
186
-
187
- assert.equal((await sender()).flags.blocked?.value, true);
188
- assert.equal((await row(messageId)).mailboxId, junk?.mailboxId);
189
- });
190
-
191
- it("refuses to mint an address row for a sender with no @", async () => {
192
- const { message } = await seedMessage(store, account, inbox, 9);
193
- await store.address.upsertEnvelopeAddress({
194
- messageId: message.messageId,
195
- addressId: deriveAddressId(account.accountConfigId, "no-at-sign"),
196
- displayName: "",
197
- normalizedEmail: "no-at-sign",
198
- addressRole: AddressRole.From,
199
- addressOrder: 0,
200
- });
201
-
202
- await assert.rejects(
203
- report(message.messageId),
204
- /not a usable email address/,
205
- );
206
-
207
- assert.deepEqual(
208
- await store.address.getAddress(account.accountConfigId, [
209
- deriveAddressId(account.accountConfigId, "no-at-sign"),
210
- ]),
211
- [],
212
- );
213
- });
214
-
215
- it("keeps the junkOnly mark that withholds the sender from autocomplete", async () => {
216
- const messageId = await arrive(junk);
217
-
218
- await report(messageId);
219
-
220
- const flags = (await sender()).flags;
221
- assert.equal(flags.blocked?.value, true);
222
- assert.equal(flags.junkOnly?.value, true);
223
- });
224
-
225
- it("names the missing Junk folder and changes nothing", async () => {
226
- await store.mailboxSpecialUse.deleteByMailboxId(junk?.mailboxId as string);
227
- await store.mailbox.delete(account.accountId, junk?.mailboxId as string);
228
- junk = undefined;
229
- const messageId = await arrive();
230
-
231
- await assert.rejects(
232
- report(messageId),
233
- (error: unknown) =>
234
- error instanceof NoJunkMailboxError &&
235
- /no Junk folder/.test(error.message) &&
236
- /Create one/.test(error.message),
237
- );
238
-
239
- assert.equal((await sender()).flags.blocked, undefined);
240
- const untouched = await row(messageId);
241
- assert.equal(untouched.mailboxId, inbox.mailboxId);
242
- assert.equal(untouched.spamReport, undefined);
243
- assert.equal(queue.length, 0);
244
- });
245
- });
246
-
247
- describe("SpamReportService.notSpam", () => {
248
- it("restores the original folder and lifts the block without granting trust", async () => {
249
- const messageId = await arrive();
250
- await report(messageId);
251
- await settle(messageId);
252
-
253
- await undo(messageId);
254
-
255
- const restored = await row(messageId);
256
- assert.equal(restored.mailboxId, inbox.mailboxId);
257
- assert.equal(restored.spamReport, undefined);
258
- const flags = (await sender()).flags;
259
- assert.equal(flags.blocked, undefined);
260
- assert.equal(flags.wellknown, undefined);
261
- assert.equal(flags.vip, undefined);
262
- });
263
-
264
- it("mints no address row for a sender that was never harvested", async () => {
265
- const messageId = await arrive(junk);
266
- await store.address.deleteAddress(
267
- account.accountConfigId,
268
- deriveAddressId(account.accountConfigId, SENDER),
269
- );
270
-
271
- await undo(messageId);
272
-
273
- assert.deepEqual(
274
- await store.address.getAddress(account.accountConfigId, [
275
- deriveAddressId(account.accountConfigId, SENDER),
276
- ]),
277
- [],
278
- );
279
- });
280
-
281
- it("leaves mail an earlier, unrelated move filed in Junk where it is", async () => {
282
- const messageId = await arrive(other);
283
- await userMoves().moveMessage(
284
- account.accountConfigId,
285
- messageId,
286
- junk?.mailboxId as string,
287
- account.accountId,
288
- );
289
- await settle(messageId);
290
- queue.length = 0;
291
-
292
- await report(messageId);
293
- await undo(messageId);
294
-
295
- assert.equal((await row(messageId)).mailboxId, junk?.mailboxId);
296
- assert.equal((await sender()).flags.blocked, undefined);
297
- });
298
-
299
- it("still restores after a second report, and a second undo moves nothing", async () => {
300
- const messageId = await arrive();
301
- await report(messageId);
302
- await settle(messageId);
303
- await report(messageId);
304
-
305
- await undo(messageId);
306
- await undo(messageId);
307
-
308
- const restored = await row(messageId);
309
- assert.equal(restored.mailboxId, inbox.mailboxId);
310
- assert.equal(restored.originalMailboxId, undefined);
311
- assert.equal(moves().length, 2);
312
- });
313
-
314
- it("refuses to restore before the move settled, but still lifts the block", async () => {
315
- const messageId = await arrive();
316
- await report(messageId);
317
-
318
- await assert.rejects(undo(messageId), (error: unknown) => {
319
- assert.ok(error instanceof MoveNotSettledError);
320
- assert.match(error.message, /has not settled yet/);
321
- return true;
322
- });
323
-
324
- assert.equal((await sender()).flags.blocked, undefined);
325
- const unmoved = await row(messageId);
326
- assert.equal(unmoved.mailboxId, junk?.mailboxId);
327
- assert.ok((unmoved.spamReport?.reportedAt ?? 0) > 0);
328
- });
329
- });
@@ -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
- }