@benclmnt/postmock 0.0.1
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/LICENSE +21 -0
- package/README.md +251 -0
- package/dist/seeds/conformance/00-core.d.ts +4 -0
- package/dist/seeds/conformance/00-core.js +12 -0
- package/dist/seeds/conformance/20-bounces.d.ts +3 -0
- package/dist/seeds/conformance/20-bounces.js +108 -0
- package/dist/seeds/conformance/40-messages.d.ts +3 -0
- package/dist/seeds/conformance/40-messages.js +144 -0
- package/dist/seeds/conformance/50-server-color.d.ts +8 -0
- package/dist/seeds/conformance/50-server-color.js +13 -0
- package/dist/seeds/conformance/70-account.d.ts +9 -0
- package/dist/seeds/conformance/70-account.js +29 -0
- package/dist/seeds/conformance/80-templates.d.ts +4 -0
- package/dist/seeds/conformance/80-templates.js +23 -0
- package/dist/seeds/conformance.d.ts +2 -0
- package/dist/seeds/conformance.js +3 -0
- package/dist/seeds/empty.d.ts +4 -0
- package/dist/seeds/empty.js +3 -0
- package/dist/seeds/lib/conformance.d.ts +16 -0
- package/dist/seeds/lib/conformance.js +18 -0
- package/dist/seeds/lib/history.d.ts +27 -0
- package/dist/seeds/lib/history.js +129 -0
- package/dist/seeds/lib/read-server.d.ts +10 -0
- package/dist/seeds/lib/read-server.js +10 -0
- package/dist/seeds/lib/template-server.d.ts +13 -0
- package/dist/seeds/lib/template-server.js +13 -0
- package/dist/src/api/account/authentication.d.ts +39 -0
- package/dist/src/api/account/authentication.js +97 -0
- package/dist/src/api/account/domains.d.ts +59 -0
- package/dist/src/api/account/domains.js +78 -0
- package/dist/src/api/account/paging.d.ts +12 -0
- package/dist/src/api/account/paging.js +35 -0
- package/dist/src/api/account/push.d.ts +14 -0
- package/dist/src/api/account/push.js +84 -0
- package/dist/src/api/account/routes.d.ts +1 -0
- package/dist/src/api/account/routes.js +288 -0
- package/dist/src/api/account/senders.d.ts +54 -0
- package/dist/src/api/account/senders.js +87 -0
- package/dist/src/api/account/servers.d.ts +40 -0
- package/dist/src/api/account/servers.js +148 -0
- package/dist/src/api/bounces/json.d.ts +25 -0
- package/dist/src/api/bounces/json.js +27 -0
- package/dist/src/api/bounces/routes.d.ts +1 -0
- package/dist/src/api/bounces/routes.js +127 -0
- package/dist/src/api/bulk/bulk.d.ts +43 -0
- package/dist/src/api/bulk/bulk.js +81 -0
- package/dist/src/api/bulk/routes.d.ts +1 -0
- package/dist/src/api/bulk/routes.js +252 -0
- package/dist/src/api/data-removals/routes.d.ts +1 -0
- package/dist/src/api/data-removals/routes.js +59 -0
- package/dist/src/api/email/json.d.ts +11 -0
- package/dist/src/api/email/json.js +33 -0
- package/dist/src/api/email/routes.d.ts +1 -0
- package/dist/src/api/email/routes.js +64 -0
- package/dist/src/api/inbound/routes.d.ts +1 -0
- package/dist/src/api/inbound/routes.js +40 -0
- package/dist/src/api/index.d.ts +1 -0
- package/dist/src/api/index.js +3 -0
- package/dist/src/api/message-streams/routes.d.ts +1 -0
- package/dist/src/api/message-streams/routes.js +211 -0
- package/dist/src/api/message-streams/streams.d.ts +28 -0
- package/dist/src/api/message-streams/streams.js +50 -0
- package/dist/src/api/messages/json.d.ts +149 -0
- package/dist/src/api/messages/json.js +114 -0
- package/dist/src/api/messages/query.d.ts +19 -0
- package/dist/src/api/messages/query.js +64 -0
- package/dist/src/api/messages/routes.d.ts +1 -0
- package/dist/src/api/messages/routes.js +203 -0
- package/dist/src/api/server/json.d.ts +26 -0
- package/dist/src/api/server/json.js +25 -0
- package/dist/src/api/server/routes.d.ts +1 -0
- package/dist/src/api/server/routes.js +17 -0
- package/dist/src/api/stats/aggregate.d.ts +58 -0
- package/dist/src/api/stats/aggregate.js +66 -0
- package/dist/src/api/stats/routes.d.ts +1 -0
- package/dist/src/api/stats/routes.js +72 -0
- package/dist/src/api/suppressions/routes.d.ts +1 -0
- package/dist/src/api/suppressions/routes.js +98 -0
- package/dist/src/api/templates/content.d.ts +21 -0
- package/dist/src/api/templates/content.js +39 -0
- package/dist/src/api/templates/routes.d.ts +1 -0
- package/dist/src/api/templates/routes.js +164 -0
- package/dist/src/api/templates/send.d.ts +13 -0
- package/dist/src/api/templates/send.js +105 -0
- package/dist/src/api/templates/templates.d.ts +100 -0
- package/dist/src/api/templates/templates.js +218 -0
- package/dist/src/api/templates/validate.d.ts +15 -0
- package/dist/src/api/templates/validate.js +77 -0
- package/dist/src/api/triggers/routes.d.ts +1 -0
- package/dist/src/api/triggers/routes.js +62 -0
- package/dist/src/api/webhooks/routes.d.ts +14 -0
- package/dist/src/api/webhooks/routes.js +192 -0
- package/dist/src/config.d.ts +17 -0
- package/dist/src/config.js +95 -0
- package/dist/src/control/app.d.ts +5 -0
- package/dist/src/control/app.js +42 -0
- package/dist/src/control/endpoints/account.d.ts +1 -0
- package/dist/src/control/endpoints/account.js +68 -0
- package/dist/src/control/endpoints/bulk.d.ts +1 -0
- package/dist/src/control/endpoints/bulk.js +29 -0
- package/dist/src/control/endpoints/core.d.ts +1 -0
- package/dist/src/control/endpoints/core.js +104 -0
- package/dist/src/control/endpoints/inbound.d.ts +1 -0
- package/dist/src/control/endpoints/inbound.js +61 -0
- package/dist/src/control/endpoints/recipients.d.ts +1 -0
- package/dist/src/control/endpoints/recipients.js +136 -0
- package/dist/src/control/endpoints/smtp.d.ts +1 -0
- package/dist/src/control/endpoints/smtp.js +74 -0
- package/dist/src/control/endpoints/tracking.d.ts +1 -0
- package/dist/src/control/endpoints/tracking.js +95 -0
- package/dist/src/control/endpoints/webhooks.d.ts +1 -0
- package/dist/src/control/endpoints/webhooks.js +36 -0
- package/dist/src/control/index.d.ts +1 -0
- package/dist/src/control/index.js +3 -0
- package/dist/src/control/registry.d.ts +25 -0
- package/dist/src/control/registry.js +19 -0
- package/dist/src/control/seed.d.ts +11 -0
- package/dist/src/control/seed.js +32 -0
- package/dist/src/control/seeding.d.ts +10 -0
- package/dist/src/control/seeding.js +27 -0
- package/dist/src/discover.d.ts +16 -0
- package/dist/src/discover.js +49 -0
- package/dist/src/errors.d.ts +49 -0
- package/dist/src/errors.js +701 -0
- package/dist/src/events.d.ts +49 -0
- package/dist/src/events.js +19 -0
- package/dist/src/http/app.d.ts +8 -0
- package/dist/src/http/app.js +51 -0
- package/dist/src/http/auth.d.ts +7 -0
- package/dist/src/http/auth.js +32 -0
- package/dist/src/http/faults.d.ts +8 -0
- package/dist/src/http/faults.js +33 -0
- package/dist/src/http/normalize.d.ts +57 -0
- package/dist/src/http/normalize.js +225 -0
- package/dist/src/http/respond.d.ts +23 -0
- package/dist/src/http/respond.js +37 -0
- package/dist/src/http/routes.d.ts +60 -0
- package/dist/src/http/routes.js +64 -0
- package/dist/src/inbound/deliver.d.ts +9 -0
- package/dist/src/inbound/deliver.js +34 -0
- package/dist/src/inbound/parse.d.ts +21 -0
- package/dist/src/inbound/parse.js +71 -0
- package/dist/src/inbound/receive.d.ts +17 -0
- package/dist/src/inbound/receive.js +89 -0
- package/dist/src/main.d.ts +2 -0
- package/dist/src/main.js +14 -0
- package/dist/src/mime/compose.d.ts +33 -0
- package/dist/src/mime/compose.js +58 -0
- package/dist/src/pipeline/addresses.d.ts +6 -0
- package/dist/src/pipeline/addresses.js +38 -0
- package/dist/src/pipeline/inactive.d.ts +8 -0
- package/dist/src/pipeline/inactive.js +20 -0
- package/dist/src/pipeline/submit.d.ts +138 -0
- package/dist/src/pipeline/submit.js +415 -0
- package/dist/src/plugins/message-events.d.ts +7 -0
- package/dist/src/plugins/message-events.js +76 -0
- package/dist/src/plugins/smtp.d.ts +15 -0
- package/dist/src/plugins/smtp.js +42 -0
- package/dist/src/plugins/stats.d.ts +7 -0
- package/dist/src/plugins/stats.js +78 -0
- package/dist/src/plugins/webhooks.d.ts +4 -0
- package/dist/src/plugins/webhooks.js +80 -0
- package/dist/src/plugins.d.ts +20 -0
- package/dist/src/plugins.js +9 -0
- package/dist/src/recipients/test-bounces.d.ts +15 -0
- package/dist/src/recipients/test-bounces.js +31 -0
- package/dist/src/recipients/transitions.d.ts +57 -0
- package/dist/src/recipients/transitions.js +279 -0
- package/dist/src/render/mustachio.d.ts +60 -0
- package/dist/src/render/mustachio.js +265 -0
- package/dist/src/runtime.d.ts +12 -0
- package/dist/src/runtime.js +11 -0
- package/dist/src/server.d.ts +25 -0
- package/dist/src/server.js +49 -0
- package/dist/src/smtp/auth.d.ts +35 -0
- package/dist/src/smtp/auth.js +66 -0
- package/dist/src/smtp/listener.d.ts +16 -0
- package/dist/src/smtp/listener.js +121 -0
- package/dist/src/smtp/mime.d.ts +36 -0
- package/dist/src/smtp/mime.js +181 -0
- package/dist/src/smtp/receive.d.ts +21 -0
- package/dist/src/smtp/receive.js +89 -0
- package/dist/src/smtp/reply.d.ts +14 -0
- package/dist/src/smtp/reply.js +24 -0
- package/dist/src/state/clock.d.ts +35 -0
- package/dist/src/state/clock.js +107 -0
- package/dist/src/state/ids.d.ts +6 -0
- package/dist/src/state/ids.js +7 -0
- package/dist/src/state/servers.d.ts +27 -0
- package/dist/src/state/servers.js +101 -0
- package/dist/src/state/store.d.ts +57 -0
- package/dist/src/state/store.js +98 -0
- package/dist/src/state/suppressions.d.ts +9 -0
- package/dist/src/state/suppressions.js +8 -0
- package/dist/src/state/types.d.ts +530 -0
- package/dist/src/state/types.js +41 -0
- package/dist/src/time.d.ts +11 -0
- package/dist/src/time.js +48 -0
- package/dist/src/tracking.d.ts +53 -0
- package/dist/src/tracking.js +150 -0
- package/dist/src/webhooks/deliver.d.ts +60 -0
- package/dist/src/webhooks/deliver.js +172 -0
- package/dist/src/webhooks/egress.d.ts +19 -0
- package/dist/src/webhooks/egress.js +40 -0
- package/dist/src/webhooks/outbound.d.ts +26 -0
- package/dist/src/webhooks/outbound.js +47 -0
- package/dist/src/webhooks/payloads.d.ts +111 -0
- package/dist/src/webhooks/payloads.js +128 -0
- package/package.json +55 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tokens and addresses every runner configures its suite with. The tokens exist only in postmock,
|
|
3
|
+
* so a misrouted request gets 401 from real Postmark and sends nothing (docs/09 §4).
|
|
4
|
+
* Addresses avoid `.test` and `.local`: postmark-python validates them (docs/08 E12).
|
|
5
|
+
*/
|
|
6
|
+
export declare const CONFORMANCE: {
|
|
7
|
+
readonly serverId: 10;
|
|
8
|
+
readonly accountToken: "postmock-account-token";
|
|
9
|
+
readonly serverToken: "postmock-server-token";
|
|
10
|
+
readonly serverName: "postmock conformance";
|
|
11
|
+
readonly senderEmail: "sender@example.com";
|
|
12
|
+
readonly recipientEmail: "recipient@example.com";
|
|
13
|
+
readonly domain: "example.com";
|
|
14
|
+
/** A second verified domain: the placeholder `From` domain of an SDK example. */
|
|
15
|
+
readonly exampleDomain: "your-verified-domain.com";
|
|
16
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tokens and addresses every runner configures its suite with. The tokens exist only in postmock,
|
|
3
|
+
* so a misrouted request gets 401 from real Postmark and sends nothing (docs/09 §4).
|
|
4
|
+
* Addresses avoid `.test` and `.local`: postmark-python validates them (docs/08 E12).
|
|
5
|
+
*/
|
|
6
|
+
export const CONFORMANCE = {
|
|
7
|
+
// Not 1: the push tests expect server 1 to be missing from the account
|
|
8
|
+
// (sdk/postmark.js/test/integration/Templates.test.ts:178-213; sdk/postmark-java/src/test/java/integration/TemplatePushTest.java:22-26).
|
|
9
|
+
serverId: 10,
|
|
10
|
+
accountToken: "postmock-account-token",
|
|
11
|
+
serverToken: "postmock-server-token",
|
|
12
|
+
serverName: "postmock conformance",
|
|
13
|
+
senderEmail: "sender@example.com",
|
|
14
|
+
recipientEmail: "recipient@example.com",
|
|
15
|
+
domain: "example.com",
|
|
16
|
+
/** A second verified domain: the placeholder `From` domain of an SDK example. */
|
|
17
|
+
exampleDomain: "your-verified-domain.com",
|
|
18
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Runtime } from "../../src/runtime.ts";
|
|
2
|
+
import type { Bounce, BounceType, InboundMessage, OutboundMessage } from "../../src/state/types.ts";
|
|
3
|
+
/** An outbound message accepted at `ReceivedAt`. */
|
|
4
|
+
export declare function pastSend(runtime: Runtime, fields: Partial<OutboundMessage> & Pick<OutboundMessage, "ServerID" | "ReceivedAt" | "To">): Promise<OutboundMessage>;
|
|
5
|
+
/**
|
|
6
|
+
* The first To recipient's server bounces the message at `at`, through the recipient state machine
|
|
7
|
+
* (a hard bounce suppresses the address). `id` comes from the caller's range (docs/11 §5).
|
|
8
|
+
*/
|
|
9
|
+
export declare const pastBounce: (runtime: Runtime, message: OutboundMessage, { id, type, at }: {
|
|
10
|
+
id: number;
|
|
11
|
+
type: BounceType;
|
|
12
|
+
at: Date;
|
|
13
|
+
}) => Promise<Bounce>;
|
|
14
|
+
/**
|
|
15
|
+
* An SMTP message to a suppressed address: Postmark accepts no message and records an SMTP API
|
|
16
|
+
* error bounce (docs/07 §1.4). `email` must be suppressed on `stream`.
|
|
17
|
+
*/
|
|
18
|
+
export declare function pastSmtpApiError(runtime: Runtime, fields: {
|
|
19
|
+
id: number;
|
|
20
|
+
serverId: number;
|
|
21
|
+
stream: string;
|
|
22
|
+
email: string;
|
|
23
|
+
tag: string | null;
|
|
24
|
+
at: Date;
|
|
25
|
+
}): Promise<Bounce>;
|
|
26
|
+
/** An inbound message stored as already processed, blocked or failed; no event fires. */
|
|
27
|
+
export declare function pastInbound(runtime: Runtime, fields: Partial<InboundMessage> & Pick<InboundMessage, "ServerID" | "ReceivedAt" | "Status">): InboundMessage;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { composeMime } from "../../src/mime/compose.js";
|
|
3
|
+
import { inactiveRecipientsError } from "../../src/pipeline/inactive.js";
|
|
4
|
+
import { recordBounceAt } from "../../src/recipients/transitions.js";
|
|
5
|
+
import { smtpApiErrorBounce } from "../../src/smtp/receive.js";
|
|
6
|
+
import { newMessageId } from "../../src/state/ids.js";
|
|
7
|
+
import { findSuppression } from "../../src/state/suppressions.js";
|
|
8
|
+
import { CONFORMANCE } from "./conformance.js";
|
|
9
|
+
// Past traffic for seeds and tests: messages accepted and bounced before "now". Each record fires
|
|
10
|
+
// the event live traffic fires, so message events and stats follow.
|
|
11
|
+
/** MIME source for the dump endpoint, with the headers Postmark adds (refs/api_messages-api.md:276). */
|
|
12
|
+
const rawSource = (m) => composeMime({
|
|
13
|
+
from: m.From,
|
|
14
|
+
to: m.To.map((a) => a.Email),
|
|
15
|
+
cc: [],
|
|
16
|
+
subject: m.Subject ?? "",
|
|
17
|
+
text: m.TextBody ?? "",
|
|
18
|
+
headers: [
|
|
19
|
+
...(m.Tag === null ? [] : [{ name: "X-PM-Tag", value: m.Tag }]),
|
|
20
|
+
{ name: "X-PM-Message-Id", value: m.MessageID },
|
|
21
|
+
{ name: "Message-ID", value: `<${randomUUID()}@mtasv.net>` },
|
|
22
|
+
],
|
|
23
|
+
attachments: [],
|
|
24
|
+
}, m.ReceivedAt);
|
|
25
|
+
/** An outbound message accepted at `ReceivedAt`. */
|
|
26
|
+
export async function pastSend(runtime, fields) {
|
|
27
|
+
const message = {
|
|
28
|
+
MessageID: newMessageId(),
|
|
29
|
+
MessageStream: "outbound",
|
|
30
|
+
From: "sender@example.com",
|
|
31
|
+
Cc: [],
|
|
32
|
+
Bcc: [],
|
|
33
|
+
ReplyTo: null,
|
|
34
|
+
Subject: "History",
|
|
35
|
+
HtmlBody: null,
|
|
36
|
+
TextBody: "History",
|
|
37
|
+
Tag: null,
|
|
38
|
+
Headers: [],
|
|
39
|
+
Attachments: [],
|
|
40
|
+
Metadata: {},
|
|
41
|
+
TrackOpens: false,
|
|
42
|
+
TrackLinks: "None",
|
|
43
|
+
Status: "Sent",
|
|
44
|
+
Sandboxed: false,
|
|
45
|
+
MessageEvents: [],
|
|
46
|
+
channel: "rest",
|
|
47
|
+
request: null,
|
|
48
|
+
rawSource: "",
|
|
49
|
+
bulkRequestId: null,
|
|
50
|
+
templateId: null,
|
|
51
|
+
suppressedRecipients: [],
|
|
52
|
+
...fields,
|
|
53
|
+
};
|
|
54
|
+
message.rawSource = rawSource(message);
|
|
55
|
+
runtime.store.state.outbound.set(message.MessageID, message);
|
|
56
|
+
await runtime.events.emit("sent", { message });
|
|
57
|
+
return message;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The first To recipient's server bounces the message at `at`, through the recipient state machine
|
|
61
|
+
* (a hard bounce suppresses the address). `id` comes from the caller's range (docs/11 §5).
|
|
62
|
+
*/
|
|
63
|
+
export const pastBounce = (runtime, message, { id, type, at }) => recordBounceAt(runtime, {
|
|
64
|
+
message,
|
|
65
|
+
email: message.To[0]?.Email,
|
|
66
|
+
type,
|
|
67
|
+
details: "smtp;550 5.1.1 The email account that you tried to reach does not exist.",
|
|
68
|
+
content: `Return-Path: <>\r\nSubject: Undeliverable: ${message.Subject}\r\n\r\nbounce dump\r\n`,
|
|
69
|
+
}, { id: runtime.store.useId("bounce", id), at });
|
|
70
|
+
/**
|
|
71
|
+
* An SMTP message to a suppressed address: Postmark accepts no message and records an SMTP API
|
|
72
|
+
* error bounce (docs/07 §1.4). `email` must be suppressed on `stream`.
|
|
73
|
+
*/
|
|
74
|
+
export async function pastSmtpApiError(runtime, fields) {
|
|
75
|
+
if (findSuppression(runtime.store.state, fields.serverId, fields.stream, fields.email) === undefined) {
|
|
76
|
+
throw new Error(`${fields.email} is not suppressed on ${fields.stream}`);
|
|
77
|
+
}
|
|
78
|
+
const subject = "History";
|
|
79
|
+
const bounce = smtpApiErrorBounce(runtime.store.useId("bounce", fields.id), fields.at, {
|
|
80
|
+
serverId: fields.serverId,
|
|
81
|
+
stream: fields.stream,
|
|
82
|
+
messageId: newMessageId(),
|
|
83
|
+
tag: fields.tag,
|
|
84
|
+
from: CONFORMANCE.senderEmail,
|
|
85
|
+
subject,
|
|
86
|
+
metadata: {},
|
|
87
|
+
source: `From: ${CONFORMANCE.senderEmail}\r\nTo: ${fields.email}\r\nSubject: ${subject}\r\n\r\n${subject}\r\n`,
|
|
88
|
+
}, fields.email, inactiveRecipientsError([fields.email]));
|
|
89
|
+
runtime.store.state.bounces.set(bounce.ID, bounce);
|
|
90
|
+
await runtime.events.emit("smtpApiError", { bounce });
|
|
91
|
+
return bounce;
|
|
92
|
+
}
|
|
93
|
+
/** An inbound message stored as already processed, blocked or failed; no event fires. */
|
|
94
|
+
export function pastInbound(runtime, fields) {
|
|
95
|
+
const server = runtime.store.state.servers.get(fields.ServerID);
|
|
96
|
+
if (server === undefined)
|
|
97
|
+
throw new Error(`no server ${fields.ServerID}`);
|
|
98
|
+
const from = { Email: "writer@example.com", Name: "Writer", MailboxHash: "" };
|
|
99
|
+
const to = { Email: server.InboundAddress, Name: "", MailboxHash: "" };
|
|
100
|
+
const message = {
|
|
101
|
+
MessageID: newMessageId(),
|
|
102
|
+
MessageStream: "inbound",
|
|
103
|
+
From: from.Email,
|
|
104
|
+
FromName: from.Name,
|
|
105
|
+
FromFull: from,
|
|
106
|
+
To: to.Email,
|
|
107
|
+
ToFull: [to],
|
|
108
|
+
Cc: "",
|
|
109
|
+
CcFull: [],
|
|
110
|
+
Bcc: "",
|
|
111
|
+
BccFull: [],
|
|
112
|
+
OriginalRecipient: to.Email,
|
|
113
|
+
ReplyTo: "",
|
|
114
|
+
Subject: "Inbound",
|
|
115
|
+
MailboxHash: "",
|
|
116
|
+
Date: fields.ReceivedAt.toUTCString().replace("GMT", "+0000"),
|
|
117
|
+
TextBody: "Inbound message.",
|
|
118
|
+
HtmlBody: "",
|
|
119
|
+
StrippedTextReply: "",
|
|
120
|
+
Tag: "",
|
|
121
|
+
Headers: [{ Name: "MIME-Version", Value: "1.0" }],
|
|
122
|
+
Attachments: [],
|
|
123
|
+
BlockedReason: fields.Status === "Blocked" ? `Inbound request blocked by domain rule: ${from.Email}` : null,
|
|
124
|
+
rawEmail: `From: ${from.Email}\r\nTo: ${to.Email}\r\nSubject: Inbound\r\n\r\nInbound message.\r\n`,
|
|
125
|
+
...fields,
|
|
126
|
+
};
|
|
127
|
+
runtime.store.state.inbound.set(message.MessageID, message);
|
|
128
|
+
return message;
|
|
129
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The server that holds the message history the read suites need (`seeds/conformance/40-messages.ts`).
|
|
3
|
+
* A runner maps the dotnet and php `READ_*` tokens to `token`; the write suites keep using the core
|
|
4
|
+
* conformance server, whose counts this history leaves alone. IDs from the T4 range (docs/11 §5).
|
|
5
|
+
*/
|
|
6
|
+
export declare const READ_SERVER: {
|
|
7
|
+
readonly id: 4000;
|
|
8
|
+
readonly token: "postmock-read-server-token";
|
|
9
|
+
readonly name: "postmock read history";
|
|
10
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The server that holds the message history the read suites need (`seeds/conformance/40-messages.ts`).
|
|
3
|
+
* A runner maps the dotnet and php `READ_*` tokens to `token`; the write suites keep using the core
|
|
4
|
+
* conformance server, whose counts this history leaves alone. IDs from the T4 range (docs/11 §5).
|
|
5
|
+
*/
|
|
6
|
+
export const READ_SERVER = {
|
|
7
|
+
id: 4000,
|
|
8
|
+
token: "postmock-read-server-token",
|
|
9
|
+
name: "postmock read history",
|
|
10
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The server whose template list starts with one template (`seeds/conformance/80-templates.ts`).
|
|
3
|
+
* postmark-cli sends with the first listed template before its own tests create any
|
|
4
|
+
* (sdk/postmark-cli/test/integration/email.template.test.ts:36-39,62-67). The dotnet suite counts the
|
|
5
|
+
* templates of the core conformance server, so they stay on their own server. IDs from the T8
|
|
6
|
+
* range (docs/11 §5).
|
|
7
|
+
*/
|
|
8
|
+
export declare const TEMPLATE_SERVER: {
|
|
9
|
+
readonly id: 8000;
|
|
10
|
+
readonly token: "postmock-template-server-token";
|
|
11
|
+
readonly name: "postmock templates";
|
|
12
|
+
readonly templateId: 8000;
|
|
13
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The server whose template list starts with one template (`seeds/conformance/80-templates.ts`).
|
|
3
|
+
* postmark-cli sends with the first listed template before its own tests create any
|
|
4
|
+
* (sdk/postmark-cli/test/integration/email.template.test.ts:36-39,62-67). The dotnet suite counts the
|
|
5
|
+
* templates of the core conformance server, so they stay on their own server. IDs from the T8
|
|
6
|
+
* range (docs/11 §5).
|
|
7
|
+
*/
|
|
8
|
+
export const TEMPLATE_SERVER = {
|
|
9
|
+
id: 8000,
|
|
10
|
+
token: "postmock-template-server-token",
|
|
11
|
+
name: "postmock templates",
|
|
12
|
+
templateId: 8000,
|
|
13
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Domain } from "../../state/types.ts";
|
|
2
|
+
/** The SPF, DKIM and Return-Path fields that domains and sender signatures share. */
|
|
3
|
+
export type Authentication = Omit<Domain, "ID" | "Name">;
|
|
4
|
+
export declare function newAuthentication(domain: string, returnPath: string, now: Date): Authentication;
|
|
5
|
+
/**
|
|
6
|
+
* A key still pending answers 505: the gem live test renews the key of a new domain and a new
|
|
7
|
+
* signature and expects it (sdk/postmark-gem/spec/integration/account_api_client_spec.rb:39-41, :76-78).
|
|
8
|
+
*/
|
|
9
|
+
export declare function refuseWhileRenewing(auth: Authentication): void;
|
|
10
|
+
/** Starts a new DKIM key. */
|
|
11
|
+
export declare function rotateDkim(auth: Authentication, domain: string, now: Date): void;
|
|
12
|
+
/** Postmark found the pending DKIM key in DNS: it becomes the active key. */
|
|
13
|
+
export declare function confirmDkim(auth: Authentication): void;
|
|
14
|
+
/** A new Return-Path needs its CNAME verified again; `""` clears it. */
|
|
15
|
+
export declare function setReturnPath(auth: Authentication, returnPath: string): void;
|
|
16
|
+
export declare function verifySpf(auth: Authentication): void;
|
|
17
|
+
/** The wire fields after the entity's own leading fields, in doc order (refs/api_domains-api.md:107-124). */
|
|
18
|
+
export declare const authenticationJson: (auth: Authentication) => {
|
|
19
|
+
SPFVerified: boolean;
|
|
20
|
+
SPFHost: string;
|
|
21
|
+
SPFTextValue: string;
|
|
22
|
+
DKIMVerified: boolean;
|
|
23
|
+
WeakDKIM: boolean;
|
|
24
|
+
DKIMHost: string;
|
|
25
|
+
DKIMTextValue: string;
|
|
26
|
+
DKIMPendingHost: string;
|
|
27
|
+
DKIMPendingTextValue: string;
|
|
28
|
+
DKIMRevokedHost: string;
|
|
29
|
+
DKIMRevokedTextValue: string;
|
|
30
|
+
SafeToRemoveRevokedKeyFromDNS: boolean;
|
|
31
|
+
DKIMUpdateStatus: "Pending" | "Verified";
|
|
32
|
+
ReturnPathDomain: string;
|
|
33
|
+
ReturnPathDomainVerified: boolean;
|
|
34
|
+
ReturnPathDomainCNAMEValue: string;
|
|
35
|
+
};
|
|
36
|
+
export declare const isHostname: (value: string) => boolean;
|
|
37
|
+
export declare const isEmail: (value: string) => boolean;
|
|
38
|
+
/** A Return-Path must be a subdomain of the domain (refs/api_domains-api.md:186). */
|
|
39
|
+
export declare const isSubdomainOf: (child: string, parent: string) => boolean;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { apiError } from "../../errors.js";
|
|
3
|
+
// refs/api_domains-api.md:226-246: a new domain has only a pending DKIM key.
|
|
4
|
+
export function newAuthentication(domain, returnPath, now) {
|
|
5
|
+
return {
|
|
6
|
+
SPFVerified: false,
|
|
7
|
+
SPFHost: domain,
|
|
8
|
+
SPFTextValue: "v=spf1 a mx include:spf.mtasv.net ~all",
|
|
9
|
+
DKIMVerified: false,
|
|
10
|
+
WeakDKIM: false,
|
|
11
|
+
DKIMHost: "",
|
|
12
|
+
DKIMTextValue: "",
|
|
13
|
+
...pendingKey(domain, now),
|
|
14
|
+
DKIMRevokedHost: "",
|
|
15
|
+
DKIMRevokedTextValue: "",
|
|
16
|
+
SafeToRemoveRevokedKeyFromDNS: false,
|
|
17
|
+
ReturnPathDomain: returnPath,
|
|
18
|
+
ReturnPathDomainVerified: false,
|
|
19
|
+
ReturnPathDomainCNAMEValue: "pm.mtasv.net",
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
// Host shape `20131031155228pm._domainkey.<domain>` (refs/api_domains-api.md:235); the key is random.
|
|
23
|
+
const pendingKey = (domain, now) => ({
|
|
24
|
+
DKIMPendingHost: `${now.toISOString().replace(/\D/g, "").slice(0, 14)}pm._domainkey.${domain}`,
|
|
25
|
+
DKIMPendingTextValue: `k=rsa; p=${randomBytes(162).toString("base64")}`,
|
|
26
|
+
DKIMUpdateStatus: "Pending",
|
|
27
|
+
});
|
|
28
|
+
/**
|
|
29
|
+
* A key still pending answers 505: the gem live test renews the key of a new domain and a new
|
|
30
|
+
* signature and expects it (sdk/postmark-gem/spec/integration/account_api_client_spec.rb:39-41, :76-78).
|
|
31
|
+
*/
|
|
32
|
+
export function refuseWhileRenewing(auth) {
|
|
33
|
+
if (auth.DKIMUpdateStatus === "Pending")
|
|
34
|
+
throw apiError(505);
|
|
35
|
+
}
|
|
36
|
+
/** Starts a new DKIM key. */
|
|
37
|
+
export function rotateDkim(auth, domain, now) {
|
|
38
|
+
refuseWhileRenewing(auth);
|
|
39
|
+
Object.assign(auth, pendingKey(domain, now));
|
|
40
|
+
}
|
|
41
|
+
/** Postmark found the pending DKIM key in DNS: it becomes the active key. */
|
|
42
|
+
export function confirmDkim(auth) {
|
|
43
|
+
if (auth.DKIMUpdateStatus !== "Pending")
|
|
44
|
+
throw new Error("no pending DKIM key");
|
|
45
|
+
if (auth.DKIMHost !== "") {
|
|
46
|
+
auth.DKIMRevokedHost = auth.DKIMHost;
|
|
47
|
+
auth.DKIMRevokedTextValue = auth.DKIMTextValue;
|
|
48
|
+
auth.SafeToRemoveRevokedKeyFromDNS = true;
|
|
49
|
+
}
|
|
50
|
+
auth.DKIMHost = auth.DKIMPendingHost;
|
|
51
|
+
auth.DKIMTextValue = auth.DKIMPendingTextValue;
|
|
52
|
+
auth.DKIMPendingHost = "";
|
|
53
|
+
auth.DKIMPendingTextValue = "";
|
|
54
|
+
auth.DKIMVerified = true;
|
|
55
|
+
auth.DKIMUpdateStatus = "Verified";
|
|
56
|
+
}
|
|
57
|
+
/** A new Return-Path needs its CNAME verified again; `""` clears it. */
|
|
58
|
+
export function setReturnPath(auth, returnPath) {
|
|
59
|
+
if (returnPath === auth.ReturnPathDomain)
|
|
60
|
+
return;
|
|
61
|
+
auth.ReturnPathDomain = returnPath;
|
|
62
|
+
auth.ReturnPathDomainVerified = false;
|
|
63
|
+
}
|
|
64
|
+
// SPF is no longer required (refs/api_domains-api.md:536). The gem live test expects `SPFVerified`
|
|
65
|
+
// true from a new domain and signature with no DNS (account_api_client_spec.rb:33, :73).
|
|
66
|
+
export function verifySpf(auth) {
|
|
67
|
+
auth.SPFVerified = true;
|
|
68
|
+
}
|
|
69
|
+
/** The wire fields after the entity's own leading fields, in doc order (refs/api_domains-api.md:107-124). */
|
|
70
|
+
export const authenticationJson = (auth) => ({
|
|
71
|
+
SPFVerified: auth.SPFVerified,
|
|
72
|
+
SPFHost: auth.SPFHost,
|
|
73
|
+
SPFTextValue: auth.SPFTextValue,
|
|
74
|
+
DKIMVerified: auth.DKIMVerified,
|
|
75
|
+
WeakDKIM: auth.WeakDKIM,
|
|
76
|
+
DKIMHost: auth.DKIMHost,
|
|
77
|
+
DKIMTextValue: auth.DKIMTextValue,
|
|
78
|
+
DKIMPendingHost: auth.DKIMPendingHost,
|
|
79
|
+
DKIMPendingTextValue: auth.DKIMPendingTextValue,
|
|
80
|
+
DKIMRevokedHost: auth.DKIMRevokedHost,
|
|
81
|
+
DKIMRevokedTextValue: auth.DKIMRevokedTextValue,
|
|
82
|
+
SafeToRemoveRevokedKeyFromDNS: auth.SafeToRemoveRevokedKeyFromDNS,
|
|
83
|
+
DKIMUpdateStatus: auth.DKIMUpdateStatus,
|
|
84
|
+
ReturnPathDomain: auth.ReturnPathDomain,
|
|
85
|
+
ReturnPathDomainVerified: auth.ReturnPathDomainVerified,
|
|
86
|
+
ReturnPathDomainCNAMEValue: auth.ReturnPathDomainCNAMEValue,
|
|
87
|
+
});
|
|
88
|
+
// A hostname with at least two labels (INFERRED; `thisisntadomain` is rejected by
|
|
89
|
+
// sdk/postmark-dotnet/src/Postmark.Tests/AdminClientDomainsTests.cs:67-82).
|
|
90
|
+
const HOSTNAME = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z][a-z0-9-]{0,62}$/i;
|
|
91
|
+
export const isHostname = (value) => HOSTNAME.test(value);
|
|
92
|
+
export const isEmail = (value) => {
|
|
93
|
+
const at = value.lastIndexOf("@");
|
|
94
|
+
return at > 0 && !/\s/.test(value) && isHostname(value.slice(at + 1));
|
|
95
|
+
};
|
|
96
|
+
/** A Return-Path must be a subdomain of the domain (refs/api_domains-api.md:186). */
|
|
97
|
+
export const isSubdomainOf = (child, parent) => child.toLowerCase().endsWith(`.${parent.toLowerCase()}`) && isHostname(child);
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { State } from "../../state/store.ts";
|
|
2
|
+
import type { Domain } from "../../state/types.ts";
|
|
3
|
+
/** A new domain; Name and Return-Path are checked by the caller. */
|
|
4
|
+
export declare const newDomain: (id: number, name: string, returnPath: string, now: Date) => Domain;
|
|
5
|
+
/** A domain whose DKIM key Postmark found in DNS, so it authorizes every `From` address on it. */
|
|
6
|
+
export declare function addVerifiedDomain(state: State, id: number, name: string, now: Date): Domain;
|
|
7
|
+
/** 510 for an unknown or non-numeric ID. */
|
|
8
|
+
export declare function findDomain(state: State, id: string): Domain;
|
|
9
|
+
/** Name checks in the order of the ErrorCodes 514, 515, 516, 512 (refs/api_overview.md:172-176). */
|
|
10
|
+
export declare function checkDomainName(state: State, name: string | undefined): string;
|
|
11
|
+
/** ErrorCode 522; the message is INFERRED (a summary row). */
|
|
12
|
+
export declare function checkReturnPath(returnPath: string, domain: string): string;
|
|
13
|
+
export declare const domainJson: (d: Domain) => {
|
|
14
|
+
SPFVerified: boolean;
|
|
15
|
+
SPFHost: string;
|
|
16
|
+
SPFTextValue: string;
|
|
17
|
+
DKIMVerified: boolean;
|
|
18
|
+
WeakDKIM: boolean;
|
|
19
|
+
DKIMHost: string;
|
|
20
|
+
DKIMTextValue: string;
|
|
21
|
+
DKIMPendingHost: string;
|
|
22
|
+
DKIMPendingTextValue: string;
|
|
23
|
+
DKIMRevokedHost: string;
|
|
24
|
+
DKIMRevokedTextValue: string;
|
|
25
|
+
SafeToRemoveRevokedKeyFromDNS: boolean;
|
|
26
|
+
DKIMUpdateStatus: "Pending" | "Verified";
|
|
27
|
+
ReturnPathDomain: string;
|
|
28
|
+
ReturnPathDomainVerified: boolean;
|
|
29
|
+
ReturnPathDomainCNAMEValue: string;
|
|
30
|
+
Name: string;
|
|
31
|
+
ID: number;
|
|
32
|
+
};
|
|
33
|
+
export declare const domainListItemJson: (d: Domain) => {
|
|
34
|
+
Name: string;
|
|
35
|
+
SPFVerified: boolean;
|
|
36
|
+
DKIMVerified: boolean;
|
|
37
|
+
WeakDKIM: boolean;
|
|
38
|
+
ReturnPathDomainVerified: boolean;
|
|
39
|
+
ID: number;
|
|
40
|
+
};
|
|
41
|
+
export declare const domainDkimJson: (d: Domain) => {
|
|
42
|
+
Name: string;
|
|
43
|
+
DKIMVerified: boolean;
|
|
44
|
+
WeakDKIM: boolean;
|
|
45
|
+
DKIMHost: string;
|
|
46
|
+
DKIMTextValue: string;
|
|
47
|
+
DKIMPendingHost: string;
|
|
48
|
+
DKIMPendingTextValue: string;
|
|
49
|
+
DKIMRevokedHost: string;
|
|
50
|
+
DKIMRevokedTextValue: string;
|
|
51
|
+
SafeToRemoveRevokedKeyFromDNS: boolean;
|
|
52
|
+
DKIMUpdateStatus: "Pending" | "Verified";
|
|
53
|
+
ID: number;
|
|
54
|
+
};
|
|
55
|
+
export declare const domainSpfJson: (d: Domain) => {
|
|
56
|
+
SPFHost: string;
|
|
57
|
+
SPFVerified: boolean;
|
|
58
|
+
SPFTextValue: string;
|
|
59
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { apiError } from "../../errors.js";
|
|
2
|
+
import { authenticationJson, confirmDkim, isHostname, isSubdomainOf, newAuthentication, } from "./authentication.js";
|
|
3
|
+
import { pathId } from "./paging.js";
|
|
4
|
+
// Domains API (docs/06 §4.3; refs/api_domains-api.md).
|
|
5
|
+
/** A new domain; Name and Return-Path are checked by the caller. */
|
|
6
|
+
export const newDomain = (id, name, returnPath, now) => ({
|
|
7
|
+
ID: id,
|
|
8
|
+
Name: name,
|
|
9
|
+
...newAuthentication(name, returnPath, now),
|
|
10
|
+
});
|
|
11
|
+
/** A domain whose DKIM key Postmark found in DNS, so it authorizes every `From` address on it. */
|
|
12
|
+
export function addVerifiedDomain(state, id, name, now) {
|
|
13
|
+
const domain = newDomain(id, name, "", now);
|
|
14
|
+
confirmDkim(domain);
|
|
15
|
+
state.domains.set(domain.ID, domain);
|
|
16
|
+
return domain;
|
|
17
|
+
}
|
|
18
|
+
/** 510 for an unknown or non-numeric ID. */
|
|
19
|
+
export function findDomain(state, id) {
|
|
20
|
+
const domain = state.domains.get(pathId(id) ?? -1);
|
|
21
|
+
if (domain === undefined)
|
|
22
|
+
throw apiError(510);
|
|
23
|
+
return domain;
|
|
24
|
+
}
|
|
25
|
+
/** Name checks in the order of the ErrorCodes 514, 515, 516, 512 (refs/api_overview.md:172-176). */
|
|
26
|
+
export function checkDomainName(state, name) {
|
|
27
|
+
if (name === undefined)
|
|
28
|
+
throw apiError(514);
|
|
29
|
+
if (name.length > 255)
|
|
30
|
+
throw apiError(515);
|
|
31
|
+
if (!isHostname(name))
|
|
32
|
+
throw apiError(516);
|
|
33
|
+
const taken = [...state.domains.values()].some((d) => d.Name.toLowerCase() === name.toLowerCase());
|
|
34
|
+
if (taken)
|
|
35
|
+
throw apiError(512);
|
|
36
|
+
return name;
|
|
37
|
+
}
|
|
38
|
+
/** ErrorCode 522; the message is INFERRED (a summary row). */
|
|
39
|
+
export function checkReturnPath(returnPath, domain) {
|
|
40
|
+
if (returnPath !== "" && !isSubdomainOf(returnPath, domain)) {
|
|
41
|
+
throw apiError(522, {
|
|
42
|
+
message: `The 'ReturnPathDomain' must be a subdomain of '${domain}'.`,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return returnPath;
|
|
46
|
+
}
|
|
47
|
+
// refs/api_domains-api.md:107-124.
|
|
48
|
+
export const domainJson = (d) => ({ Name: d.Name, ...authenticationJson(d), ID: d.ID });
|
|
49
|
+
// refs/api_domains-api.md:43-48.
|
|
50
|
+
export const domainListItemJson = (d) => ({
|
|
51
|
+
Name: d.Name,
|
|
52
|
+
SPFVerified: d.SPFVerified,
|
|
53
|
+
DKIMVerified: d.DKIMVerified,
|
|
54
|
+
WeakDKIM: d.WeakDKIM,
|
|
55
|
+
ReturnPathDomainVerified: d.ReturnPathDomainVerified,
|
|
56
|
+
ID: d.ID,
|
|
57
|
+
});
|
|
58
|
+
// refs/api_domains-api.md:611-622.
|
|
59
|
+
export const domainDkimJson = (d) => ({
|
|
60
|
+
Name: d.Name,
|
|
61
|
+
DKIMVerified: d.DKIMVerified,
|
|
62
|
+
WeakDKIM: d.WeakDKIM,
|
|
63
|
+
DKIMHost: d.DKIMHost,
|
|
64
|
+
DKIMTextValue: d.DKIMTextValue,
|
|
65
|
+
DKIMPendingHost: d.DKIMPendingHost,
|
|
66
|
+
DKIMPendingTextValue: d.DKIMPendingTextValue,
|
|
67
|
+
DKIMRevokedHost: d.DKIMRevokedHost,
|
|
68
|
+
DKIMRevokedTextValue: d.DKIMRevokedTextValue,
|
|
69
|
+
SafeToRemoveRevokedKeyFromDNS: d.SafeToRemoveRevokedKeyFromDNS,
|
|
70
|
+
DKIMUpdateStatus: d.DKIMUpdateStatus,
|
|
71
|
+
ID: d.ID,
|
|
72
|
+
});
|
|
73
|
+
// refs/api_domains-api.md:564-566.
|
|
74
|
+
export const domainSpfJson = (d) => ({
|
|
75
|
+
SPFHost: d.SPFHost,
|
|
76
|
+
SPFVerified: d.SPFVerified,
|
|
77
|
+
SPFTextValue: d.SPFTextValue,
|
|
78
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type Query } from "../../http/normalize.ts";
|
|
2
|
+
/**
|
|
3
|
+
* One page of an account list. Servers answer ErrorCode 600, domains and senders 500.
|
|
4
|
+
* The messages are INFERRED: both rows are summaries (docs/02 §4.4).
|
|
5
|
+
*/
|
|
6
|
+
export declare function accountPage<T>(query: Query, key: string, items: readonly T[], code: 500 | 600): Record<string, unknown>;
|
|
7
|
+
/** A numeric path ID; anything else matches no entity. */
|
|
8
|
+
export declare const pathId: (value: string) => number | undefined;
|
|
9
|
+
/** Entities in ID order (INFERRED). */
|
|
10
|
+
export declare const byId: <T extends {
|
|
11
|
+
ID: number;
|
|
12
|
+
}>(items: Iterable<T>) => T[];
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { apiError } from "../../errors.js";
|
|
3
|
+
import { queryInt } from "../../http/normalize.js";
|
|
4
|
+
import { paged } from "../../http/respond.js";
|
|
5
|
+
// `count` (at most 500) and `offset` are required on the servers, domains and senders lists
|
|
6
|
+
// (refs/api_servers-api.md:383-384; refs/api_domains-api.md:34-35; refs/api_signatures-api.md:34-35;
|
|
7
|
+
// the 500 cap for servers: ErrorCode 600, docs/02 §4.4).
|
|
8
|
+
const pageQuery = z.object({
|
|
9
|
+
count: queryInt.pipe(z.number().int().min(0).max(500)),
|
|
10
|
+
offset: queryInt.pipe(z.number().int().min(0)),
|
|
11
|
+
});
|
|
12
|
+
/**
|
|
13
|
+
* One page of an account list. Servers answer ErrorCode 600, domains and senders 500.
|
|
14
|
+
* The messages are INFERRED: both rows are summaries (docs/02 §4.4).
|
|
15
|
+
*/
|
|
16
|
+
export function accountPage(query, key, items, code) {
|
|
17
|
+
const page = query.pick(pageQuery);
|
|
18
|
+
if (!page.success) {
|
|
19
|
+
const field = String(page.error.issues[0]?.path[0]);
|
|
20
|
+
throw apiError(code, {
|
|
21
|
+
message: field === "count"
|
|
22
|
+
? "Parameter 'count' must be an integer between 0 and 500."
|
|
23
|
+
: "Parameter 'offset' must be an integer of 0 or more.",
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
const { count, offset } = page.data;
|
|
27
|
+
// "Postmark API returns 0 as total if you request 0 documents" (sdk/postmark-gem/lib/postmark/client.rb:77).
|
|
28
|
+
if (count === 0)
|
|
29
|
+
return { TotalCount: 0, [key]: [] };
|
|
30
|
+
return paged(key, items, count, offset);
|
|
31
|
+
}
|
|
32
|
+
/** A numeric path ID; anything else matches no entity. */
|
|
33
|
+
export const pathId = (value) => /^\d+$/.test(value) ? Number(value) : undefined;
|
|
34
|
+
/** Entities in ID order (INFERRED). */
|
|
35
|
+
export const byId = (items) => [...items].sort((a, b) => a.ID - b.ID);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Store } from "../../state/store.ts";
|
|
2
|
+
import type { Template } from "../../state/types.ts";
|
|
3
|
+
interface PushAction {
|
|
4
|
+
Action: "Create" | "Edit";
|
|
5
|
+
TemplateId?: number;
|
|
6
|
+
Alias: string;
|
|
7
|
+
Name: string;
|
|
8
|
+
TemplateType: Template["TemplateType"];
|
|
9
|
+
}
|
|
10
|
+
export declare function pushTemplates(store: Store, body: unknown): {
|
|
11
|
+
TotalCount: number;
|
|
12
|
+
Templates: PushAction[];
|
|
13
|
+
};
|
|
14
|
+
export {};
|