@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,181 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import libmime from "libmime";
|
|
3
|
+
import { Unsupported } from "../http/respond.js";
|
|
4
|
+
/** mailparser options: keep the bodies as sent; derive nothing. */
|
|
5
|
+
export const PARSE_OPTIONS = {
|
|
6
|
+
skipHtmlToText: true,
|
|
7
|
+
skipTextToHtml: true,
|
|
8
|
+
skipTextLinks: true,
|
|
9
|
+
skipImageLinks: true,
|
|
10
|
+
};
|
|
11
|
+
/** Header lines that become draft fields or MIME structure, not custom `Headers` (INFERRED). */
|
|
12
|
+
const STRUCTURAL = new Set([
|
|
13
|
+
"from",
|
|
14
|
+
"to",
|
|
15
|
+
"cc",
|
|
16
|
+
"bcc",
|
|
17
|
+
"reply-to",
|
|
18
|
+
"subject",
|
|
19
|
+
"date",
|
|
20
|
+
"message-id",
|
|
21
|
+
"mime-version",
|
|
22
|
+
"content-type",
|
|
23
|
+
"content-transfer-encoding",
|
|
24
|
+
]);
|
|
25
|
+
/** X-PM-* headers that set draft fields (docs/07 §1.3). Other X-PM-* go to `Headers`. */
|
|
26
|
+
const FIELD_HEADERS = new Set([
|
|
27
|
+
"x-pm-message-stream",
|
|
28
|
+
"x-pm-tag",
|
|
29
|
+
"x-pm-trackopens",
|
|
30
|
+
"x-pm-tracklinks",
|
|
31
|
+
"x-pm-keepid",
|
|
32
|
+
]);
|
|
33
|
+
const METADATA_PREFIX = "x-pm-metadata-";
|
|
34
|
+
/**
|
|
35
|
+
* Header lines in order, unfolded, RFC 2047 encoded-words decoded (nodemailer encodes every
|
|
36
|
+
* non-ASCII header value, docs/07 §1.7). Empty values are absent.
|
|
37
|
+
*/
|
|
38
|
+
function rawHeaders(mail) {
|
|
39
|
+
return mail.headerLines.map(({ line }) => {
|
|
40
|
+
const colon = line.indexOf(":");
|
|
41
|
+
return {
|
|
42
|
+
name: line.slice(0, colon).trim(),
|
|
43
|
+
value: libmime.decodeWords(line
|
|
44
|
+
.slice(colon + 1)
|
|
45
|
+
.replace(/\r?\n(?=[ \t])/g, "")
|
|
46
|
+
.trim()),
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
const flatten = (list) => list.flatMap((a) => (a.group === undefined ? [a] : flatten(a.group)));
|
|
51
|
+
const addresses = (field) => field === undefined ? [] : flatten([field].flat().flatMap((o) => o.value));
|
|
52
|
+
/**
|
|
53
|
+
* A Postmark address list: `"Name" <a@b>, c@d` (docs/03 §1.8), with decoded display names.
|
|
54
|
+
* An entry mailparser found no address in stays as its text, so the pipeline rejects it.
|
|
55
|
+
*/
|
|
56
|
+
const addressText = (list) => list.length === 0
|
|
57
|
+
? undefined
|
|
58
|
+
: list
|
|
59
|
+
.map((a) => {
|
|
60
|
+
if (!a.address)
|
|
61
|
+
return a.name;
|
|
62
|
+
if (a.name === "")
|
|
63
|
+
return a.address;
|
|
64
|
+
return `"${a.name.replace(/["\\]/g, "\\$&")}" <${a.address}>`;
|
|
65
|
+
})
|
|
66
|
+
.join(", ");
|
|
67
|
+
/**
|
|
68
|
+
* Maps one parsed SMTP transaction to a raw draft. `submitOutbound` checks every value
|
|
69
|
+
* (docs/11 §2). Recipients come from the envelope (docs/07 Mock must). The `To` and `Cc` headers
|
|
70
|
+
* only sort them and name them; an `RCPT TO` address neither header names is a Bcc, since clients
|
|
71
|
+
* strip the `Bcc` header (docs/07 §1.1). A header address outside the envelope is dropped (INFERRED).
|
|
72
|
+
*/
|
|
73
|
+
export function draftFromMime(mail, rcptTo, sender) {
|
|
74
|
+
const headers = rawHeaders(mail);
|
|
75
|
+
const first = (name) => headers.find((h) => h.name.toLowerCase() === name && h.value !== "")?.value;
|
|
76
|
+
const envelope = new Set(rcptTo.map((r) => r.toLowerCase()));
|
|
77
|
+
const claimed = new Set();
|
|
78
|
+
// A malformed entry (no address) stays, so the pipeline rejects it.
|
|
79
|
+
const sort = (list) => list.filter((a) => {
|
|
80
|
+
if (!a.address)
|
|
81
|
+
return true;
|
|
82
|
+
const key = a.address.toLowerCase();
|
|
83
|
+
if (!envelope.has(key) || claimed.has(key))
|
|
84
|
+
return false;
|
|
85
|
+
claimed.add(key);
|
|
86
|
+
return true;
|
|
87
|
+
});
|
|
88
|
+
const to = sort(addresses(mail.to));
|
|
89
|
+
const cc = sort(addresses(mail.cc));
|
|
90
|
+
const bcc = rcptTo.filter((r) => !claimed.has(r.toLowerCase()));
|
|
91
|
+
const streamHeader = first("x-pm-message-stream");
|
|
92
|
+
let stream = streamHeader ?? "outbound";
|
|
93
|
+
if (sender.tokenStream !== null) {
|
|
94
|
+
// docs/07 Q13: an SMTP token with a header naming another stream is not captured.
|
|
95
|
+
if (streamHeader !== undefined && streamHeader !== sender.tokenStream) {
|
|
96
|
+
throw new Unsupported(`X-PM-Message-Stream '${streamHeader}' with an SMTP token for '${sender.tokenStream}' is not captured (docs/07 Q13)`);
|
|
97
|
+
}
|
|
98
|
+
stream = sender.tokenStream;
|
|
99
|
+
}
|
|
100
|
+
const metadata = {};
|
|
101
|
+
const custom = [];
|
|
102
|
+
const keepId = first("x-pm-keepid")?.toLowerCase() === "true";
|
|
103
|
+
for (const header of headers) {
|
|
104
|
+
const lower = header.name.toLowerCase();
|
|
105
|
+
if (lower.startsWith(METADATA_PREFIX)) {
|
|
106
|
+
addMetadata(metadata, header.name.slice(METADATA_PREFIX.length), header.value);
|
|
107
|
+
}
|
|
108
|
+
else if (lower === "message-id" && keepId) {
|
|
109
|
+
custom.push(header);
|
|
110
|
+
}
|
|
111
|
+
else if (!STRUCTURAL.has(lower) && !FIELD_HEADERS.has(lower)) {
|
|
112
|
+
custom.push(header);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const draft = {
|
|
116
|
+
From: addressText(addresses(mail.from)),
|
|
117
|
+
To: addressText(to),
|
|
118
|
+
Cc: addressText(cc),
|
|
119
|
+
Bcc: bcc.length === 0 ? undefined : bcc.join(", "),
|
|
120
|
+
ReplyTo: addressText(addresses(mail.replyTo)),
|
|
121
|
+
Subject: mail.subject,
|
|
122
|
+
HtmlBody: mail.html === false ? undefined : mail.html,
|
|
123
|
+
TextBody: mail.text,
|
|
124
|
+
Tag: first("x-pm-tag"),
|
|
125
|
+
MessageStream: stream,
|
|
126
|
+
Headers: custom.map((h) => ({ Name: h.name, Value: h.value })),
|
|
127
|
+
// A part with no file name, disposition or Content-ID is a body alternative (for example
|
|
128
|
+
// `text/calendar; method=REQUEST`), not an attachment; it stays in the raw source.
|
|
129
|
+
Attachments: mail.attachments
|
|
130
|
+
.filter((a) => a.filename !== undefined || a.contentDisposition !== undefined || a.cid !== undefined)
|
|
131
|
+
.map((a) => ({
|
|
132
|
+
Name: a.filename ?? "",
|
|
133
|
+
Content: a.content.toString("base64"),
|
|
134
|
+
ContentType: a.contentType,
|
|
135
|
+
ContentID: a.cid === undefined ? null : `cid:${a.cid}`,
|
|
136
|
+
})),
|
|
137
|
+
Metadata: metadata,
|
|
138
|
+
TrackOpens: first("x-pm-trackopens"),
|
|
139
|
+
TrackLinks: first("x-pm-tracklinks"),
|
|
140
|
+
};
|
|
141
|
+
return { draft, stream, keepId };
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* "When sending with SMTP, if you add duplicate keys, we will append the additional keys with an
|
|
145
|
+
* incremental number" (refs/support_article_1125-custom-metadata-faq.md:70). Duplicates compare
|
|
146
|
+
* without case as on the API (`:68-69`); that SMTP uses the API rule and the suffix form
|
|
147
|
+
* `key1`, `key2` are INFERRED.
|
|
148
|
+
*/
|
|
149
|
+
function addMetadata(metadata, key, value) {
|
|
150
|
+
const taken = (k) => Object.keys(metadata).some((m) => m.toLowerCase() === k.toLowerCase());
|
|
151
|
+
let name = key;
|
|
152
|
+
for (let n = 1; taken(name); n += 1)
|
|
153
|
+
name = `${key}${n}`;
|
|
154
|
+
metadata[name] = value;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* The copy Postmark delivers and serves as the raw source: incoming `X-PM-*` headers removed
|
|
158
|
+
* (docs/07 §1.3), `X-PM-Tag` added as in the dump example (refs/api_messages-api.md:276), and
|
|
159
|
+
* every `Message-ID` replaced unless `X-PM-KeepID: true`
|
|
160
|
+
* (refs/user-guide_send-email-with-smtp.md:101). The replacement `<uuid@mtasv.net>` is INFERRED.
|
|
161
|
+
* The dump example's `X-PM-Message-Id` is left out: the pipeline mints the MessageID after this
|
|
162
|
+
* source is built. The body bytes stay unchanged.
|
|
163
|
+
*/
|
|
164
|
+
export function deliveredSource(raw, tag, keepId) {
|
|
165
|
+
const split = /\r?\n\r?\n/.exec(raw);
|
|
166
|
+
const head = split === null ? raw : raw.slice(0, split.index);
|
|
167
|
+
const body = split === null ? "" : raw.slice(split.index);
|
|
168
|
+
const eol = head.includes("\r\n") ? "\r\n" : "\n";
|
|
169
|
+
const lines = head.split(/\r?\n(?![ \t])/);
|
|
170
|
+
const kept = lines.filter((line) => {
|
|
171
|
+
const name = line.slice(0, line.indexOf(":")).trim().toLowerCase();
|
|
172
|
+
return !name.startsWith("x-pm-") && (keepId || name !== "message-id");
|
|
173
|
+
});
|
|
174
|
+
const added = [
|
|
175
|
+
...(tag === undefined ? [] : [`X-PM-Tag: ${tag}`]),
|
|
176
|
+
...(keepId && kept.some((l) => /^message-id\s*:/i.test(l))
|
|
177
|
+
? []
|
|
178
|
+
: [`Message-ID: <${randomUUID()}@mtasv.net>`]),
|
|
179
|
+
];
|
|
180
|
+
return [...kept, ...added].join(eol) + body;
|
|
181
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ErrorBody } from "../errors.ts";
|
|
2
|
+
import type { Runtime } from "../runtime.ts";
|
|
3
|
+
import { type Bounce } from "../state/types.ts";
|
|
4
|
+
import type { SmtpSender } from "./auth.ts";
|
|
5
|
+
/**
|
|
6
|
+
* One accepted DATA transaction. Postmark accepts every message at SMTP and turns each
|
|
7
|
+
* message-level error into an `SMTPApiError` bounce (docs/07 §1.4). Returns the Postmark MessageID.
|
|
8
|
+
*/
|
|
9
|
+
export declare function receive(runtime: Runtime, sender: SmtpSender, raw: Buffer, rcptTo: string[]): Promise<string>;
|
|
10
|
+
export interface Failure {
|
|
11
|
+
serverId: number;
|
|
12
|
+
stream: string;
|
|
13
|
+
messageId: string;
|
|
14
|
+
tag: string | null;
|
|
15
|
+
from: string | null;
|
|
16
|
+
subject: string;
|
|
17
|
+
metadata: Record<string, string>;
|
|
18
|
+
source: string;
|
|
19
|
+
}
|
|
20
|
+
/** The `SMTPApiError` bounce of one recipient; a seed builds past ones with it. */
|
|
21
|
+
export declare function smtpApiErrorBounce(id: number, at: Date, failure: Failure, email: string, error: ErrorBody): Bounce;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { simpleParser } from "mailparser";
|
|
2
|
+
import { submitOutbound } from "../pipeline/submit.js";
|
|
3
|
+
import { newMessageId } from "../state/ids.js";
|
|
4
|
+
import { suppressionKey } from "../state/store.js";
|
|
5
|
+
import { BOUNCE_TYPES } from "../state/types.js";
|
|
6
|
+
import { deliveredSource, draftFromMime, PARSE_OPTIONS } from "./mime.js";
|
|
7
|
+
/**
|
|
8
|
+
* One accepted DATA transaction. Postmark accepts every message at SMTP and turns each
|
|
9
|
+
* message-level error into an `SMTPApiError` bounce (docs/07 §1.4). Returns the Postmark MessageID.
|
|
10
|
+
*/
|
|
11
|
+
export async function receive(runtime, sender, raw, rcptTo) {
|
|
12
|
+
// Raw MIME as text. nodemailer writes 7-bit ASCII unless the content needs 8BITMIME/SMTPUTF8
|
|
13
|
+
// (docs/07 §1.7); UTF-8 is the only 8-bit form kept readable.
|
|
14
|
+
const source = raw.toString("utf8");
|
|
15
|
+
const mail = await simpleParser(raw, PARSE_OPTIONS);
|
|
16
|
+
const { draft, stream, keepId } = draftFromMime(mail, rcptTo, sender);
|
|
17
|
+
const result = await submitOutbound(runtime, {
|
|
18
|
+
auth: { kind: "server", server: sender.server },
|
|
19
|
+
channel: "smtp",
|
|
20
|
+
draft,
|
|
21
|
+
request: source,
|
|
22
|
+
rawSource: deliveredSource(source, draft.Tag, keepId),
|
|
23
|
+
bulkRequestId: null,
|
|
24
|
+
templateId: null,
|
|
25
|
+
});
|
|
26
|
+
const failure = {
|
|
27
|
+
serverId: sender.server.ID,
|
|
28
|
+
stream,
|
|
29
|
+
tag: draft.Tag ?? null,
|
|
30
|
+
from: draft.From ?? null,
|
|
31
|
+
subject: mail.subject ?? "",
|
|
32
|
+
metadata: draft.Metadata,
|
|
33
|
+
source,
|
|
34
|
+
};
|
|
35
|
+
switch (result.outcome) {
|
|
36
|
+
case "accepted":
|
|
37
|
+
return result.message.MessageID;
|
|
38
|
+
case "partiallySuppressed": {
|
|
39
|
+
const { message } = result;
|
|
40
|
+
const suppressed = [...message.To, ...message.Cc, ...message.Bcc]
|
|
41
|
+
.map((a) => a.Email)
|
|
42
|
+
.filter((email) => runtime.store.state.suppressions.has(suppressionKey(message.ServerID, message.MessageStream, email)));
|
|
43
|
+
await bounce(runtime, { ...failure, messageId: message.MessageID }, suppressed, result.error);
|
|
44
|
+
return message.MessageID;
|
|
45
|
+
}
|
|
46
|
+
case "rejected": {
|
|
47
|
+
const messageId = newMessageId();
|
|
48
|
+
await bounce(runtime, { ...failure, messageId }, rcptTo, result.error);
|
|
49
|
+
return messageId;
|
|
50
|
+
}
|
|
51
|
+
case "validated":
|
|
52
|
+
throw new Error("SMTP never authenticates with the test token");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* One `SMTPApiError` (100007) bounce per affected recipient. Description is the short error; the
|
|
57
|
+
* raw content holds the error code, the long error and the SMTP message
|
|
58
|
+
* (refs/user-guide_send-email-with-smtp.md:87). One bounce per recipient, the content layout, and
|
|
59
|
+
* `Inactive`/`CanActivate` false are INFERRED (docs/07 Q9).
|
|
60
|
+
*/
|
|
61
|
+
async function bounce(runtime, failure, recipients, error) {
|
|
62
|
+
const { store, clock, events } = runtime;
|
|
63
|
+
for (const email of recipients) {
|
|
64
|
+
const record = smtpApiErrorBounce(store.nextId("bounce"), clock.now(), failure, email, error);
|
|
65
|
+
store.state.bounces.set(record.ID, record);
|
|
66
|
+
await events.emit("smtpApiError", { bounce: record });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/** The `SMTPApiError` bounce of one recipient; a seed builds past ones with it. */
|
|
70
|
+
export function smtpApiErrorBounce(id, at, failure, email, error) {
|
|
71
|
+
return {
|
|
72
|
+
ID: id,
|
|
73
|
+
ServerID: failure.serverId,
|
|
74
|
+
MessageStream: failure.stream,
|
|
75
|
+
MessageID: failure.messageId,
|
|
76
|
+
Type: "SMTPApiError",
|
|
77
|
+
Tag: failure.tag,
|
|
78
|
+
Description: error.Message,
|
|
79
|
+
Details: BOUNCE_TYPES.SMTPApiError.Name,
|
|
80
|
+
Email: email,
|
|
81
|
+
From: failure.from,
|
|
82
|
+
Subject: failure.subject,
|
|
83
|
+
BouncedAt: at,
|
|
84
|
+
Inactive: false,
|
|
85
|
+
CanActivate: false,
|
|
86
|
+
Content: `ErrorCode: ${error.ErrorCode}\r\nMessage: ${error.Message}\r\n\r\n${failure.source}`,
|
|
87
|
+
Metadata: failure.metadata,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Store } from "../state/store.ts";
|
|
2
|
+
import type { SmtpFault } from "../state/types.ts";
|
|
3
|
+
/**
|
|
4
|
+
* An SMTP error reply. `smtp-server` sends `responseCode` and the message, and adds the enhanced
|
|
5
|
+
* status code itself. A 421 closes the connection.
|
|
6
|
+
*/
|
|
7
|
+
export declare class SmtpReply extends Error {
|
|
8
|
+
readonly responseCode: number;
|
|
9
|
+
constructor(responseCode: number, message: string);
|
|
10
|
+
}
|
|
11
|
+
/** Reply code for behavior postmock does not know yet; the SMTP twin of REST 501 `Unsupported`. */
|
|
12
|
+
export declare const UNSUPPORTED_CODE = 502;
|
|
13
|
+
/** Consumes one armed fault for the stage and throws its reply. */
|
|
14
|
+
export declare function takeFault(store: Store, stage: SmtpFault["stage"]): void;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An SMTP error reply. `smtp-server` sends `responseCode` and the message, and adds the enhanced
|
|
3
|
+
* status code itself. A 421 closes the connection.
|
|
4
|
+
*/
|
|
5
|
+
export class SmtpReply extends Error {
|
|
6
|
+
responseCode;
|
|
7
|
+
constructor(responseCode, message) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.responseCode = responseCode;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/** Reply code for behavior postmock does not know yet; the SMTP twin of REST 501 `Unsupported`. */
|
|
13
|
+
export const UNSUPPORTED_CODE = 502;
|
|
14
|
+
/** Consumes one armed fault for the stage and throws its reply. */
|
|
15
|
+
export function takeFault(store, stage) {
|
|
16
|
+
const faults = store.state.smtpFaults;
|
|
17
|
+
const fault = faults.find((f) => f.stage === stage);
|
|
18
|
+
if (fault === undefined)
|
|
19
|
+
return;
|
|
20
|
+
fault.remaining -= 1;
|
|
21
|
+
if (fault.remaining === 0)
|
|
22
|
+
faults.splice(faults.indexOf(fault), 1);
|
|
23
|
+
throw new SmtpReply(fault.code, fault.message);
|
|
24
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
type Run = () => void | Promise<void>;
|
|
2
|
+
/**
|
|
3
|
+
* Real time plus an offset that `advance()` grows (docs/09 §5 `clock/advance`).
|
|
4
|
+
* A scheduled task runs once: when real time reaches it, or when `advance()` passes it.
|
|
5
|
+
* Advances and real-timer tasks run one at a time, in the order they start.
|
|
6
|
+
*/
|
|
7
|
+
export declare class Clock {
|
|
8
|
+
private offsetMs;
|
|
9
|
+
private seq;
|
|
10
|
+
private advancing;
|
|
11
|
+
private queue;
|
|
12
|
+
private readonly tasks;
|
|
13
|
+
private readonly realNow;
|
|
14
|
+
constructor(realNow?: () => number);
|
|
15
|
+
now(): Date;
|
|
16
|
+
schedule(delayMs: number, run: Run): void;
|
|
17
|
+
/**
|
|
18
|
+
* Moves time forward by `ms`, after any advance already running. Runs each task due by the end in
|
|
19
|
+
* due order, with `now()` set to its due time, and awaits it. A task scheduled during the advance
|
|
20
|
+
* runs too when it falls due by the end.
|
|
21
|
+
*/
|
|
22
|
+
advance(ms: number): Promise<void>;
|
|
23
|
+
/** Resolves when every advance and real-timer task started so far has finished. */
|
|
24
|
+
idle(): Promise<void>;
|
|
25
|
+
/** Drops pending tasks and the offset. Throws during an advance, which would undo it. */
|
|
26
|
+
reset(): void;
|
|
27
|
+
/** Captures offset and pending tasks; the returned function puts them back. */
|
|
28
|
+
checkpoint(): () => void;
|
|
29
|
+
private add;
|
|
30
|
+
private enqueue;
|
|
31
|
+
/** Time never moves back. */
|
|
32
|
+
private moveTo;
|
|
33
|
+
private take;
|
|
34
|
+
}
|
|
35
|
+
export {};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// setTimeout fires at once for a delay above 2^31-1 ms; such a task waits for `advance()` only.
|
|
2
|
+
const MAX_TIMER_MS = 2 ** 31 - 1;
|
|
3
|
+
/**
|
|
4
|
+
* Real time plus an offset that `advance()` grows (docs/09 §5 `clock/advance`).
|
|
5
|
+
* A scheduled task runs once: when real time reaches it, or when `advance()` passes it.
|
|
6
|
+
* Advances and real-timer tasks run one at a time, in the order they start.
|
|
7
|
+
*/
|
|
8
|
+
export class Clock {
|
|
9
|
+
offsetMs = 0;
|
|
10
|
+
seq = 0;
|
|
11
|
+
advancing = false;
|
|
12
|
+
queue = Promise.resolve();
|
|
13
|
+
tasks = new Set();
|
|
14
|
+
realNow;
|
|
15
|
+
constructor(realNow = Date.now) {
|
|
16
|
+
this.realNow = realNow;
|
|
17
|
+
}
|
|
18
|
+
now() {
|
|
19
|
+
return new Date(this.realNow() + this.offsetMs);
|
|
20
|
+
}
|
|
21
|
+
schedule(delayMs, run) {
|
|
22
|
+
if (!Number.isInteger(delayMs) || delayMs < 0) {
|
|
23
|
+
throw new Error(`schedule needs delayMs >= 0, got ${delayMs}`);
|
|
24
|
+
}
|
|
25
|
+
this.add({ due: this.now().getTime() + delayMs, seq: this.seq++, run, timer: undefined });
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Moves time forward by `ms`, after any advance already running. Runs each task due by the end in
|
|
29
|
+
* due order, with `now()` set to its due time, and awaits it. A task scheduled during the advance
|
|
30
|
+
* runs too when it falls due by the end.
|
|
31
|
+
*/
|
|
32
|
+
advance(ms) {
|
|
33
|
+
if (!Number.isInteger(ms) || ms < 0) {
|
|
34
|
+
return Promise.reject(new Error(`clock.advance needs ms >= 0, got ${ms}`));
|
|
35
|
+
}
|
|
36
|
+
return this.enqueue(async () => {
|
|
37
|
+
this.advancing = true;
|
|
38
|
+
try {
|
|
39
|
+
const target = this.now().getTime() + ms;
|
|
40
|
+
for (;;) {
|
|
41
|
+
const next = [...this.tasks]
|
|
42
|
+
.filter((t) => t.due <= target)
|
|
43
|
+
.sort((a, b) => a.due - b.due || a.seq - b.seq)[0];
|
|
44
|
+
if (next === undefined)
|
|
45
|
+
break;
|
|
46
|
+
this.moveTo(next.due);
|
|
47
|
+
await this.take(next);
|
|
48
|
+
}
|
|
49
|
+
this.moveTo(target);
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
this.advancing = false;
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
/** Resolves when every advance and real-timer task started so far has finished. */
|
|
57
|
+
idle() {
|
|
58
|
+
return this.queue;
|
|
59
|
+
}
|
|
60
|
+
/** Drops pending tasks and the offset. Throws during an advance, which would undo it. */
|
|
61
|
+
reset() {
|
|
62
|
+
if (this.advancing)
|
|
63
|
+
throw new Error("clock.reset during an advance");
|
|
64
|
+
for (const task of this.tasks)
|
|
65
|
+
clearTimeout(task.timer);
|
|
66
|
+
this.tasks.clear();
|
|
67
|
+
this.offsetMs = 0;
|
|
68
|
+
}
|
|
69
|
+
/** Captures offset and pending tasks; the returned function puts them back. */
|
|
70
|
+
checkpoint() {
|
|
71
|
+
const offsetMs = this.offsetMs;
|
|
72
|
+
const tasks = [...this.tasks].map(({ due, seq, run }) => ({ due, seq, run }));
|
|
73
|
+
return () => {
|
|
74
|
+
this.reset();
|
|
75
|
+
this.offsetMs = offsetMs;
|
|
76
|
+
for (const task of tasks)
|
|
77
|
+
this.add({ ...task, timer: undefined });
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
add(task) {
|
|
81
|
+
const delay = task.due - this.now().getTime();
|
|
82
|
+
if (delay <= MAX_TIMER_MS) {
|
|
83
|
+
// A task that throws on a real timer is a postmock bug: rethrow so the process crashes.
|
|
84
|
+
task.timer = setTimeout(() => {
|
|
85
|
+
this.enqueue(() => this.take(task)).catch((error) => queueMicrotask(() => {
|
|
86
|
+
throw error;
|
|
87
|
+
}));
|
|
88
|
+
}, Math.max(0, delay)).unref();
|
|
89
|
+
}
|
|
90
|
+
this.tasks.add(task);
|
|
91
|
+
}
|
|
92
|
+
enqueue(work) {
|
|
93
|
+
const result = this.queue.then(work);
|
|
94
|
+
this.queue = result.then(() => { }, () => { });
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
/** Time never moves back. */
|
|
98
|
+
moveTo(instant) {
|
|
99
|
+
this.offsetMs = Math.max(this.now().getTime(), instant) - this.realNow();
|
|
100
|
+
}
|
|
101
|
+
async take(task) {
|
|
102
|
+
if (!this.tasks.delete(task))
|
|
103
|
+
return;
|
|
104
|
+
clearTimeout(task.timer);
|
|
105
|
+
await task.run();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Postmark MessageID and bulk Id: a UUID (docs/08 E7). Lowercase hex as in doc examples (INFERRED). */
|
|
2
|
+
export declare const newMessageId: () => string;
|
|
3
|
+
/** A server or account token. The UUID shape matches no documented format (INFERRED). */
|
|
4
|
+
export declare const newToken: () => string;
|
|
5
|
+
/** Lowercase hex, for inbound hashes and SMTP token keys. */
|
|
6
|
+
export declare const newHex: (bytes: number) => string;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { randomBytes, randomUUID } from "node:crypto";
|
|
2
|
+
/** Postmark MessageID and bulk Id: a UUID (docs/08 E7). Lowercase hex as in doc examples (INFERRED). */
|
|
3
|
+
export const newMessageId = () => randomUUID();
|
|
4
|
+
/** A server or account token. The UUID shape matches no documented format (INFERRED). */
|
|
5
|
+
export const newToken = () => randomUUID();
|
|
6
|
+
/** Lowercase hex, for inbound hashes and SMTP token keys. */
|
|
7
|
+
export const newHex = (bytes) => randomBytes(bytes).toString("hex");
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export declare const TEST_TOKEN = "POSTMARK_API_TEST";
|
|
2
|
+
import { type State, type Store } from "./store.ts";
|
|
3
|
+
import type { MessageStream, Server } from "./types.ts";
|
|
4
|
+
export type ServerSettings = Partial<Omit<Server, "ServerLink" | "InboundAddress">>;
|
|
5
|
+
/**
|
|
6
|
+
* Creates a server with one API token and the default streams. `settings.ID` claims a fixed ID.
|
|
7
|
+
* A token that another server already holds throws: auth could not tell the two apart.
|
|
8
|
+
*/
|
|
9
|
+
export declare function createServer(store: Store, now: Date, settings?: ServerSettings): Server;
|
|
10
|
+
/** Adds an account token. Tokens compare without case (docs/02 §3.1). */
|
|
11
|
+
export declare function addAccountToken(store: Store, token: string): void;
|
|
12
|
+
/**
|
|
13
|
+
* What `POSTMARK_API_TEST` sends against: a new server with default settings and the default
|
|
14
|
+
* streams, stored nowhere. It validates like a live server and delivers nothing (docs/03 §4).
|
|
15
|
+
* That it sees only the default streams and settings is INFERRED.
|
|
16
|
+
*/
|
|
17
|
+
export interface TestTokenContext {
|
|
18
|
+
kind: "test";
|
|
19
|
+
server: Server;
|
|
20
|
+
streams: MessageStream[];
|
|
21
|
+
}
|
|
22
|
+
export declare const testTokenContext: (now: Date) => TestTokenContext;
|
|
23
|
+
/** A stream of the server the request authenticated as, stored or test-token. */
|
|
24
|
+
export declare function findStream(state: State, auth: {
|
|
25
|
+
kind: "server";
|
|
26
|
+
server: Server;
|
|
27
|
+
} | TestTokenContext, streamId: string): MessageStream | undefined;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
export const TEST_TOKEN = "POSTMARK_API_TEST";
|
|
2
|
+
import { newHex, newToken } from "./ids.js";
|
|
3
|
+
import { streamKey } from "./store.js";
|
|
4
|
+
// A new server has these three streams (docs/04 §4.3; Q7 for the broadcast id).
|
|
5
|
+
const DEFAULT_STREAMS = [
|
|
6
|
+
["outbound", "Default Transactional Stream", "Transactional"],
|
|
7
|
+
["inbound", "Default Inbound Stream", "Inbound"],
|
|
8
|
+
["broadcast", "Default Broadcast Stream", "Broadcasts"],
|
|
9
|
+
];
|
|
10
|
+
function newServer(id, tokens, settings) {
|
|
11
|
+
const hash = settings.InboundHash ?? newHex(16);
|
|
12
|
+
return {
|
|
13
|
+
Name: `Server ${id}`,
|
|
14
|
+
ApiTokens: tokens,
|
|
15
|
+
Color: "purple",
|
|
16
|
+
SmtpApiActivated: true,
|
|
17
|
+
RawEmailEnabled: false,
|
|
18
|
+
DeliveryType: "Live",
|
|
19
|
+
InboundHookUrl: "",
|
|
20
|
+
BounceHookUrl: "",
|
|
21
|
+
OpenHookUrl: "",
|
|
22
|
+
DeliveryHookUrl: "",
|
|
23
|
+
ClickHookUrl: "",
|
|
24
|
+
PostFirstOpenOnly: false,
|
|
25
|
+
InboundDomain: "",
|
|
26
|
+
InboundSpamThreshold: 0,
|
|
27
|
+
TrackOpens: false,
|
|
28
|
+
TrackLinks: "None",
|
|
29
|
+
IncludeBounceContentInHook: false,
|
|
30
|
+
EnableSmtpApiErrorHooks: false,
|
|
31
|
+
...settings,
|
|
32
|
+
ID: id,
|
|
33
|
+
InboundHash: hash,
|
|
34
|
+
ServerLink: `https://postmarkapp.com/servers/${id}/streams`,
|
|
35
|
+
InboundAddress: `${hash}@inbound.postmarkapp.com`,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const defaultStreams = (serverId, now) => DEFAULT_STREAMS.map(([id, name, type]) => ({
|
|
39
|
+
ID: id,
|
|
40
|
+
ServerID: serverId,
|
|
41
|
+
Name: name,
|
|
42
|
+
Description: null,
|
|
43
|
+
MessageStreamType: type,
|
|
44
|
+
CreatedAt: now,
|
|
45
|
+
UpdatedAt: null,
|
|
46
|
+
ArchivedAt: null,
|
|
47
|
+
ExpectedPurgeDate: null,
|
|
48
|
+
SubscriptionManagementConfiguration: {
|
|
49
|
+
UnsubscribeHandlingType: type === "Broadcasts" ? "Postmark" : "None",
|
|
50
|
+
},
|
|
51
|
+
}));
|
|
52
|
+
/**
|
|
53
|
+
* Creates a server with one API token and the default streams. `settings.ID` claims a fixed ID.
|
|
54
|
+
* A token that another server already holds throws: auth could not tell the two apart.
|
|
55
|
+
*/
|
|
56
|
+
export function createServer(store, now, settings = {}) {
|
|
57
|
+
const tokens = settings.ApiTokens ?? [newToken()];
|
|
58
|
+
const held = new Map([...store.state.servers.values()].flatMap((other) => other.ApiTokens.map((t) => [t.toLowerCase(), { token: t, server: other.ID }])));
|
|
59
|
+
for (const token of tokens) {
|
|
60
|
+
refuseTestToken(token);
|
|
61
|
+
const holder = held.get(token.toLowerCase());
|
|
62
|
+
if (holder !== undefined) {
|
|
63
|
+
throw new Error(`server ${holder.server} already holds token ${holder.token}`);
|
|
64
|
+
}
|
|
65
|
+
if (tokens.filter((t) => t.toLowerCase() === token.toLowerCase()).length > 1) {
|
|
66
|
+
throw new Error(`token ${token} appears twice`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const id = settings.ID === undefined ? store.nextId("server") : store.useId("server", settings.ID);
|
|
70
|
+
const server = newServer(id, tokens, settings);
|
|
71
|
+
store.state.servers.set(id, server);
|
|
72
|
+
for (const stream of defaultStreams(id, now)) {
|
|
73
|
+
store.state.streams.set(streamKey(id, stream.ID), stream);
|
|
74
|
+
}
|
|
75
|
+
return server;
|
|
76
|
+
}
|
|
77
|
+
// Auth treats POSTMARK_API_TEST as the test token before any lookup (docs/02 §3.3).
|
|
78
|
+
function refuseTestToken(token) {
|
|
79
|
+
if (token.toLowerCase() === TEST_TOKEN.toLowerCase()) {
|
|
80
|
+
throw new Error(`${TEST_TOKEN} is the test token, not a stored token`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** Adds an account token. Tokens compare without case (docs/02 §3.1). */
|
|
84
|
+
export function addAccountToken(store, token) {
|
|
85
|
+
refuseTestToken(token);
|
|
86
|
+
if (store.state.account.tokens.some((t) => t.toLowerCase() === token.toLowerCase())) {
|
|
87
|
+
throw new Error(`account token ${token} exists`);
|
|
88
|
+
}
|
|
89
|
+
store.state.account.tokens.push(token);
|
|
90
|
+
}
|
|
91
|
+
export const testTokenContext = (now) => ({
|
|
92
|
+
kind: "test",
|
|
93
|
+
server: newServer(0, [TEST_TOKEN], { Name: TEST_TOKEN, InboundHash: "" }),
|
|
94
|
+
streams: defaultStreams(0, now),
|
|
95
|
+
});
|
|
96
|
+
/** A stream of the server the request authenticated as, stored or test-token. */
|
|
97
|
+
export function findStream(state, auth, streamId) {
|
|
98
|
+
return auth.kind === "test"
|
|
99
|
+
? auth.streams.find((s) => s.ID === streamId)
|
|
100
|
+
: state.streams.get(streamKey(auth.server.ID, streamId));
|
|
101
|
+
}
|