@remit/smtp-worker 0.0.21 → 0.0.23
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
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
import { AccountAuthType, OutboxMessageStatus } from "@remit/domain-enums";
|
|
8
8
|
import { type Logger, recordSmtpFailure } from "@remit/logger-lambda";
|
|
9
9
|
import { RefreshTokenError } from "@remit/mail-oauth-service";
|
|
10
|
+
import type { CredentialResolution } from "@remit/mailbox-service/account-credentials";
|
|
10
11
|
import type { SecretsService } from "@remit/secrets-service";
|
|
11
12
|
import {
|
|
12
13
|
buildMailMessage,
|
|
@@ -72,10 +73,11 @@ export interface SendMessageDeps {
|
|
|
72
73
|
* Resolve credentials for the account. Called after fetching the account.
|
|
73
74
|
* For password accounts this may resolve immediately from the stored hash.
|
|
74
75
|
* For OAuth accounts this mints an access token via the token service.
|
|
76
|
+
* Returns `{ status: "missing" }` when the account stores no credential.
|
|
75
77
|
* Throws RefreshTokenError on OAuth failures — callers should not need to
|
|
76
78
|
* handle this here; the caller of sendMessage handles it.
|
|
77
79
|
*/
|
|
78
|
-
resolveCredentials: (account: AccountItem) => Promise<
|
|
80
|
+
resolveCredentials: (account: AccountItem) => Promise<CredentialResolution>;
|
|
79
81
|
/**
|
|
80
82
|
* Persist the account's connectionState. Called when a terminal OAuth/SMTP
|
|
81
83
|
* auth failure is detected so the account is fenced off until the user
|
|
@@ -203,9 +205,26 @@ export const sendMessage = async (
|
|
|
203
205
|
// Resolve credentials. On a terminal OAuth auth failure (token revoked),
|
|
204
206
|
// flip the account to reauth_required and ACK — do not retry. Transient /
|
|
205
207
|
// config failures rethrow for SQS retry/backoff.
|
|
206
|
-
let credentials: SmtpCredentials;
|
|
208
|
+
let credentials: SmtpCredentials | undefined;
|
|
207
209
|
try {
|
|
208
|
-
|
|
210
|
+
const resolution = await deps.resolveCredentials(account);
|
|
211
|
+
if (resolution.status === "missing") {
|
|
212
|
+
// Terminal, never a retry (issue #1120). The account still sends if it
|
|
213
|
+
// carries an SMTP-specific credential of its own, so this hands
|
|
214
|
+
// resolveSmtpConfig no credential and lets it decide; where there is
|
|
215
|
+
// none either, the send settles `blocked` below.
|
|
216
|
+
log.warn(
|
|
217
|
+
{
|
|
218
|
+
accountId,
|
|
219
|
+
reason: resolution.reason,
|
|
220
|
+
connectionState: resolution.terminalState,
|
|
221
|
+
},
|
|
222
|
+
"Account stores no credential",
|
|
223
|
+
);
|
|
224
|
+
await deps.updateConnectionState(accountId, resolution.terminalState);
|
|
225
|
+
} else {
|
|
226
|
+
credentials = resolution.credentials;
|
|
227
|
+
}
|
|
209
228
|
} catch (err) {
|
|
210
229
|
if (err instanceof RefreshTokenError) {
|
|
211
230
|
if (err.error.kind === "reauth-required") {
|
|
@@ -145,10 +145,13 @@ const buildDeps = (
|
|
|
145
145
|
(async (_account) => {
|
|
146
146
|
recorded.resolveCalls += 1;
|
|
147
147
|
return {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
:
|
|
148
|
+
status: "resolved" as const,
|
|
149
|
+
credentials: {
|
|
150
|
+
kind: "password" as const,
|
|
151
|
+
password: _account.passwordHash
|
|
152
|
+
? "resolved-password"
|
|
153
|
+
: "no-password-configured",
|
|
154
|
+
},
|
|
152
155
|
};
|
|
153
156
|
}),
|
|
154
157
|
updateConnectionState: async (accountId, state) => {
|
|
@@ -329,6 +332,24 @@ describe("sendMessage handler", () => {
|
|
|
329
332
|
assert.equal(recorded.updates.length, 0);
|
|
330
333
|
});
|
|
331
334
|
|
|
335
|
+
it("drops the event for a row the enqueue settled at `failed`", async () => {
|
|
336
|
+
// The other half of #936. An error from SQS `SendMessage` says the response
|
|
337
|
+
// was lost, not that the broker refused the event, so the row is settled
|
|
338
|
+
// outside this fence and the event that landed anyway dies here. Settling
|
|
339
|
+
// it at `draft` instead would send it — and leave the user a row they can
|
|
340
|
+
// send a second time.
|
|
341
|
+
const { deps, recorded } = buildDeps({
|
|
342
|
+
outbox: buildOutbox({ status: "failed" }),
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
await sendMessage(event, silentLogger, deps);
|
|
346
|
+
|
|
347
|
+
assert.equal(recorded.sendCalls, 0, "must not send");
|
|
348
|
+
assert.equal(recorded.marked.length, 0);
|
|
349
|
+
assert.equal(recorded.updates.length, 0);
|
|
350
|
+
assert.equal(recorded.statuses.length, 0);
|
|
351
|
+
});
|
|
352
|
+
|
|
332
353
|
it("settles the row as unfiled when the append event cannot be queued", async () => {
|
|
333
354
|
const { deps, recorded } = buildDeps({
|
|
334
355
|
account: buildAccount({ smtpHost: "smtp.example.com", smtpPort: 587 }),
|
|
@@ -550,6 +571,25 @@ describe("sendMessage OAuth reauth/ACK contract", () => {
|
|
|
550
571
|
});
|
|
551
572
|
});
|
|
552
573
|
|
|
574
|
+
it("account storing no credential: settles `blocked` and never retries (issue #1120)", async () => {
|
|
575
|
+
const { deps, recorded } = buildDeps({
|
|
576
|
+
account: buildAccount({ passwordHash: undefined }),
|
|
577
|
+
resolveCredentials: async () => ({
|
|
578
|
+
status: "missing" as const,
|
|
579
|
+
terminalState: "credentials_missing" as const,
|
|
580
|
+
reason: "authType=password but no passwordHash",
|
|
581
|
+
}),
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
await sendMessage(event, silentLogger, deps);
|
|
585
|
+
|
|
586
|
+
assert.equal(recorded.sendCalls, 0, "must not send");
|
|
587
|
+
assert.deepEqual(recorded.connectionStateUpdates, [
|
|
588
|
+
{ accountId: "acc-1", state: "credentials_missing" },
|
|
589
|
+
]);
|
|
590
|
+
assert.equal(recorded.updates[0]?.patch.status, "blocked");
|
|
591
|
+
});
|
|
592
|
+
|
|
553
593
|
it("on transient credential error: rethrows", async () => {
|
|
554
594
|
const { deps, recorded } = buildDeps({
|
|
555
595
|
account: buildAccount({ smtpHost: "smtp.example.com", smtpPort: 587 }),
|