@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.
- package/dist/account-credentials.d.ts +83 -0
- package/dist/account-credentials.js +104 -0
- package/dist/attribute-mapper.d.ts +36 -0
- package/dist/attribute-mapper.js +119 -0
- package/dist/body-parse.d.ts +28 -0
- package/{src/body-parse.ts → dist/body-parse.js} +18 -26
- package/dist/body-part-mapper.d.ts +92 -0
- package/dist/body-part-mapper.js +301 -0
- package/dist/body-sync-queue.d.ts +59 -0
- package/dist/body-sync-queue.js +56 -0
- package/dist/body-sync-terminal.d.ts +58 -0
- package/dist/body-sync-terminal.js +95 -0
- package/dist/body-sync.d.ts +351 -0
- package/dist/body-sync.js +1204 -0
- package/dist/calendar-parts.d.ts +23 -0
- package/dist/calendar-parts.js +18 -0
- package/dist/classification-backfill.d.ts +91 -0
- package/dist/classification-backfill.js +180 -0
- package/dist/classify-message.d.ts +31 -0
- package/dist/classify-message.js +89 -0
- package/dist/connection-factory.d.ts +54 -0
- package/dist/connection-factory.js +54 -0
- package/dist/connection-test.d.ts +41 -0
- package/dist/connection-test.js +152 -0
- package/dist/external-move.d.ts +94 -0
- package/dist/external-move.js +173 -0
- package/dist/filter-operations.d.ts +42 -0
- package/dist/filter-operations.js +211 -0
- package/dist/filters/filter-config.d.ts +14 -0
- package/dist/filters/filter-config.js +10 -0
- package/{src/filters/list-id.ts → dist/filters/list-id.d.ts} +2 -16
- package/dist/filters/list-id.js +26 -0
- package/dist/filters/match.d.ts +69 -0
- package/dist/filters/match.js +165 -0
- package/dist/filters/pipeline.d.ts +64 -0
- package/dist/filters/pipeline.js +100 -0
- package/{src/flag-push-terminal.ts → dist/flag-push-terminal.d.ts} +19 -77
- package/dist/flag-push-terminal.js +63 -0
- package/dist/flag-push.d.ts +69 -0
- package/dist/flag-push.js +111 -0
- package/dist/flag-queue.d.ts +175 -0
- package/dist/flag-queue.js +291 -0
- package/dist/folder-operations.d.ts +39 -0
- package/dist/folder-operations.js +112 -0
- package/dist/folder-placement.d.ts +13 -0
- package/dist/folder-placement.js +23 -0
- package/dist/heuristics/authenticationResults.d.ts +10 -0
- package/dist/heuristics/authenticationResults.js +187 -0
- package/dist/heuristics/classifyByHeaders.d.ts +82 -0
- package/dist/heuristics/classifyByHeaders.js +365 -0
- package/dist/heuristics/classifyPlacement.d.ts +67 -0
- package/dist/heuristics/classifyPlacement.js +153 -0
- package/dist/heuristics/machineSenders.d.ts +21 -0
- package/{src/heuristics/machineSenders.ts → dist/heuristics/machineSenders.js} +34 -40
- package/dist/heuristics/senderMismatch.d.ts +81 -0
- package/dist/heuristics/senderMismatch.js +192 -0
- package/dist/heuristics/socialDomains.d.ts +14 -0
- package/{src/heuristics/socialDomains.ts → dist/heuristics/socialDomains.js} +9 -9
- package/dist/heuristics/transactionalDomains.d.ts +18 -0
- package/{src/heuristics/transactionalDomains.ts → dist/heuristics/transactionalDomains.js} +8 -8
- package/dist/imapflow-connection.d.ts +377 -0
- package/dist/imapflow-connection.js +1147 -0
- package/dist/index.d.ts +63 -0
- package/dist/index.js +65 -0
- package/dist/list-id-backfill.d.ts +83 -0
- package/dist/list-id-backfill.js +132 -0
- package/dist/mailbox-cursor-rebuild.d.ts +74 -0
- package/dist/mailbox-cursor-rebuild.js +71 -0
- package/{src/mailbox-cursor.ts → dist/mailbox-cursor.d.ts} +20 -98
- package/dist/mailbox-cursor.js +102 -0
- package/dist/mailbox-intent.d.ts +41 -0
- package/{src/mailbox-intent.ts → dist/mailbox-intent.js} +39 -63
- package/dist/mailbox-management.d.ts +240 -0
- package/dist/mailbox-management.js +576 -0
- package/dist/mailbox-presence.d.ts +52 -0
- package/{src/mailbox-presence.ts → dist/mailbox-presence.js} +19 -30
- package/dist/mailbox-queue.d.ts +126 -0
- package/dist/mailbox-queue.js +214 -0
- package/dist/mailbox-sync.d.ts +93 -0
- package/dist/mailbox-sync.js +490 -0
- package/dist/mailbox-upstream.d.ts +38 -0
- package/{src/mailbox-upstream.ts → dist/mailbox-upstream.js} +37 -47
- package/dist/message-category.d.ts +51 -0
- package/{src/message-category.ts → dist/message-category.js} +28 -71
- package/dist/message-move.d.ts +285 -0
- package/dist/message-move.js +858 -0
- package/dist/message-parser.d.ts +14 -0
- package/dist/message-parser.js +17 -0
- package/{src/message-presence.ts → dist/message-presence.d.ts} +1 -13
- package/dist/message-presence.js +29 -0
- package/dist/message-read.d.ts +51 -0
- package/dist/message-read.js +31 -0
- package/dist/message-sync.d.ts +345 -0
- package/dist/message-sync.js +1358 -0
- package/dist/mime-walker.d.ts +102 -0
- package/dist/mime-walker.js +198 -0
- package/dist/organize-job.d.ts +20 -0
- package/dist/organize-job.js +37 -0
- package/dist/outbox-attachment-content.d.ts +37 -0
- package/dist/outbox-attachment-content.js +50 -0
- package/dist/outbox-attachment-filename.d.ts +19 -0
- package/dist/outbox-attachment-filename.js +64 -0
- package/dist/outbox-attachment.d.ts +148 -0
- package/dist/outbox-attachment.js +273 -0
- package/dist/outbox-queue.d.ts +73 -0
- package/dist/outbox-queue.js +284 -0
- package/{src/outbox-status.ts → dist/outbox-status.d.ts} +1 -6
- package/dist/outbox-status.js +18 -0
- package/dist/ownership.d.ts +24 -0
- package/dist/ownership.js +83 -0
- package/dist/placement-move-terminal.d.ts +67 -0
- package/dist/placement-move-terminal.js +88 -0
- package/dist/placement-move.d.ts +83 -0
- package/dist/placement-move.js +216 -0
- package/{src/placement-settled.ts → dist/placement-settled.d.ts} +19 -50
- package/dist/placement-settled.js +75 -0
- package/dist/quarantine.d.ts +95 -0
- package/dist/quarantine.js +131 -0
- package/dist/refusal.d.ts +31 -0
- package/dist/refusal.js +3 -0
- package/{src/restore-source-placement.ts → dist/restore-source-placement.d.ts} +29 -101
- package/dist/restore-source-placement.js +79 -0
- package/dist/search-text-backfill.d.ts +15 -0
- package/dist/search-text-backfill.js +33 -0
- package/dist/snippet.d.ts +61 -0
- package/dist/snippet.js +129 -0
- package/dist/spam-report.d.ts +86 -0
- package/dist/spam-report.js +241 -0
- package/dist/sync-warning.d.ts +21 -0
- package/dist/sync-warning.js +26 -0
- package/dist/sync-watermarks.d.ts +138 -0
- package/dist/sync-watermarks.js +205 -0
- package/dist/test-helpers/fake-imap.d.ts +151 -0
- package/dist/test-helpers/fake-imap.js +508 -0
- package/dist/test-helpers/folder-roles.d.ts +34 -0
- package/dist/test-helpers/folder-roles.js +47 -0
- package/dist/test-helpers/mime-tree-generator.d.ts +82 -0
- package/dist/test-helpers/mime-tree-generator.js +329 -0
- package/dist/test-helpers/test-store.d.ts +69 -0
- package/dist/test-helpers/test-store.js +193 -0
- package/dist/text/normalizer.d.ts +20 -0
- package/dist/text/normalizer.js +102 -0
- package/dist/thread-message-rows.d.ts +69 -0
- package/dist/thread-message-rows.js +40 -0
- package/dist/thread-root.d.ts +7 -0
- package/dist/thread-root.js +25 -0
- package/dist/types.d.ts +343 -0
- package/dist/types.js +19 -0
- package/package.json +86 -73
- package/src/account-credentials.ts +0 -210
- package/src/adversarial-mime.e2e.test.ts +0 -307
- package/src/attribute-mapper.test.ts +0 -104
- package/src/attribute-mapper.ts +0 -148
- package/src/body-parse.test.ts +0 -55
- package/src/body-part-mapper.property.test.ts +0 -354
- package/src/body-part-mapper.ts +0 -360
- package/src/body-sync-auto-read-writeonce.test.ts +0 -219
- package/src/body-sync-calendar-suggestion.test.ts +0 -433
- package/src/body-sync-category-denormalize.test.ts +0 -352
- package/src/body-sync-category-override.test.ts +0 -501
- package/src/body-sync-category-writeonce.test.ts +0 -234
- package/src/body-sync-filter.test.ts +0 -237
- package/src/body-sync-message-gone.test.ts +0 -139
- package/src/body-sync-placement-flags.test.ts +0 -347
- package/src/body-sync-placement-writeonce.test.ts +0 -337
- package/src/body-sync-quarantine.test.ts +0 -276
- package/src/body-sync-queue.test.ts +0 -124
- package/src/body-sync-queue.ts +0 -131
- package/src/body-sync-resync.test.ts +0 -605
- package/src/body-sync-terminal.test.ts +0 -310
- package/src/body-sync-terminal.ts +0 -176
- package/src/body-sync-unclassifiable-once.test.ts +0 -260
- package/src/body-sync-unsubscribed-auto-read.test.ts +0 -176
- package/src/body-sync.ts +0 -1970
- package/src/calendar-parts.ts +0 -41
- package/src/classification-backfill.test.ts +0 -615
- package/src/classification-backfill.ts +0 -363
- package/src/classify-message.test.ts +0 -205
- package/src/classify-message.ts +0 -116
- package/src/connection-factory.ts +0 -102
- package/src/connection-test.ts +0 -208
- package/src/credentials.test.ts +0 -181
- package/src/external-move.test.ts +0 -369
- package/src/external-move.ts +0 -272
- package/src/filters/anchor-drift.ts +0 -55
- package/src/filters/filter-config.test.ts +0 -229
- package/src/filters/filter-config.ts +0 -69
- package/src/filters/list-id.test.ts +0 -50
- package/src/filters/match.test.ts +0 -429
- package/src/filters/match.ts +0 -274
- package/src/filters/message-embedder.test.ts +0 -39
- package/src/filters/message-embedder.ts +0 -33
- package/src/filters/pipeline.test.ts +0 -330
- package/src/filters/pipeline.ts +0 -284
- package/src/flag-push-terminal.test.ts +0 -209
- package/src/flag-push.test.ts +0 -408
- package/src/flag-push.ts +0 -198
- package/src/flag-queue-stars.test.ts +0 -171
- package/src/flag-queue.ts +0 -509
- package/src/folder-placement.ts +0 -56
- package/src/heuristics/authenticationResults.test.ts +0 -189
- package/src/heuristics/authenticationResults.ts +0 -212
- package/src/heuristics/classifyByHeaders.test.ts +0 -889
- package/src/heuristics/classifyByHeaders.ts +0 -467
- package/src/heuristics/classifyPlacement.test.ts +0 -366
- package/src/heuristics/classifyPlacement.ts +0 -211
- package/src/heuristics/classifyRealMail.test.ts +0 -242
- package/src/heuristics/machineSenders.test.ts +0 -101
- package/src/heuristics/senderMismatch.test.ts +0 -346
- package/src/heuristics/senderMismatch.ts +0 -259
- package/src/imapflow-connect-retry.test.ts +0 -134
- package/src/imapflow-connection.e2e.test.ts +0 -335
- package/src/imapflow-connection.integ.test.ts +0 -316
- package/src/imapflow-connection.test.ts +0 -469
- package/src/imapflow-connection.ts +0 -1475
- package/src/index.ts +0 -350
- package/src/list-id-backfill.test.ts +0 -573
- package/src/list-id-backfill.ts +0 -274
- package/src/mailbox-cursor-rebuild.test.ts +0 -171
- package/src/mailbox-cursor-rebuild.ts +0 -146
- package/src/mailbox-cursor.test.ts +0 -249
- package/src/mailbox-delete-intent.test.ts +0 -246
- package/src/mailbox-management.test.ts +0 -356
- package/src/mailbox-management.ts +0 -839
- package/src/mailbox-presence.test.ts +0 -72
- package/src/mailbox-queue.ts +0 -415
- package/src/mailbox-rename-intent.test.ts +0 -389
- package/src/mailbox-rename-subtree.test.ts +0 -664
- package/src/mailbox-sync.test.ts +0 -848
- package/src/mailbox-sync.ts +0 -724
- package/src/message-delete-junk-reconcile.test.ts +0 -117
- package/src/message-delete-no-trash.test.ts +0 -245
- package/src/message-delete-unsettled-placement.test.ts +0 -396
- package/src/message-move-copy.test.ts +0 -562
- package/src/message-move-empty-trash.test.ts +0 -326
- package/src/message-move-same-mailbox.test.ts +0 -139
- package/src/message-move-unsettled-placement.test.ts +0 -378
- package/src/message-move.ts +0 -1348
- package/src/message-parser.ts +0 -26
- package/src/message-presence.test.ts +0 -50
- package/src/message-sync-address-spoof.test.ts +0 -293
- package/src/message-sync-category.test.ts +0 -142
- package/src/message-sync-changedsince.test.ts +0 -840
- package/src/message-sync-flags.test.ts +0 -158
- package/src/message-sync-junk-harvest.test.ts +0 -244
- package/src/message-sync-quarantine.test.ts +0 -416
- package/src/message-sync-repoint.test.ts +0 -419
- package/src/message-sync.ts +0 -1963
- package/src/mime-walker.test.ts +0 -311
- package/src/mime-walker.ts +0 -305
- package/src/outbox-attachment-content.test.ts +0 -153
- package/src/outbox-attachment-content.ts +0 -104
- package/src/outbox-attachment-filename.test.ts +0 -132
- package/src/outbox-attachment-filename.ts +0 -76
- package/src/outbox-attachment.test.ts +0 -738
- package/src/outbox-attachment.ts +0 -536
- package/src/outbox-failed-recovery.test.ts +0 -370
- package/src/outbox-queue-settled-row.test.ts +0 -140
- package/src/outbox-queue.test.ts +0 -291
- package/src/outbox-queue.ts +0 -508
- package/src/pass-through-unit-of-work.ts +0 -17
- package/src/placement-move-terminal.test.ts +0 -300
- package/src/placement-move-terminal.ts +0 -148
- package/src/placement-move-unsettled.test.ts +0 -275
- package/src/placement-move.ts +0 -336
- package/src/placement-settled.test.ts +0 -147
- package/src/placement-transition-race.test.ts +0 -482
- package/src/quarantine.test.ts +0 -191
- package/src/quarantine.ts +0 -228
- package/src/snippet.test.ts +0 -213
- package/src/snippet.ts +0 -152
- package/src/spam-report.test.ts +0 -801
- package/src/spam-report.ts +0 -357
- package/src/stale-message-reconcile.test.ts +0 -82
- package/src/stale-message-reconcile.ts +0 -55
- package/src/sync-watermarks.test.ts +0 -409
- package/src/sync-watermarks.ts +0 -319
- package/src/system-flag-wire-format.test.ts +0 -77
- package/src/test-helpers/folder-roles.ts +0 -66
- package/src/test-helpers/isolated-mailbox.ts +0 -44
- package/src/test-helpers/mailfuzz-connection.ts +0 -27
- package/src/test-helpers/mime-tree-generator.ts +0 -461
- package/src/text/normalizer.test.ts +0 -223
- package/src/text/normalizer.ts +0 -140
- package/src/thread-message-rows.ts +0 -77
- package/src/types/mailparser-augment.d.ts +0 -15
- package/src/types/natural-porter-stemmer.d.ts +0 -50
- package/src/types/stopword.d.ts +0 -69
- package/src/types.ts +0 -371
- package/test/fixtures/mime-adversarial-e2e/01-octet-stream-pdf.eml +0 -21
- package/test/fixtures/mime-adversarial-e2e/02-nested-related-cid-image.eml +0 -31
- package/test/fixtures/mime-adversarial-e2e/03-calendar-invite.eml +0 -33
- package/test/fixtures/mime-adversarial-e2e/04-forwarded-rfc822-with-attachments.eml +0 -42
- package/test/fixtures/mime-adversarial-e2e/05-tnef-winmail.eml +0 -25
- package/test/fixtures/mime-adversarial-e2e/06-8bit-non-ascii.eml +0 -15
- package/test/fixtures/mime-adversarial-e2e/07-quoted-printable-soft-breaks.eml +0 -17
- package/test/fixtures/mime-adversarial-e2e/08-alternative-empty-text.eml +0 -18
- package/tsconfig.json +0 -8
package/src/outbox-queue.ts
DELETED
|
@@ -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
|
-
}
|