@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remit/smtp-worker",
3
- "version": "0.0.30",
3
+ "version": "0.0.32",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "exports": {
@@ -11,12 +11,16 @@
11
11
  "./data-ports": {
12
12
  "types": "./src/data-ports.ts",
13
13
  "default": "./src/data-ports.ts"
14
+ },
15
+ "./process": {
16
+ "types": "./src/process.ts",
17
+ "default": "./src/process.ts"
14
18
  }
15
19
  },
16
20
  "scripts": {
17
21
  "bundle": "node build.mjs",
18
22
  "test:typecheck": "tsgo --noEmit",
19
- "test:run": "node $NODE_TEST_FLAGS --env-file=../../localhost-test-unit.env --experimental-test-module-mocks --experimental-test-coverage --test-coverage-include='src/**' --test-coverage-exclude='src/**/*.test.ts' --test-coverage-exclude='src/**/*.test.tsx' --test-coverage-exclude='src/handlers/send-message.ts' --test-coverage-lines=83 --test 'src/**/*.test.ts'",
23
+ "test:run": "node $NODE_TEST_FLAGS --env-file=../../localhost-test-unit.env --experimental-test-module-mocks --experimental-test-coverage --test-coverage-include='src/**' --test-coverage-exclude='src/**/*.test.ts' --test-coverage-exclude='src/**/*.test.tsx' --test-coverage-exclude='src/lambda-context.ts' --test-coverage-lines=83 --test 'src/**/*.test.ts'",
20
24
  "test": "npm run test:typecheck && npm run test:run",
21
25
  "dev": "node --import tsx src/e2e-processor-shim.ts"
22
26
  },
package/src/data-ports.ts CHANGED
@@ -1,24 +1,6 @@
1
- import type {
2
- IAccountRepository,
3
- IAddressRepository,
4
- IEnvelopeRepository,
5
- IMessageRepository,
6
- IOutboxAttachmentRepository,
7
- IOutboxMessageRepository,
8
- } from "@remit/data-ports";
1
+ import type { SmtpDataPorts } from "./ports.js";
9
2
 
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
- }
3
+ export type { SmtpDataPorts } from "./ports.js";
22
4
 
23
5
  // `@remit/drizzle-service` is loaded lazily, inside this function, instead of as
24
6
  // a static top-level import. A static import is bundled (and evaluated at module
@@ -29,6 +11,7 @@ export interface SmtpDataPorts {
29
11
  // installed.
30
12
  const buildSqliteDataPorts = async (): Promise<SmtpDataPorts> => {
31
13
  const {
14
+ BETTER_SQLITE3_LIMITS,
32
15
  createSqliteRepositories,
33
16
  openSqliteDatabases,
34
17
  sqliteDatabasePathsFromEnv,
@@ -36,6 +19,7 @@ const buildSqliteDataPorts = async (): Promise<SmtpDataPorts> => {
36
19
 
37
20
  const repositories = createSqliteRepositories(
38
21
  await openSqliteDatabases(sqliteDatabasePathsFromEnv()),
22
+ BETTER_SQLITE3_LIMITS,
39
23
  );
40
24
 
41
25
  return {
package/src/events.ts CHANGED
@@ -15,3 +15,8 @@ export interface ProcessOutboxEvent extends BaseEvent {
15
15
  }
16
16
 
17
17
  export type SmtpEvent = SendMessageEvent | ProcessOutboxEvent;
18
+
19
+ export interface AppendSentMessageEvent extends BaseEvent {
20
+ type: "APPEND_SENT_MESSAGE";
21
+ outboxMessageId: string;
22
+ }
@@ -1,4 +1,4 @@
1
- import type { MailFailureKind } from "@remit/logger-lambda/metrics";
1
+ import type { MailFailureKind } from "@remit/logger";
2
2
  import { RefreshTokenError } from "@remit/mail-oauth-service";
3
3
  import { SmtpConnectionError } from "@remit/smtp-service";
4
4
 
@@ -1,11 +1,7 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { describe, it } from "node:test";
3
- import type { OutboxAttachmentItem } from "@remit/data-ports";
4
3
  import { createMockStorageService } from "@remit/storage-service";
5
- import {
6
- createAttachmentLoader,
7
- createAttachmentReader,
8
- } from "./attachment-storage.js";
4
+ import { createAttachmentReader } from "./attachment-storage.js";
9
5
 
10
6
  describe("createAttachmentReader", () => {
11
7
  it("refuses to read on a Lambda with no bucket, naming the missing setting", () => {
@@ -51,56 +47,3 @@ describe("createAttachmentReader", () => {
51
47
  assert.equal(created, 1);
52
48
  });
53
49
  });
54
-
55
- describe("createAttachmentLoader", () => {
56
- it("reads the draft's stored files for the tenant it is given", async () => {
57
- const storage = createMockStorageService();
58
- await storage.storeOutboxAttachment({
59
- accountConfigId: "cfg",
60
- accountId: "acc",
61
- outboxMessageId: "ob",
62
- outboxAttachmentId: "att",
63
- content: Buffer.from("%PDF"),
64
- });
65
- const row: OutboxAttachmentItem = {
66
- outboxAttachmentId: "att",
67
- outboxMessageId: "ob",
68
- accountId: "acc",
69
- accountConfigId: "cfg",
70
- filename: "report.pdf",
71
- contentType: "application/pdf",
72
- sizeBytes: 4,
73
- state: "Stored",
74
- storageKey: "k",
75
- reservationExpiresAt: 0,
76
- createdAt: 0,
77
- updatedAt: 0,
78
- };
79
- const asked: string[][] = [];
80
- const load = createAttachmentLoader(
81
- storage,
82
- async () => ({
83
- listByOutboxMessage: async (accountConfigId, outboxMessageId) => {
84
- asked.push([accountConfigId, outboxMessageId]);
85
- return [row];
86
- },
87
- }),
88
- () => 1_000_000,
89
- );
90
-
91
- const files = await load(
92
- { accountConfigId: "cfg", accountId: "acc" },
93
- "ob",
94
- );
95
-
96
- assert.deepEqual(asked, [["cfg", "ob"]]);
97
- assert.deepEqual(
98
- files.map((file) => [
99
- file.filename,
100
- file.contentType,
101
- String(file.content),
102
- ]),
103
- [["report.pdf", "application/pdf", "%PDF"]],
104
- );
105
- });
106
- });
@@ -1,8 +1,4 @@
1
- import type { IOutboxAttachmentRepository } from "@remit/data-ports";
2
- import { loadOutboxAttachmentContents } from "@remit/mailbox-service/outbox-attachment-content";
3
- import type { MailAttachment } from "@remit/smtp-service";
4
1
  import type { StorageService } from "@remit/storage-service";
5
- import type { SendTenant } from "./send-message-core.js";
6
2
 
7
3
  export type AttachmentReader = Pick<StorageService, "retrieveOutboxAttachment">;
8
4
 
@@ -31,22 +27,3 @@ export const createAttachmentReader = (
31
27
  },
32
28
  };
33
29
  };
34
-
35
- /** The files a message goes out with, read from this worker's ports and storage. */
36
- export const createAttachmentLoader =
37
- (
38
- reader: AttachmentReader,
39
- getRepository: () => Promise<
40
- Pick<IOutboxAttachmentRepository, "listByOutboxMessage">
41
- >,
42
- now: () => number = Date.now,
43
- ) =>
44
- async (
45
- tenant: SendTenant,
46
- outboxMessageId: string,
47
- ): Promise<MailAttachment[]> =>
48
- loadOutboxAttachmentContents(
49
- { attachments: await getRepository(), storage: reader },
50
- { ...tenant, outboxMessageId },
51
- Math.floor(now() / 1000),
52
- );
@@ -0,0 +1,108 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import type { MessageItem, OutboxMessageItem } from "@remit/data-ports";
4
+ import { noopLogger } from "@remit/logger";
5
+ import {
6
+ type EngagementCounterDeps,
7
+ writeEngagementCounters,
8
+ } from "./engagement-counters.js";
9
+
10
+ const tenant = { accountConfigId: "cfg-1", accountId: "acc-1" };
11
+
12
+ const reply = (overrides: Partial<OutboxMessageItem>): OutboxMessageItem =>
13
+ ({
14
+ outboxMessageId: "obx-1",
15
+ accountId: "acc-1",
16
+ accountConfigId: "cfg-1",
17
+ fromAddress: "alice@example.com",
18
+ toAddresses: ["bob@example.com"],
19
+ messageIdValue: "msg-1@example.com",
20
+ status: "sent",
21
+ createdAt: 0,
22
+ updatedAt: 0,
23
+ ...overrides,
24
+ }) as OutboxMessageItem;
25
+
26
+ const parent = { messageId: "msg-parent" } as MessageItem;
27
+
28
+ const recordingDeps = (
29
+ parentSender: string,
30
+ ): { deps: EngagementCounterDeps; replies: string[]; lookups: string[] } => {
31
+ const replies: string[] = [];
32
+ const lookups: string[] = [];
33
+ return {
34
+ replies,
35
+ lookups,
36
+ deps: {
37
+ resolveAddressId: (accountConfigId, email) =>
38
+ `addr-${accountConfigId}-${email}`,
39
+ incrementOutboundCount: async () => {},
40
+ incrementReplyCount: async (_accountConfigId, addressId) => {
41
+ replies.push(addressId);
42
+ },
43
+ findMessageByHeader: async (accountId) => {
44
+ lookups.push(accountId);
45
+ return parent;
46
+ },
47
+ getEnvelopeFromEmail: async () => parentSender,
48
+ },
49
+ };
50
+ };
51
+
52
+ describe("writeEngagementCounters reply targets", () => {
53
+ it("increments replyCount when In-Reply-To resolves to a recipient", async () => {
54
+ const { deps, replies } = recordingDeps("bob@example.com");
55
+
56
+ await writeEngagementCounters(
57
+ reply({ inReplyTo: "parent-msg@example.com" }),
58
+ tenant,
59
+ deps,
60
+ noopLogger,
61
+ );
62
+
63
+ assert.deepEqual(replies, ["addr-cfg-1-bob@example.com"]);
64
+ });
65
+
66
+ it("does not increment replyCount when the resolved sender is not a recipient", async () => {
67
+ const { deps, replies } = recordingDeps("stranger@example.com");
68
+
69
+ await writeEngagementCounters(
70
+ reply({ inReplyTo: "parent-msg@example.com" }),
71
+ tenant,
72
+ deps,
73
+ noopLogger,
74
+ );
75
+
76
+ assert.deepEqual(replies, []);
77
+ });
78
+
79
+ it("collapses duplicate replyCount increments via the resolved Address", async () => {
80
+ const { deps, replies } = recordingDeps("bob@example.com");
81
+
82
+ await writeEngagementCounters(
83
+ reply({
84
+ inReplyTo: "parent-msg@example.com",
85
+ references: ["root-msg@example.com", "parent-msg@example.com"],
86
+ }),
87
+ tenant,
88
+ deps,
89
+ noopLogger,
90
+ );
91
+
92
+ assert.equal(replies.length, 1);
93
+ });
94
+
95
+ it("looks the parent up by accountId, never by accountConfigId", async () => {
96
+ const { deps, lookups } = recordingDeps("bob@example.com");
97
+
98
+ await writeEngagementCounters(
99
+ reply({ inReplyTo: "parent-msg@example.com" }),
100
+ tenant,
101
+ deps,
102
+ noopLogger,
103
+ );
104
+
105
+ assert.ok(lookups.length > 0);
106
+ assert.ok(lookups.every((id) => id === "acc-1"));
107
+ });
108
+ });
@@ -1,7 +1,31 @@
1
- import type { OutboxMessageItem } from "@remit/data-ports";
1
+ import type { MessageItem, OutboxMessageItem } from "@remit/data-ports";
2
2
  import { bareMessageId, wireMessageId } from "@remit/data-ports/id";
3
3
  import type { Logger } from "@remit/logger";
4
- import type { EngagementCounterDeps, SendTenant } from "./send-message-core.js";
4
+
5
+ /** Tenant scope carried from the loaded account, never read off a looked-up row. */
6
+ export interface SendTenant {
7
+ accountConfigId: string;
8
+ accountId: string;
9
+ }
10
+
11
+ export interface EngagementCounterDeps {
12
+ resolveAddressId: (accountConfigId: string, email: string) => string;
13
+ incrementOutboundCount: (
14
+ accountConfigId: string,
15
+ addressId: string,
16
+ now: number,
17
+ ) => Promise<void>;
18
+ incrementReplyCount: (
19
+ accountConfigId: string,
20
+ addressId: string,
21
+ now: number,
22
+ ) => Promise<void>;
23
+ findMessageByHeader: (
24
+ accountId: string,
25
+ messageIdHeader: string,
26
+ ) => Promise<MessageItem | null>;
27
+ getEnvelopeFromEmail: (messageId: string) => Promise<string | null>;
28
+ }
5
29
 
6
30
  /**
7
31
  * Engagement counters for an outbound send.
@@ -65,7 +89,7 @@ const normalizeEmail = (raw: string): string | null => {
65
89
  const trimmed = raw.trim();
66
90
  if (trimmed === "") return null;
67
91
  const angle = trimmed.match(/<([^>]+)>/);
68
- const candidate = angle ? angle[1] : trimmed;
92
+ const candidate = angle?.[1] ?? trimmed;
69
93
  if (!candidate.includes("@")) return null;
70
94
  return candidate.toLowerCase();
71
95
  };
@@ -3,7 +3,7 @@ import {
3
3
  deserializeEncryptedPayload,
4
4
  type SecretsService,
5
5
  } from "@remit/secrets-service";
6
- import type { SmtpConfig } from "@remit/smtp-service";
6
+ import type { SmtpServerConfig } from "@remit/smtp-service";
7
7
 
8
8
  export interface SmtpConfigMissing {
9
9
  ok: false;
@@ -12,7 +12,7 @@ export interface SmtpConfigMissing {
12
12
 
13
13
  export interface SmtpConfigResolved {
14
14
  ok: true;
15
- config: SmtpConfig;
15
+ config: SmtpServerConfig;
16
16
  }
17
17
 
18
18
  export type ResolvedSmtpConfig = SmtpConfigResolved | SmtpConfigMissing;