@remit/smtp-worker 0.0.22 → 0.0.24
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,
|
|
@@ -14,6 +15,7 @@ import {
|
|
|
14
15
|
SmtpConnectionError,
|
|
15
16
|
type sendMail,
|
|
16
17
|
} from "@remit/smtp-service";
|
|
18
|
+
import { attemptBudget } from "@remit/sqs-client/attempt-budget";
|
|
17
19
|
import type { SendMessageEvent } from "../events.js";
|
|
18
20
|
import { writeEngagementCounters } from "./engagement-counters.js";
|
|
19
21
|
import {
|
|
@@ -72,10 +74,11 @@ export interface SendMessageDeps {
|
|
|
72
74
|
* Resolve credentials for the account. Called after fetching the account.
|
|
73
75
|
* For password accounts this may resolve immediately from the stored hash.
|
|
74
76
|
* For OAuth accounts this mints an access token via the token service.
|
|
77
|
+
* Returns `{ status: "missing" }` when the account stores no credential.
|
|
75
78
|
* Throws RefreshTokenError on OAuth failures — callers should not need to
|
|
76
79
|
* handle this here; the caller of sendMessage handles it.
|
|
77
80
|
*/
|
|
78
|
-
resolveCredentials: (account: AccountItem) => Promise<
|
|
81
|
+
resolveCredentials: (account: AccountItem) => Promise<CredentialResolution>;
|
|
79
82
|
/**
|
|
80
83
|
* Persist the account's connectionState. Called when a terminal OAuth/SMTP
|
|
81
84
|
* auth failure is detected so the account is fenced off until the user
|
|
@@ -131,24 +134,9 @@ const SENDABLE_STATUSES: ReadonlySet<OutboxMessageItem["status"]> = new Set([
|
|
|
131
134
|
OutboxMessageStatus.sending,
|
|
132
135
|
]);
|
|
133
136
|
|
|
134
|
-
/**
|
|
135
|
-
* Fallback when `SEND_MESSAGE_MAX_ATTEMPTS` is unset (local dev, unit tests).
|
|
136
|
-
* Matches `remit-smtp`'s `maxReceiveCount` (`deploy/vps/queues.json`), same
|
|
137
|
-
* pattern as `MESSAGE_MOVE_MAX_ATTEMPTS` / `FLAG_PUSH_MAX_ATTEMPTS` in
|
|
138
|
-
* imap-worker.
|
|
139
|
-
*/
|
|
140
|
-
const DEFAULT_SEND_MESSAGE_MAX_ATTEMPTS = 3;
|
|
141
|
-
|
|
142
137
|
export const getSendMessageMaxAttempts = (
|
|
143
138
|
processEnv: NodeJS.ProcessEnv = process.env,
|
|
144
|
-
): number =>
|
|
145
|
-
const raw = processEnv.SEND_MESSAGE_MAX_ATTEMPTS;
|
|
146
|
-
if (!raw) return DEFAULT_SEND_MESSAGE_MAX_ATTEMPTS;
|
|
147
|
-
const parsed = Number.parseInt(raw, 10);
|
|
148
|
-
return Number.isFinite(parsed) && parsed > 0
|
|
149
|
-
? parsed
|
|
150
|
-
: DEFAULT_SEND_MESSAGE_MAX_ATTEMPTS;
|
|
151
|
-
};
|
|
139
|
+
): number => attemptBudget("SEND_MESSAGE_MAX_ATTEMPTS", 3, processEnv);
|
|
152
140
|
|
|
153
141
|
export const SEND_MESSAGE_MAX_ATTEMPTS = getSendMessageMaxAttempts();
|
|
154
142
|
|
|
@@ -203,9 +191,26 @@ export const sendMessage = async (
|
|
|
203
191
|
// Resolve credentials. On a terminal OAuth auth failure (token revoked),
|
|
204
192
|
// flip the account to reauth_required and ACK — do not retry. Transient /
|
|
205
193
|
// config failures rethrow for SQS retry/backoff.
|
|
206
|
-
let credentials: SmtpCredentials;
|
|
194
|
+
let credentials: SmtpCredentials | undefined;
|
|
207
195
|
try {
|
|
208
|
-
|
|
196
|
+
const resolution = await deps.resolveCredentials(account);
|
|
197
|
+
if (resolution.status === "missing") {
|
|
198
|
+
// Terminal, never a retry (issue #1120). The account still sends if it
|
|
199
|
+
// carries an SMTP-specific credential of its own, so this hands
|
|
200
|
+
// resolveSmtpConfig no credential and lets it decide; where there is
|
|
201
|
+
// none either, the send settles `blocked` below.
|
|
202
|
+
log.warn(
|
|
203
|
+
{
|
|
204
|
+
accountId,
|
|
205
|
+
reason: resolution.reason,
|
|
206
|
+
connectionState: resolution.terminalState,
|
|
207
|
+
},
|
|
208
|
+
"Account stores no credential",
|
|
209
|
+
);
|
|
210
|
+
await deps.updateConnectionState(accountId, resolution.terminalState);
|
|
211
|
+
} else {
|
|
212
|
+
credentials = resolution.credentials;
|
|
213
|
+
}
|
|
209
214
|
} catch (err) {
|
|
210
215
|
if (err instanceof RefreshTokenError) {
|
|
211
216
|
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) => {
|
|
@@ -568,6 +571,25 @@ describe("sendMessage OAuth reauth/ACK contract", () => {
|
|
|
568
571
|
});
|
|
569
572
|
});
|
|
570
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
|
+
|
|
571
593
|
it("on transient credential error: rethrows", async () => {
|
|
572
594
|
const { deps, recorded } = buildDeps({
|
|
573
595
|
account: buildAccount({ smtpHost: "smtp.example.com", smtpPort: 587 }),
|