@remit/smtp-worker 0.0.30 → 0.0.32
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 +6 -2
- package/src/data-ports.ts +4 -20
- package/src/events.ts +5 -0
- package/src/failure-kind.ts +1 -1
- package/src/handlers/attachment-storage.test.ts +1 -58
- package/src/handlers/attachment-storage.ts +0 -23
- package/src/handlers/engagement-counters.test.ts +108 -0
- package/src/handlers/engagement-counters.ts +27 -3
- package/src/handlers/resolve-smtp-config.ts +2 -2
- package/src/handlers/send-message.test.ts +544 -864
- package/src/handlers/send-message.ts +510 -174
- package/src/index.test.ts +9 -4
- package/src/index.ts +10 -1
- package/src/lambda-context.test.ts +34 -0
- package/src/lambda-context.ts +41 -0
- package/src/ports.ts +21 -0
- package/src/process-bundle.test.ts +62 -0
- package/src/process.ts +11 -0
- package/src/processor.ts +7 -3
- package/src/handlers/send-message-core.ts +0 -514
package/src/index.test.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* that always sees 1 never reaches the settle at all, and the row dead-letters
|
|
5
5
|
* at `sending` exactly as it did before #951.
|
|
6
6
|
*
|
|
7
|
-
* The
|
|
7
|
+
* The processor is replaced here, so this file is about the wiring and not
|
|
8
8
|
* about what the handler does with it — and replacing it is also what keeps
|
|
9
9
|
* the real one's data ports and queue producers out of a unit test.
|
|
10
10
|
*/
|
|
@@ -14,11 +14,12 @@ import type { Context, SQSEvent } from "aws-lambda";
|
|
|
14
14
|
|
|
15
15
|
const receiveCounts: number[] = [];
|
|
16
16
|
|
|
17
|
-
mock.module("./
|
|
17
|
+
mock.module("./processor.js", {
|
|
18
18
|
namedExports: {
|
|
19
|
-
|
|
19
|
+
processEvent: async (
|
|
20
20
|
_event: unknown,
|
|
21
21
|
_log: unknown,
|
|
22
|
+
_ctx: unknown,
|
|
22
23
|
receiveCount: number,
|
|
23
24
|
): Promise<void> => {
|
|
24
25
|
receiveCounts.push(receiveCount);
|
|
@@ -26,6 +27,10 @@ mock.module("./handlers/send-message.js", {
|
|
|
26
27
|
},
|
|
27
28
|
});
|
|
28
29
|
|
|
30
|
+
mock.module("./lambda-context.js", {
|
|
31
|
+
namedExports: { buildLambdaSendContext: async () => ({}) },
|
|
32
|
+
});
|
|
33
|
+
|
|
29
34
|
const { handler, parseReceiveCount } = await import("./index.js");
|
|
30
35
|
|
|
31
36
|
const context = {
|
|
@@ -74,7 +79,7 @@ describe("parseReceiveCount — SQS ApproximateReceiveCount parsing", () => {
|
|
|
74
79
|
});
|
|
75
80
|
|
|
76
81
|
describe("the record's delivery count reaches the send handler", () => {
|
|
77
|
-
it("threads ApproximateReceiveCount
|
|
82
|
+
it("threads ApproximateReceiveCount into the processor", async () => {
|
|
78
83
|
receiveCounts.length = 0;
|
|
79
84
|
|
|
80
85
|
const response = await handler(sendMessageEvent("3"), context);
|
package/src/index.ts
CHANGED
|
@@ -9,12 +9,20 @@ import { installNodeLogger, withTelemetry } from "@remit/logger-lambda/node";
|
|
|
9
9
|
import type { SQSBatchResponse, SQSEvent } from "aws-lambda";
|
|
10
10
|
import type { SmtpEvent } from "./events.js";
|
|
11
11
|
import { smtpFailureKind } from "./failure-kind.js";
|
|
12
|
+
import type { SendMessageContext } from "./handlers/send-message.js";
|
|
13
|
+
import { buildLambdaSendContext } from "./lambda-context.js";
|
|
12
14
|
import { processEvent } from "./processor.js";
|
|
13
15
|
|
|
14
16
|
installNodeLogger();
|
|
15
17
|
|
|
16
18
|
const log = createLogger();
|
|
17
19
|
|
|
20
|
+
let sendContext: Promise<SendMessageContext> | null = null;
|
|
21
|
+
const lambdaSendContext = (): Promise<SendMessageContext> => {
|
|
22
|
+
sendContext ??= buildLambdaSendContext();
|
|
23
|
+
return sendContext;
|
|
24
|
+
};
|
|
25
|
+
|
|
18
26
|
/**
|
|
19
27
|
* Parse SQS's `ApproximateReceiveCount` record attribute (1 on first
|
|
20
28
|
* delivery). Missing/malformed defaults to 1 so a record with no attribute
|
|
@@ -44,7 +52,8 @@ export const handler = withTelemetry(
|
|
|
44
52
|
|
|
45
53
|
const queue = queueNameFromEventSource(record.eventSourceARN);
|
|
46
54
|
const sendStart = Date.now();
|
|
47
|
-
const failed = await
|
|
55
|
+
const failed = await lambdaSendContext()
|
|
56
|
+
.then((ctx) => processEvent(smtpEvent, log, ctx, receiveCount))
|
|
48
57
|
.then(() => {
|
|
49
58
|
recordQueueEvent({
|
|
50
59
|
queue,
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import { getSendMessageMaxAttempts } from "./lambda-context.js";
|
|
4
|
+
|
|
5
|
+
describe("the retry budget is the queue's, read from the environment", () => {
|
|
6
|
+
it("takes the deployment's own maxReceiveCount when it is set", () => {
|
|
7
|
+
// The e2e stack sets 1 (`deploy/vps/e2e.env`): its pollers hold a record
|
|
8
|
+
// for 300s, so a spec cannot wait out three deliveries.
|
|
9
|
+
assert.equal(
|
|
10
|
+
getSendMessageMaxAttempts({ SEND_MESSAGE_MAX_ATTEMPTS: "1" }),
|
|
11
|
+
1,
|
|
12
|
+
);
|
|
13
|
+
assert.equal(
|
|
14
|
+
getSendMessageMaxAttempts({ SEND_MESSAGE_MAX_ATTEMPTS: "5" }),
|
|
15
|
+
5,
|
|
16
|
+
);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("falls back to remit-smtp's maxReceiveCount when it is unset or unusable", () => {
|
|
20
|
+
assert.equal(getSendMessageMaxAttempts({}), 3);
|
|
21
|
+
assert.equal(
|
|
22
|
+
getSendMessageMaxAttempts({ SEND_MESSAGE_MAX_ATTEMPTS: "" }),
|
|
23
|
+
3,
|
|
24
|
+
);
|
|
25
|
+
assert.equal(
|
|
26
|
+
getSendMessageMaxAttempts({ SEND_MESSAGE_MAX_ATTEMPTS: "not-a-number" }),
|
|
27
|
+
3,
|
|
28
|
+
);
|
|
29
|
+
assert.equal(
|
|
30
|
+
getSendMessageMaxAttempts({ SEND_MESSAGE_MAX_ATTEMPTS: "0" }),
|
|
31
|
+
3,
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { recordSmtpFailure } from "@remit/logger-lambda/metrics";
|
|
2
|
+
import {
|
|
3
|
+
createMailOAuthService,
|
|
4
|
+
microsoftProviderConfig,
|
|
5
|
+
} from "@remit/mail-oauth-service";
|
|
6
|
+
import { createSecretsService } from "@remit/secrets-service";
|
|
7
|
+
import { createKmsDataKeyProvider } from "@remit/secrets-service/kms";
|
|
8
|
+
import { directSmtpSocket } from "@remit/smtp-service";
|
|
9
|
+
import { attemptBudget } from "@remit/sqs-client/attempt-budget";
|
|
10
|
+
import { createStandardQueueProducer } from "@remit/sqs-client/producer";
|
|
11
|
+
import { createStorageService } from "@remit/storage-service/s3";
|
|
12
|
+
import { env } from "expect-env";
|
|
13
|
+
import { buildDataPortsFromEnv } from "./data-ports.js";
|
|
14
|
+
import { createAttachmentReader } from "./handlers/attachment-storage.js";
|
|
15
|
+
import type { SendMessageContext } from "./handlers/send-message.js";
|
|
16
|
+
|
|
17
|
+
export const getSendMessageMaxAttempts = (
|
|
18
|
+
processEnv: NodeJS.ProcessEnv = process.env,
|
|
19
|
+
): number => attemptBudget("SEND_MESSAGE_MAX_ATTEMPTS", 3, processEnv);
|
|
20
|
+
|
|
21
|
+
export const buildLambdaSendContext =
|
|
22
|
+
async (): Promise<SendMessageContext> => ({
|
|
23
|
+
ports: await buildDataPortsFromEnv(),
|
|
24
|
+
secrets: createSecretsService(createKmsDataKeyProvider(env.KMS_KEY_ID)),
|
|
25
|
+
storage: createAttachmentReader(process.env, createStorageService),
|
|
26
|
+
messageManagement: createStandardQueueProducer({
|
|
27
|
+
queueUrl: env.SQS_QUEUE_URL_MESSAGE_MGMT,
|
|
28
|
+
}),
|
|
29
|
+
tokenService: createMailOAuthService(
|
|
30
|
+
microsoftProviderConfig({
|
|
31
|
+
clientId: process.env.MSOAUTH_CLIENT_ID ?? "",
|
|
32
|
+
clientSecret: process.env.MSOAUTH_CLIENT_SECRET ?? "",
|
|
33
|
+
overrides: process.env.MSOAUTH_TOKEN_ENDPOINT
|
|
34
|
+
? { tokenEndpoint: process.env.MSOAUTH_TOKEN_ENDPOINT }
|
|
35
|
+
: undefined,
|
|
36
|
+
}),
|
|
37
|
+
),
|
|
38
|
+
getSocket: directSmtpSocket,
|
|
39
|
+
maxAttempts: getSendMessageMaxAttempts(),
|
|
40
|
+
recordSmtpFailure,
|
|
41
|
+
});
|
package/src/ports.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
IAccountRepository,
|
|
3
|
+
IAddressRepository,
|
|
4
|
+
IEnvelopeRepository,
|
|
5
|
+
IMessageRepository,
|
|
6
|
+
IOutboxAttachmentRepository,
|
|
7
|
+
IOutboxMessageRepository,
|
|
8
|
+
} from "@remit/data-ports";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The repositories the SMTP send path reads and writes. One neutral seam so the
|
|
12
|
+
* same handler serves every stack.
|
|
13
|
+
*/
|
|
14
|
+
export interface SmtpDataPorts {
|
|
15
|
+
account: IAccountRepository;
|
|
16
|
+
outboxMessage: IOutboxMessageRepository;
|
|
17
|
+
outboxAttachment: IOutboxAttachmentRepository;
|
|
18
|
+
address: IAddressRepository;
|
|
19
|
+
message: IMessageRepository;
|
|
20
|
+
envelope: IEnvelopeRepository;
|
|
21
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { describe, it } from "node:test";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { build } from "esbuild";
|
|
6
|
+
|
|
7
|
+
const LAMBDA_ONLY = [
|
|
8
|
+
/(^|\/)aws-lambda(\/|$)/,
|
|
9
|
+
/(^|\/)@aws-sdk\//,
|
|
10
|
+
/(^|\/)expect-env(\/|$)/,
|
|
11
|
+
/(^|\/)sqs-client(\/|$)/,
|
|
12
|
+
/(^|\/)logger-lambda(\/|$)/,
|
|
13
|
+
/(^|\/)prom-client(\/|$)/,
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const ENV_READERS: readonly string[] = [];
|
|
17
|
+
|
|
18
|
+
const packageDir = fileURLToPath(new URL("..", import.meta.url));
|
|
19
|
+
|
|
20
|
+
const bundleInputs = async () => {
|
|
21
|
+
const { metafile } = await build({
|
|
22
|
+
absWorkingDir: packageDir,
|
|
23
|
+
entryPoints: ["src/process.ts"],
|
|
24
|
+
bundle: true,
|
|
25
|
+
write: false,
|
|
26
|
+
metafile: true,
|
|
27
|
+
platform: "node",
|
|
28
|
+
format: "esm",
|
|
29
|
+
logLevel: "silent",
|
|
30
|
+
});
|
|
31
|
+
return metafile.inputs;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
describe("the ./process entry", () => {
|
|
35
|
+
it("bundles without the Lambda runtime, the AWS SDK, SQS, expect-env or the Node metrics server", async () => {
|
|
36
|
+
const reached = Object.entries(await bundleInputs()).flatMap(
|
|
37
|
+
([path, input]) => [path, ...input.imports.map((entry) => entry.path)],
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
assert.deepEqual(
|
|
41
|
+
reached.filter((path) =>
|
|
42
|
+
LAMBDA_ONLY.some((pattern) => pattern.test(path)),
|
|
43
|
+
),
|
|
44
|
+
[],
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("reaches no workspace source that reads process.env", async () => {
|
|
49
|
+
const sources = Object.keys(await bundleInputs()).filter(
|
|
50
|
+
(path) => !path.startsWith("<") && !path.includes("node_modules/"),
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
assert.deepEqual(
|
|
54
|
+
sources.filter((path) =>
|
|
55
|
+
/\bprocess\.env\b/.test(
|
|
56
|
+
readFileSync(new URL(path, `file://${packageDir}`), "utf8"),
|
|
57
|
+
),
|
|
58
|
+
),
|
|
59
|
+
ENV_READERS,
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
});
|
package/src/process.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
AppendSentMessageEvent,
|
|
3
|
+
SendMessageEvent,
|
|
4
|
+
SmtpEvent,
|
|
5
|
+
} from "./events.js";
|
|
6
|
+
export type { AttachmentReader } from "./handlers/attachment-storage.js";
|
|
7
|
+
export {
|
|
8
|
+
handleSendMessage,
|
|
9
|
+
type SendMessageContext,
|
|
10
|
+
} from "./handlers/send-message.js";
|
|
11
|
+
export type { SmtpDataPorts } from "./ports.js";
|
package/src/processor.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import type { Logger } from "@remit/logger";
|
|
2
2
|
import type { SmtpEvent } from "./events.js";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
handleSendMessage,
|
|
5
|
+
type SendMessageContext,
|
|
6
|
+
} from "./handlers/send-message.js";
|
|
4
7
|
|
|
5
8
|
export const processEvent = async (
|
|
6
9
|
event: SmtpEvent,
|
|
7
10
|
log: Logger,
|
|
11
|
+
ctx: SendMessageContext,
|
|
8
12
|
/**
|
|
9
13
|
* SQS's own delivery count for the record carrying this event (1 on first
|
|
10
14
|
* delivery). Read by SEND_MESSAGE so it knows from it when this is the
|
|
@@ -12,11 +16,11 @@ export const processEvent = async (
|
|
|
12
16
|
* record, so it can resolve retry exhaustion into a terminal outcome
|
|
13
17
|
* (issue #951) instead of dead-lettering blindly.
|
|
14
18
|
*/
|
|
15
|
-
receiveCount
|
|
19
|
+
receiveCount: number,
|
|
16
20
|
): Promise<void> => {
|
|
17
21
|
switch (event.type) {
|
|
18
22
|
case "SEND_MESSAGE":
|
|
19
|
-
return handleSendMessage(event,
|
|
23
|
+
return handleSendMessage(event, ctx, receiveCount);
|
|
20
24
|
case "PROCESS_OUTBOX":
|
|
21
25
|
// Future: batch process all queued messages
|
|
22
26
|
log.info(
|