@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.
Files changed (209) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +251 -0
  3. package/dist/seeds/conformance/00-core.d.ts +4 -0
  4. package/dist/seeds/conformance/00-core.js +12 -0
  5. package/dist/seeds/conformance/20-bounces.d.ts +3 -0
  6. package/dist/seeds/conformance/20-bounces.js +108 -0
  7. package/dist/seeds/conformance/40-messages.d.ts +3 -0
  8. package/dist/seeds/conformance/40-messages.js +144 -0
  9. package/dist/seeds/conformance/50-server-color.d.ts +8 -0
  10. package/dist/seeds/conformance/50-server-color.js +13 -0
  11. package/dist/seeds/conformance/70-account.d.ts +9 -0
  12. package/dist/seeds/conformance/70-account.js +29 -0
  13. package/dist/seeds/conformance/80-templates.d.ts +4 -0
  14. package/dist/seeds/conformance/80-templates.js +23 -0
  15. package/dist/seeds/conformance.d.ts +2 -0
  16. package/dist/seeds/conformance.js +3 -0
  17. package/dist/seeds/empty.d.ts +4 -0
  18. package/dist/seeds/empty.js +3 -0
  19. package/dist/seeds/lib/conformance.d.ts +16 -0
  20. package/dist/seeds/lib/conformance.js +18 -0
  21. package/dist/seeds/lib/history.d.ts +27 -0
  22. package/dist/seeds/lib/history.js +129 -0
  23. package/dist/seeds/lib/read-server.d.ts +10 -0
  24. package/dist/seeds/lib/read-server.js +10 -0
  25. package/dist/seeds/lib/template-server.d.ts +13 -0
  26. package/dist/seeds/lib/template-server.js +13 -0
  27. package/dist/src/api/account/authentication.d.ts +39 -0
  28. package/dist/src/api/account/authentication.js +97 -0
  29. package/dist/src/api/account/domains.d.ts +59 -0
  30. package/dist/src/api/account/domains.js +78 -0
  31. package/dist/src/api/account/paging.d.ts +12 -0
  32. package/dist/src/api/account/paging.js +35 -0
  33. package/dist/src/api/account/push.d.ts +14 -0
  34. package/dist/src/api/account/push.js +84 -0
  35. package/dist/src/api/account/routes.d.ts +1 -0
  36. package/dist/src/api/account/routes.js +288 -0
  37. package/dist/src/api/account/senders.d.ts +54 -0
  38. package/dist/src/api/account/senders.js +87 -0
  39. package/dist/src/api/account/servers.d.ts +40 -0
  40. package/dist/src/api/account/servers.js +148 -0
  41. package/dist/src/api/bounces/json.d.ts +25 -0
  42. package/dist/src/api/bounces/json.js +27 -0
  43. package/dist/src/api/bounces/routes.d.ts +1 -0
  44. package/dist/src/api/bounces/routes.js +127 -0
  45. package/dist/src/api/bulk/bulk.d.ts +43 -0
  46. package/dist/src/api/bulk/bulk.js +81 -0
  47. package/dist/src/api/bulk/routes.d.ts +1 -0
  48. package/dist/src/api/bulk/routes.js +252 -0
  49. package/dist/src/api/data-removals/routes.d.ts +1 -0
  50. package/dist/src/api/data-removals/routes.js +59 -0
  51. package/dist/src/api/email/json.d.ts +11 -0
  52. package/dist/src/api/email/json.js +33 -0
  53. package/dist/src/api/email/routes.d.ts +1 -0
  54. package/dist/src/api/email/routes.js +64 -0
  55. package/dist/src/api/inbound/routes.d.ts +1 -0
  56. package/dist/src/api/inbound/routes.js +40 -0
  57. package/dist/src/api/index.d.ts +1 -0
  58. package/dist/src/api/index.js +3 -0
  59. package/dist/src/api/message-streams/routes.d.ts +1 -0
  60. package/dist/src/api/message-streams/routes.js +211 -0
  61. package/dist/src/api/message-streams/streams.d.ts +28 -0
  62. package/dist/src/api/message-streams/streams.js +50 -0
  63. package/dist/src/api/messages/json.d.ts +149 -0
  64. package/dist/src/api/messages/json.js +114 -0
  65. package/dist/src/api/messages/query.d.ts +19 -0
  66. package/dist/src/api/messages/query.js +64 -0
  67. package/dist/src/api/messages/routes.d.ts +1 -0
  68. package/dist/src/api/messages/routes.js +203 -0
  69. package/dist/src/api/server/json.d.ts +26 -0
  70. package/dist/src/api/server/json.js +25 -0
  71. package/dist/src/api/server/routes.d.ts +1 -0
  72. package/dist/src/api/server/routes.js +17 -0
  73. package/dist/src/api/stats/aggregate.d.ts +58 -0
  74. package/dist/src/api/stats/aggregate.js +66 -0
  75. package/dist/src/api/stats/routes.d.ts +1 -0
  76. package/dist/src/api/stats/routes.js +72 -0
  77. package/dist/src/api/suppressions/routes.d.ts +1 -0
  78. package/dist/src/api/suppressions/routes.js +98 -0
  79. package/dist/src/api/templates/content.d.ts +21 -0
  80. package/dist/src/api/templates/content.js +39 -0
  81. package/dist/src/api/templates/routes.d.ts +1 -0
  82. package/dist/src/api/templates/routes.js +164 -0
  83. package/dist/src/api/templates/send.d.ts +13 -0
  84. package/dist/src/api/templates/send.js +105 -0
  85. package/dist/src/api/templates/templates.d.ts +100 -0
  86. package/dist/src/api/templates/templates.js +218 -0
  87. package/dist/src/api/templates/validate.d.ts +15 -0
  88. package/dist/src/api/templates/validate.js +77 -0
  89. package/dist/src/api/triggers/routes.d.ts +1 -0
  90. package/dist/src/api/triggers/routes.js +62 -0
  91. package/dist/src/api/webhooks/routes.d.ts +14 -0
  92. package/dist/src/api/webhooks/routes.js +192 -0
  93. package/dist/src/config.d.ts +17 -0
  94. package/dist/src/config.js +95 -0
  95. package/dist/src/control/app.d.ts +5 -0
  96. package/dist/src/control/app.js +42 -0
  97. package/dist/src/control/endpoints/account.d.ts +1 -0
  98. package/dist/src/control/endpoints/account.js +68 -0
  99. package/dist/src/control/endpoints/bulk.d.ts +1 -0
  100. package/dist/src/control/endpoints/bulk.js +29 -0
  101. package/dist/src/control/endpoints/core.d.ts +1 -0
  102. package/dist/src/control/endpoints/core.js +104 -0
  103. package/dist/src/control/endpoints/inbound.d.ts +1 -0
  104. package/dist/src/control/endpoints/inbound.js +61 -0
  105. package/dist/src/control/endpoints/recipients.d.ts +1 -0
  106. package/dist/src/control/endpoints/recipients.js +136 -0
  107. package/dist/src/control/endpoints/smtp.d.ts +1 -0
  108. package/dist/src/control/endpoints/smtp.js +74 -0
  109. package/dist/src/control/endpoints/tracking.d.ts +1 -0
  110. package/dist/src/control/endpoints/tracking.js +95 -0
  111. package/dist/src/control/endpoints/webhooks.d.ts +1 -0
  112. package/dist/src/control/endpoints/webhooks.js +36 -0
  113. package/dist/src/control/index.d.ts +1 -0
  114. package/dist/src/control/index.js +3 -0
  115. package/dist/src/control/registry.d.ts +25 -0
  116. package/dist/src/control/registry.js +19 -0
  117. package/dist/src/control/seed.d.ts +11 -0
  118. package/dist/src/control/seed.js +32 -0
  119. package/dist/src/control/seeding.d.ts +10 -0
  120. package/dist/src/control/seeding.js +27 -0
  121. package/dist/src/discover.d.ts +16 -0
  122. package/dist/src/discover.js +49 -0
  123. package/dist/src/errors.d.ts +49 -0
  124. package/dist/src/errors.js +701 -0
  125. package/dist/src/events.d.ts +49 -0
  126. package/dist/src/events.js +19 -0
  127. package/dist/src/http/app.d.ts +8 -0
  128. package/dist/src/http/app.js +51 -0
  129. package/dist/src/http/auth.d.ts +7 -0
  130. package/dist/src/http/auth.js +32 -0
  131. package/dist/src/http/faults.d.ts +8 -0
  132. package/dist/src/http/faults.js +33 -0
  133. package/dist/src/http/normalize.d.ts +57 -0
  134. package/dist/src/http/normalize.js +225 -0
  135. package/dist/src/http/respond.d.ts +23 -0
  136. package/dist/src/http/respond.js +37 -0
  137. package/dist/src/http/routes.d.ts +60 -0
  138. package/dist/src/http/routes.js +64 -0
  139. package/dist/src/inbound/deliver.d.ts +9 -0
  140. package/dist/src/inbound/deliver.js +34 -0
  141. package/dist/src/inbound/parse.d.ts +21 -0
  142. package/dist/src/inbound/parse.js +71 -0
  143. package/dist/src/inbound/receive.d.ts +17 -0
  144. package/dist/src/inbound/receive.js +89 -0
  145. package/dist/src/main.d.ts +2 -0
  146. package/dist/src/main.js +14 -0
  147. package/dist/src/mime/compose.d.ts +33 -0
  148. package/dist/src/mime/compose.js +58 -0
  149. package/dist/src/pipeline/addresses.d.ts +6 -0
  150. package/dist/src/pipeline/addresses.js +38 -0
  151. package/dist/src/pipeline/inactive.d.ts +8 -0
  152. package/dist/src/pipeline/inactive.js +20 -0
  153. package/dist/src/pipeline/submit.d.ts +138 -0
  154. package/dist/src/pipeline/submit.js +415 -0
  155. package/dist/src/plugins/message-events.d.ts +7 -0
  156. package/dist/src/plugins/message-events.js +76 -0
  157. package/dist/src/plugins/smtp.d.ts +15 -0
  158. package/dist/src/plugins/smtp.js +42 -0
  159. package/dist/src/plugins/stats.d.ts +7 -0
  160. package/dist/src/plugins/stats.js +78 -0
  161. package/dist/src/plugins/webhooks.d.ts +4 -0
  162. package/dist/src/plugins/webhooks.js +80 -0
  163. package/dist/src/plugins.d.ts +20 -0
  164. package/dist/src/plugins.js +9 -0
  165. package/dist/src/recipients/test-bounces.d.ts +15 -0
  166. package/dist/src/recipients/test-bounces.js +31 -0
  167. package/dist/src/recipients/transitions.d.ts +57 -0
  168. package/dist/src/recipients/transitions.js +279 -0
  169. package/dist/src/render/mustachio.d.ts +60 -0
  170. package/dist/src/render/mustachio.js +265 -0
  171. package/dist/src/runtime.d.ts +12 -0
  172. package/dist/src/runtime.js +11 -0
  173. package/dist/src/server.d.ts +25 -0
  174. package/dist/src/server.js +49 -0
  175. package/dist/src/smtp/auth.d.ts +35 -0
  176. package/dist/src/smtp/auth.js +66 -0
  177. package/dist/src/smtp/listener.d.ts +16 -0
  178. package/dist/src/smtp/listener.js +121 -0
  179. package/dist/src/smtp/mime.d.ts +36 -0
  180. package/dist/src/smtp/mime.js +181 -0
  181. package/dist/src/smtp/receive.d.ts +21 -0
  182. package/dist/src/smtp/receive.js +89 -0
  183. package/dist/src/smtp/reply.d.ts +14 -0
  184. package/dist/src/smtp/reply.js +24 -0
  185. package/dist/src/state/clock.d.ts +35 -0
  186. package/dist/src/state/clock.js +107 -0
  187. package/dist/src/state/ids.d.ts +6 -0
  188. package/dist/src/state/ids.js +7 -0
  189. package/dist/src/state/servers.d.ts +27 -0
  190. package/dist/src/state/servers.js +101 -0
  191. package/dist/src/state/store.d.ts +57 -0
  192. package/dist/src/state/store.js +98 -0
  193. package/dist/src/state/suppressions.d.ts +9 -0
  194. package/dist/src/state/suppressions.js +8 -0
  195. package/dist/src/state/types.d.ts +530 -0
  196. package/dist/src/state/types.js +41 -0
  197. package/dist/src/time.d.ts +11 -0
  198. package/dist/src/time.js +48 -0
  199. package/dist/src/tracking.d.ts +53 -0
  200. package/dist/src/tracking.js +150 -0
  201. package/dist/src/webhooks/deliver.d.ts +60 -0
  202. package/dist/src/webhooks/deliver.js +172 -0
  203. package/dist/src/webhooks/egress.d.ts +19 -0
  204. package/dist/src/webhooks/egress.js +40 -0
  205. package/dist/src/webhooks/outbound.d.ts +26 -0
  206. package/dist/src/webhooks/outbound.js +47 -0
  207. package/dist/src/webhooks/payloads.d.ts +111 -0
  208. package/dist/src/webhooks/payloads.js +128 -0
  209. package/package.json +55 -0
@@ -0,0 +1,211 @@
1
+ import { z } from "zod";
2
+ import { apiError } from "../../errors.js";
3
+ import { absent, parseBody, queryBool } from "../../http/normalize.js";
4
+ import { Unsupported } from "../../http/respond.js";
5
+ import { defineRoute } from "../../http/routes.js";
6
+ import { streamKey } from "../../state/store.js";
7
+ import { formatTimestamp } from "../../time.js";
8
+ import { liveStream, PURGE_DELAY_MS, schedulePurge, streamJson } from "./streams.js";
9
+ // Message Streams API: docs/04 §4. Error texts come from the docs/02 §4.4 table.
10
+ const MAX_STREAMS = 10; // refs/api_message-streams-api.md:4
11
+ /** Default streams that cannot be archived (refs/api_overview.md:136). */
12
+ const UNARCHIVABLE = new Set(["outbound", "inbound"]);
13
+ /** 1220 when the account has no Message Streams API access (docs/04 §4.4). */
14
+ function streamsOf(ctx) {
15
+ if (!ctx.store.state.account.messageStreamsApiEnabled)
16
+ throw apiError(1220);
17
+ return [...ctx.store.state.streams.values()].filter((s) => s.ServerID === ctx.auth.server.ID);
18
+ }
19
+ function streamOf(ctx) {
20
+ streamsOf(ctx);
21
+ return liveStream(ctx.store.state, ctx.auth.server.ID, ctx.params.id);
22
+ }
23
+ /** Enum text matched without case, stored in doc case (INFERRED, like query keys: docs/08 R3). */
24
+ const oneOf = (values, text) => values.find((v) => v.toLowerCase() === text.toLowerCase());
25
+ const STREAM_TYPES = ["Transactional", "Broadcasts", "Inbound"];
26
+ const HANDLING_TYPES = ["None", "Postmark", "Custom"];
27
+ defineRoute({
28
+ method: "GET",
29
+ path: "/message-streams",
30
+ auth: "server",
31
+ handler: (ctx) => {
32
+ const typeText = ctx.query.get("MessageStreamType") ?? "All";
33
+ const type = oneOf(["All", ...STREAM_TYPES], typeText);
34
+ if (type === undefined)
35
+ throw apiError(1221);
36
+ const archived = queryBool.safeParse(ctx.query.get("IncludeArchivedStreams") ?? "false");
37
+ if (!archived.success)
38
+ throw new Unsupported("invalid IncludeArchivedStreams: not captured");
39
+ // count and offset are accepted and ignored (docs/08 R6).
40
+ const streams = streamsOf(ctx).filter((s) => (type === "All" || s.MessageStreamType === type) &&
41
+ (archived.data || s.ArchivedAt === null));
42
+ return { MessageStreams: streams.map(streamJson), TotalCount: streams.length };
43
+ },
44
+ });
45
+ defineRoute({
46
+ method: "GET",
47
+ path: "/message-streams/:id",
48
+ auth: "server",
49
+ handler: (ctx) => streamJson(streamOf(ctx)),
50
+ });
51
+ const handlingSchema = z
52
+ .object({ UnsubscribeHandlingType: absent(z.string()) })
53
+ .nullable()
54
+ .optional();
55
+ /**
56
+ * The checked `UnsubscribeHandlingType` for a stream type (1238–1240). Broadcasts require
57
+ * unsubscribe management (refs/api_message-streams-api.md:50); a Transactional or Inbound stream
58
+ * with `Postmark` or `Custom` is not captured.
59
+ */
60
+ function handlingType(ctx, streamType, text) {
61
+ if (text === undefined)
62
+ return streamType === "Broadcasts" ? "Postmark" : "None";
63
+ const value = oneOf(HANDLING_TYPES, text);
64
+ if (value === undefined)
65
+ throw apiError(1240);
66
+ if (value === "Custom" && !ctx.store.state.account.customUnsubscribeEnabled)
67
+ throw apiError(1238);
68
+ if (streamType === "Broadcasts" && value === "None")
69
+ throw apiError(1239);
70
+ if (streamType !== "Broadcasts" && value !== "None") {
71
+ throw new Unsupported(`UnsubscribeHandlingType ${value} on a ${streamType} stream: not captured`);
72
+ }
73
+ return value;
74
+ }
75
+ /** 1234: HTML in a description (tag rule INFERRED). Length limits (1224, 1231) are not documented. */
76
+ function checkDescription(description) {
77
+ if (description !== undefined && /<\/?[a-z!][^>]*>/i.test(description))
78
+ throw apiError(1234);
79
+ }
80
+ const createSchema = z.object({
81
+ ID: z.unknown(),
82
+ Name: absent(z.string()),
83
+ Description: absent(z.string()),
84
+ MessageStreamType: absent(z.string()),
85
+ SubscriptionManagementConfiguration: handlingSchema,
86
+ });
87
+ defineRoute({
88
+ method: "POST",
89
+ path: "/message-streams",
90
+ auth: "server",
91
+ handler: (ctx) => {
92
+ const streams = streamsOf(ctx);
93
+ const parsed = parseBody(createSchema, ctx.body ?? {});
94
+ if (!parsed.success)
95
+ throw new Unsupported("malformed stream body: error not captured");
96
+ const body = parsed.data;
97
+ const id = body.ID;
98
+ if (typeof id !== "string" || id === "")
99
+ throw apiError(1222);
100
+ if (id.toLowerCase().startsWith("pm-"))
101
+ throw apiError(1233);
102
+ // Allowed characters after the first letter are INFERRED.
103
+ if (!/^[A-Za-z][A-Za-z0-9_-]{0,29}$/.test(id))
104
+ throw apiError(1227);
105
+ if (body.Name === undefined)
106
+ throw apiError(1223);
107
+ checkDescription(body.Description);
108
+ const type = oneOf(STREAM_TYPES, body.MessageStreamType ?? "");
109
+ if (type === "Inbound")
110
+ throw apiError(1228); // every server has its inbound stream
111
+ if (type === undefined)
112
+ throw apiError(1221);
113
+ // IDs clash without case (INFERRED); lookups keep case.
114
+ if (streams.some((s) => s.ID.toLowerCase() === id.toLowerCase()))
115
+ throw apiError(1230);
116
+ if (streams.length >= MAX_STREAMS)
117
+ throw apiError(1225);
118
+ const handling = handlingType(ctx, type, body.SubscriptionManagementConfiguration?.UnsubscribeHandlingType);
119
+ const stream = {
120
+ ID: id,
121
+ ServerID: ctx.auth.server.ID,
122
+ Name: body.Name,
123
+ Description: body.Description ?? null,
124
+ MessageStreamType: type,
125
+ CreatedAt: ctx.clock.now(),
126
+ UpdatedAt: null,
127
+ ArchivedAt: null,
128
+ ExpectedPurgeDate: null,
129
+ SubscriptionManagementConfiguration: { UnsubscribeHandlingType: handling },
130
+ };
131
+ // The ID of a purged stream is free again (INFERRED).
132
+ ctx.store.state.purgedStreams.delete(streamKey(stream.ServerID, id));
133
+ ctx.store.state.streams.set(streamKey(stream.ServerID, id), stream);
134
+ return streamJson(stream);
135
+ },
136
+ });
137
+ // dotnet sends `Name: null` to keep the name (R9); an empty string is no valid name (INFERRED).
138
+ const editSchema = z.object({
139
+ Name: z.string().nullable().optional(),
140
+ Description: absent(z.string()),
141
+ SubscriptionManagementConfiguration: handlingSchema,
142
+ });
143
+ // java sends the whole stream object; only these three fields change (docs/04 §4.1).
144
+ defineRoute({
145
+ method: "PATCH",
146
+ path: "/message-streams/:id",
147
+ auth: "server",
148
+ handler: (ctx) => {
149
+ const stream = streamOf(ctx);
150
+ if (stream.ArchivedAt !== null)
151
+ throw new Unsupported("edit of an archived stream: not captured");
152
+ const parsed = parseBody(editSchema, ctx.body ?? {});
153
+ if (!parsed.success)
154
+ throw new Unsupported("malformed stream body: error not captured");
155
+ const body = parsed.data;
156
+ checkDescription(body.Description);
157
+ const text = body.SubscriptionManagementConfiguration?.UnsubscribeHandlingType;
158
+ const handling = text === undefined
159
+ ? stream.SubscriptionManagementConfiguration.UnsubscribeHandlingType
160
+ : handlingType(ctx, stream.MessageStreamType, text);
161
+ if (body.Name === "")
162
+ throw apiError(1223);
163
+ if (body.Name !== undefined && body.Name !== null)
164
+ stream.Name = body.Name;
165
+ if (body.Description !== undefined)
166
+ stream.Description = body.Description;
167
+ stream.SubscriptionManagementConfiguration = { UnsubscribeHandlingType: handling };
168
+ stream.UpdatedAt = ctx.clock.now();
169
+ return streamJson(stream);
170
+ },
171
+ });
172
+ // Archive and unarchive accept an empty body and `{}` (docs/04 §6); the body is not read.
173
+ defineRoute({
174
+ method: "POST",
175
+ path: "/message-streams/:id/archive",
176
+ auth: "server",
177
+ handler: (ctx) => {
178
+ const stream = streamOf(ctx);
179
+ if (UNARCHIVABLE.has(stream.ID))
180
+ throw apiError(1229);
181
+ if (stream.ArchivedAt !== null)
182
+ throw new Unsupported("archive of an archived stream: not captured");
183
+ const now = ctx.clock.now();
184
+ stream.ArchivedAt = now;
185
+ stream.ExpectedPurgeDate = new Date(now.getTime() + PURGE_DELAY_MS);
186
+ schedulePurge(ctx, stream, stream.ExpectedPurgeDate);
187
+ // ID string and ServerID integer (docs/04 §4.2 disagreements; dotnet reads int ServerID).
188
+ return {
189
+ ID: stream.ID,
190
+ ServerID: stream.ServerID,
191
+ ExpectedPurgeDate: formatTimestamp(stream.ExpectedPurgeDate),
192
+ };
193
+ },
194
+ });
195
+ defineRoute({
196
+ method: "POST",
197
+ path: "/message-streams/:id/unarchive",
198
+ auth: "server",
199
+ handler: (ctx) => {
200
+ streamsOf(ctx);
201
+ const key = streamKey(ctx.auth.server.ID, ctx.params.id);
202
+ if (ctx.store.state.purgedStreams.has(key))
203
+ throw apiError(1232);
204
+ const stream = streamOf(ctx);
205
+ if (stream.ArchivedAt === null)
206
+ throw new Unsupported("unarchive of an active stream: not captured");
207
+ stream.ArchivedAt = null;
208
+ stream.ExpectedPurgeDate = null;
209
+ return streamJson(stream);
210
+ },
211
+ });
@@ -0,0 +1,28 @@
1
+ import type { Runtime } from "../../runtime.ts";
2
+ import type { State } from "../../state/store.ts";
3
+ import type { MessageStream } from "../../state/types.ts";
4
+ /** Archived streams are deleted 45 days after archiving (refs/api_message-streams-api.md:48). */
5
+ export declare const PURGE_DELAY_MS: number;
6
+ /**
7
+ * Deletes an archived stream with its suppressions and bounces at its purge date, unless it was
8
+ * unarchived before. "deleted (alongside associated data)":
9
+ * sdk/postmark-dotnet/src/Postmark/PostmarkClient.cs:1242. Messages of the stream stay (INFERRED).
10
+ */
11
+ export declare function schedulePurge(runtime: Runtime, stream: MessageStream, due: Date): void;
12
+ /** A stream of the server, or 422 / 1226. IDs match with case kept (INFERRED). */
13
+ export declare function liveStream(state: State, serverId: number, id: string): MessageStream;
14
+ /** refs/api_message-streams-api.md:40-50, in doc field order. */
15
+ export declare const streamJson: (s: MessageStream) => {
16
+ ID: string;
17
+ ServerID: number;
18
+ Name: string;
19
+ Description: string | null;
20
+ MessageStreamType: import("../../state/types.ts").MessageStreamType;
21
+ CreatedAt: string;
22
+ UpdatedAt: string | null;
23
+ ArchivedAt: string | null;
24
+ ExpectedPurgeDate: string | null;
25
+ SubscriptionManagementConfiguration: {
26
+ UnsubscribeHandlingType: import("../../state/types.ts").UnsubscribeHandlingType;
27
+ };
28
+ };
@@ -0,0 +1,50 @@
1
+ import { apiError } from "../../errors.js";
2
+ import { streamKey } from "../../state/store.js";
3
+ import { formatTimestamp } from "../../time.js";
4
+ /** Archived streams are deleted 45 days after archiving (refs/api_message-streams-api.md:48). */
5
+ export const PURGE_DELAY_MS = 45 * 24 * 60 * 60 * 1000;
6
+ /**
7
+ * Deletes an archived stream with its suppressions and bounces at its purge date, unless it was
8
+ * unarchived before. "deleted (alongside associated data)":
9
+ * sdk/postmark-dotnet/src/Postmark/PostmarkClient.cs:1242. Messages of the stream stay (INFERRED).
10
+ */
11
+ export function schedulePurge(runtime, stream, due) {
12
+ const key = streamKey(stream.ServerID, stream.ID);
13
+ runtime.clock.schedule(due.getTime() - runtime.clock.now().getTime(), () => {
14
+ const state = runtime.store.state;
15
+ if (state.streams.get(key)?.ExpectedPurgeDate?.getTime() !== due.getTime())
16
+ return;
17
+ state.streams.delete(key);
18
+ state.purgedStreams.add(key);
19
+ const inStream = (row) => row.ServerID === stream.ServerID && row.MessageStream === stream.ID;
20
+ for (const [k, row] of state.suppressions)
21
+ if (inStream(row))
22
+ state.suppressions.delete(k);
23
+ for (const [id, bounce] of state.bounces)
24
+ if (inStream(bounce))
25
+ state.bounces.delete(id);
26
+ });
27
+ }
28
+ /** A stream of the server, or 422 / 1226. IDs match with case kept (INFERRED). */
29
+ export function liveStream(state, serverId, id) {
30
+ const stream = state.streams.get(streamKey(serverId, id));
31
+ if (stream === undefined)
32
+ throw apiError(1226, { family: "streams" });
33
+ return stream;
34
+ }
35
+ const stamp = (d) => (d === null ? null : formatTimestamp(d));
36
+ /** refs/api_message-streams-api.md:40-50, in doc field order. */
37
+ export const streamJson = (s) => ({
38
+ ID: s.ID,
39
+ ServerID: s.ServerID,
40
+ Name: s.Name,
41
+ Description: s.Description,
42
+ MessageStreamType: s.MessageStreamType,
43
+ CreatedAt: formatTimestamp(s.CreatedAt),
44
+ UpdatedAt: stamp(s.UpdatedAt),
45
+ ArchivedAt: stamp(s.ArchivedAt),
46
+ ExpectedPurgeDate: stamp(s.ExpectedPurgeDate),
47
+ SubscriptionManagementConfiguration: {
48
+ UnsubscribeHandlingType: s.SubscriptionManagementConfiguration.UnsubscribeHandlingType,
49
+ },
50
+ });
@@ -0,0 +1,149 @@
1
+ import type { ClickEvent, InboundMessage, OpenEvent, OutboundMessage } from "../../state/types.ts";
2
+ /** Search item (refs/api_messages-api.md:62-89). `Attachments` are names (docs/06 §1.5, Q3). */
3
+ export declare const outboundJson: (m: OutboundMessage) => {
4
+ Tag: string;
5
+ MessageID: string;
6
+ MessageStream: string;
7
+ To: import("../../state/types.ts").Address[];
8
+ Cc: import("../../state/types.ts").Address[];
9
+ Bcc: import("../../state/types.ts").Address[];
10
+ Recipients: string[];
11
+ ReceivedAt: string;
12
+ From: string;
13
+ Subject: string;
14
+ Attachments: string[];
15
+ Status: import("../../state/types.ts").OutboundStatus;
16
+ TrackOpens: boolean;
17
+ TrackLinks: import("../../state/types.ts").TrackLinks;
18
+ Metadata: Record<string, string>;
19
+ Sandboxed: boolean;
20
+ };
21
+ /** Details (refs/api_messages-api.md:147-238). */
22
+ export declare const outboundDetailsJson: (m: OutboundMessage) => {
23
+ Tag: string;
24
+ MessageID: string;
25
+ MessageStream: string;
26
+ To: import("../../state/types.ts").Address[];
27
+ Cc: import("../../state/types.ts").Address[];
28
+ Bcc: import("../../state/types.ts").Address[];
29
+ Recipients: string[];
30
+ ReceivedAt: string;
31
+ From: string;
32
+ Subject: string;
33
+ Attachments: string[];
34
+ Status: import("../../state/types.ts").OutboundStatus;
35
+ TrackOpens: boolean;
36
+ TrackLinks: import("../../state/types.ts").TrackLinks;
37
+ Metadata: Record<string, string>;
38
+ Sandboxed: boolean;
39
+ TextBody: string;
40
+ HtmlBody: string;
41
+ Body: string;
42
+ MessageEvents: {
43
+ Recipient: string;
44
+ Type: import("../../state/types.ts").MessageEventType;
45
+ ReceivedAt: string;
46
+ Details: Record<string, string>;
47
+ }[];
48
+ };
49
+ /**
50
+ * Search item (refs/api_messages-api.md:335-372). postmark.js also reads `Bcc`, `BccFull` and
51
+ * `MessageStream` (sdk/postmark.js/src/client/models/messages/InboundMessage.ts:8-28).
52
+ */
53
+ export declare const inboundJson: (m: InboundMessage) => {
54
+ From: string;
55
+ FromName: string;
56
+ FromFull: {
57
+ Email: string;
58
+ Name: string;
59
+ };
60
+ To: string;
61
+ ToFull: import("../../state/types.ts").InboundAddress[];
62
+ CcFull: import("../../state/types.ts").InboundAddress[];
63
+ Cc: string;
64
+ Bcc: string;
65
+ BccFull: import("../../state/types.ts").InboundAddress[];
66
+ ReplyTo: string;
67
+ OriginalRecipient: string;
68
+ Subject: string;
69
+ Date: string;
70
+ MailboxHash: string;
71
+ Tag: string;
72
+ Attachments: {
73
+ Name: string;
74
+ ContentID: string;
75
+ ContentType: string;
76
+ ContentLength: number;
77
+ }[];
78
+ MessageID: string;
79
+ MessageStream: string;
80
+ Status: import("../../state/types.ts").InboundStatus;
81
+ };
82
+ /** Details (refs/api_messages-api.md:431-518). */
83
+ export declare const inboundDetailsJson: (m: InboundMessage) => {
84
+ From: string;
85
+ FromName: string;
86
+ FromFull: {
87
+ Email: string;
88
+ Name: string;
89
+ };
90
+ To: string;
91
+ ToFull: import("../../state/types.ts").InboundAddress[];
92
+ CcFull: import("../../state/types.ts").InboundAddress[];
93
+ Cc: string;
94
+ Bcc: string;
95
+ BccFull: import("../../state/types.ts").InboundAddress[];
96
+ ReplyTo: string;
97
+ OriginalRecipient: string;
98
+ Subject: string;
99
+ Date: string;
100
+ MailboxHash: string;
101
+ Tag: string;
102
+ Attachments: {
103
+ Name: string;
104
+ ContentID: string;
105
+ ContentType: string;
106
+ ContentLength: number;
107
+ }[];
108
+ MessageStream: string;
109
+ TextBody: string;
110
+ HtmlBody: string;
111
+ StrippedTextReply: string;
112
+ Headers: import("../../state/types.ts").Header[];
113
+ MessageID: string;
114
+ BlockedReason: string;
115
+ Status: import("../../state/types.ts").InboundStatus;
116
+ };
117
+ /**
118
+ * An open (refs/api_messages-api.md:671-699). The single-message example has no `RecordType`
119
+ * (:769-797). `FirstOpen` and `ReadSeconds` are not in the Messages API docs; they stay out.
120
+ */
121
+ export declare const openJson: (o: OpenEvent, single: boolean) => {
122
+ Client?: import("../../state/types.ts").ClientInfo;
123
+ OS?: import("../../state/types.ts").ClientInfo;
124
+ Platform?: "Desktop" | "Mobile" | "Unknown" | "WebMail";
125
+ UserAgent: string;
126
+ Geo?: import("../../state/types.ts").Geo;
127
+ MessageID: string;
128
+ MessageStream: string;
129
+ ReceivedAt: string;
130
+ Tag: string;
131
+ Recipient: string;
132
+ RecordType?: string;
133
+ };
134
+ /** A click (refs/api_messages-api.md:879-909, :979-1009). */
135
+ export declare const clickJson: (c: ClickEvent, single: boolean) => {
136
+ Client?: import("../../state/types.ts").ClientInfo;
137
+ OS?: import("../../state/types.ts").ClientInfo;
138
+ Platform?: "Desktop" | "Mobile" | "Unknown" | "WebMail";
139
+ UserAgent: string;
140
+ Geo?: import("../../state/types.ts").Geo;
141
+ MessageID: string;
142
+ MessageStream: string;
143
+ ReceivedAt: string;
144
+ Tag: string;
145
+ Recipient: string;
146
+ RecordType?: string;
147
+ ClickLocation: "HTML" | "Text";
148
+ OriginalLink: string;
149
+ };
@@ -0,0 +1,114 @@
1
+ import { recipientsOf } from "../../recipients/transitions.js";
2
+ import { formatTimestamp } from "../../time.js";
3
+ // Wire shapes of the Messages API, in doc field order. A null string field is sent as "", as the
4
+ // inbound examples show for `Tag` and `MailboxHash` (INFERRED for outbound).
5
+ /** Search item (refs/api_messages-api.md:62-89). `Attachments` are names (docs/06 §1.5, Q3). */
6
+ export const outboundJson = (m) => ({
7
+ Tag: m.Tag ?? "",
8
+ MessageID: m.MessageID,
9
+ MessageStream: m.MessageStream,
10
+ To: m.To,
11
+ Cc: m.Cc,
12
+ Bcc: m.Bcc,
13
+ // Cc and Bcc addresses in `Recipients` are INFERRED; the example has To only.
14
+ Recipients: recipientsOf(m),
15
+ ReceivedAt: formatTimestamp(m.ReceivedAt),
16
+ From: m.From,
17
+ Subject: m.Subject ?? "",
18
+ Attachments: m.Attachments.map((a) => a.Name),
19
+ Status: m.Status,
20
+ TrackOpens: m.TrackOpens,
21
+ TrackLinks: m.TrackLinks,
22
+ Metadata: m.Metadata,
23
+ Sandboxed: m.Sandboxed,
24
+ });
25
+ const messageEventJson = (e) => ({
26
+ Recipient: e.Recipient,
27
+ Type: e.Type,
28
+ ReceivedAt: formatTimestamp(e.ReceivedAt),
29
+ Details: e.Details,
30
+ });
31
+ /** Details (refs/api_messages-api.md:147-238). */
32
+ export const outboundDetailsJson = (m) => ({
33
+ TextBody: m.TextBody ?? "",
34
+ HtmlBody: m.HtmlBody ?? "",
35
+ Body: m.rawSource,
36
+ ...outboundJson(m),
37
+ MessageEvents: m.MessageEvents.map(messageEventJson),
38
+ });
39
+ /**
40
+ * Search item (refs/api_messages-api.md:335-372). postmark.js also reads `Bcc`, `BccFull` and
41
+ * `MessageStream` (sdk/postmark.js/src/client/models/messages/InboundMessage.ts:8-28).
42
+ */
43
+ export const inboundJson = (m) => ({
44
+ From: m.From,
45
+ FromName: m.FromName,
46
+ FromFull: { Email: m.FromFull.Email, Name: m.FromFull.Name },
47
+ To: m.To,
48
+ ToFull: m.ToFull,
49
+ CcFull: m.CcFull,
50
+ Cc: m.Cc,
51
+ Bcc: m.Bcc,
52
+ BccFull: m.BccFull,
53
+ ReplyTo: m.ReplyTo,
54
+ OriginalRecipient: m.OriginalRecipient,
55
+ Subject: m.Subject,
56
+ Date: m.Date,
57
+ MailboxHash: m.MailboxHash,
58
+ Tag: m.Tag,
59
+ Attachments: m.Attachments.map((a) => ({
60
+ Name: a.Name,
61
+ ContentID: a.ContentID,
62
+ ContentType: a.ContentType,
63
+ ContentLength: a.ContentLength,
64
+ })),
65
+ MessageID: m.MessageID,
66
+ MessageStream: m.MessageStream,
67
+ Status: m.Status,
68
+ });
69
+ /** Details (refs/api_messages-api.md:431-518). */
70
+ export const inboundDetailsJson = (m) => {
71
+ const { MessageID, Status, ...rest } = inboundJson(m);
72
+ return {
73
+ ...rest,
74
+ TextBody: m.TextBody,
75
+ HtmlBody: m.HtmlBody,
76
+ StrippedTextReply: m.StrippedTextReply,
77
+ Headers: m.Headers,
78
+ MessageID,
79
+ BlockedReason: m.BlockedReason ?? "",
80
+ Status,
81
+ };
82
+ };
83
+ /** Client, OS, Platform and Geo are left out when unknown (refs/api_messages-api.md:707). */
84
+ const agentJson = (e) => ({
85
+ ...(e.Client && { Client: e.Client }),
86
+ ...(e.OS && { OS: e.OS }),
87
+ ...(e.Platform && { Platform: e.Platform }),
88
+ UserAgent: e.UserAgent,
89
+ ...(e.Geo && { Geo: e.Geo }),
90
+ });
91
+ const tail = (e) => ({
92
+ MessageID: e.MessageID,
93
+ MessageStream: e.MessageStream,
94
+ ReceivedAt: formatTimestamp(e.ReceivedAt),
95
+ Tag: e.Tag ?? "",
96
+ Recipient: e.Recipient,
97
+ });
98
+ /**
99
+ * An open (refs/api_messages-api.md:671-699). The single-message example has no `RecordType`
100
+ * (:769-797). `FirstOpen` and `ReadSeconds` are not in the Messages API docs; they stay out.
101
+ */
102
+ export const openJson = (o, single) => ({
103
+ ...(!single && { RecordType: "Open" }),
104
+ ...agentJson(o),
105
+ ...tail(o),
106
+ });
107
+ /** A click (refs/api_messages-api.md:879-909, :979-1009). */
108
+ export const clickJson = (c, single) => ({
109
+ ...(!single && { RecordType: "Click" }),
110
+ ClickLocation: c.ClickLocation,
111
+ ...agentJson(c),
112
+ OriginalLink: c.OriginalLink,
113
+ ...tail(c),
114
+ });
@@ -0,0 +1,19 @@
1
+ import { type Query } from "../../http/normalize.ts";
2
+ /** Messages, opens and clicks leave the API after the default retention (docs/06 §1.1). */
3
+ export declare const RETENTION_MS: number;
4
+ export declare const retained: (at: Date, now: Date) => boolean;
5
+ /**
6
+ * `count` 1–500 and `offset` ≥ 0 are required; `count + offset` ≤ 10 000
7
+ * (refs/api_messages-api.md:34-35, :630-631). The clicks and single-message pages do not state the
8
+ * 10 000 cap; the mock applies it there too (INFERRED, docs/06 §5.4). A page past the cap is
9
+ * rejected, never clamped.
10
+ */
11
+ export declare function paging(query: Query): {
12
+ count: number;
13
+ offset: number;
14
+ };
15
+ /** `fromdate`/`todate`, inclusive, Eastern time; a date-only `todate` covers its whole day. */
16
+ export declare function dateRange(query: Query): (at: Date) => boolean;
17
+ /** A `status` value from `allowed`, matched without case. */
18
+ export declare function status<S extends string>(query: Query, allowed: Record<string, readonly S[]>): readonly S[] | undefined;
19
+ export declare const sameText: (a: string | null | undefined, b: string) => boolean;
@@ -0,0 +1,64 @@
1
+ import { apiError } from "../../errors.js";
2
+ import { parseQueryDate } from "../../http/normalize.js";
3
+ import { formatEasternDate } from "../../time.js";
4
+ // Paging and filters shared by the Messages API reads (docs/06 §1.1, §1.7).
5
+ // ErrorCode 700 is DOC (refs/api_overview.md:112); its message texts are INFERRED.
6
+ /** Messages, opens and clicks leave the API after the default retention (docs/06 §1.1). */
7
+ export const RETENTION_MS = 45 * 24 * 60 * 60 * 1000;
8
+ export const retained = (at, now) => now.getTime() - at.getTime() <= RETENTION_MS;
9
+ const invalid = (message) => apiError(700, { message });
10
+ const integer = (query, name) => {
11
+ const raw = query.get(name);
12
+ if (raw === undefined)
13
+ return undefined;
14
+ return /^-?\d+$/.test(raw) ? Number(raw) : Number.NaN;
15
+ };
16
+ /**
17
+ * `count` 1–500 and `offset` ≥ 0 are required; `count + offset` ≤ 10 000
18
+ * (refs/api_messages-api.md:34-35, :630-631). The clicks and single-message pages do not state the
19
+ * 10 000 cap; the mock applies it there too (INFERRED, docs/06 §5.4). A page past the cap is
20
+ * rejected, never clamped.
21
+ */
22
+ export function paging(query) {
23
+ const count = integer(query, "count");
24
+ const offset = integer(query, "offset");
25
+ if (count === undefined || !(count >= 1 && count <= 500)) {
26
+ throw invalid("The 'count' parameter must be an integer between 1 and 500.");
27
+ }
28
+ if (offset === undefined || !(offset >= 0)) {
29
+ throw invalid("The 'offset' parameter must be an integer of 0 or more.");
30
+ }
31
+ if (count + offset > 10000)
32
+ throw invalid("Count + Offset cannot exceed 10,000 messages.");
33
+ return { count, offset };
34
+ }
35
+ /** `fromdate`/`todate`, inclusive, Eastern time; a date-only `todate` covers its whole day. */
36
+ export function dateRange(query) {
37
+ const bound = (name) => {
38
+ const raw = query.get(name);
39
+ if (raw === undefined)
40
+ return undefined;
41
+ const parsed = parseQueryDate(raw);
42
+ if (parsed === undefined)
43
+ throw invalid(`The '${name}' parameter is not a valid date.`);
44
+ return parsed;
45
+ };
46
+ const from = bound("fromdate");
47
+ const to = bound("todate");
48
+ return (at) => (from === undefined || at.getTime() >= from.instant.getTime()) &&
49
+ (to === undefined ||
50
+ (to.dateOnly
51
+ ? formatEasternDate(at) <= formatEasternDate(to.instant)
52
+ : at.getTime() <= to.instant.getTime()));
53
+ }
54
+ /** A `status` value from `allowed`, matched without case. */
55
+ export function status(query, allowed) {
56
+ const raw = query.get("status");
57
+ if (raw === undefined)
58
+ return undefined;
59
+ const statuses = allowed[raw.toLowerCase()];
60
+ if (statuses === undefined)
61
+ throw invalid(`The 'status' parameter '${raw}' is not valid.`);
62
+ return statuses;
63
+ }
64
+ export const sameText = (a, b) => a != null && a.toLowerCase() === b.toLowerCase();
@@ -0,0 +1 @@
1
+ export {};