@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,508 +0,0 @@
1
- import { randomUUID } from "node:crypto";
2
- import { SendMessageCommand, type SQSClient } from "@aws-sdk/client-sqs";
3
- import type {
4
- IAccountRepository,
5
- IOutboxMessageRepository,
6
- OutboxMessageItem,
7
- } from "@remit/data-ports";
8
- import {
9
- BadRequestError,
10
- ConflictError,
11
- MailSyncOffError,
12
- } from "@remit/data-ports/errors";
13
- import { AccountService, OutboxMessageStatus } from "@remit/domain-enums";
14
- import { createQueueProducer } from "@remit/sqs-client/producer";
15
- import type { OutboxAttachmentService } from "./outbox-attachment.js";
16
- import { isOpenForWork } from "./outbox-status.js";
17
-
18
- interface SendMessageEvent {
19
- type: "SEND_MESSAGE";
20
- eventId: string;
21
- timestamp: number;
22
- accountId: string;
23
- outboxMessageId: string;
24
- }
25
-
26
- export interface OutboxQueueLogger {
27
- info(obj: Record<string, unknown>, msg: string): void;
28
- error(obj: Record<string, unknown>, msg: string): void;
29
- }
30
-
31
- const noopLogger: OutboxQueueLogger = {
32
- info: () => {},
33
- error: () => {},
34
- };
35
-
36
- export interface OutboxQueueConfig {
37
- outboxMessageService: IOutboxMessageRepository;
38
- outboxAttachmentService: OutboxAttachmentService;
39
- accountService: IAccountRepository;
40
- sqsSmtpQueueUrl: string;
41
- sqsEndpoint?: string;
42
- sqsClient?: SQSClient;
43
- logger?: OutboxQueueLogger;
44
- }
45
-
46
- export interface CreateDraftInput {
47
- accountId: string;
48
- accountConfigId: string;
49
- fromAddress: string;
50
- fromName?: string;
51
- toAddresses: string[];
52
- ccAddresses?: string[];
53
- bccAddresses?: string[];
54
- subject?: string;
55
- textBody?: string;
56
- htmlBody?: string;
57
- inReplyTo?: string;
58
- references?: string[];
59
- }
60
-
61
- export interface UpdateDraftInput {
62
- toAddresses?: string[];
63
- ccAddresses?: string[];
64
- bccAddresses?: string[];
65
- subject?: string;
66
- textBody?: string;
67
- htmlBody?: string;
68
- inReplyTo?: string;
69
- references?: string[];
70
- }
71
-
72
- /**
73
- * Nobody to send to. `@minItems(1)` on the create input is the only thing that
74
- * has ever stood between a zero-recipient message and nodemailer, which refuses
75
- * an empty envelope — and refuses it inside the SMTP worker, where the failure
76
- * lands in the DLQ rather than in front of the person who pressed Send. Neither
77
- * route into the queue passes that schema: a draft is created with a recipient
78
- * and can be edited down to none, and `send` never sees the create input at all.
79
- *
80
- * Cc and Bcc count. A message addressed only in Bcc is a real message with a
81
- * real envelope; only a message with no address anywhere has nowhere to go.
82
- */
83
- const hasNowhereToGo = (message: {
84
- toAddresses?: string[];
85
- ccAddresses?: string[];
86
- bccAddresses?: string[];
87
- }): boolean =>
88
- (message.toAddresses?.length ?? 0) === 0 &&
89
- (message.ccAddresses?.length ?? 0) === 0 &&
90
- (message.bccAddresses?.length ?? 0) === 0;
91
-
92
- const NO_RECIPIENT_MESSAGE =
93
- "This message has nobody to send to. Add a recipient before sending it.";
94
-
95
- const MAIL_SYNC_OFF_SEND_MESSAGE =
96
- "Mail is turned off for this account, so it cannot send. Turn Mail back on for this account in Settings to send from it.";
97
-
98
- const ENQUEUE_FAILED_MESSAGE =
99
- "This message could not be handed to the outgoing queue, so it was not sent. Send it again.";
100
-
101
- const MOVED_WHILE_EDITING_MESSAGE =
102
- "This message started sending while it was being edited, so the change was not saved. Open the Outbox to see where it stands.";
103
-
104
- const MOVED_WHILE_SENDING_MESSAGE =
105
- "This message already left the queue and cannot be sent again. Open the Outbox to see where it stands.";
106
-
107
- /**
108
- * Where a row goes when the enqueue reports a failure — never `draft`, and
109
- * never back to `draft` where it came from.
110
- *
111
- * A throw from `SendMessage` does not prove the broker refused the event. This
112
- * queue is not FIFO on the send path and carries no deduplication id, so a
113
- * timeout or a lost response leaves an event that may still be delivered. And
114
- * `draft` is inside the worker's send fence (`SENDABLE_STATUSES`, smtp-worker
115
- * `send-message-core.ts`): a row put back there is sendable by the landed event
116
- * and by the user pressing Send, which is two copies of one message. `failed`
117
- * is outside that fence, so the landed event is dropped on arrival, and a
118
- * `failed` row is editable and sendable by hand (#933) — the recovery survives.
119
- *
120
- * `blocked` is outside the fence too and names a cause the enqueue did not
121
- * change, so a row that came from there goes back to it.
122
- */
123
- const settledStatusFor = (
124
- priorStatus: OutboxMessageItem["status"],
125
- ): OutboxMessageItem["status"] =>
126
- priorStatus === OutboxMessageStatus.blocked
127
- ? OutboxMessageStatus.blocked
128
- : OutboxMessageStatus.failed;
129
-
130
- const generateMessageId = (domain: string): string => {
131
- const timestamp = Date.now();
132
- const random = randomUUID().replace(/-/g, "").slice(0, 16);
133
- return `${timestamp}.${random}@${domain}`;
134
- };
135
-
136
- const extractDomain = (email: string): string => {
137
- const atIndex = email.lastIndexOf("@");
138
- if (atIndex === -1) return "localhost";
139
- return email.slice(atIndex + 1);
140
- };
141
-
142
- export class OutboxQueueService {
143
- private outboxMessageService: IOutboxMessageRepository;
144
- private outboxAttachmentService: OutboxAttachmentService;
145
- private accountService: IAccountRepository;
146
- private sqs: SQSClient;
147
- private queueUrl: string;
148
- private log: OutboxQueueLogger;
149
-
150
- constructor(config: OutboxQueueConfig) {
151
- const {
152
- outboxMessageService,
153
- outboxAttachmentService,
154
- accountService,
155
- sqsSmtpQueueUrl,
156
- sqsEndpoint,
157
- } = config;
158
- this.outboxMessageService = outboxMessageService;
159
- this.outboxAttachmentService = outboxAttachmentService;
160
- this.accountService = accountService;
161
- this.queueUrl = sqsSmtpQueueUrl;
162
- this.log = config.logger ?? noopLogger;
163
-
164
- this.sqs =
165
- config.sqsClient ??
166
- createQueueProducer({
167
- queueUrl: sqsSmtpQueueUrl,
168
- endpoint: sqsEndpoint,
169
- });
170
- }
171
-
172
- createDraft = async (input: CreateDraftInput): Promise<OutboxMessageItem> => {
173
- const domain = extractDomain(input.fromAddress);
174
- const messageIdValue = generateMessageId(domain);
175
-
176
- const outbox = await this.outboxMessageService.create({
177
- accountId: input.accountId,
178
- accountConfigId: input.accountConfigId,
179
- fromAddress: input.fromAddress,
180
- fromName: input.fromName,
181
- toAddresses: input.toAddresses,
182
- ccAddresses: input.ccAddresses,
183
- bccAddresses: input.bccAddresses,
184
- subject: input.subject,
185
- textBody: input.textBody,
186
- htmlBody: input.htmlBody,
187
- inReplyTo: input.inReplyTo,
188
- references: input.references,
189
- messageIdValue,
190
- status: OutboxMessageStatus.draft,
191
- });
192
-
193
- this.log.info(
194
- { outboxMessageId: outbox.outboxMessageId, accountId: input.accountId },
195
- "Created draft outbox message",
196
- );
197
-
198
- return outbox;
199
- };
200
-
201
- updateDraft = async (
202
- accountConfigId: string,
203
- outboxMessageId: string,
204
- input: UpdateDraftInput,
205
- ): Promise<OutboxMessageItem> => {
206
- const existing = await this.outboxMessageService.get(
207
- accountConfigId,
208
- outboxMessageId,
209
- "act",
210
- );
211
- if (!isOpenForWork(existing.status)) {
212
- throw new ConflictError(
213
- `This message is already ${existing.status} and can no longer be edited as a draft. Start a new message to change it.`,
214
- );
215
- }
216
-
217
- // Editing a settled failure returns the row to `draft`: it is no longer the
218
- // message that failed, and the Outbox renders a `failed` row with its
219
- // `lastError` — a failure reported against text that never went out. The
220
- // same row moves to Drafts carrying its content, recipients and
221
- // attachments, so there is no copy to reconcile.
222
- //
223
- // Conditional on the status this decision was read from. A concurrent send
224
- // can move the row to `queued` between the two, and an unconditional write
225
- // would pull it back to `draft` with its event already on the wire — the
226
- // worker's fence takes `draft`, so that row goes out and stays sendable.
227
- const updated = await this.outboxMessageService.updateIfStatus(
228
- accountConfigId,
229
- outboxMessageId,
230
- existing.status,
231
- {
232
- ...(existing.status !== OutboxMessageStatus.draft && {
233
- status: OutboxMessageStatus.draft,
234
- }),
235
- ...(input.toAddresses !== undefined && {
236
- toAddresses: input.toAddresses,
237
- }),
238
- ...(input.ccAddresses !== undefined && {
239
- ccAddresses: input.ccAddresses,
240
- }),
241
- ...(input.bccAddresses !== undefined && {
242
- bccAddresses: input.bccAddresses,
243
- }),
244
- ...(input.subject !== undefined && { subject: input.subject }),
245
- ...(input.textBody !== undefined && { textBody: input.textBody }),
246
- ...(input.htmlBody !== undefined && { htmlBody: input.htmlBody }),
247
- ...(input.inReplyTo !== undefined && { inReplyTo: input.inReplyTo }),
248
- ...(input.references !== undefined && {
249
- references: input.references,
250
- }),
251
- },
252
- );
253
- if (!updated) throw new ConflictError(MOVED_WHILE_EDITING_MESSAGE);
254
-
255
- this.log.info({ outboxMessageId }, "Updated draft outbox message");
256
-
257
- return updated;
258
- };
259
-
260
- private refuseWithoutMail = async (accountId: string): Promise<void> => {
261
- const account = await this.accountService.get(accountId);
262
- if (account.syncedServices.includes(AccountService.Mail)) return;
263
- throw new MailSyncOffError(MAIL_SYNC_OFF_SEND_MESSAGE, accountId);
264
- };
265
-
266
- send = async (
267
- accountConfigId: string,
268
- outboxMessageId: string,
269
- ): Promise<OutboxMessageItem> => {
270
- const existing = await this.outboxMessageService.get(
271
- accountConfigId,
272
- outboxMessageId,
273
- "act",
274
- );
275
- if (!isOpenForWork(existing.status)) {
276
- throw new ConflictError(
277
- `This message is already ${existing.status} and cannot be sent again. Open the Outbox to see where it stands.`,
278
- );
279
- }
280
-
281
- if (hasNowhereToGo(existing)) {
282
- throw new BadRequestError(NO_RECIPIENT_MESSAGE);
283
- }
284
- await this.refuseWithoutMail(existing.accountId);
285
-
286
- const unfinished = await this.outboxAttachmentService.unfinishedUpload(
287
- accountConfigId,
288
- outboxMessageId,
289
- );
290
- if (unfinished) {
291
- throw new BadRequestError(
292
- `"${unfinished.filename}" has not finished uploading. Wait for it, or remove it, and send again.`,
293
- );
294
- }
295
-
296
- // Conditional on the status this send was decided against, so two presses —
297
- // or a press racing the worker — produce one queued row and one conflict
298
- // rather than two events for the same message.
299
- const updated = await this.outboxMessageService.updateIfStatus(
300
- accountConfigId,
301
- outboxMessageId,
302
- existing.status,
303
- { status: OutboxMessageStatus.queued },
304
- );
305
- if (!updated) throw new ConflictError(MOVED_WHILE_SENDING_MESSAGE);
306
-
307
- // A reservation made from another tab between the check above and the
308
- // move to `queued` got past it; one made after the move is refused, so a
309
- // second look now is the last word.
310
- const lateUpload = await this.outboxAttachmentService.unfinishedUpload(
311
- accountConfigId,
312
- outboxMessageId,
313
- );
314
- if (lateUpload) {
315
- await this.outboxMessageService.updateIfStatus(
316
- accountConfigId,
317
- outboxMessageId,
318
- OutboxMessageStatus.queued,
319
- { status: existing.status },
320
- );
321
- throw new BadRequestError(
322
- `"${lateUpload.filename}" has not finished uploading. Wait for it, or remove it, and send again.`,
323
- );
324
- }
325
-
326
- // `queued` is a dead end for a row the queue never accepted: `send` takes
327
- // draft, failed and blocked, `deleteDraft` those three plus unfiled, so a
328
- // row parked at `queued` by a failed enqueue is neither sendable nor
329
- // discardable (#845.8). Settle it and let the enqueue failure surface —
330
- // the row stays reachable, the caller still hears no.
331
- await this.enqueueSend(existing.accountId, outboxMessageId).catch(
332
- async (error: unknown) => {
333
- await this.settleUnqueued(
334
- accountConfigId,
335
- outboxMessageId,
336
- existing.status,
337
- );
338
- throw error;
339
- },
340
- );
341
-
342
- this.log.info(
343
- { outboxMessageId, accountId: existing.accountId },
344
- "Queued outbox message for sending",
345
- );
346
-
347
- return updated;
348
- };
349
-
350
- createAndSend = async (
351
- input: CreateDraftInput,
352
- ): Promise<OutboxMessageItem> => {
353
- if (hasNowhereToGo(input)) {
354
- throw new BadRequestError(NO_RECIPIENT_MESSAGE);
355
- }
356
- await this.refuseWithoutMail(input.accountId);
357
-
358
- const domain = extractDomain(input.fromAddress);
359
- const messageIdValue = generateMessageId(domain);
360
-
361
- const outbox = await this.outboxMessageService.create({
362
- accountId: input.accountId,
363
- accountConfigId: input.accountConfigId,
364
- fromAddress: input.fromAddress,
365
- fromName: input.fromName,
366
- toAddresses: input.toAddresses,
367
- ccAddresses: input.ccAddresses,
368
- bccAddresses: input.bccAddresses,
369
- subject: input.subject,
370
- textBody: input.textBody,
371
- htmlBody: input.htmlBody,
372
- inReplyTo: input.inReplyTo,
373
- references: input.references,
374
- messageIdValue,
375
- status: OutboxMessageStatus.queued,
376
- });
377
-
378
- // Same dead end as in `send`: a row parked at `queued` by an enqueue that
379
- // threw is neither sendable nor discardable (#845.8, #931, #936). This row
380
- // is new, so there is no prior status — it settles at `failed`, carrying
381
- // the reason, which keeps the composed text editable and sendable while
382
- // staying outside the fence a landed event has to pass.
383
- await this.enqueueSend(input.accountId, outbox.outboxMessageId).catch(
384
- async (error: unknown) => {
385
- await this.settleUnqueued(
386
- input.accountConfigId,
387
- outbox.outboxMessageId,
388
- OutboxMessageStatus.failed,
389
- );
390
- throw error;
391
- },
392
- );
393
-
394
- this.log.info(
395
- { outboxMessageId: outbox.outboxMessageId, accountId: input.accountId },
396
- "Created and queued outbox message for sending",
397
- );
398
-
399
- return outbox;
400
- };
401
-
402
- deleteDraft = async (
403
- accountConfigId: string,
404
- outboxMessageId: string,
405
- ): Promise<void> => {
406
- const existing = await this.outboxMessageService.get(
407
- accountConfigId,
408
- outboxMessageId,
409
- "act",
410
- );
411
- // `unfiled` is discardable: the message was delivered but never landed in
412
- // a Sent folder, so this row is the only copy the user has and dismissing
413
- // it is their acknowledgement.
414
- if (
415
- existing.status !== OutboxMessageStatus.draft &&
416
- existing.status !== OutboxMessageStatus.failed &&
417
- existing.status !== OutboxMessageStatus.blocked &&
418
- existing.status !== OutboxMessageStatus.unfiled
419
- ) {
420
- throw new ConflictError(
421
- `This message is already ${existing.status} and can no longer be discarded. Open the Outbox to see where it stands.`,
422
- );
423
- }
424
-
425
- // Files first, row second. Nothing but this row points at those objects,
426
- // so a row deleted ahead of a sweep that then fails leaves bytes no one
427
- // can reach; the other order leaves a draft whose files are gone, which
428
- // is at least visible. A storage failure aborts the discard outright.
429
- await this.outboxAttachmentService.discardAll(
430
- accountConfigId,
431
- existing.accountId,
432
- outboxMessageId,
433
- );
434
-
435
- await this.outboxMessageService.delete(accountConfigId, outboxMessageId);
436
-
437
- this.log.info({ outboxMessageId }, "Deleted outbox message");
438
- };
439
-
440
- /**
441
- * Take a row back out of `queued` after the enqueue reported a failure.
442
- *
443
- * Conditional on `queued`, because the event may have landed regardless — an
444
- * error from `SendMessage` says the response was lost, not that the broker
445
- * refused it. If the worker already has the row, it holds the newer truth and
446
- * this write does nothing.
447
- *
448
- * Never rejects. The enqueue failure is what went wrong and what the caller
449
- * has to hear; a settle that also fails would replace it with the wrong
450
- * cause, so it is logged and the row is left where #936 found it — which the
451
- * caller's error at least names.
452
- */
453
- private settleUnqueued = async (
454
- accountConfigId: string,
455
- outboxMessageId: string,
456
- priorStatus: OutboxMessageItem["status"],
457
- ): Promise<void> => {
458
- const status = settledStatusFor(priorStatus);
459
- await this.outboxMessageService
460
- .updateIfStatus(
461
- accountConfigId,
462
- outboxMessageId,
463
- OutboxMessageStatus.queued,
464
- {
465
- status,
466
- ...(status === OutboxMessageStatus.failed && {
467
- lastError: ENQUEUE_FAILED_MESSAGE,
468
- }),
469
- },
470
- )
471
- .catch((settleError: unknown) => {
472
- this.log.error(
473
- { outboxMessageId, settleError: String(settleError) },
474
- "Could not settle an outbox message the queue refused",
475
- );
476
- });
477
- };
478
-
479
- private enqueueSend = async (
480
- accountId: string,
481
- outboxMessageId: string,
482
- ): Promise<void> => {
483
- const event: SendMessageEvent = {
484
- type: "SEND_MESSAGE",
485
- eventId: randomUUID(),
486
- timestamp: Date.now(),
487
- accountId,
488
- outboxMessageId,
489
- };
490
-
491
- const useFifo = this.queueUrl.endsWith(".fifo");
492
- await this.sqs.send(
493
- new SendMessageCommand({
494
- QueueUrl: this.queueUrl,
495
- MessageBody: JSON.stringify(event),
496
- ...(useFifo && {
497
- MessageGroupId: accountId,
498
- MessageDeduplicationId: event.eventId,
499
- }),
500
- }),
501
- );
502
-
503
- this.log.info(
504
- { eventId: event.eventId, accountId, outboxMessageId },
505
- "Enqueued SEND_MESSAGE event",
506
- );
507
- };
508
- }
@@ -1,17 +0,0 @@
1
- import type { IUnitOfWork, UnitOfWorkRepositories } from "@remit/data-ports";
2
-
3
- /**
4
- * Runs the write set against fixed repositories with no surrounding
5
- * transaction: the writes are not atomic. For a backend that has no
6
- * cross-entity transaction this matches its own guarantees; the SQLite path
7
- * injects a real transactional unit of work instead.
8
- */
9
- export class PassThroughUnitOfWork implements IUnitOfWork {
10
- constructor(private repos: UnitOfWorkRepositories) {}
11
-
12
- transaction<T>(
13
- fn: (repos: UnitOfWorkRepositories) => Promise<T>,
14
- ): Promise<T> {
15
- return fn(this.repos);
16
- }
17
- }