@mxraven/mail 0.1.0

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.
@@ -0,0 +1,409 @@
1
+ import { Buffer } from "node:buffer";
2
+ import { createHash, createHmac, timingSafeEqual } from "node:crypto";
3
+ //#region src/webhook/payload.ts
4
+ /**
5
+ * mxRaven webhook payload shapes.
6
+ */
7
+ /** Identifies the shape of a decoded webhook payload. */
8
+ const eventType = {
9
+ /** A `DELIVER_WEBHOOK` delivery carrying a complete inbound message. */
10
+ inboundEmail: "inbound_email",
11
+ /** A `NOTIFY_WEBHOOK` delivery carrying the status of an object-storage write. */
12
+ storageStatus: "s3_egress_status",
13
+ /**
14
+ * A `NOTIFY_WEBHOOK` delivery carrying the status of an SMTP delivery.
15
+ *
16
+ * The SMTP producer does not send an `event_type` field, so this value is
17
+ * assigned by {@link decode}.
18
+ */
19
+ deliveryStatus: "delivery_status"
20
+ };
21
+ /** The lifecycle state reported by a delivery-status webhook. */
22
+ const statusOutcome = {
23
+ /** A delivery attempt started. */
24
+ attempted: "attempted",
25
+ /** The destination accepted the message. */
26
+ delivered: "delivered",
27
+ /** The destination temporarily rejected the message. */
28
+ deferred: "deferred",
29
+ /** Delivery failed permanently. */
30
+ failed: "failed",
31
+ /** The delivery deadline passed before success. */
32
+ expired: "expired",
33
+ /** Delivery was skipped by a suppression rule. It is reported for SMTP deliveries only. */
34
+ suppressed: "suppressed"
35
+ };
36
+ /**
37
+ * The final routing action recorded for an inbound message.
38
+ *
39
+ * Values are the mxRaven worker enum names.
40
+ */
41
+ const terminalAction = {
42
+ /** Deliver through a dedicated IP pool. */
43
+ deliverDedicated: "TERMINAL_ACTION_TYPE_DELIVER_DEDICATED",
44
+ /** Relay through the configured smarthost. */
45
+ smarthostRelay: "TERMINAL_ACTION_TYPE_SMARTHOST_RELAY",
46
+ /** Deliver to a webhook endpoint. */
47
+ deliverWebhook: "TERMINAL_ACTION_TYPE_DELIVER_WEBHOOK",
48
+ /** Forward the message to an SMTP destination. */
49
+ smtpForward: "TERMINAL_ACTION_TYPE_SMTP_FORWARD",
50
+ /** Relay the message to another MX. */
51
+ relay: "TERMINAL_ACTION_TYPE_RELAY",
52
+ /** Store the message in object storage. */
53
+ s3Store: "TERMINAL_ACTION_TYPE_S3_STORE",
54
+ /** Send an automatic reply. */
55
+ autoReply: "TERMINAL_ACTION_TYPE_AUTO_REPLY",
56
+ /** Accept and discard the message. */
57
+ drop: "TERMINAL_ACTION_TYPE_DROP",
58
+ /** Reject the message. */
59
+ reject: "TERMINAL_ACTION_TYPE_REJECT",
60
+ /** Deliver the message normally. */
61
+ deliver: "TERMINAL_ACTION_TYPE_DELIVER",
62
+ /** Handle a DSN at an SRS return address. */
63
+ srsReturn: "TERMINAL_ACTION_TYPE_SRS_RETURN",
64
+ /** Generate a local DSN. */
65
+ localDsn: "TERMINAL_ACTION_TYPE_LOCAL_DSN"
66
+ };
67
+ //#endregion
68
+ //#region src/webhook/decode.ts
69
+ /**
70
+ * mxRaven webhook payload decoding.
71
+ */
72
+ /**
73
+ * Decodes a webhook payload.
74
+ *
75
+ * Dispatching is based on the payload's `event_type` field. SMTP delivery
76
+ * statuses do not carry an `event_type`, so a body with a `status` field and no
77
+ * recognized event type is decoded as a delivery status.
78
+ *
79
+ * @param body - The raw JSON payload bytes, or a decoded string.
80
+ * @returns The decoded event.
81
+ * @throws `Error` When the body is not valid JSON or the payload is
82
+ * unrecognized.
83
+ *
84
+ * @public
85
+ */
86
+ function decode(body) {
87
+ const text = typeof body === "string" ? body : new TextDecoder().decode(body);
88
+ let parsed;
89
+ try {
90
+ parsed = JSON.parse(text);
91
+ } catch (error) {
92
+ throw new Error("webhook: decode payload", { cause: error });
93
+ }
94
+ if (parsed === null || typeof parsed !== "object") throw new Error("webhook: unrecognized payload");
95
+ const probe = parsed;
96
+ switch (probe.event_type) {
97
+ case eventType.inboundEmail: return {
98
+ type: eventType.inboundEmail,
99
+ inboundEmail: parsed
100
+ };
101
+ case eventType.storageStatus: return {
102
+ type: eventType.storageStatus,
103
+ storageStatus: parsed
104
+ };
105
+ default:
106
+ if (probe.status !== void 0 && probe.status !== null) return {
107
+ type: eventType.deliveryStatus,
108
+ deliveryStatus: parsed
109
+ };
110
+ throw new Error(`webhook: unrecognized payload: event_type ${JSON.stringify(probe.event_type)}`);
111
+ }
112
+ }
113
+ //#endregion
114
+ //#region src/webhook/errors.ts
115
+ /**
116
+ * A webhook signature did not match the request.
117
+ *
118
+ * @public
119
+ */
120
+ var InvalidSignatureError = class extends Error {
121
+ /**
122
+ * @param message - A human-readable description.
123
+ */
124
+ constructor(message = "webhook: invalid signature") {
125
+ super(message);
126
+ this.name = "InvalidSignatureError";
127
+ }
128
+ };
129
+ //#endregion
130
+ //#region src/webhook/body.ts
131
+ /**
132
+ * Bounded body reads shared by the webhook verifier and raw-email download.
133
+ *
134
+ * @internal
135
+ */
136
+ /**
137
+ * Reads a web stream, rejecting when it exceeds a byte limit.
138
+ *
139
+ * @param stream - The stream to read, or `null` for an empty body.
140
+ * @param limit - The maximum number of bytes to read.
141
+ * @param signal - Cancels the read.
142
+ * @param label - A label used in error messages.
143
+ * @returns The collected bytes.
144
+ * @throws `Error` When the stream exceeds the limit or the signal aborts.
145
+ */
146
+ async function readBoundedBody(stream, limit, signal, label) {
147
+ if (stream === null) return /* @__PURE__ */ new Uint8Array();
148
+ if (signal?.aborted === true) throw abortError(signal.reason);
149
+ const reader = stream.getReader();
150
+ const chunks = [];
151
+ let total = 0;
152
+ let aborted = false;
153
+ let abortReason = void 0;
154
+ const onAbort = () => {
155
+ aborted = true;
156
+ abortReason = signal?.reason;
157
+ reader.cancel(signal?.reason).catch(() => void 0);
158
+ };
159
+ signal?.addEventListener("abort", onAbort, { once: true });
160
+ try {
161
+ for (;;) {
162
+ const { done, value } = await reader.read();
163
+ if (done) break;
164
+ if (value !== void 0) {
165
+ total += value.byteLength;
166
+ if (total > limit) {
167
+ reader.cancel().catch(() => void 0);
168
+ throw new Error(`webhook: ${label} exceeds ${limit} bytes`);
169
+ }
170
+ chunks.push(value);
171
+ }
172
+ }
173
+ } finally {
174
+ signal?.removeEventListener("abort", onAbort);
175
+ reader.releaseLock();
176
+ }
177
+ if (aborted) throw abortError(abortReason);
178
+ const result = new Uint8Array(total);
179
+ let offset = 0;
180
+ for (const chunk of chunks) {
181
+ result.set(chunk, offset);
182
+ offset += chunk.byteLength;
183
+ }
184
+ return result;
185
+ }
186
+ /** Builds the error used when an aborted signal is observed. */
187
+ function abortError(reason) {
188
+ if (reason instanceof Error) return reason;
189
+ return new Error("webhook: operation aborted", { cause: reason });
190
+ }
191
+ //#endregion
192
+ //#region src/webhook/raw.ts
193
+ /**
194
+ * Downloads the raw message referenced by a `DELIVER_WEBHOOK` payload.
195
+ */
196
+ /** Bounds a download when the payload does not declare a size. */
197
+ const MAX_RAW_EMAIL_BYTES = 64 << 20;
198
+ /**
199
+ * Downloads the raw RFC 822 message referenced by a `DELIVER_WEBHOOK` payload.
200
+ *
201
+ * The payload's short-lived bearer token is sent in the `Authorization` header,
202
+ * and the downloaded bytes are verified against the declared size and SHA-256
203
+ * digest. Treat `raw_email.access_token` as a secret and do not log it.
204
+ *
205
+ * @param raw - The `raw_email` object from an inbound-email payload.
206
+ * @param options - An optional cancellation signal and `fetch` implementation.
207
+ * @returns The exact message bytes.
208
+ * @throws `Error` When the URL or token is empty, the response is unsuccessful,
209
+ * or the downloaded bytes fail the size or digest check.
210
+ *
211
+ * @example
212
+ * ```ts
213
+ * const bytes = await fetchRawEmail(event.inboundEmail.rawEmail, { signal });
214
+ * ```
215
+ *
216
+ * @public
217
+ */
218
+ async function fetchRawEmail(raw, options = {}) {
219
+ const url = raw.url.trim();
220
+ if (url === "") throw new Error("webhook: raw email URL is empty");
221
+ const token = raw.access_token.trim();
222
+ if (token === "") throw new Error("webhook: raw email access token is empty");
223
+ const fetcher = options.fetch ?? globalThis.fetch;
224
+ const tokenType = (raw.token_type ?? "").trim();
225
+ const response = await fetcher(url, {
226
+ method: "GET",
227
+ headers: { Authorization: `${tokenType === "" ? "Bearer" : tokenType} ${token}` },
228
+ signal: options.signal
229
+ });
230
+ if (!response.ok) {
231
+ const statusText = response.statusText === "" ? "" : ` ${response.statusText}`;
232
+ throw new Error(`webhook: fetch raw email: unexpected status ${response.status}${statusText}`);
233
+ }
234
+ const size = raw.size_bytes ?? 0;
235
+ const limit = size > 0 ? size + 1 : MAX_RAW_EMAIL_BYTES;
236
+ const body = await readBoundedBody(response.body, limit, options.signal, "raw email");
237
+ if (size > 0 && body.byteLength !== size) throw new Error(`webhook: raw email size = ${body.byteLength} bytes, want ${size}`);
238
+ const digest = (raw.sha256_hex ?? "").trim();
239
+ if (digest !== "") {
240
+ if (createHash("sha256").update(body).digest("hex").toLowerCase() !== digest.toLowerCase()) throw new Error("webhook: raw email SHA-256 mismatch");
241
+ }
242
+ return body;
243
+ }
244
+ //#endregion
245
+ //#region src/webhook/verifier.ts
246
+ /**
247
+ * mxRaven webhook signature verification.
248
+ */
249
+ /**
250
+ * The mxRaven signature headers.
251
+ *
252
+ * HTTP header names are case-insensitive; these constants carry the canonical
253
+ * spelling used by mxRaven.
254
+ */
255
+ const webhookHeaders = {
256
+ /** The delivery ID, which is the delivery task ID. */
257
+ webhookId: "X-MxRaven-Webhook-ID",
258
+ /** The Unix signing time in seconds. */
259
+ timestamp: "X-MxRaven-Timestamp",
260
+ /** The `sha256=<hex>` HMAC. */
261
+ signature: "X-MxRaven-Signature",
262
+ /** The signing key ID. */
263
+ signatureKid: "X-MxRaven-Signature-Kid"
264
+ };
265
+ const DEFAULT_TOLERANCE = 3e5;
266
+ const DEFAULT_MAX_BODY_BYTES = 1 << 20;
267
+ /**
268
+ * Verifies the signature of an mxRaven webhook request.
269
+ *
270
+ * A verifier is safe for concurrent use once constructed. Configure it with a
271
+ * signing secret or with per-key secrets for rotation.
272
+ *
273
+ * @example
274
+ * ```ts
275
+ * const secret = process.env.MXRAVEN_WEBHOOK_SECRET;
276
+ * if (secret === undefined || secret === "") {
277
+ * throw new Error("MXRAVEN_WEBHOOK_SECRET is required");
278
+ * }
279
+ *
280
+ * const verifier = new Verifier({ secret });
281
+ * const event = await verifier.verifyAndDecode(request);
282
+ * ```
283
+ *
284
+ * @public
285
+ */
286
+ var Verifier = class {
287
+ secret;
288
+ keys;
289
+ tolerance;
290
+ maxBodyBytes;
291
+ /**
292
+ * @param options - The signing secret and verification limits.
293
+ * @throws `Error` When no secret is provided or an option is invalid.
294
+ */
295
+ constructor(options = {}) {
296
+ const keys = /* @__PURE__ */ new Map();
297
+ for (const [kid, secret] of options.keys ?? []) {
298
+ if (kid.trim() === "") throw new Error("webhook: signing key ID must not be empty");
299
+ if (secret === "") throw new Error("webhook: signing secret must not be empty");
300
+ keys.set(kid, secret);
301
+ }
302
+ const tolerance = options.tolerance ?? DEFAULT_TOLERANCE;
303
+ if (tolerance < 0) throw new Error("webhook: tolerance must not be negative");
304
+ const maxBodyBytes = options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;
305
+ if (!Number.isInteger(maxBodyBytes) || maxBodyBytes <= 0) throw new Error("webhook: maximum body size must be positive");
306
+ const secret = options.secret;
307
+ if ((secret === void 0 || secret === "") && keys.size === 0) throw new Error("webhook: a signing secret is required");
308
+ if (secret !== void 0 && secret === "") throw new Error("webhook: signing secret must not be empty");
309
+ this.secret = secret;
310
+ this.keys = keys;
311
+ this.tolerance = tolerance;
312
+ this.maxBodyBytes = maxBodyBytes;
313
+ }
314
+ /**
315
+ * Verifies a request's signature.
316
+ *
317
+ * The body is read from a clone, so the caller can still read the original
318
+ * request body afterwards.
319
+ *
320
+ * @param request - The incoming request.
321
+ * @param options - An optional cancellation signal.
322
+ * @throws {@link InvalidSignatureError} When the signature does not match.
323
+ * @throws `Error` When required headers are missing, the timestamp is stale,
324
+ * or the body exceeds the configured limit.
325
+ */
326
+ async verify(request, options = {}) {
327
+ const body = await this.readBody(request, options.signal);
328
+ this.check(request, body);
329
+ }
330
+ /**
331
+ * Verifies a request and decodes its payload.
332
+ *
333
+ * @param request - The incoming request.
334
+ * @param options - An optional cancellation signal.
335
+ * @returns The decoded event.
336
+ * @throws {@link InvalidSignatureError} When the signature does not match.
337
+ * @throws `Error` When verification or decoding fails.
338
+ */
339
+ async verifyAndDecode(request, options = {}) {
340
+ const body = await this.readBody(request, options.signal);
341
+ this.check(request, body);
342
+ return decode(body);
343
+ }
344
+ async readBody(request, signal) {
345
+ return readBoundedBody(request.clone().body, this.maxBodyBytes, signal, "request body");
346
+ }
347
+ check(request, body) {
348
+ const webhookId = request.headers.get(webhookHeaders.webhookId)?.trim() ?? "";
349
+ if (webhookId === "") throw new Error("webhook: missing webhook ID header");
350
+ const timestamp = request.headers.get(webhookHeaders.timestamp)?.trim() ?? "";
351
+ if (timestamp === "") throw new Error("webhook: missing timestamp header");
352
+ const signature = request.headers.get(webhookHeaders.signature)?.trim() ?? "";
353
+ if (signature === "") throw new Error("webhook: missing signature header");
354
+ if (this.tolerance > 0) {
355
+ if (!/^\d+$/.test(timestamp)) throw new Error(`webhook: invalid timestamp ${JSON.stringify(timestamp)}`);
356
+ if (Math.abs(Date.now() - Number.parseInt(timestamp, 10) * 1e3) > this.tolerance) throw new Error("webhook: timestamp is outside the accepted clock skew");
357
+ }
358
+ const secret = this.secretFor(request.headers.get(webhookHeaders.signatureKid));
359
+ if (!signature.startsWith("sha256=")) throw new Error("webhook: unsupported signature algorithm");
360
+ const provided = decodeHex(signature.slice(7));
361
+ if (provided === void 0) throw new Error("webhook: malformed signature");
362
+ const expected = signRequest(secret, {
363
+ method: request.method,
364
+ url: new URL(request.url),
365
+ timestamp,
366
+ webhookId,
367
+ body
368
+ });
369
+ if (provided.length !== expected.length || !timingSafeEqual(provided, expected)) throw new InvalidSignatureError();
370
+ }
371
+ secretFor(kid) {
372
+ const value = (kid ?? "").trim();
373
+ if (this.keys.size > 0) {
374
+ if (value === "") throw new Error("webhook: missing signature key ID");
375
+ const secret = this.keys.get(value);
376
+ if (secret === void 0) throw new Error(`webhook: unknown signature key ID ${JSON.stringify(value)}`);
377
+ return secret;
378
+ }
379
+ return this.secret ?? "";
380
+ }
381
+ };
382
+ /** Computes the expected HMAC over the canonical request string. */
383
+ function signRequest(secret, input) {
384
+ const bodyHash = createHash("sha256").update(input.body).digest("hex");
385
+ const canonical = [
386
+ input.timestamp,
387
+ input.webhookId,
388
+ input.method,
389
+ input.url.host.toLowerCase(),
390
+ canonicalTarget(input.url),
391
+ bodyHash
392
+ ].join("\n");
393
+ return createHmac("sha256", secret).update(canonical).digest();
394
+ }
395
+ /** Returns the escaped path plus the raw query, or `/`. */
396
+ function canonicalTarget(url) {
397
+ let value = url.pathname === "" ? "/" : url.pathname;
398
+ if (url.search !== "") value += url.search;
399
+ return value;
400
+ }
401
+ /** Decodes a lowercase or uppercase hex string, or returns `undefined`. */
402
+ function decodeHex(value) {
403
+ if (value.length === 0 || value.length % 2 !== 0 || !/^[0-9a-fA-F]+$/.test(value)) return;
404
+ return Buffer.from(value, "hex");
405
+ }
406
+ //#endregion
407
+ export { InvalidSignatureError, Verifier, decode, eventType, fetchRawEmail, statusOutcome, terminalAction, webhookHeaders };
408
+
409
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/webhook/payload.ts","../../src/webhook/decode.ts","../../src/webhook/errors.ts","../../src/webhook/body.ts","../../src/webhook/raw.ts","../../src/webhook/verifier.ts"],"sourcesContent":["/**\n * mxRaven webhook payload shapes.\n */\n\n/** Identifies the shape of a decoded webhook payload. */\nexport const eventType = {\n /** A `DELIVER_WEBHOOK` delivery carrying a complete inbound message. */\n inboundEmail: \"inbound_email\",\n /** A `NOTIFY_WEBHOOK` delivery carrying the status of an object-storage write. */\n storageStatus: \"s3_egress_status\",\n /**\n * A `NOTIFY_WEBHOOK` delivery carrying the status of an SMTP delivery.\n *\n * The SMTP producer does not send an `event_type` field, so this value is\n * assigned by {@link decode}.\n */\n deliveryStatus: \"delivery_status\",\n} as const;\n\n/** Identifies the shape of a decoded webhook payload. */\nexport type EventType = (typeof eventType)[keyof typeof eventType];\n\n/** The lifecycle state reported by a delivery-status webhook. */\nexport const statusOutcome = {\n /** A delivery attempt started. */\n attempted: \"attempted\",\n /** The destination accepted the message. */\n delivered: \"delivered\",\n /** The destination temporarily rejected the message. */\n deferred: \"deferred\",\n /** Delivery failed permanently. */\n failed: \"failed\",\n /** The delivery deadline passed before success. */\n expired: \"expired\",\n /** Delivery was skipped by a suppression rule. It is reported for SMTP deliveries only. */\n suppressed: \"suppressed\",\n} as const;\n\n/** The lifecycle state reported by a delivery-status webhook. */\nexport type StatusOutcome = (typeof statusOutcome)[keyof typeof statusOutcome];\n\n/**\n * The final routing action recorded for an inbound message.\n *\n * Values are the mxRaven worker enum names.\n */\nexport const terminalAction = {\n /** Deliver through a dedicated IP pool. */\n deliverDedicated: \"TERMINAL_ACTION_TYPE_DELIVER_DEDICATED\",\n /** Relay through the configured smarthost. */\n smarthostRelay: \"TERMINAL_ACTION_TYPE_SMARTHOST_RELAY\",\n /** Deliver to a webhook endpoint. */\n deliverWebhook: \"TERMINAL_ACTION_TYPE_DELIVER_WEBHOOK\",\n /** Forward the message to an SMTP destination. */\n smtpForward: \"TERMINAL_ACTION_TYPE_SMTP_FORWARD\",\n /** Relay the message to another MX. */\n relay: \"TERMINAL_ACTION_TYPE_RELAY\",\n /** Store the message in object storage. */\n s3Store: \"TERMINAL_ACTION_TYPE_S3_STORE\",\n /** Send an automatic reply. */\n autoReply: \"TERMINAL_ACTION_TYPE_AUTO_REPLY\",\n /** Accept and discard the message. */\n drop: \"TERMINAL_ACTION_TYPE_DROP\",\n /** Reject the message. */\n reject: \"TERMINAL_ACTION_TYPE_REJECT\",\n /** Deliver the message normally. */\n deliver: \"TERMINAL_ACTION_TYPE_DELIVER\",\n /** Handle a DSN at an SRS return address. */\n srsReturn: \"TERMINAL_ACTION_TYPE_SRS_RETURN\",\n /** Generate a local DSN. */\n localDsn: \"TERMINAL_ACTION_TYPE_LOCAL_DSN\",\n} as const;\n\n/** The final routing action recorded for an inbound message. */\nexport type TerminalAction = (typeof terminalAction)[keyof typeof terminalAction];\n\n/** A decoded webhook delivery. */\nexport type Event =\n | { readonly type: typeof eventType.inboundEmail; readonly inboundEmail: InboundEmail }\n | { readonly type: typeof eventType.deliveryStatus; readonly deliveryStatus: DeliveryStatus }\n | { readonly type: typeof eventType.storageStatus; readonly storageStatus: StorageStatus };\n\n/** The final routing outcome for an inbound message. */\nexport interface RoutingDecision {\n /** The final terminal action. Values are the mxRaven worker enum names. */\n readonly terminal_action: TerminalAction;\n /** The rule that selected the terminal action, when one matched. */\n readonly matched_rule_id?: string;\n /** Whether the listener default was used because no rule produced a terminal action. */\n readonly used_listener_default: boolean;\n}\n\n/** Spam and malware scan results. */\nexport interface Verdicts {\n /** The scanner action description. */\n readonly action: string;\n /** The spam score that was assigned. */\n readonly score: number;\n /** The threshold the message was compared against. */\n readonly required_score: number;\n /** Whether the message was classified as spam. */\n readonly is_spam: boolean;\n /** Whether malware was detected. */\n readonly has_malware: boolean;\n /** The detected malware signatures. */\n readonly malware_names: readonly string[];\n /** Whether scanning was skipped. */\n readonly is_skipped: boolean;\n /** The scanner error, when scanning failed. */\n readonly error: string;\n}\n\n/** The SMTP envelope of a message. */\nexport interface WebhookEnvelope {\n /** The envelope sender. It may be empty for a null reverse-path. */\n readonly mail_from: string;\n /** The envelope recipients. */\n readonly rcpt_to: readonly string[];\n}\n\n/** A summary of the parsed message headers. */\nexport interface MessageSummary {\n /** The decoded Subject header. */\n readonly subject?: string;\n /** The decoded From mailboxes. */\n readonly from?: readonly string[];\n /** The decoded To mailboxes. */\n readonly to?: readonly string[];\n /** The decoded Cc mailboxes. */\n readonly cc?: readonly string[];\n /** The Message-ID header. */\n readonly message_id?: string;\n /** The raw Date header. */\n readonly date?: string;\n}\n\n/** One message header occurrence. */\nexport interface HeaderField {\n /** The header field name. */\n readonly name: string;\n /** The header field value. */\n readonly value: string;\n}\n\n/**\n * Time-limited access to the raw RFC 822 message.\n *\n * Use {@link fetchRawEmail} to download the content.\n */\nexport interface RawEmail {\n /** The message download URL. */\n readonly url: string;\n /** The authorization scheme, normally `Bearer`. */\n readonly token_type?: string;\n /** The bearer token for the download URL. Treat it as a secret and do not log it. */\n readonly access_token: string;\n /** The Unix time, in seconds, when the token expires. */\n readonly expires_at_utc?: number;\n /** The raw message size. */\n readonly size_bytes?: number;\n /** The lowercase hex SHA-256 of the raw message bytes. */\n readonly sha256_hex?: string;\n /** The parsed message media type. */\n readonly content_type?: string;\n}\n\n/** The `DELIVER_WEBHOOK` payload. */\nexport interface InboundEmail {\n /** Always `inbound_email`. */\n readonly event_type: string;\n /** The delivery task identifier, stable across retries. */\n readonly task_id: string;\n /** The owning tenant. */\n readonly tenant_id: string;\n /** The listener that accepted the message. */\n readonly listener_id: string;\n /** The delivery attempt number, starting at 1. */\n readonly attempt: number;\n /** The Unix time, in seconds, when mxRaven accepted the message. */\n readonly accepted_at_utc: number;\n /** The Unix time, in seconds, when this delivery was built. */\n readonly occurred_at_utc: number;\n /** The final routing outcome. */\n readonly routing_decision: RoutingDecision;\n /** The spam and malware scan results, when scanning ran. */\n readonly verdicts?: Verdicts;\n /** The SMTP envelope. */\n readonly envelope: WebhookEnvelope;\n /** The parsed message header summary. */\n readonly message: MessageSummary;\n /** Every message header in the order received. */\n readonly headers: readonly HeaderField[];\n /** Time-limited access to the raw RFC 822 message. */\n readonly raw_email: RawEmail;\n}\n\n/** The SMTP `NOTIFY_WEBHOOK` payload. */\nexport interface DeliveryStatus {\n /** The delivery task identifier, stable across attempts. */\n readonly task_id: string;\n /** The owning tenant. */\n readonly tenant_id: string;\n /** The source listener. */\n readonly listener_id: string;\n /** The delivery outcome. */\n readonly status: StatusOutcome;\n /** The delivery attempt number, starting at 1. */\n readonly attempt: number;\n /** The Unix time, in seconds, when mxRaven accepted the message. */\n readonly accepted_at_utc: number;\n /** The Unix time, in seconds, when this status was built. */\n readonly occurred_at_utc: number;\n /** The egress source address, when known. */\n readonly source_ip?: string;\n /** The recipient domain. */\n readonly destination_domain?: string;\n /** The remote MTA hostname, when known. */\n readonly remote_host?: string;\n /** The remote SMTP reply code. */\n readonly smtp_code?: number;\n /** The RFC 3463 enhanced status code. */\n readonly enhanced_status_code?: string;\n /** The remote reply text, truncated to 512 bytes. */\n readonly remote_response?: string;\n /** The Unix time, in seconds, of the next attempt for a deferred status. */\n readonly next_retry_at_utc?: number;\n /** Links a generated DSN back to its original task. */\n readonly correlation_task_id?: string;\n}\n\n/** The object-storage `NOTIFY_WEBHOOK` payload. */\nexport interface StorageStatus {\n /** Always `s3_egress_status`. */\n readonly event_type: string;\n /** The storage task identifier. */\n readonly task_id?: string;\n /** The owning tenant. */\n readonly tenant_id?: string;\n /** The source listener. */\n readonly listener_id?: string;\n /** The storage outcome. */\n readonly status: StatusOutcome;\n /** The delivery attempt number, starting at 1. */\n readonly attempt: number;\n /** The Unix time, in seconds, when mxRaven accepted the message. */\n readonly accepted_at_utc?: number;\n /** The Unix time, in seconds, when this status was built. */\n readonly occurred_at_utc: number;\n /** The storage integration identifier. */\n readonly storage_ref?: string;\n /** The destination bucket. */\n readonly bucket_name?: string;\n /** The stored object key. */\n readonly object_key?: string;\n /** The storage endpoint host. */\n readonly endpoint_host?: string;\n /** The storage provider response status. */\n readonly status_code?: number;\n /** The storage provider error code. */\n readonly error_code?: string;\n /** The failure description, truncated to 512 bytes. */\n readonly message?: string;\n /** The Unix time, in seconds, of the next attempt for a deferred status. */\n readonly next_retry_at_utc?: number;\n}\n","/**\n * mxRaven webhook payload decoding.\n */\n\nimport {\n eventType,\n type DeliveryStatus,\n type Event,\n type InboundEmail,\n type StorageStatus,\n} from \"./payload.js\";\n\n/**\n * Decodes a webhook payload.\n *\n * Dispatching is based on the payload's `event_type` field. SMTP delivery\n * statuses do not carry an `event_type`, so a body with a `status` field and no\n * recognized event type is decoded as a delivery status.\n *\n * @param body - The raw JSON payload bytes, or a decoded string.\n * @returns The decoded event.\n * @throws `Error` When the body is not valid JSON or the payload is\n * unrecognized.\n *\n * @public\n */\nexport function decode(body: Uint8Array | string): Event {\n const text = typeof body === \"string\" ? body : new TextDecoder().decode(body);\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(text);\n } catch (error) {\n throw new Error(\"webhook: decode payload\", { cause: error });\n }\n if (parsed === null || typeof parsed !== \"object\") {\n throw new Error(\"webhook: unrecognized payload\");\n }\n\n const probe = parsed as { event_type?: unknown; status?: unknown };\n switch (probe.event_type) {\n case eventType.inboundEmail:\n return { type: eventType.inboundEmail, inboundEmail: parsed as InboundEmail };\n case eventType.storageStatus:\n return { type: eventType.storageStatus, storageStatus: parsed as StorageStatus };\n default:\n if (probe.status !== undefined && probe.status !== null) {\n return { type: eventType.deliveryStatus, deliveryStatus: parsed as DeliveryStatus };\n }\n throw new Error(\n `webhook: unrecognized payload: event_type ${JSON.stringify(probe.event_type)}`,\n );\n }\n}\n","/**\n * A webhook signature did not match the request.\n *\n * @public\n */\nexport class InvalidSignatureError extends Error {\n /**\n * @param message - A human-readable description.\n */\n constructor(message = \"webhook: invalid signature\") {\n super(message);\n this.name = \"InvalidSignatureError\";\n }\n}\n","/**\n * Bounded body reads shared by the webhook verifier and raw-email download.\n *\n * @internal\n */\n\n/**\n * Reads a web stream, rejecting when it exceeds a byte limit.\n *\n * @param stream - The stream to read, or `null` for an empty body.\n * @param limit - The maximum number of bytes to read.\n * @param signal - Cancels the read.\n * @param label - A label used in error messages.\n * @returns The collected bytes.\n * @throws `Error` When the stream exceeds the limit or the signal aborts.\n */\nexport async function readBoundedBody(\n stream: ReadableStream<Uint8Array> | null,\n limit: number,\n signal: AbortSignal | undefined,\n label: string,\n): Promise<Uint8Array> {\n if (stream === null) {\n return new Uint8Array();\n }\n if (signal?.aborted === true) {\n throw abortError(signal.reason);\n }\n\n const reader = stream.getReader();\n const chunks: Uint8Array[] = [];\n let total = 0;\n let aborted = false;\n let abortReason: unknown = undefined;\n\n const onAbort = (): void => {\n aborted = true;\n abortReason = signal?.reason;\n void reader.cancel(signal?.reason).catch(() => undefined);\n };\n signal?.addEventListener(\"abort\", onAbort, { once: true });\n\n try {\n for (;;) {\n const { done, value } = await reader.read();\n if (done) {\n break;\n }\n if (value !== undefined) {\n total += value.byteLength;\n if (total > limit) {\n void reader.cancel().catch(() => undefined);\n throw new Error(`webhook: ${label} exceeds ${limit} bytes`);\n }\n chunks.push(value);\n }\n }\n } finally {\n signal?.removeEventListener(\"abort\", onAbort);\n reader.releaseLock();\n }\n\n if (aborted) {\n throw abortError(abortReason);\n }\n\n const result = new Uint8Array(total);\n let offset = 0;\n for (const chunk of chunks) {\n result.set(chunk, offset);\n offset += chunk.byteLength;\n }\n return result;\n}\n\n/** Builds the error used when an aborted signal is observed. */\nfunction abortError(reason: unknown): Error {\n if (reason instanceof Error) {\n return reason;\n }\n return new Error(\"webhook: operation aborted\", { cause: reason });\n}\n","/**\n * Downloads the raw message referenced by a `DELIVER_WEBHOOK` payload.\n */\n\nimport { createHash } from \"node:crypto\";\n\nimport { readBoundedBody } from \"./body.js\";\nimport type { RawEmail } from \"./payload.js\";\n\n/** Bounds a download when the payload does not declare a size. */\nconst MAX_RAW_EMAIL_BYTES = 64 << 20;\n\n/** A minimal `fetch`-compatible function. @public */\nexport type FetchLike = (url: string, init?: RequestInit) => Promise<Response>;\n\n/** Options for {@link fetchRawEmail}. */\nexport interface FetchRawEmailOptions {\n /** Cancels the download. */\n readonly signal?: AbortSignal;\n /** The `fetch` implementation to use. Defaults to the global `fetch`. */\n readonly fetch?: FetchLike;\n}\n\n/**\n * Downloads the raw RFC 822 message referenced by a `DELIVER_WEBHOOK` payload.\n *\n * The payload's short-lived bearer token is sent in the `Authorization` header,\n * and the downloaded bytes are verified against the declared size and SHA-256\n * digest. Treat `raw_email.access_token` as a secret and do not log it.\n *\n * @param raw - The `raw_email` object from an inbound-email payload.\n * @param options - An optional cancellation signal and `fetch` implementation.\n * @returns The exact message bytes.\n * @throws `Error` When the URL or token is empty, the response is unsuccessful,\n * or the downloaded bytes fail the size or digest check.\n *\n * @example\n * ```ts\n * const bytes = await fetchRawEmail(event.inboundEmail.rawEmail, { signal });\n * ```\n *\n * @public\n */\nexport async function fetchRawEmail(\n raw: RawEmail,\n options: FetchRawEmailOptions = {},\n): Promise<Uint8Array> {\n const url = raw.url.trim();\n if (url === \"\") {\n throw new Error(\"webhook: raw email URL is empty\");\n }\n const token = raw.access_token.trim();\n if (token === \"\") {\n throw new Error(\"webhook: raw email access token is empty\");\n }\n\n const fetcher = options.fetch ?? globalThis.fetch;\n const tokenType = (raw.token_type ?? \"\").trim();\n const scheme = tokenType === \"\" ? \"Bearer\" : tokenType;\n\n const response = await fetcher(url, {\n method: \"GET\",\n headers: { Authorization: `${scheme} ${token}` },\n signal: options.signal,\n });\n if (!response.ok) {\n const statusText = response.statusText === \"\" ? \"\" : ` ${response.statusText}`;\n throw new Error(`webhook: fetch raw email: unexpected status ${response.status}${statusText}`);\n }\n\n const size = raw.size_bytes ?? 0;\n const limit = size > 0 ? size + 1 : MAX_RAW_EMAIL_BYTES;\n const body = await readBoundedBody(response.body, limit, options.signal, \"raw email\");\n\n if (size > 0 && body.byteLength !== size) {\n throw new Error(`webhook: raw email size = ${body.byteLength} bytes, want ${size}`);\n }\n\n const digest = (raw.sha256_hex ?? \"\").trim();\n if (digest !== \"\") {\n const actual = createHash(\"sha256\").update(body).digest(\"hex\");\n if (actual.toLowerCase() !== digest.toLowerCase()) {\n throw new Error(\"webhook: raw email SHA-256 mismatch\");\n }\n }\n return body;\n}\n","/**\n * mxRaven webhook signature verification.\n */\n\nimport { Buffer } from \"node:buffer\";\nimport { createHash, createHmac, timingSafeEqual } from \"node:crypto\";\n\nimport { readBoundedBody } from \"./body.js\";\nimport { decode } from \"./decode.js\";\nimport { InvalidSignatureError } from \"./errors.js\";\nimport type { Event } from \"./payload.js\";\n\n/**\n * The mxRaven signature headers.\n *\n * HTTP header names are case-insensitive; these constants carry the canonical\n * spelling used by mxRaven.\n */\nexport const webhookHeaders = {\n /** The delivery ID, which is the delivery task ID. */\n webhookId: \"X-MxRaven-Webhook-ID\",\n /** The Unix signing time in seconds. */\n timestamp: \"X-MxRaven-Timestamp\",\n /** The `sha256=<hex>` HMAC. */\n signature: \"X-MxRaven-Signature\",\n /** The signing key ID. */\n signatureKid: \"X-MxRaven-Signature-Kid\",\n} as const;\n\n/** Options for a {@link Verifier}. */\nexport interface VerifierOptions {\n /** The signing secret, used as literal bytes and not decoded. */\n readonly secret?: string;\n /** Per-key secrets for key rotation, keyed by signing key ID. */\n readonly keys?: ReadonlyMap<string, string>;\n /** The maximum accepted clock skew in milliseconds. Defaults to 5 minutes; `0` disables the check. */\n readonly tolerance?: number;\n /** The maximum request body size in bytes. Defaults to 1 MiB. */\n readonly maxBodyBytes?: number;\n}\n\n/** Per-call options for verification. */\nexport interface VerifyOptions {\n /** Cancels the body read. */\n readonly signal?: AbortSignal;\n}\n\nconst DEFAULT_TOLERANCE = 5 * 60 * 1000;\nconst DEFAULT_MAX_BODY_BYTES = 1 << 20;\n\n/**\n * Verifies the signature of an mxRaven webhook request.\n *\n * A verifier is safe for concurrent use once constructed. Configure it with a\n * signing secret or with per-key secrets for rotation.\n *\n * @example\n * ```ts\n * const secret = process.env.MXRAVEN_WEBHOOK_SECRET;\n * if (secret === undefined || secret === \"\") {\n * throw new Error(\"MXRAVEN_WEBHOOK_SECRET is required\");\n * }\n *\n * const verifier = new Verifier({ secret });\n * const event = await verifier.verifyAndDecode(request);\n * ```\n *\n * @public\n */\nexport class Verifier {\n private readonly secret: string | undefined;\n private readonly keys: ReadonlyMap<string, string>;\n private readonly tolerance: number;\n private readonly maxBodyBytes: number;\n\n /**\n * @param options - The signing secret and verification limits.\n * @throws `Error` When no secret is provided or an option is invalid.\n */\n constructor(options: VerifierOptions = {}) {\n const keys = new Map<string, string>();\n for (const [kid, secret] of options.keys ?? []) {\n if (kid.trim() === \"\") {\n throw new Error(\"webhook: signing key ID must not be empty\");\n }\n if (secret === \"\") {\n throw new Error(\"webhook: signing secret must not be empty\");\n }\n keys.set(kid, secret);\n }\n\n const tolerance = options.tolerance ?? DEFAULT_TOLERANCE;\n if (tolerance < 0) {\n throw new Error(\"webhook: tolerance must not be negative\");\n }\n const maxBodyBytes = options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;\n if (!Number.isInteger(maxBodyBytes) || maxBodyBytes <= 0) {\n throw new Error(\"webhook: maximum body size must be positive\");\n }\n\n const secret = options.secret;\n if ((secret === undefined || secret === \"\") && keys.size === 0) {\n throw new Error(\"webhook: a signing secret is required\");\n }\n if (secret !== undefined && secret === \"\") {\n throw new Error(\"webhook: signing secret must not be empty\");\n }\n\n this.secret = secret;\n this.keys = keys;\n this.tolerance = tolerance;\n this.maxBodyBytes = maxBodyBytes;\n }\n\n /**\n * Verifies a request's signature.\n *\n * The body is read from a clone, so the caller can still read the original\n * request body afterwards.\n *\n * @param request - The incoming request.\n * @param options - An optional cancellation signal.\n * @throws {@link InvalidSignatureError} When the signature does not match.\n * @throws `Error` When required headers are missing, the timestamp is stale,\n * or the body exceeds the configured limit.\n */\n async verify(request: Request, options: VerifyOptions = {}): Promise<void> {\n const body = await this.readBody(request, options.signal);\n this.check(request, body);\n }\n\n /**\n * Verifies a request and decodes its payload.\n *\n * @param request - The incoming request.\n * @param options - An optional cancellation signal.\n * @returns The decoded event.\n * @throws {@link InvalidSignatureError} When the signature does not match.\n * @throws `Error` When verification or decoding fails.\n */\n async verifyAndDecode(request: Request, options: VerifyOptions = {}): Promise<Event> {\n const body = await this.readBody(request, options.signal);\n this.check(request, body);\n return decode(body);\n }\n\n private async readBody(request: Request, signal?: AbortSignal): Promise<Uint8Array> {\n const clone = request.clone();\n return readBoundedBody(clone.body, this.maxBodyBytes, signal, \"request body\");\n }\n\n private check(request: Request, body: Uint8Array): void {\n const webhookId = request.headers.get(webhookHeaders.webhookId)?.trim() ?? \"\";\n if (webhookId === \"\") {\n throw new Error(\"webhook: missing webhook ID header\");\n }\n const timestamp = request.headers.get(webhookHeaders.timestamp)?.trim() ?? \"\";\n if (timestamp === \"\") {\n throw new Error(\"webhook: missing timestamp header\");\n }\n const signature = request.headers.get(webhookHeaders.signature)?.trim() ?? \"\";\n if (signature === \"\") {\n throw new Error(\"webhook: missing signature header\");\n }\n\n if (this.tolerance > 0) {\n if (!/^\\d+$/.test(timestamp)) {\n throw new Error(`webhook: invalid timestamp ${JSON.stringify(timestamp)}`);\n }\n const skew = Math.abs(Date.now() - Number.parseInt(timestamp, 10) * 1000);\n if (skew > this.tolerance) {\n throw new Error(\"webhook: timestamp is outside the accepted clock skew\");\n }\n }\n\n const secret = this.secretFor(request.headers.get(webhookHeaders.signatureKid));\n\n const scheme = \"sha256=\";\n if (!signature.startsWith(scheme)) {\n throw new Error(\"webhook: unsupported signature algorithm\");\n }\n const provided = decodeHex(signature.slice(scheme.length));\n if (provided === undefined) {\n throw new Error(\"webhook: malformed signature\");\n }\n\n const expected = signRequest(secret, {\n method: request.method,\n url: new URL(request.url),\n timestamp,\n webhookId,\n body,\n });\n if (provided.length !== expected.length || !timingSafeEqual(provided, expected)) {\n throw new InvalidSignatureError();\n }\n }\n\n private secretFor(kid: string | null): string {\n const value = (kid ?? \"\").trim();\n if (this.keys.size > 0) {\n if (value === \"\") {\n throw new Error(\"webhook: missing signature key ID\");\n }\n const secret = this.keys.get(value);\n if (secret === undefined) {\n throw new Error(`webhook: unknown signature key ID ${JSON.stringify(value)}`);\n }\n return secret;\n }\n return this.secret ?? \"\";\n }\n}\n\n/** The inputs to the canonical signature string. */\ninterface SignatureInput {\n readonly method: string;\n readonly url: URL;\n readonly timestamp: string;\n readonly webhookId: string;\n readonly body: Uint8Array;\n}\n\n/** Computes the expected HMAC over the canonical request string. */\nfunction signRequest(secret: string, input: SignatureInput): Buffer {\n const bodyHash = createHash(\"sha256\").update(input.body).digest(\"hex\");\n const canonical = [\n input.timestamp,\n input.webhookId,\n input.method,\n input.url.host.toLowerCase(),\n canonicalTarget(input.url),\n bodyHash,\n ].join(\"\\n\");\n return createHmac(\"sha256\", secret).update(canonical).digest();\n}\n\n/** Returns the escaped path plus the raw query, or `/`. */\nfunction canonicalTarget(url: URL): string {\n let value = url.pathname === \"\" ? \"/\" : url.pathname;\n if (url.search !== \"\") {\n value += url.search;\n }\n return value;\n}\n\n/** Decodes a lowercase or uppercase hex string, or returns `undefined`. */\nfunction decodeHex(value: string): Buffer | undefined {\n if (value.length === 0 || value.length % 2 !== 0 || !/^[0-9a-fA-F]+$/.test(value)) {\n return undefined;\n }\n return Buffer.from(value, \"hex\");\n}\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,101 @@
1
+ {
2
+ "name": "@mxraven/mail",
3
+ "version": "0.1.0",
4
+ "description": "TypeScript SDK for the mxRaven mail runtime: SMTP submission, webhook verification, and recipient feedback.",
5
+ "keywords": [
6
+ "email",
7
+ "feedback",
8
+ "mail",
9
+ "mxraven",
10
+ "sdk",
11
+ "smtp",
12
+ "webhook"
13
+ ],
14
+ "homepage": "https://github.com/synqronlabs/mxraven-js",
15
+ "bugs": {
16
+ "url": "https://github.com/synqronlabs/mxraven-js/issues"
17
+ },
18
+ "license": "Apache-2.0",
19
+ "author": "SynqronLabs Private Limited",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/synqronlabs/mxraven-js.git"
23
+ },
24
+ "files": [
25
+ "dist"
26
+ ],
27
+ "type": "module",
28
+ "sideEffects": false,
29
+ "main": "./dist/index.cjs",
30
+ "module": "./dist/index.js",
31
+ "types": "./dist/index.d.ts",
32
+ "exports": {
33
+ ".": {
34
+ "import": {
35
+ "types": "./dist/index.d.ts",
36
+ "default": "./dist/index.js"
37
+ },
38
+ "require": {
39
+ "types": "./dist/index.d.cts",
40
+ "default": "./dist/index.cjs"
41
+ }
42
+ },
43
+ "./webhook": {
44
+ "import": {
45
+ "types": "./dist/webhook/index.d.ts",
46
+ "default": "./dist/webhook/index.js"
47
+ },
48
+ "require": {
49
+ "types": "./dist/webhook/index.d.cts",
50
+ "default": "./dist/webhook/index.cjs"
51
+ }
52
+ },
53
+ "./feedback": {
54
+ "import": {
55
+ "types": "./dist/feedback/index.d.ts",
56
+ "default": "./dist/feedback/index.js"
57
+ },
58
+ "require": {
59
+ "types": "./dist/feedback/index.d.cts",
60
+ "default": "./dist/feedback/index.cjs"
61
+ }
62
+ },
63
+ "./package.json": "./package.json"
64
+ },
65
+ "publishConfig": {
66
+ "access": "public",
67
+ "provenance": true
68
+ },
69
+ "devDependencies": {
70
+ "@changesets/cli": "^3.0.2",
71
+ "@types/node": "^26.5.1",
72
+ "eslint-plugin-tsdoc": "^0.5.2",
73
+ "oxfmt": "^0.67.0",
74
+ "oxlint": "^1.82.0",
75
+ "tsdown": "~0.23.0",
76
+ "typedoc": "^0.28.20",
77
+ "typedoc-plugin-markdown": "^4.13.0",
78
+ "typescript": "^6.0.3",
79
+ "vitest": "^5.0.0"
80
+ },
81
+ "engines": {
82
+ "node": ">=20.19.0"
83
+ },
84
+ "scripts": {
85
+ "build": "tsdown",
86
+ "typecheck": "tsc --noEmit",
87
+ "test": "vitest run",
88
+ "test:watch": "vitest",
89
+ "lint": "oxlint",
90
+ "lint:fix": "oxlint --fix",
91
+ "format": "oxfmt",
92
+ "format:check": "oxfmt --check",
93
+ "docs": "typedoc",
94
+ "docs:html": "typedoc --options typedoc.pages.json",
95
+ "docs:watch": "typedoc --watch",
96
+ "changeset": "changeset",
97
+ "version": "changeset version",
98
+ "release": "pnpm run build && changeset publish",
99
+ "check": "pnpm run format:check && pnpm run lint && pnpm run typecheck && pnpm run test && pnpm run build"
100
+ }
101
+ }