@remit/smtp-worker 0.0.19 → 0.0.20
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/package.json
CHANGED
|
@@ -4,7 +4,7 @@ import type {
|
|
|
4
4
|
OutboxMessageItem,
|
|
5
5
|
UpdateOutboxMessageInput,
|
|
6
6
|
} from "@remit/data-ports";
|
|
7
|
-
import { AccountAuthType } from "@remit/domain-enums";
|
|
7
|
+
import { AccountAuthType, OutboxMessageStatus } from "@remit/domain-enums";
|
|
8
8
|
import type { Logger } from "@remit/logger-lambda";
|
|
9
9
|
import { RefreshTokenError } from "@remit/mail-oauth-service";
|
|
10
10
|
import type { SecretsService } from "@remit/secrets-service";
|
|
@@ -90,6 +90,15 @@ export interface SendMessageDeps {
|
|
|
90
90
|
engagement: EngagementCounterDeps;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
const UNFILED_NOT_QUEUED =
|
|
94
|
+
"Sent, but not filed: the copy for the Sent folder could not be queued.";
|
|
95
|
+
|
|
96
|
+
const SENDABLE_STATUSES: ReadonlySet<OutboxMessageItem["status"]> = new Set([
|
|
97
|
+
OutboxMessageStatus.draft,
|
|
98
|
+
OutboxMessageStatus.queued,
|
|
99
|
+
OutboxMessageStatus.sending,
|
|
100
|
+
]);
|
|
101
|
+
|
|
93
102
|
export const sendMessage = async (
|
|
94
103
|
event: SendMessageEvent,
|
|
95
104
|
log: Logger,
|
|
@@ -106,8 +115,15 @@ export const sendMessage = async (
|
|
|
106
115
|
const { accountConfigId } = account;
|
|
107
116
|
|
|
108
117
|
const outbox = await deps.getOutbox(accountConfigId, outboxMessageId);
|
|
109
|
-
|
|
110
|
-
|
|
118
|
+
// The fence against SQS at-least-once redelivery, stated as the states a
|
|
119
|
+
// send may proceed FROM. Every other state has already been on the wire, so
|
|
120
|
+
// naming them one by one is how a state added later silently starts sending
|
|
121
|
+
// twice.
|
|
122
|
+
if (!SENDABLE_STATUSES.has(outbox.status)) {
|
|
123
|
+
log.info(
|
|
124
|
+
{ outboxMessageId, status: outbox.status },
|
|
125
|
+
"Message already left the queue, skipping",
|
|
126
|
+
);
|
|
111
127
|
return;
|
|
112
128
|
}
|
|
113
129
|
|
|
@@ -229,13 +245,21 @@ export const sendMessage = async (
|
|
|
229
245
|
);
|
|
230
246
|
});
|
|
231
247
|
|
|
248
|
+
// The filing is what deletes this row, so an enqueue that never lands
|
|
249
|
+
// leaves it at `sent` — a delivered message the Outbox hides and no Sent
|
|
250
|
+
// folder holds. Settle it here instead: the send itself is done, so
|
|
251
|
+
// throwing would only redeliver an event that cannot re-send.
|
|
232
252
|
await deps
|
|
233
253
|
.emitAppendSentMessage(accountId, outboxMessageId)
|
|
234
|
-
.catch((error: unknown) => {
|
|
235
|
-
log.
|
|
254
|
+
.catch(async (error: unknown) => {
|
|
255
|
+
log.error(
|
|
236
256
|
{ outboxMessageId, error: String(error) },
|
|
237
|
-
"Failed to enqueue APPEND_SENT_MESSAGE
|
|
257
|
+
"Failed to enqueue APPEND_SENT_MESSAGE; settling the row as unfiled",
|
|
238
258
|
);
|
|
259
|
+
await deps.updateOutbox(accountConfigId, outboxMessageId, {
|
|
260
|
+
status: OutboxMessageStatus.unfiled,
|
|
261
|
+
lastError: UNFILED_NOT_QUEUED,
|
|
262
|
+
});
|
|
239
263
|
});
|
|
240
264
|
return;
|
|
241
265
|
}
|
|
@@ -309,6 +309,37 @@ describe("sendMessage handler", () => {
|
|
|
309
309
|
assert.equal(recorded.statuses.length, 0);
|
|
310
310
|
});
|
|
311
311
|
|
|
312
|
+
it("never re-sends a message that was sent but could not be filed", async () => {
|
|
313
|
+
// The redelivery fence is the only thing between SQS at-least-once and a
|
|
314
|
+
// second copy on the wire. `unfiled` is past the send, so it belongs on
|
|
315
|
+
// the same side of the fence as `sent`.
|
|
316
|
+
const { deps, recorded } = buildDeps({
|
|
317
|
+
outbox: buildOutbox({ status: "unfiled" }),
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
await sendMessage(event, silentLogger, deps);
|
|
321
|
+
|
|
322
|
+
assert.equal(recorded.sendCalls, 0);
|
|
323
|
+
assert.equal(recorded.marked.length, 0);
|
|
324
|
+
assert.equal(recorded.updates.length, 0);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it("settles the row as unfiled when the append event cannot be queued", async () => {
|
|
328
|
+
const { deps, recorded } = buildDeps({
|
|
329
|
+
account: buildAccount({ smtpHost: "smtp.example.com", smtpPort: 587 }),
|
|
330
|
+
appendThrows: new Error("queue unreachable"),
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
await sendMessage(event, silentLogger, deps);
|
|
334
|
+
|
|
335
|
+
// The send itself succeeded. Without a settle the row keeps the `sent`
|
|
336
|
+
// status the Outbox list hides, and no APPEND is ever going to delete it.
|
|
337
|
+
assert.equal(recorded.marked.length, 1);
|
|
338
|
+
const patch = recorded.updates.at(-1)?.patch;
|
|
339
|
+
assert.equal(patch?.status, "unfiled");
|
|
340
|
+
assert.match(String(patch?.lastError), /could not be queued/);
|
|
341
|
+
});
|
|
342
|
+
|
|
312
343
|
it("drops event when account is deleted (tombstone fence)", async () => {
|
|
313
344
|
const { deps, recorded } = buildDeps({
|
|
314
345
|
account: buildAccount({
|