@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,84 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { apiError } from "../../errors.js";
|
|
3
|
+
import { absent, intLike, parseBody } from "../../http/normalize.js";
|
|
4
|
+
import { Unsupported } from "../../http/respond.js";
|
|
5
|
+
// PUT /templates/push (docs/06 §3.4; refs/api_templates-api.md:353-430).
|
|
6
|
+
const pushSchema = z.object({
|
|
7
|
+
SourceServerID: absent(intLike),
|
|
8
|
+
DestinationServerID: absent(intLike),
|
|
9
|
+
PerformChanges: z.boolean(),
|
|
10
|
+
});
|
|
11
|
+
// docs/06 §6: 100 templates per server.
|
|
12
|
+
const TEMPLATE_LIMIT = 100;
|
|
13
|
+
/** The fields a push copies; a template whose fields all match needs no change (INFERRED). */
|
|
14
|
+
const CONTENT = ["Name", "Subject", "HtmlBody", "TextBody", "LayoutTemplate"];
|
|
15
|
+
export function pushTemplates(store, body) {
|
|
16
|
+
const parsed = parseBody(pushSchema, body);
|
|
17
|
+
if (!parsed.success) {
|
|
18
|
+
throw new Unsupported(`push body: ${parsed.error.issues[0]?.message}: Postmark's answer is not captured`);
|
|
19
|
+
}
|
|
20
|
+
const { SourceServerID, DestinationServerID, PerformChanges } = parsed.data;
|
|
21
|
+
const { state } = store;
|
|
22
|
+
const server = (id) => (id === undefined ? undefined : state.servers.get(id));
|
|
23
|
+
const sourceServer = server(SourceServerID);
|
|
24
|
+
const destinationServer = server(DestinationServerID);
|
|
25
|
+
// Wire text from the live tests (sdk/postmark.js/test/integration/Templates.test.ts:178-211).
|
|
26
|
+
if (sourceServer === undefined && destinationServer === undefined) {
|
|
27
|
+
throw apiError(601, { message: "The source and destination servers were not found." });
|
|
28
|
+
}
|
|
29
|
+
if (destinationServer === undefined) {
|
|
30
|
+
throw apiError(601, { message: "The destination server was not found." });
|
|
31
|
+
}
|
|
32
|
+
if (sourceServer === undefined)
|
|
33
|
+
throw apiError(601, { message: "The source server was not found." });
|
|
34
|
+
const active = (serverId) => [...state.templates.values()]
|
|
35
|
+
.filter((t) => t.ServerID === serverId && t.Active)
|
|
36
|
+
.sort((a, b) => a.TemplateId - b.TemplateId);
|
|
37
|
+
// The destination matches by alias (refs/api_templates-api.md:359). Every template has one
|
|
38
|
+
// (docs/06 §3.2), so 1124 means a source server without templates.
|
|
39
|
+
const sources = active(sourceServer.ID);
|
|
40
|
+
if (sources.length === 0) {
|
|
41
|
+
// The message is INFERRED (a summary row).
|
|
42
|
+
throw apiError(1124, { message: "No templates with aliases found to push." });
|
|
43
|
+
}
|
|
44
|
+
const destination = active(destinationServer.ID);
|
|
45
|
+
// Aliases compare without case (INFERRED).
|
|
46
|
+
const byAlias = new Map(destination.map((t) => [t.Alias.toLowerCase(), t]));
|
|
47
|
+
const changes = sources.flatMap((source) => {
|
|
48
|
+
const target = byAlias.get(source.Alias.toLowerCase());
|
|
49
|
+
if (target !== undefined && target.TemplateType !== source.TemplateType)
|
|
50
|
+
throw apiError(1125);
|
|
51
|
+
if (target !== undefined && CONTENT.every((key) => target[key] === source[key]))
|
|
52
|
+
return [];
|
|
53
|
+
return [{ source, target }];
|
|
54
|
+
});
|
|
55
|
+
const creates = changes.filter((c) => c.target === undefined).length;
|
|
56
|
+
if (destination.length + creates > TEMPLATE_LIMIT)
|
|
57
|
+
throw apiError(1105);
|
|
58
|
+
const templates = changes.map(({ source, target }) => {
|
|
59
|
+
let templateId = target?.TemplateId;
|
|
60
|
+
if (PerformChanges) {
|
|
61
|
+
const content = Object.fromEntries(CONTENT.map((key) => [key, source[key]]));
|
|
62
|
+
if (target !== undefined)
|
|
63
|
+
Object.assign(target, content);
|
|
64
|
+
else {
|
|
65
|
+
templateId = store.nextId("template");
|
|
66
|
+
state.templates.set(templateId, {
|
|
67
|
+
...source,
|
|
68
|
+
TemplateId: templateId,
|
|
69
|
+
ServerID: destinationServer.ID,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
Action: target === undefined ? "Create" : "Edit",
|
|
75
|
+
// postmark.js types TemplateId as optional; a dry-run Create has no destination template yet,
|
|
76
|
+
// so it omits TemplateId (INFERRED; the doc example is a real push, refs/api_templates-api.md:420-428).
|
|
77
|
+
...(templateId !== undefined && { TemplateId: templateId }),
|
|
78
|
+
Alias: source.Alias,
|
|
79
|
+
Name: source.Name,
|
|
80
|
+
TemplateType: source.TemplateType,
|
|
81
|
+
};
|
|
82
|
+
});
|
|
83
|
+
return { TotalCount: templates.length, Templates: templates };
|
|
84
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { apiError } from "../../errors.js";
|
|
3
|
+
import { absent, parseBody } from "../../http/normalize.js";
|
|
4
|
+
import { Unsupported } from "../../http/respond.js";
|
|
5
|
+
import { defineRoute } from "../../http/routes.js";
|
|
6
|
+
import { createServer } from "../../state/servers.js";
|
|
7
|
+
import { serverJson } from "../server/json.js";
|
|
8
|
+
import { refuseWhileRenewing, rotateDkim, setReturnPath, verifySpf } from "./authentication.js";
|
|
9
|
+
import { checkDomainName, checkReturnPath, domainDkimJson, domainJson, domainListItemJson, domainSpfJson, findDomain, newDomain, } from "./domains.js";
|
|
10
|
+
import { accountPage, byId } from "./paging.js";
|
|
11
|
+
import { pushTemplates } from "./push.js";
|
|
12
|
+
import { checkFromEmail, checkNote, checkReplyTo, checkSenderReturnPath, findSender, newSender, senderJson, senderListItemJson, } from "./senders.js";
|
|
13
|
+
import { definedSettings, deleteServer, editServer, findServer, parseServerInput, } from "./servers.js";
|
|
14
|
+
// The account-token API: servers, domains, sender signatures, template push (docs/06 §3.4, §4.2–4.4).
|
|
15
|
+
// A body that is no JSON object, or a field of the wrong JSON type, has no documented ErrorCode.
|
|
16
|
+
function parseAccountBody(schema, body) {
|
|
17
|
+
const parsed = parseBody(schema, body ?? {});
|
|
18
|
+
if (!parsed.success) {
|
|
19
|
+
throw new Unsupported(`${z.prettifyError(parsed.error)}: Postmark's answer is not captured`);
|
|
20
|
+
}
|
|
21
|
+
return parsed.data;
|
|
22
|
+
}
|
|
23
|
+
// Servers (refs/api_servers-api.md).
|
|
24
|
+
defineRoute({
|
|
25
|
+
method: "GET",
|
|
26
|
+
path: "/servers",
|
|
27
|
+
auth: "account",
|
|
28
|
+
handler: ({ store, query }) => {
|
|
29
|
+
// "a string search" (refs/api_servers-api.md:385); case-insensitive is INFERRED.
|
|
30
|
+
const name = query.get("name")?.toLowerCase();
|
|
31
|
+
const servers = byId(store.state.servers.values()).filter((s) => name === undefined || s.Name.toLowerCase().includes(name));
|
|
32
|
+
return accountPage(query, "Servers", servers.map(serverJson), 600);
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
defineRoute({
|
|
36
|
+
method: "GET",
|
|
37
|
+
path: "/servers/:id",
|
|
38
|
+
auth: "account",
|
|
39
|
+
handler: ({ store, params }) => serverJson(findServer(store.state, params.id)),
|
|
40
|
+
});
|
|
41
|
+
defineRoute({
|
|
42
|
+
method: "POST",
|
|
43
|
+
path: "/servers",
|
|
44
|
+
auth: "account",
|
|
45
|
+
handler: ({ store, clock, body }) => {
|
|
46
|
+
const input = parseServerInput(store.state, body, null);
|
|
47
|
+
if (input.Name === undefined)
|
|
48
|
+
throw apiError(608, { message: "A server name is required." });
|
|
49
|
+
return serverJson(createServer(store, clock.now(), definedSettings(input)));
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
defineRoute({
|
|
53
|
+
method: "PUT",
|
|
54
|
+
path: "/servers/:id",
|
|
55
|
+
auth: "account",
|
|
56
|
+
handler: ({ store, params, body }) => {
|
|
57
|
+
const server = findServer(store.state, params.id);
|
|
58
|
+
return serverJson(editServer(store.state, server, body));
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
defineRoute({
|
|
62
|
+
method: "DELETE",
|
|
63
|
+
path: "/servers/:id",
|
|
64
|
+
auth: "account",
|
|
65
|
+
handler: ({ store, params }) => {
|
|
66
|
+
if (!store.state.account.serverDeletionEnabled)
|
|
67
|
+
throw apiError(604);
|
|
68
|
+
const server = findServer(store.state, params.id);
|
|
69
|
+
deleteServer(store.state, server);
|
|
70
|
+
// refs/api_servers-api.md:520.
|
|
71
|
+
return { ErrorCode: 0, Message: `Server ${server.Name} removed.` };
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
// Domains (refs/api_domains-api.md).
|
|
75
|
+
defineRoute({
|
|
76
|
+
method: "GET",
|
|
77
|
+
path: "/domains",
|
|
78
|
+
auth: "account",
|
|
79
|
+
handler: ({ store, query }) => accountPage(query, "Domains", byId(store.state.domains.values()).map(domainListItemJson), 500),
|
|
80
|
+
});
|
|
81
|
+
defineRoute({
|
|
82
|
+
method: "GET",
|
|
83
|
+
path: "/domains/:id",
|
|
84
|
+
auth: "account",
|
|
85
|
+
handler: ({ store, params }) => domainJson(findDomain(store.state, params.id)),
|
|
86
|
+
});
|
|
87
|
+
const createDomainSchema = z.object({
|
|
88
|
+
Name: absent(z.string()),
|
|
89
|
+
ReturnPathDomain: absent(z.string()),
|
|
90
|
+
});
|
|
91
|
+
defineRoute({
|
|
92
|
+
method: "POST",
|
|
93
|
+
path: "/domains",
|
|
94
|
+
auth: "account",
|
|
95
|
+
handler: ({ store, clock, body }) => {
|
|
96
|
+
const input = parseAccountBody(createDomainSchema, body);
|
|
97
|
+
const name = checkDomainName(store.state, input.Name);
|
|
98
|
+
const returnPath = checkReturnPath(input.ReturnPathDomain ?? "", name);
|
|
99
|
+
const domain = newDomain(store.nextId("domain"), name, returnPath, clock.now());
|
|
100
|
+
store.state.domains.set(domain.ID, domain);
|
|
101
|
+
return domainJson(domain);
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
// `""` clears the Return-Path, unlike docs/08 R9: dotnet sends `""` for "clear"
|
|
105
|
+
// (sdk/postmark-dotnet/src/Postmark/PostmarkAdminClient.cs:345). `null` keeps it.
|
|
106
|
+
const returnPathEdit = z.string().nullable().optional();
|
|
107
|
+
defineRoute({
|
|
108
|
+
method: "PUT",
|
|
109
|
+
path: "/domains/:id",
|
|
110
|
+
auth: "account",
|
|
111
|
+
handler: ({ store, params, body }) => {
|
|
112
|
+
const domain = findDomain(store.state, params.id);
|
|
113
|
+
const input = parseAccountBody(z.object({ ReturnPathDomain: returnPathEdit }), body);
|
|
114
|
+
if (typeof input.ReturnPathDomain === "string") {
|
|
115
|
+
setReturnPath(domain, checkReturnPath(input.ReturnPathDomain, domain.Name));
|
|
116
|
+
}
|
|
117
|
+
return domainJson(domain);
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
defineRoute({
|
|
121
|
+
method: "DELETE",
|
|
122
|
+
path: "/domains/:id",
|
|
123
|
+
auth: "account",
|
|
124
|
+
handler: ({ store, params }) => {
|
|
125
|
+
const domain = findDomain(store.state, params.id);
|
|
126
|
+
store.state.domains.delete(domain.ID);
|
|
127
|
+
// refs/api_domains-api.md:377.
|
|
128
|
+
return { ErrorCode: 0, Message: `Domain ${domain.Name} removed.` };
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
// Verification reads the state the control API sets (`POST /control/domains/:id/verify`): postmock
|
|
132
|
+
// has no DNS to look at.
|
|
133
|
+
for (const path of ["/domains/:id/verifyDkim", "/domains/:id/verifyReturnPath"]) {
|
|
134
|
+
defineRoute({
|
|
135
|
+
method: "PUT",
|
|
136
|
+
path,
|
|
137
|
+
auth: "account",
|
|
138
|
+
handler: ({ store, params }) => domainJson(findDomain(store.state, params.id)),
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
defineRoute({
|
|
142
|
+
method: "POST",
|
|
143
|
+
path: "/domains/:id/verifyspf",
|
|
144
|
+
auth: "account",
|
|
145
|
+
handler: ({ store, params }) => {
|
|
146
|
+
const domain = findDomain(store.state, params.id);
|
|
147
|
+
verifySpf(domain);
|
|
148
|
+
return domainSpfJson(domain);
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
defineRoute({
|
|
152
|
+
method: "POST",
|
|
153
|
+
path: "/domains/:id/rotatedkim",
|
|
154
|
+
auth: "account",
|
|
155
|
+
handler: ({ store, clock, params }) => {
|
|
156
|
+
const domain = findDomain(store.state, params.id);
|
|
157
|
+
rotateDkim(domain, domain.Name, clock.now());
|
|
158
|
+
return domainDkimJson(domain);
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
// Sender signatures (refs/api_signatures-api.md).
|
|
162
|
+
defineRoute({
|
|
163
|
+
method: "GET",
|
|
164
|
+
path: "/senders",
|
|
165
|
+
auth: "account",
|
|
166
|
+
handler: ({ store, query }) => accountPage(query, "SenderSignatures", byId(store.state.senders.values()).map(senderListItemJson), 500),
|
|
167
|
+
});
|
|
168
|
+
defineRoute({
|
|
169
|
+
method: "GET",
|
|
170
|
+
path: "/senders/:id",
|
|
171
|
+
auth: "account",
|
|
172
|
+
handler: ({ store, params }) => senderJson(findSender(store.state, params.id)),
|
|
173
|
+
});
|
|
174
|
+
const senderSchema = z.object({
|
|
175
|
+
FromEmail: absent(z.string()),
|
|
176
|
+
Name: absent(z.string()),
|
|
177
|
+
ReplyToEmail: absent(z.string()),
|
|
178
|
+
ReturnPathDomain: returnPathEdit,
|
|
179
|
+
ConfirmationPersonalNote: absent(z.string()),
|
|
180
|
+
});
|
|
181
|
+
defineRoute({
|
|
182
|
+
method: "POST",
|
|
183
|
+
path: "/senders",
|
|
184
|
+
auth: "account",
|
|
185
|
+
handler: ({ store, clock, body }) => {
|
|
186
|
+
if (body === undefined)
|
|
187
|
+
throw apiError(502);
|
|
188
|
+
const input = parseAccountBody(senderSchema, body);
|
|
189
|
+
const email = checkFromEmail(store.state, input.FromEmail);
|
|
190
|
+
// Name is required (refs/api_signatures-api.md:199); no ErrorCode names it.
|
|
191
|
+
if (input.Name === undefined) {
|
|
192
|
+
throw new Unsupported("a signature without Name: Postmark's answer is not captured");
|
|
193
|
+
}
|
|
194
|
+
const sender = newSender(store.nextId("sender"), {
|
|
195
|
+
FromEmail: email,
|
|
196
|
+
Name: input.Name,
|
|
197
|
+
ReplyToEmail: input.ReplyToEmail === undefined ? "" : checkReplyTo(input.ReplyToEmail),
|
|
198
|
+
ReturnPathDomain: checkSenderReturnPath(input.ReturnPathDomain ?? "", email),
|
|
199
|
+
ConfirmationPersonalNote: checkNote(input.ConfirmationPersonalNote ?? ""),
|
|
200
|
+
}, clock.now());
|
|
201
|
+
store.state.senders.set(sender.ID, sender);
|
|
202
|
+
return senderJson(sender);
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
// Every field is optional on edit: the dotnet live test edits only the Return-Path
|
|
206
|
+
// (sdk/postmark-dotnet/src/Postmark.Tests/AdminClientSenderSignatureTests.cs:141).
|
|
207
|
+
defineRoute({
|
|
208
|
+
method: "PUT",
|
|
209
|
+
path: "/senders/:id",
|
|
210
|
+
auth: "account",
|
|
211
|
+
handler: ({ store, params, body }) => {
|
|
212
|
+
const sender = findSender(store.state, params.id);
|
|
213
|
+
if (body === undefined)
|
|
214
|
+
throw apiError(502);
|
|
215
|
+
const input = parseAccountBody(senderSchema.omit({ FromEmail: true }), body);
|
|
216
|
+
// Check every field before the first change: a rejected edit changes nothing.
|
|
217
|
+
const replyTo = input.ReplyToEmail === undefined ? undefined : checkReplyTo(input.ReplyToEmail);
|
|
218
|
+
const returnPath = typeof input.ReturnPathDomain === "string"
|
|
219
|
+
? checkSenderReturnPath(input.ReturnPathDomain, sender.EmailAddress)
|
|
220
|
+
: undefined;
|
|
221
|
+
const note = input.ConfirmationPersonalNote === undefined
|
|
222
|
+
? undefined
|
|
223
|
+
: checkNote(input.ConfirmationPersonalNote);
|
|
224
|
+
if (input.Name !== undefined)
|
|
225
|
+
sender.Name = input.Name;
|
|
226
|
+
if (replyTo !== undefined)
|
|
227
|
+
sender.ReplyToEmailAddress = replyTo;
|
|
228
|
+
if (returnPath !== undefined)
|
|
229
|
+
setReturnPath(sender, returnPath);
|
|
230
|
+
if (note !== undefined)
|
|
231
|
+
sender.ConfirmationPersonalNote = note;
|
|
232
|
+
return senderJson(sender);
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
defineRoute({
|
|
236
|
+
method: "DELETE",
|
|
237
|
+
path: "/senders/:id",
|
|
238
|
+
auth: "account",
|
|
239
|
+
handler: ({ store, params }) => {
|
|
240
|
+
const sender = findSender(store.state, params.id);
|
|
241
|
+
store.state.senders.delete(sender.ID);
|
|
242
|
+
// refs/api_signatures-api.md:424.
|
|
243
|
+
return { ErrorCode: 0, Message: `Signature ${sender.EmailAddress} removed.` };
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
defineRoute({
|
|
247
|
+
method: "POST",
|
|
248
|
+
path: "/senders/:id/resend",
|
|
249
|
+
auth: "account",
|
|
250
|
+
handler: ({ store, params }) => {
|
|
251
|
+
const sender = findSender(store.state, params.id);
|
|
252
|
+
if (sender.Confirmed)
|
|
253
|
+
throw apiError(506);
|
|
254
|
+
// refs/api_signatures-api.md:466.
|
|
255
|
+
return {
|
|
256
|
+
ErrorCode: 0,
|
|
257
|
+
Message: `Confirmation email for Sender Signature ${sender.EmailAddress} was re-sent.`,
|
|
258
|
+
};
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
defineRoute({
|
|
262
|
+
method: "POST",
|
|
263
|
+
path: "/senders/:id/verifyspf",
|
|
264
|
+
auth: "account",
|
|
265
|
+
handler: ({ store, params }) => {
|
|
266
|
+
const sender = findSender(store.state, params.id);
|
|
267
|
+
verifySpf(sender);
|
|
268
|
+
return senderJson(sender);
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
defineRoute({
|
|
272
|
+
method: "POST",
|
|
273
|
+
path: "/senders/:id/requestnewdkim",
|
|
274
|
+
auth: "account",
|
|
275
|
+
handler: ({ store, params }) => {
|
|
276
|
+
const sender = findSender(store.state, params.id);
|
|
277
|
+
refuseWhileRenewing(sender);
|
|
278
|
+
// The doc shows only the 505 body; dotnet reads {ErrorCode, Message}, postmark.js a signature.
|
|
279
|
+
throw new Unsupported("the requestnewdkim success body is not captured");
|
|
280
|
+
},
|
|
281
|
+
});
|
|
282
|
+
// Template push (refs/api_templates-api.md:353-430). A literal `push` beats T3's `:idOrAlias`.
|
|
283
|
+
defineRoute({
|
|
284
|
+
method: "PUT",
|
|
285
|
+
path: "/templates/push",
|
|
286
|
+
auth: "account",
|
|
287
|
+
handler: ({ store, body }) => pushTemplates(store, body),
|
|
288
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { State } from "../../state/store.ts";
|
|
2
|
+
import type { SenderSignature } from "../../state/types.ts";
|
|
3
|
+
export interface NewSender {
|
|
4
|
+
FromEmail: string;
|
|
5
|
+
Name: string;
|
|
6
|
+
ReplyToEmail: string;
|
|
7
|
+
ReturnPathDomain: string;
|
|
8
|
+
ConfirmationPersonalNote: string;
|
|
9
|
+
}
|
|
10
|
+
export declare const emailDomain: (email: string) => string;
|
|
11
|
+
/** An unconfirmed signature with a pending DKIM key (refs/api_signatures-api.md:250-273). */
|
|
12
|
+
export declare const newSender: (id: number, s: NewSender, now: Date) => SenderSignature;
|
|
13
|
+
/**
|
|
14
|
+
* ErrorCode 501 for an unknown or non-numeric ID. The doc gives 422 or 404; 404 is INFERRED from the
|
|
15
|
+
* postmark.js cleanup that tolerates a 404 on delete (sdk/postmark.js/test/integration/Signatures.test.ts:37-40).
|
|
16
|
+
*/
|
|
17
|
+
export declare function findSender(state: State, id: string): SenderSignature;
|
|
18
|
+
export declare function checkFromEmail(state: State, email: string | undefined): string;
|
|
19
|
+
export declare function checkReplyTo(email: string): string;
|
|
20
|
+
export declare function checkNote(note: string): string;
|
|
21
|
+
export declare const checkSenderReturnPath: (returnPath: string, email: string) => string;
|
|
22
|
+
export declare const senderJson: (s: SenderSignature) => {
|
|
23
|
+
SPFVerified: boolean;
|
|
24
|
+
SPFHost: string;
|
|
25
|
+
SPFTextValue: string;
|
|
26
|
+
DKIMVerified: boolean;
|
|
27
|
+
WeakDKIM: boolean;
|
|
28
|
+
DKIMHost: string;
|
|
29
|
+
DKIMTextValue: string;
|
|
30
|
+
DKIMPendingHost: string;
|
|
31
|
+
DKIMPendingTextValue: string;
|
|
32
|
+
DKIMRevokedHost: string;
|
|
33
|
+
DKIMRevokedTextValue: string;
|
|
34
|
+
SafeToRemoveRevokedKeyFromDNS: boolean;
|
|
35
|
+
DKIMUpdateStatus: "Pending" | "Verified";
|
|
36
|
+
ReturnPathDomain: string;
|
|
37
|
+
ReturnPathDomainVerified: boolean;
|
|
38
|
+
ReturnPathDomainCNAMEValue: string;
|
|
39
|
+
Domain: string;
|
|
40
|
+
EmailAddress: string;
|
|
41
|
+
ReplyToEmailAddress: string;
|
|
42
|
+
Name: string;
|
|
43
|
+
Confirmed: boolean;
|
|
44
|
+
ID: number;
|
|
45
|
+
ConfirmationPersonalNote: string;
|
|
46
|
+
};
|
|
47
|
+
export declare const senderListItemJson: (s: SenderSignature) => {
|
|
48
|
+
Domain: string;
|
|
49
|
+
EmailAddress: string;
|
|
50
|
+
ReplyToEmailAddress: string;
|
|
51
|
+
Name: string;
|
|
52
|
+
Confirmed: boolean;
|
|
53
|
+
ID: number;
|
|
54
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { apiError } from "../../errors.js";
|
|
2
|
+
import { authenticationJson, isEmail, newAuthentication } from "./authentication.js";
|
|
3
|
+
import { checkReturnPath } from "./domains.js";
|
|
4
|
+
import { pathId } from "./paging.js";
|
|
5
|
+
export const emailDomain = (email) => email.slice(email.lastIndexOf("@") + 1);
|
|
6
|
+
/** An unconfirmed signature with a pending DKIM key (refs/api_signatures-api.md:250-273). */
|
|
7
|
+
export const newSender = (id, s, now) => ({
|
|
8
|
+
ID: id,
|
|
9
|
+
Domain: emailDomain(s.FromEmail),
|
|
10
|
+
EmailAddress: s.FromEmail,
|
|
11
|
+
ReplyToEmailAddress: s.ReplyToEmail,
|
|
12
|
+
Name: s.Name,
|
|
13
|
+
Confirmed: false,
|
|
14
|
+
ConfirmationPersonalNote: s.ConfirmationPersonalNote,
|
|
15
|
+
...newAuthentication(emailDomain(s.FromEmail), s.ReturnPathDomain, now),
|
|
16
|
+
});
|
|
17
|
+
/**
|
|
18
|
+
* ErrorCode 501 for an unknown or non-numeric ID. The doc gives 422 or 404; 404 is INFERRED from the
|
|
19
|
+
* postmark.js cleanup that tolerates a 404 on delete (sdk/postmark.js/test/integration/Signatures.test.ts:37-40).
|
|
20
|
+
*/
|
|
21
|
+
export function findSender(state, id) {
|
|
22
|
+
const sender = state.senders.get(pathId(id) ?? -1);
|
|
23
|
+
if (sender === undefined)
|
|
24
|
+
throw apiError(501, { status: 404, message: "Signature not found." });
|
|
25
|
+
return sender;
|
|
26
|
+
}
|
|
27
|
+
// Free mail domains cannot hold a signature (ErrorCode 503). The list is INFERRED.
|
|
28
|
+
const PUBLIC_DOMAINS = new Set([
|
|
29
|
+
"gmail.com",
|
|
30
|
+
"googlemail.com",
|
|
31
|
+
"yahoo.com",
|
|
32
|
+
"hotmail.com",
|
|
33
|
+
"outlook.com",
|
|
34
|
+
"live.com",
|
|
35
|
+
"aol.com",
|
|
36
|
+
"icloud.com",
|
|
37
|
+
]);
|
|
38
|
+
export function checkFromEmail(state, email) {
|
|
39
|
+
if (email === undefined)
|
|
40
|
+
throw apiError(520);
|
|
41
|
+
if (email.length > 255)
|
|
42
|
+
throw tooLong("FromEmail");
|
|
43
|
+
if (!isEmail(email))
|
|
44
|
+
throw invalidEmail("FromEmail");
|
|
45
|
+
if (PUBLIC_DOMAINS.has(emailDomain(email).toLowerCase()))
|
|
46
|
+
throw apiError(503);
|
|
47
|
+
const taken = [...state.senders.values()].some((s) => s.EmailAddress.toLowerCase() === email.toLowerCase());
|
|
48
|
+
if (taken)
|
|
49
|
+
throw apiError(504, { message: "This signature already exists." });
|
|
50
|
+
return email;
|
|
51
|
+
}
|
|
52
|
+
export function checkReplyTo(email) {
|
|
53
|
+
if (email.length > 255)
|
|
54
|
+
throw tooLong("ReplyToEmail");
|
|
55
|
+
if (!isEmail(email))
|
|
56
|
+
throw invalidEmail("ReplyToEmail");
|
|
57
|
+
return email;
|
|
58
|
+
}
|
|
59
|
+
// The note has at most 400 characters (refs/api_signatures-api.md:202).
|
|
60
|
+
export function checkNote(note) {
|
|
61
|
+
if (note.length > 400)
|
|
62
|
+
throw tooLong("ConfirmationPersonalNote");
|
|
63
|
+
return note;
|
|
64
|
+
}
|
|
65
|
+
export const checkSenderReturnPath = (returnPath, email) => checkReturnPath(returnPath, emailDomain(email));
|
|
66
|
+
const tooLong = (field) => apiError(521, { message: `The '${field}' field is too long.` });
|
|
67
|
+
const invalidEmail = (field) => apiError(522, { message: `The '${field}' field is not a valid email address.` });
|
|
68
|
+
// refs/api_signatures-api.md:107-129.
|
|
69
|
+
export const senderJson = (s) => ({
|
|
70
|
+
Domain: s.Domain,
|
|
71
|
+
EmailAddress: s.EmailAddress,
|
|
72
|
+
ReplyToEmailAddress: s.ReplyToEmailAddress,
|
|
73
|
+
Name: s.Name,
|
|
74
|
+
Confirmed: s.Confirmed,
|
|
75
|
+
...authenticationJson(s),
|
|
76
|
+
ID: s.ID,
|
|
77
|
+
ConfirmationPersonalNote: s.ConfirmationPersonalNote,
|
|
78
|
+
});
|
|
79
|
+
// refs/api_signatures-api.md:43-48.
|
|
80
|
+
export const senderListItemJson = (s) => ({
|
|
81
|
+
Domain: s.Domain,
|
|
82
|
+
EmailAddress: s.EmailAddress,
|
|
83
|
+
ReplyToEmailAddress: s.ReplyToEmailAddress,
|
|
84
|
+
Name: s.Name,
|
|
85
|
+
Confirmed: s.Confirmed,
|
|
86
|
+
ID: s.ID,
|
|
87
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { State } from "../../state/store.ts";
|
|
3
|
+
import { type Server } from "../../state/types.ts";
|
|
4
|
+
declare const settingsSchema: z.ZodObject<{
|
|
5
|
+
Name: z.ZodPreprocess<z.ZodOptional<z.ZodString>, unknown>;
|
|
6
|
+
Color: z.ZodPreprocess<z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<"blue" | "green" | "grey" | "orange" | "purple" | "red" | "turquoise" | "yellow", string>>>, unknown>;
|
|
7
|
+
SmtpApiActivated: z.ZodPreprocess<z.ZodOptional<z.ZodBoolean>, unknown>;
|
|
8
|
+
RawEmailEnabled: z.ZodPreprocess<z.ZodOptional<z.ZodBoolean>, unknown>;
|
|
9
|
+
DeliveryType: z.ZodPreprocess<z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<"Live" | "Sandbox", string>>>, unknown>;
|
|
10
|
+
InboundHookUrl: z.ZodPreprocess<z.ZodOptional<z.ZodString>, unknown>;
|
|
11
|
+
BounceHookUrl: z.ZodPreprocess<z.ZodOptional<z.ZodString>, unknown>;
|
|
12
|
+
OpenHookUrl: z.ZodPreprocess<z.ZodOptional<z.ZodString>, unknown>;
|
|
13
|
+
DeliveryHookUrl: z.ZodPreprocess<z.ZodOptional<z.ZodString>, unknown>;
|
|
14
|
+
ClickHookUrl: z.ZodPreprocess<z.ZodOptional<z.ZodString>, unknown>;
|
|
15
|
+
PostFirstOpenOnly: z.ZodPreprocess<z.ZodOptional<z.ZodBoolean>, unknown>;
|
|
16
|
+
InboundDomain: z.ZodPreprocess<z.ZodOptional<z.ZodString>, unknown>;
|
|
17
|
+
InboundSpamThreshold: z.ZodPreprocess<z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodNumber, z.ZodPipe<z.ZodString, z.ZodTransform<number, string>>]>, z.ZodNumber>>, unknown>;
|
|
18
|
+
TrackOpens: z.ZodPreprocess<z.ZodOptional<z.ZodBoolean>, unknown>;
|
|
19
|
+
TrackLinks: z.ZodPreprocess<z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<"HtmlAndText" | "HtmlOnly" | "None" | "TextOnly", string>>>, unknown>;
|
|
20
|
+
IncludeBounceContentInHook: z.ZodPreprocess<z.ZodOptional<z.ZodBoolean>, unknown>;
|
|
21
|
+
EnableSmtpApiErrorHooks: z.ZodPreprocess<z.ZodOptional<z.ZodBoolean>, unknown>;
|
|
22
|
+
}, z.core.$strip>;
|
|
23
|
+
export type ServerInput = z.output<typeof settingsSchema>;
|
|
24
|
+
/**
|
|
25
|
+
* The settings of a create or edit body. A body that is no JSON object is
|
|
26
|
+
* 609. A bad value gets its field's ErrorCode (refs/api_overview.md:101-108); a field of the wrong
|
|
27
|
+
* JSON type has no documented code, so it answers 501.
|
|
28
|
+
*/
|
|
29
|
+
export declare function parseServerInput(state: State, body: unknown, self: Server | null): ServerInput;
|
|
30
|
+
/**
|
|
31
|
+
* Applies an edit body to a server: `PUT /servers/{id}` (account token) and `PUT /server` (server
|
|
32
|
+
* token) take the same fields. DeliveryType is fixed at create (refs/api_servers-api.md:38).
|
|
33
|
+
*/
|
|
34
|
+
export declare function editServer(state: State, server: Server, body: unknown): Server;
|
|
35
|
+
/** The settings a create or edit sets: absent fields keep their value (docs/08 R9). */
|
|
36
|
+
export declare const definedSettings: (input: ServerInput) => Partial<Server>;
|
|
37
|
+
export declare function findServer(state: State, id: string): Server;
|
|
38
|
+
/** Removes a server and everything it owns. */
|
|
39
|
+
export declare function deleteServer(state: State, server: Server): void;
|
|
40
|
+
export {};
|