agent-inbox 0.2.4 → 0.2.5
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/CLAUDE.md +1 -92
- package/README.md +6 -73
- package/dist/federation/connection-manager.d.ts +0 -8
- package/dist/federation/connection-manager.d.ts.map +1 -1
- package/dist/federation/connection-manager.js +0 -12
- package/dist/federation/connection-manager.js.map +1 -1
- package/dist/federation/delivery-queue.d.ts +3 -11
- package/dist/federation/delivery-queue.d.ts.map +1 -1
- package/dist/federation/delivery-queue.js +8 -38
- package/dist/federation/delivery-queue.js.map +1 -1
- package/dist/index.d.ts +0 -17
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -98
- package/dist/index.js.map +1 -1
- package/dist/jsonrpc/mail-push-types.d.ts +22 -2
- package/dist/jsonrpc/mail-push-types.d.ts.map +1 -1
- package/dist/jsonrpc/mail-push-types.js +18 -1
- package/dist/jsonrpc/mail-push-types.js.map +1 -1
- package/dist/jsonrpc/mail-push.d.ts +12 -1
- package/dist/jsonrpc/mail-push.d.ts.map +1 -1
- package/dist/jsonrpc/mail-push.js +13 -2
- package/dist/jsonrpc/mail-push.js.map +1 -1
- package/dist/jsonrpc/mail-server.d.ts.map +1 -1
- package/dist/jsonrpc/mail-server.js +42 -18
- package/dist/jsonrpc/mail-server.js.map +1 -1
- package/dist/router/message-router.d.ts +0 -15
- package/dist/router/message-router.d.ts.map +1 -1
- package/dist/router/message-router.js +3 -25
- package/dist/router/message-router.js.map +1 -1
- package/dist/storage/interface.d.ts +2 -9
- package/dist/storage/interface.d.ts.map +1 -1
- package/dist/storage/memory.d.ts +1 -4
- package/dist/storage/memory.d.ts.map +1 -1
- package/dist/storage/memory.js +6 -12
- package/dist/storage/memory.js.map +1 -1
- package/dist/storage/sqlite.d.ts +1 -6
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +6 -28
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/types.d.ts +0 -79
- package/dist/types.d.ts.map +1 -1
- package/docs/DESIGN.md +0 -15
- package/package.json +3 -28
- package/rules/agent-inbox.md +0 -1
- package/src/federation/connection-manager.ts +0 -12
- package/src/federation/delivery-queue.ts +8 -38
- package/src/index.ts +0 -148
- package/src/jsonrpc/mail-push-types.ts +43 -2
- package/src/jsonrpc/mail-push.ts +34 -3
- package/src/jsonrpc/mail-server.ts +45 -26
- package/src/router/message-router.ts +4 -41
- package/src/storage/interface.ts +2 -11
- package/src/storage/memory.ts +8 -15
- package/src/storage/sqlite.ts +9 -36
- package/src/types.ts +0 -73
- package/test/load.test.ts +1 -1
- package/test/mail-push.test.ts +101 -1
- package/test/mail-server.test.ts +108 -0
- package/AGENTS.md +0 -18
- package/dist/federation/queue-store.d.ts +0 -42
- package/dist/federation/queue-store.d.ts.map +0 -1
- package/dist/federation/queue-store.js +0 -87
- package/dist/federation/queue-store.js.map +0 -1
- package/dist/index.d.mts +0 -2
- package/dist/index.mjs +0 -1
- package/dist/index.mjs.map +0 -1
- package/dist/mail/address-book.d.ts +0 -43
- package/dist/mail/address-book.d.ts.map +0 -1
- package/dist/mail/address-book.js +0 -95
- package/dist/mail/address-book.js.map +0 -1
- package/dist/mail/attachment-store.d.ts +0 -31
- package/dist/mail/attachment-store.d.ts.map +0 -1
- package/dist/mail/attachment-store.js +0 -74
- package/dist/mail/attachment-store.js.map +0 -1
- package/dist/mail/email-mapper.d.ts +0 -41
- package/dist/mail/email-mapper.d.ts.map +0 -1
- package/dist/mail/email-mapper.js +0 -216
- package/dist/mail/email-mapper.js.map +0 -1
- package/dist/mail/fs-attachment-store.d.ts +0 -38
- package/dist/mail/fs-attachment-store.d.ts.map +0 -1
- package/dist/mail/fs-attachment-store.js +0 -165
- package/dist/mail/fs-attachment-store.js.map +0 -1
- package/dist/mail/mail-gateway.d.ts +0 -114
- package/dist/mail/mail-gateway.d.ts.map +0 -1
- package/dist/mail/mail-gateway.js +0 -402
- package/dist/mail/mail-gateway.js.map +0 -1
- package/dist/mail/provider-transport.d.ts +0 -138
- package/dist/mail/provider-transport.d.ts.map +0 -1
- package/dist/mail/provider-transport.js +0 -434
- package/dist/mail/provider-transport.js.map +0 -1
- package/dist/mail/rate-limiter.d.ts +0 -20
- package/dist/mail/rate-limiter.d.ts.map +0 -1
- package/dist/mail/rate-limiter.js +0 -56
- package/dist/mail/rate-limiter.js.map +0 -1
- package/dist/mail/smtp-transport.d.ts +0 -141
- package/dist/mail/smtp-transport.d.ts.map +0 -1
- package/dist/mail/smtp-transport.js +0 -415
- package/dist/mail/smtp-transport.js.map +0 -1
- package/dist/mail/types.d.ts +0 -177
- package/dist/mail/types.d.ts.map +0 -1
- package/dist/mail/types.js +0 -11
- package/dist/mail/types.js.map +0 -1
- package/dist/router/destination.d.ts +0 -69
- package/dist/router/destination.d.ts.map +0 -1
- package/dist/router/destination.js +0 -106
- package/dist/router/destination.js.map +0 -1
- package/docs/MAIL-INTEROP-PLAN.md +0 -660
- package/renovate.json5 +0 -6
- package/src/federation/queue-store.ts +0 -124
- package/src/mail/address-book.ts +0 -111
- package/src/mail/attachment-store.ts +0 -90
- package/src/mail/email-mapper.ts +0 -288
- package/src/mail/fs-attachment-store.ts +0 -163
- package/src/mail/mail-gateway.ts +0 -505
- package/src/mail/provider-transport.ts +0 -577
- package/src/mail/rate-limiter.ts +0 -51
- package/src/mail/smtp-transport.ts +0 -589
- package/src/mail/types.ts +0 -221
- package/src/router/destination.ts +0 -140
- package/test/federation/delivery-queue-sqlite.test.ts +0 -158
- package/test/mail/address-book.test.ts +0 -111
- package/test/mail/attachment-store-contract.test.ts +0 -92
- package/test/mail/attachment-store.test.ts +0 -69
- package/test/mail/destination.test.ts +0 -115
- package/test/mail/dsn-parse.test.ts +0 -239
- package/test/mail/email-mapper.test.ts +0 -341
- package/test/mail/external-id.test.ts +0 -43
- package/test/mail/fs-attachment-store.test.ts +0 -134
- package/test/mail/full-flow-e2e.test.ts +0 -200
- package/test/mail/mail-gateway.test.ts +0 -419
- package/test/mail/mail-transport-contract.test.ts +0 -134
- package/test/mail/mock-mail.ts +0 -161
- package/test/mail/mock-postmark.ts +0 -66
- package/test/mail/provider-transport.test.ts +0 -381
- package/test/mail/rate-limiter.test.ts +0 -48
- package/test/mail/router-mail-integration.test.ts +0 -138
- package/test/mail/smtp-e2e.test.ts +0 -98
- package/test/mail/smtp-transport.test.ts +0 -138
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared MailTransport contract suite.
|
|
3
|
-
*
|
|
4
|
-
* Any backend that provides a MailTransportHarness must pass this suite
|
|
5
|
-
* unchanged — it encodes the hardening guarantees from §2/§3 of the plan.
|
|
6
|
-
* Run it against new backends by adding another `runMailTransportContract(...)`.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
10
|
-
import type { MailTransportHarness } from "./mock-mail.js";
|
|
11
|
-
import { mockMailHarness, makeInboundMail, makeOutboundMail } from "./mock-mail.js";
|
|
12
|
-
import { postmarkHarness } from "./mock-postmark.js";
|
|
13
|
-
|
|
14
|
-
export function runMailTransportContract(
|
|
15
|
-
name: string,
|
|
16
|
-
makeHarness: () => MailTransportHarness
|
|
17
|
-
): void {
|
|
18
|
-
describe(`MailTransport contract: ${name}`, () => {
|
|
19
|
-
let h: MailTransportHarness;
|
|
20
|
-
|
|
21
|
-
beforeEach(async () => {
|
|
22
|
-
h = makeHarness();
|
|
23
|
-
await h.transport.start();
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
afterEach(async () => {
|
|
27
|
-
await h.transport.stop();
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it("start() leaves the transport in a ready state", async () => {
|
|
31
|
-
expect(h.transport.state).toBe("ready");
|
|
32
|
-
expect((await h.transport.health()).state).toBe("ready");
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it("start() is idempotent", async () => {
|
|
36
|
-
await h.transport.start();
|
|
37
|
-
await h.transport.start();
|
|
38
|
-
expect(h.transport.state).toBe("ready");
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("send() returns delivered (never throws) on success", async () => {
|
|
42
|
-
h.programNextSend({ disposition: "delivered", remoteMessageId: "x" });
|
|
43
|
-
const res = await h.transport.send(makeOutboundMail());
|
|
44
|
-
expect(res.disposition).toBe("delivered");
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it("send() returns — does not throw — on transient failure", async () => {
|
|
48
|
-
h.programNextSend({ disposition: "transient", code: 451 });
|
|
49
|
-
const res = await h.transport.send(makeOutboundMail());
|
|
50
|
-
expect(res.disposition).toBe("transient");
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it("send() returns — does not throw — on permanent failure", async () => {
|
|
54
|
-
h.programNextSend({ disposition: "permanent", code: 550 });
|
|
55
|
-
const res = await h.transport.send(makeOutboundMail());
|
|
56
|
-
expect(res.disposition).toBe("permanent");
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it("is idempotent on idempotencyKey: a duplicate delivered send produces no second email", async () => {
|
|
60
|
-
const env = makeOutboundMail({ idempotencyKey: "dup-1" });
|
|
61
|
-
const first = await h.transport.send(env);
|
|
62
|
-
const second = await h.transport.send(env);
|
|
63
|
-
expect(first.disposition).toBe("delivered");
|
|
64
|
-
expect(second.disposition).toBe("delivered");
|
|
65
|
-
// Both report delivered, but the second is a replay, not a new email.
|
|
66
|
-
expect(second.detail).toMatch(/idempotent|replay/i);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it("ACKs inbound only after the handler resolves", async () => {
|
|
70
|
-
const order: string[] = [];
|
|
71
|
-
h.transport.onReceive(async () => {
|
|
72
|
-
order.push("handler-start");
|
|
73
|
-
await Promise.resolve();
|
|
74
|
-
order.push("handler-end");
|
|
75
|
-
});
|
|
76
|
-
await h.injectInbound(makeInboundMail());
|
|
77
|
-
// injectInbound resolving (the ACK) must come strictly after handler-end.
|
|
78
|
-
expect(order).toEqual(["handler-start", "handler-end"]);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it("NACKs inbound (rejects) when the handler throws", async () => {
|
|
82
|
-
h.transport.onReceive(async () => {
|
|
83
|
-
throw new Error("storage commit failed");
|
|
84
|
-
});
|
|
85
|
-
await expect(h.injectInbound(makeInboundMail())).rejects.toThrow(
|
|
86
|
-
/storage commit failed/
|
|
87
|
-
);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it("delivers the inbound message to the handler intact", async () => {
|
|
91
|
-
let received: unknown;
|
|
92
|
-
h.transport.onReceive(async (mail) => {
|
|
93
|
-
received = mail;
|
|
94
|
-
});
|
|
95
|
-
const mail = makeInboundMail({ subject: "specific-subject" });
|
|
96
|
-
await h.injectInbound(mail);
|
|
97
|
-
expect((received as { subject: string }).subject).toBe("specific-subject");
|
|
98
|
-
});
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Run the contract against the in-process mock and the Postmark provider
|
|
103
|
-
// backend (via a fake client). Real SMTP I/O is covered by smtp-e2e.test.ts.
|
|
104
|
-
runMailTransportContract("MockMailTransport", () => mockMailHarness());
|
|
105
|
-
runMailTransportContract("PostmarkTransport", () => postmarkHarness());
|
|
106
|
-
|
|
107
|
-
describe("MockMailTransport specifics", () => {
|
|
108
|
-
let h: ReturnType<typeof mockMailHarness>;
|
|
109
|
-
|
|
110
|
-
beforeEach(async () => {
|
|
111
|
-
h = mockMailHarness();
|
|
112
|
-
await h.transport.start();
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it("send() throws (programmer error) when not started", async () => {
|
|
116
|
-
const fresh = mockMailHarness();
|
|
117
|
-
await expect(fresh.transport.send(makeOutboundMail())).rejects.toThrow(
|
|
118
|
-
/state/
|
|
119
|
-
);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it("records one entry in sent per delivered email and dedups replays", async () => {
|
|
123
|
-
const env = makeOutboundMail({ idempotencyKey: "k1" });
|
|
124
|
-
await h.transport.send(env);
|
|
125
|
-
await h.transport.send(env);
|
|
126
|
-
expect(h.mock.sent).toHaveLength(1);
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it("does not record sent for a transient failure", async () => {
|
|
130
|
-
h.programNextSend({ disposition: "transient" });
|
|
131
|
-
await h.transport.send(makeOutboundMail({ idempotencyKey: "k2" }));
|
|
132
|
-
expect(h.mock.sent).toHaveLength(0);
|
|
133
|
-
});
|
|
134
|
-
});
|
package/test/mail/mock-mail.ts
DELETED
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* In-process mail transport for tests, plus the harness abstraction the shared
|
|
3
|
-
* contract suite runs against.
|
|
4
|
-
*
|
|
5
|
-
* A real backend's test wrapper implements MailTransportHarness too, so the
|
|
6
|
-
* same contract suite (mail-transport-contract.test.ts) validates it without
|
|
7
|
-
* modification. See docs/MAIL-INTEROP-PLAN.md §11.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import type {
|
|
11
|
-
MailTransport,
|
|
12
|
-
MailCapabilities,
|
|
13
|
-
MailTransportState,
|
|
14
|
-
MailHealth,
|
|
15
|
-
OutboundMail,
|
|
16
|
-
MailSendResult,
|
|
17
|
-
InboundMail,
|
|
18
|
-
InboundHandler,
|
|
19
|
-
} from "../../src/mail/types.js";
|
|
20
|
-
|
|
21
|
-
const DEFAULT_CAPS: MailCapabilities = {
|
|
22
|
-
outbound: "api",
|
|
23
|
-
signsDkim: true,
|
|
24
|
-
verifiesInboundAuth: true,
|
|
25
|
-
inbound: "webhook",
|
|
26
|
-
maxMessageBytes: 25 * 1024 * 1024,
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* The control surface a contract test needs over any transport: drive inbound,
|
|
31
|
-
* and program the next outbound outcome. Real backends provide their own.
|
|
32
|
-
*/
|
|
33
|
-
export interface MailTransportHarness {
|
|
34
|
-
transport: MailTransport;
|
|
35
|
-
/** Drive one inbound message; resolves on ACK, rejects on NACK (handler throw). */
|
|
36
|
-
injectInbound(mail: InboundMail): Promise<void>;
|
|
37
|
-
/** Force the next send() to produce this result. */
|
|
38
|
-
programNextSend(result: MailSendResult): void;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export class MockMailTransport implements MailTransport {
|
|
42
|
-
readonly capabilities: MailCapabilities;
|
|
43
|
-
private _state: MailTransportState = "stopped";
|
|
44
|
-
private handler?: InboundHandler;
|
|
45
|
-
|
|
46
|
-
/** Recorded successfully-issued sends (one entry per external email produced). */
|
|
47
|
-
readonly sent: OutboundMail[] = [];
|
|
48
|
-
/** idempotencyKeys that have already produced a delivered external email. */
|
|
49
|
-
private delivered = new Set<string>();
|
|
50
|
-
private programmed: MailSendResult[] = [];
|
|
51
|
-
|
|
52
|
-
constructor(caps: Partial<MailCapabilities> = {}) {
|
|
53
|
-
this.capabilities = { ...DEFAULT_CAPS, ...caps };
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
get state(): MailTransportState {
|
|
57
|
-
return this._state;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async start(): Promise<void> {
|
|
61
|
-
if (this._state === "ready") return; // idempotent
|
|
62
|
-
this._state = "starting";
|
|
63
|
-
this._state = "ready";
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
async stop(): Promise<void> {
|
|
67
|
-
this._state = "stopping";
|
|
68
|
-
this._state = "stopped";
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
async health(): Promise<MailHealth> {
|
|
72
|
-
return { state: this._state };
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
onReceive(handler: InboundHandler): void {
|
|
76
|
-
this.handler = handler;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
programNextSend(result: MailSendResult): void {
|
|
80
|
-
this.programmed.push(result);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
async send(envelope: OutboundMail): Promise<MailSendResult> {
|
|
84
|
-
if (this._state !== "ready") {
|
|
85
|
-
throw new Error(`send() called while transport state is "${this._state}"`);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Idempotent: a duplicate key for an already-delivered message must not
|
|
89
|
-
// produce a second external email.
|
|
90
|
-
if (this.delivered.has(envelope.idempotencyKey)) {
|
|
91
|
-
return {
|
|
92
|
-
disposition: "delivered",
|
|
93
|
-
remoteMessageId: `mock-dup-${envelope.idempotencyKey}`,
|
|
94
|
-
detail: "idempotent replay",
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const result: MailSendResult =
|
|
99
|
-
this.programmed.shift() ?? {
|
|
100
|
-
disposition: "delivered",
|
|
101
|
-
remoteMessageId: `mock-${envelope.idempotencyKey}`,
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
if (result.disposition === "delivered") {
|
|
105
|
-
this.sent.push(envelope);
|
|
106
|
-
this.delivered.add(envelope.idempotencyKey);
|
|
107
|
-
}
|
|
108
|
-
return result;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/** Test helper: drive an inbound message through the registered handler. */
|
|
112
|
-
async deliver(mail: InboundMail): Promise<void> {
|
|
113
|
-
if (!this.handler) throw new Error("no inbound handler registered");
|
|
114
|
-
if (this._state !== "ready") {
|
|
115
|
-
throw new Error(`deliver() while state is "${this._state}"`);
|
|
116
|
-
}
|
|
117
|
-
await this.handler(mail); // throw bubbles up as a NACK
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/** Build a MailTransportHarness around a MockMailTransport. */
|
|
122
|
-
export function mockMailHarness(
|
|
123
|
-
caps?: Partial<MailCapabilities>
|
|
124
|
-
): MailTransportHarness & { mock: MockMailTransport } {
|
|
125
|
-
const mock = new MockMailTransport(caps);
|
|
126
|
-
return {
|
|
127
|
-
mock,
|
|
128
|
-
transport: mock,
|
|
129
|
-
injectInbound: (mail) => mock.deliver(mail),
|
|
130
|
-
programNextSend: (result) => mock.programNextSend(result),
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/** Minimal valid InboundMail for tests; override fields as needed. */
|
|
135
|
-
export function makeInboundMail(over: Partial<InboundMail> = {}): InboundMail {
|
|
136
|
-
return {
|
|
137
|
-
envelopeFrom: "ext@example.org",
|
|
138
|
-
envelopeTo: ["agent@example.com"],
|
|
139
|
-
from: { address: "ext@example.org" },
|
|
140
|
-
to: [{ address: "agent@example.com" }],
|
|
141
|
-
subject: "hello",
|
|
142
|
-
text: "hi there",
|
|
143
|
-
headers: { raw: {} },
|
|
144
|
-
sizeBytes: 100,
|
|
145
|
-
receivedAt: new Date().toISOString(),
|
|
146
|
-
...over,
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/** Minimal valid OutboundMail for tests. */
|
|
151
|
-
export function makeOutboundMail(over: Partial<OutboundMail> = {}): OutboundMail {
|
|
152
|
-
return {
|
|
153
|
-
idempotencyKey: "msg-1",
|
|
154
|
-
from: { address: "agent@example.com" },
|
|
155
|
-
to: [{ address: "ext@example.org" }],
|
|
156
|
-
subject: "re: hello",
|
|
157
|
-
text: "reply body",
|
|
158
|
-
headers: { messageId: "<msg-1@example.com>" },
|
|
159
|
-
...over,
|
|
160
|
-
};
|
|
161
|
-
}
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Fake Postmark client + harness so the shared MailTransport contract suite can
|
|
3
|
-
* run against PostmarkTransport without the SDK or network. The fake produces
|
|
4
|
-
* raw Postmark responses/errors keyed by a desired disposition, and the real
|
|
5
|
-
* transport mapping turns them into MailSendResults — so the contract suite
|
|
6
|
-
* also exercises postmarkResponseToResult / postmarkErrorToResult.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
PostmarkTransport,
|
|
11
|
-
type PostmarkClient,
|
|
12
|
-
type PostmarkSendResponse,
|
|
13
|
-
} from "../../src/mail/provider-transport.js";
|
|
14
|
-
import type { MailSendResult } from "../../src/mail/types.js";
|
|
15
|
-
import type { MailTransportHarness } from "./mock-mail.js";
|
|
16
|
-
|
|
17
|
-
type Disposition = MailSendResult["disposition"];
|
|
18
|
-
|
|
19
|
-
export interface FakePostmarkClient extends PostmarkClient {
|
|
20
|
-
/** Raw messages passed to sendEmail. */
|
|
21
|
-
sent: Record<string, unknown>[];
|
|
22
|
-
/** Program the next send's outcome. */
|
|
23
|
-
program(disposition: Disposition): void;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function fakePostmarkClient(): FakePostmarkClient {
|
|
27
|
-
const queue: Disposition[] = [];
|
|
28
|
-
const sent: Record<string, unknown>[] = [];
|
|
29
|
-
let counter = 0;
|
|
30
|
-
return {
|
|
31
|
-
sent,
|
|
32
|
-
program(d) {
|
|
33
|
-
queue.push(d);
|
|
34
|
-
},
|
|
35
|
-
async sendEmail(msg): Promise<PostmarkSendResponse> {
|
|
36
|
-
sent.push(msg);
|
|
37
|
-
const d = queue.shift() ?? "delivered";
|
|
38
|
-
if (d === "delivered") {
|
|
39
|
-
return { ErrorCode: 0, MessageID: `pm-${++counter}` };
|
|
40
|
-
}
|
|
41
|
-
if (d === "transient") {
|
|
42
|
-
throw Object.assign(new Error("rate limited"), { statusCode: 429 });
|
|
43
|
-
}
|
|
44
|
-
// permanent — inactive recipient (Postmark code 406, HTTP 422)
|
|
45
|
-
throw Object.assign(new Error("inactive recipient"), {
|
|
46
|
-
statusCode: 422,
|
|
47
|
-
code: 406,
|
|
48
|
-
});
|
|
49
|
-
},
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function postmarkHarness(): MailTransportHarness & {
|
|
54
|
-
client: FakePostmarkClient;
|
|
55
|
-
postmark: PostmarkTransport;
|
|
56
|
-
} {
|
|
57
|
-
const client = fakePostmarkClient();
|
|
58
|
-
const transport = new PostmarkTransport({ client });
|
|
59
|
-
return {
|
|
60
|
-
client,
|
|
61
|
-
postmark: transport,
|
|
62
|
-
transport,
|
|
63
|
-
injectInbound: (mail) => transport.receiveInbound(mail),
|
|
64
|
-
programNextSend: (result) => client.program(result.disposition),
|
|
65
|
-
};
|
|
66
|
-
}
|