@c9up/goldstone 0.0.0-stage → 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.
- package/LICENSE +21 -0
- package/README.md +238 -2
- package/dist/GoldstoneProvider.d.ts +33 -0
- package/dist/GoldstoneProvider.d.ts.map +1 -0
- package/dist/GoldstoneProvider.js +55 -0
- package/dist/GoldstoneProvider.js.map +1 -0
- package/dist/InboxManager.d.ts +17 -0
- package/dist/InboxManager.d.ts.map +1 -0
- package/dist/InboxManager.js +39 -0
- package/dist/InboxManager.js.map +1 -0
- package/dist/Mailbox.d.ts +83 -0
- package/dist/Mailbox.d.ts.map +1 -0
- package/dist/Mailbox.js +326 -0
- package/dist/Mailbox.js.map +1 -0
- package/dist/ReceivedMessage.d.ts +152 -0
- package/dist/ReceivedMessage.d.ts.map +1 -0
- package/dist/ReceivedMessage.js +179 -0
- package/dist/ReceivedMessage.js.map +1 -0
- package/dist/augmentations.d.ts +18 -0
- package/dist/augmentations.d.ts.map +1 -0
- package/dist/augmentations.js +10 -0
- package/dist/augmentations.js.map +1 -0
- package/dist/authenticity.d.ts +81 -0
- package/dist/authenticity.d.ts.map +1 -0
- package/dist/authenticity.js +102 -0
- package/dist/authenticity.js.map +1 -0
- package/dist/config.d.ts +85 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +50 -0
- package/dist/config.js.map +1 -0
- package/dist/errors.d.ts +15 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +50 -0
- package/dist/errors.js.map +1 -0
- package/dist/inbound/context.d.ts +43 -0
- package/dist/inbound/context.d.ts.map +1 -0
- package/dist/inbound/context.js +56 -0
- package/dist/inbound/context.js.map +1 -0
- package/dist/inbound/mailgun.d.ts +57 -0
- package/dist/inbound/mailgun.d.ts.map +1 -0
- package/dist/inbound/mailgun.js +132 -0
- package/dist/inbound/mailgun.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/native/generated.d.ts +106 -0
- package/dist/native/generated.d.ts.map +1 -0
- package/dist/native/generated.js +7 -0
- package/dist/native/generated.js.map +1 -0
- package/dist/native.d.ts +23 -0
- package/dist/native.d.ts.map +1 -0
- package/dist/native.js +45 -0
- package/dist/native.js.map +1 -0
- package/dist/services/main.d.ts +22 -0
- package/dist/services/main.d.ts.map +1 -0
- package/dist/services/main.js +43 -0
- package/dist/services/main.js.map +1 -0
- package/dist/vendor/nativeBinary.d.ts +67 -0
- package/dist/vendor/nativeBinary.d.ts.map +1 -0
- package/dist/vendor/nativeBinary.js +156 -0
- package/dist/vendor/nativeBinary.js.map +1 -0
- package/index.darwin-arm64.node +0 -0
- package/index.darwin-x64.node +0 -0
- package/index.linux-arm64-gnu.node +0 -0
- package/index.linux-x64-gnu.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +67 -3
- package/src/GoldstoneProvider.ts +75 -0
- package/src/InboxManager.ts +51 -0
- package/src/Mailbox.ts +433 -0
- package/src/ReceivedMessage.ts +287 -0
- package/src/augmentations.ts +22 -0
- package/src/authenticity.ts +183 -0
- package/src/config.ts +132 -0
- package/src/errors.ts +57 -0
- package/src/inbound/context.ts +92 -0
- package/src/inbound/mailgun.ts +201 -0
- package/src/index.ts +53 -0
- package/src/native/generated.ts +141 -0
- package/src/native.ts +62 -0
- package/src/services/main.ts +51 -0
- package/src/vendor/nativeBinary.ts +186 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A received mail, parsed as the hostile input it is.
|
|
3
|
+
*
|
|
4
|
+
* const message = await parseMessage(raw)
|
|
5
|
+
* message.subject
|
|
6
|
+
* message.html // sanitized: safe to render
|
|
7
|
+
* message.attachments[0].fileName // safe to save under
|
|
8
|
+
*
|
|
9
|
+
* The parsing happens in Rust, in one call and off the JavaScript thread:
|
|
10
|
+
* the raw bytes go in, this comes out. The size is checked before a byte is
|
|
11
|
+
* parsed, the structure is bounded after, and what the sender controls — the
|
|
12
|
+
* HTML, the attachment names — is made safe before it reaches the
|
|
13
|
+
* application.
|
|
14
|
+
*/
|
|
15
|
+
import { fromNative, nativeOptions, } from "./authenticity.js";
|
|
16
|
+
import { fromEngine, GoldstoneError } from "./errors.js";
|
|
17
|
+
import { engine, snapshot } from "./native.js";
|
|
18
|
+
export class ReceivedMessage {
|
|
19
|
+
messageId;
|
|
20
|
+
subject;
|
|
21
|
+
date;
|
|
22
|
+
from;
|
|
23
|
+
to;
|
|
24
|
+
cc;
|
|
25
|
+
bcc;
|
|
26
|
+
replyTo;
|
|
27
|
+
inReplyTo;
|
|
28
|
+
references;
|
|
29
|
+
headers;
|
|
30
|
+
text;
|
|
31
|
+
/**
|
|
32
|
+
* Sanitized for display: no script, no event handler, no `javascript:`
|
|
33
|
+
* URL, and no remote content unless `remoteContent: "allow"`.
|
|
34
|
+
*/
|
|
35
|
+
html;
|
|
36
|
+
/**
|
|
37
|
+
* Why `html` is absent although the message has an HTML body: over
|
|
38
|
+
* `maxHtmlBytes` (`E_GOLDSTONE_HTML_TOO_LARGE`), nested too deep
|
|
39
|
+
* (`…_HTML_TOO_DEEP`), or building a tree out of proportion to its markup
|
|
40
|
+
* (`…_HTML_TOO_COMPLEX`). Sanitizing such HTML would cost time that grows
|
|
41
|
+
* faster than its size; the text and the rest of the message are there.
|
|
42
|
+
*/
|
|
43
|
+
htmlError;
|
|
44
|
+
attachments;
|
|
45
|
+
/**
|
|
46
|
+
* Who really sent it — present when the message was read with
|
|
47
|
+
* {@link readMessage} (as a mailbox reads it), absent from
|
|
48
|
+
* {@link parseMessage}, which does no DNS.
|
|
49
|
+
*/
|
|
50
|
+
authenticity;
|
|
51
|
+
#unsanitizedHtml;
|
|
52
|
+
constructor(parsed, authenticity) {
|
|
53
|
+
this.authenticity = authenticity;
|
|
54
|
+
this.messageId = parsed.messageId;
|
|
55
|
+
this.subject = parsed.subject;
|
|
56
|
+
this.date = parsed.date === undefined ? undefined : new Date(parsed.date);
|
|
57
|
+
this.from = parsed.from;
|
|
58
|
+
this.to = parsed.to;
|
|
59
|
+
this.cc = parsed.cc;
|
|
60
|
+
this.bcc = parsed.bcc;
|
|
61
|
+
this.replyTo = parsed.replyTo;
|
|
62
|
+
this.inReplyTo = parsed.inReplyTo;
|
|
63
|
+
this.references = parsed.references;
|
|
64
|
+
this.headers = parsed.headers;
|
|
65
|
+
this.text = parsed.text;
|
|
66
|
+
this.html = parsed.html;
|
|
67
|
+
this.htmlError =
|
|
68
|
+
parsed.htmlError === undefined ? undefined : fromEngine(parsed.htmlError);
|
|
69
|
+
this.#unsanitizedHtml = parsed.unsanitizedHtml;
|
|
70
|
+
this.attachments = parsed.attachments.map((a) => ({
|
|
71
|
+
fileName: a.fileName,
|
|
72
|
+
originalName: a.originalName,
|
|
73
|
+
contentType: a.contentType,
|
|
74
|
+
size: a.content.length,
|
|
75
|
+
content: a.content,
|
|
76
|
+
contentId: a.contentId,
|
|
77
|
+
inline: a.inline,
|
|
78
|
+
}));
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The first header of that name, any case: the copy the message's
|
|
82
|
+
* fields (`from`, `subject`…) read too. Every copy is in `headers`.
|
|
83
|
+
*/
|
|
84
|
+
header(name) {
|
|
85
|
+
const wanted = name.toLowerCase();
|
|
86
|
+
return this.headers.find((h) => h.name.toLowerCase() === wanted)?.value;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The HTML exactly as the sender wrote it. Never render it: it can carry
|
|
90
|
+
* scripts. It exists for what has to see the original — archiving,
|
|
91
|
+
* forwarding, forensic inspection.
|
|
92
|
+
*/
|
|
93
|
+
unsanitizedHtml() {
|
|
94
|
+
return this.#unsanitizedHtml;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const LIMIT_KEYS = [
|
|
98
|
+
"maxBytes",
|
|
99
|
+
"maxParts",
|
|
100
|
+
"maxDepth",
|
|
101
|
+
"maxHeaders",
|
|
102
|
+
"maxHeaderBytes",
|
|
103
|
+
"maxAttachments",
|
|
104
|
+
"maxAttachmentBytes",
|
|
105
|
+
"maxHtmlBytes",
|
|
106
|
+
];
|
|
107
|
+
function nativeLimits(limits) {
|
|
108
|
+
if (limits === undefined)
|
|
109
|
+
return undefined;
|
|
110
|
+
for (const key of LIMIT_KEYS) {
|
|
111
|
+
const value = limits[key];
|
|
112
|
+
if (value !== undefined &&
|
|
113
|
+
!(Number.isInteger(value) && value > 0 && value <= 0xffff_ffff)) {
|
|
114
|
+
throw new GoldstoneError("E_GOLDSTONE_INVALID_LIMITS", `Goldstone: ${key} must be a positive integer under 2^32, got ${String(value)}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return { ...limits };
|
|
118
|
+
}
|
|
119
|
+
function htmlOptions(options) {
|
|
120
|
+
const { remoteContent } = options;
|
|
121
|
+
if (remoteContent !== undefined &&
|
|
122
|
+
remoteContent !== "block" &&
|
|
123
|
+
remoteContent !== "allow") {
|
|
124
|
+
throw new GoldstoneError("E_GOLDSTONE_INVALID_OPTIONS", `Goldstone: remoteContent must be "block" or "allow", got ${String(remoteContent)}`);
|
|
125
|
+
}
|
|
126
|
+
return { remoteContent: remoteContent === "allow" };
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Parse one received message — raw RFC 5322 bytes, as IMAP fetches them or
|
|
130
|
+
* a provider posts them. The work runs off the event loop.
|
|
131
|
+
*
|
|
132
|
+
* Rejects with a `GoldstoneError` on a message over a limit
|
|
133
|
+
* (`E_GOLDSTONE_MESSAGE_TOO_LARGE`, `…_TOO_MANY_PARTS`, `…_TOO_DEEP`,
|
|
134
|
+
* `…_TOO_MANY_HEADERS`, `…_HEADERS_TOO_LARGE`, `…_MISSING_BOUNDARIES`,
|
|
135
|
+
* `…_BOUNDARY_TOO_LONG`, `…_TOO_MANY_ATTACHMENTS`, `…_ATTACHMENT_TOO_LARGE`) or not a message at
|
|
136
|
+
* all (`E_GOLDSTONE_NOT_A_MESSAGE`).
|
|
137
|
+
*/
|
|
138
|
+
export async function parseMessage(raw, options = {}) {
|
|
139
|
+
return new ReceivedMessage(await parseNative(raw, options));
|
|
140
|
+
}
|
|
141
|
+
async function parseNative(raw, options) {
|
|
142
|
+
const bounds = nativeLimits(options.limits);
|
|
143
|
+
const html = htmlOptions(options);
|
|
144
|
+
const bytes = snapshot(raw);
|
|
145
|
+
try {
|
|
146
|
+
return await engine().parseMessage(bytes, bounds, html);
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
if (error instanceof GoldstoneError)
|
|
150
|
+
throw error;
|
|
151
|
+
throw fromEngine(error);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Parse a received message AND check who sent it, the verdict on
|
|
156
|
+
* `message.authenticity`. Both come from one parse: the From the verdict
|
|
157
|
+
* speaks for is the From the message shows.
|
|
158
|
+
*/
|
|
159
|
+
export async function readMessage(raw, options = {}) {
|
|
160
|
+
if (options.authenticate === false) {
|
|
161
|
+
return new ReceivedMessage(await parseNative(raw, options));
|
|
162
|
+
}
|
|
163
|
+
const bounds = nativeLimits(options.limits);
|
|
164
|
+
const html = htmlOptions(options);
|
|
165
|
+
const authOptions = nativeOptions(options.authenticate === true || options.authenticate === undefined
|
|
166
|
+
? { maxBytes: options.limits?.maxBytes }
|
|
167
|
+
: { maxBytes: options.limits?.maxBytes, ...options.authenticate });
|
|
168
|
+
const bytes = snapshot(raw);
|
|
169
|
+
try {
|
|
170
|
+
const read = await engine().readMessage(bytes, bounds, authOptions, html);
|
|
171
|
+
return new ReceivedMessage(read.message, fromNative(read.authenticity));
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
if (error instanceof GoldstoneError)
|
|
175
|
+
throw error;
|
|
176
|
+
throw fromEngine(error);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
//# sourceMappingURL=ReceivedMessage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ReceivedMessage.js","sourceRoot":"","sources":["../src/ReceivedMessage.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Teach ream's `ContainerBindings` what `container.make(...)` returns for the
|
|
3
|
+
* tokens goldstone binds.
|
|
4
|
+
*
|
|
5
|
+
* Type-only, and ream stays an OPTIONAL peer: nothing here reaches a runtime
|
|
6
|
+
* import, and a `declare module` for a specifier that does not resolve is
|
|
7
|
+
* simply inert.
|
|
8
|
+
*/
|
|
9
|
+
import type { InboxManager } from "./InboxManager.js";
|
|
10
|
+
declare module "@c9up/ream/types" {
|
|
11
|
+
interface ContainerBindings {
|
|
12
|
+
/** The configured mailboxes, bound by `GoldstoneProvider`. */
|
|
13
|
+
"goldstone.inbox": InboxManager;
|
|
14
|
+
/** The same binding under the bare role name. */
|
|
15
|
+
inbox: InboxManager;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=augmentations.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"augmentations.d.ts","sourceRoot":"","sources":["../src/augmentations.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEtD,OAAO,QAAQ,kBAAkB,CAAC;IACjC,UAAU,iBAAiB;QAC1B,8DAA8D;QAC9D,iBAAiB,EAAE,YAAY,CAAC;QAChC,iDAAiD;QACjD,KAAK,EAAE,YAAY,CAAC;KACpB;CACD"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Teach ream's `ContainerBindings` what `container.make(...)` returns for the
|
|
3
|
+
* tokens goldstone binds.
|
|
4
|
+
*
|
|
5
|
+
* Type-only, and ream stays an OPTIONAL peer: nothing here reaches a runtime
|
|
6
|
+
* import, and a `declare module` for a specifier that does not resolve is
|
|
7
|
+
* simply inert.
|
|
8
|
+
*/
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=augmentations.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"augmentations.js","sourceRoot":"","sources":["../src/augmentations.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who really sent a message: DKIM, SPF, DMARC and ARC, checked in Rust
|
|
3
|
+
* against DNS.
|
|
4
|
+
*
|
|
5
|
+
* const authenticity = await authenticate(raw)
|
|
6
|
+
* if (authenticity.summary !== 'pass') {
|
|
7
|
+
* // the From cannot be trusted
|
|
8
|
+
* }
|
|
9
|
+
*
|
|
10
|
+
* `summary` is `pass` only when the From domain's DMARC passes — through a
|
|
11
|
+
* DKIM signature or an SPF result aligned with that domain — and that From
|
|
12
|
+
* is the one the message shows: exactly one From header holding exactly one
|
|
13
|
+
* address, whose domain is `fromDomain`. A From header repeated, a From with
|
|
14
|
+
* several addresses, or any header allowed once (Subject, Date, To…) that
|
|
15
|
+
* appears twice is a `fail`. A check that could not complete (DNS down, too
|
|
16
|
+
* slow) is `temperror`, never a pass.
|
|
17
|
+
*
|
|
18
|
+
* Only the address's domain is authenticated, never the display name:
|
|
19
|
+
* `"ceo@bank.test" <a@evil.test>` can pass, for `evil.test`.
|
|
20
|
+
*/
|
|
21
|
+
import type { JsAuthOptions, JsVerdict } from "./native/generated.js";
|
|
22
|
+
/** A check's outcome, in the words of RFC 8601. */
|
|
23
|
+
export type AuthResult = "pass" | "fail" | "softfail" | "neutral" | "none" | "temperror" | "permerror";
|
|
24
|
+
export type DmarcPolicy = "none" | "quarantine" | "reject" | "unspecified";
|
|
25
|
+
export interface DkimVerdict {
|
|
26
|
+
readonly result: AuthResult;
|
|
27
|
+
/** The signing domain (`d=`). */
|
|
28
|
+
readonly domain?: string;
|
|
29
|
+
readonly selector?: string;
|
|
30
|
+
/** Why it did not pass. */
|
|
31
|
+
readonly reason?: string;
|
|
32
|
+
}
|
|
33
|
+
export interface Authenticity {
|
|
34
|
+
/**
|
|
35
|
+
* One per signature: the ones verified (at most `maxDkimSignatures`),
|
|
36
|
+
* and the ones refused without a lookup, which do not count against it.
|
|
37
|
+
*/
|
|
38
|
+
readonly dkim: readonly DkimVerdict[];
|
|
39
|
+
/** `none` when no envelope was given. */
|
|
40
|
+
readonly spf: AuthResult;
|
|
41
|
+
readonly dmarc: AuthResult;
|
|
42
|
+
readonly dmarcPolicy: DmarcPolicy;
|
|
43
|
+
/** The From domain DMARC was evaluated for. */
|
|
44
|
+
readonly dmarcDomain?: string;
|
|
45
|
+
/**
|
|
46
|
+
* The domain of the message's one From address: what `summary` speaks
|
|
47
|
+
* for. Absent when the From is missing or ambiguous (repeated, several
|
|
48
|
+
* addresses). The display name is never authenticated.
|
|
49
|
+
*/
|
|
50
|
+
readonly fromDomain?: string;
|
|
51
|
+
readonly arc: AuthResult;
|
|
52
|
+
/**
|
|
53
|
+
* `pass` only when DMARC passed for `fromDomain`; `fail` when it failed
|
|
54
|
+
* or the From cannot be told apart; `temperror` when a check could not
|
|
55
|
+
* complete.
|
|
56
|
+
*/
|
|
57
|
+
readonly summary: "pass" | "fail" | "none" | "temperror";
|
|
58
|
+
}
|
|
59
|
+
export interface AuthenticateOptions {
|
|
60
|
+
/**
|
|
61
|
+
* The IP of the server that delivered the message — with `helo` and
|
|
62
|
+
* `mailFrom`, what SPF checks. A provider's inbound webhook usually gives
|
|
63
|
+
* it; a message read over IMAP usually does not, and SPF is then `none`.
|
|
64
|
+
*/
|
|
65
|
+
ip?: string;
|
|
66
|
+
helo?: string;
|
|
67
|
+
/** The SMTP MAIL FROM (return path). */
|
|
68
|
+
mailFrom?: string;
|
|
69
|
+
/** DKIM signatures checked, at most — each is a DNS lookup. Default 5. */
|
|
70
|
+
maxDkimSignatures?: number;
|
|
71
|
+
/** For the whole check; past it, `temperror`. Default 10 000 ms. */
|
|
72
|
+
timeoutMs?: number;
|
|
73
|
+
/** Size of the raw message, checked first. Default 25 MiB. */
|
|
74
|
+
maxBytes?: number;
|
|
75
|
+
}
|
|
76
|
+
/** The engine's verdict, as the application reads it. */
|
|
77
|
+
export declare function fromNative(v: JsVerdict): Authenticity;
|
|
78
|
+
export declare function nativeOptions(options: AuthenticateOptions | undefined): JsAuthOptions | undefined;
|
|
79
|
+
/** Check one received message — raw RFC 5322 bytes. */
|
|
80
|
+
export declare function authenticate(raw: Uint8Array, options?: AuthenticateOptions): Promise<Authenticity>;
|
|
81
|
+
//# sourceMappingURL=authenticity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"authenticity.d.ts","sourceRoot":"","sources":["../src/authenticity.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAGtE,mDAAmD;AACnD,MAAM,MAAM,UAAU,GACnB,MAAM,GACN,MAAM,GACN,UAAU,GACV,SAAS,GACT,MAAM,GACN,WAAW,GACX,WAAW,CAAC;AAEf,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,YAAY,GAAG,QAAQ,GAAG,aAAa,CAAC;AAE3E,MAAM,WAAW,WAAW;IAC3B,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,iCAAiC;IACjC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,2BAA2B;IAC3B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,YAAY;IAC5B;;;OAGG;IACH,QAAQ,CAAC,IAAI,EAAE,SAAS,WAAW,EAAE,CAAC;IACtC,yCAAyC;IACzC,QAAQ,CAAC,GAAG,EAAE,UAAU,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;IAC3B,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC;IAClC,+CAA+C;IAC/C,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,GAAG,EAAE,UAAU,CAAC;IACzB;;;;OAIG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,WAAW,CAAC;CACzD;AAED,MAAM,WAAW,mBAAmB;IACnC;;;;OAIG;IACH,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,wCAAwC;IACxC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,0EAA0E;IAC1E,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,oEAAoE;IACpE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAwCD,yDAAyD;AACzD,wBAAgB,UAAU,CAAC,CAAC,EAAE,SAAS,GAAG,YAAY,CAgBrD;AAID,wBAAgB,aAAa,CAC5B,OAAO,EAAE,mBAAmB,GAAG,SAAS,GACtC,aAAa,GAAG,SAAS,CAe3B;AAED,uDAAuD;AACvD,wBAAsB,YAAY,CACjC,GAAG,EAAE,UAAU,EACf,OAAO,CAAC,EAAE,mBAAmB,GAC3B,OAAO,CAAC,YAAY,CAAC,CASvB"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who really sent a message: DKIM, SPF, DMARC and ARC, checked in Rust
|
|
3
|
+
* against DNS.
|
|
4
|
+
*
|
|
5
|
+
* const authenticity = await authenticate(raw)
|
|
6
|
+
* if (authenticity.summary !== 'pass') {
|
|
7
|
+
* // the From cannot be trusted
|
|
8
|
+
* }
|
|
9
|
+
*
|
|
10
|
+
* `summary` is `pass` only when the From domain's DMARC passes — through a
|
|
11
|
+
* DKIM signature or an SPF result aligned with that domain — and that From
|
|
12
|
+
* is the one the message shows: exactly one From header holding exactly one
|
|
13
|
+
* address, whose domain is `fromDomain`. A From header repeated, a From with
|
|
14
|
+
* several addresses, or any header allowed once (Subject, Date, To…) that
|
|
15
|
+
* appears twice is a `fail`. A check that could not complete (DNS down, too
|
|
16
|
+
* slow) is `temperror`, never a pass.
|
|
17
|
+
*
|
|
18
|
+
* Only the address's domain is authenticated, never the display name:
|
|
19
|
+
* `"ceo@bank.test" <a@evil.test>` can pass, for `evil.test`.
|
|
20
|
+
*/
|
|
21
|
+
import { fromEngine, GoldstoneError } from "./errors.js";
|
|
22
|
+
import { engine, snapshot } from "./native.js";
|
|
23
|
+
function result(value) {
|
|
24
|
+
switch (value) {
|
|
25
|
+
case "pass":
|
|
26
|
+
case "fail":
|
|
27
|
+
case "softfail":
|
|
28
|
+
case "neutral":
|
|
29
|
+
case "none":
|
|
30
|
+
case "temperror":
|
|
31
|
+
case "permerror":
|
|
32
|
+
return value;
|
|
33
|
+
default:
|
|
34
|
+
// Not something the engine answers; whatever it is, not a pass.
|
|
35
|
+
return "permerror";
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function policy(value) {
|
|
39
|
+
switch (value) {
|
|
40
|
+
case "none":
|
|
41
|
+
case "quarantine":
|
|
42
|
+
case "reject":
|
|
43
|
+
return value;
|
|
44
|
+
default:
|
|
45
|
+
return "unspecified";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function summary(value) {
|
|
49
|
+
switch (value) {
|
|
50
|
+
case "pass":
|
|
51
|
+
case "fail":
|
|
52
|
+
case "none":
|
|
53
|
+
return value;
|
|
54
|
+
default:
|
|
55
|
+
return "temperror";
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** The engine's verdict, as the application reads it. */
|
|
59
|
+
export function fromNative(v) {
|
|
60
|
+
return {
|
|
61
|
+
dkim: v.dkim.map((d) => ({
|
|
62
|
+
result: result(d.result),
|
|
63
|
+
domain: d.domain,
|
|
64
|
+
selector: d.selector,
|
|
65
|
+
reason: d.reason,
|
|
66
|
+
})),
|
|
67
|
+
spf: result(v.spf),
|
|
68
|
+
dmarc: result(v.dmarc),
|
|
69
|
+
dmarcPolicy: policy(v.dmarcPolicy),
|
|
70
|
+
dmarcDomain: v.dmarcDomain,
|
|
71
|
+
fromDomain: v.fromDomain,
|
|
72
|
+
arc: result(v.arc),
|
|
73
|
+
summary: summary(v.summary),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
const BOUNDED = ["maxDkimSignatures", "timeoutMs", "maxBytes"];
|
|
77
|
+
export function nativeOptions(options) {
|
|
78
|
+
if (options === undefined)
|
|
79
|
+
return undefined;
|
|
80
|
+
for (const key of BOUNDED) {
|
|
81
|
+
const value = options[key];
|
|
82
|
+
if (value !== undefined &&
|
|
83
|
+
!(Number.isInteger(value) && value > 0 && value <= 0xffff_ffff)) {
|
|
84
|
+
throw new GoldstoneError("E_GOLDSTONE_INVALID_OPTIONS", `Goldstone: ${key} must be a positive integer under 2^32, got ${String(value)}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return { ...options };
|
|
88
|
+
}
|
|
89
|
+
/** Check one received message — raw RFC 5322 bytes. */
|
|
90
|
+
export async function authenticate(raw, options) {
|
|
91
|
+
const bounds = nativeOptions(options);
|
|
92
|
+
const bytes = snapshot(raw);
|
|
93
|
+
try {
|
|
94
|
+
return fromNative(await engine().authenticateMessage(bytes, bounds));
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
if (error instanceof GoldstoneError)
|
|
98
|
+
throw error;
|
|
99
|
+
throw fromEngine(error);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=authenticity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"authenticity.js","sourceRoot":"","sources":["../src/authenticity.ts"],"names":[],"mappings":"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"}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shape `config/inbox.ts` returns: the mailboxes the application reads.
|
|
3
|
+
*
|
|
4
|
+
* export default defineConfig({
|
|
5
|
+
* mailbox: 'support',
|
|
6
|
+
* mailboxes: {
|
|
7
|
+
* support: {
|
|
8
|
+
* host: 'imap.example.com',
|
|
9
|
+
* auth: { user: env.get('IMAP_USER'), pass: env.get('IMAP_PASSWORD') },
|
|
10
|
+
* },
|
|
11
|
+
* billing: {
|
|
12
|
+
* host: 'outlook.office365.com',
|
|
13
|
+
* auth: { user: 'billing@example.com', accessToken: () => tokens.for('billing') },
|
|
14
|
+
* },
|
|
15
|
+
* },
|
|
16
|
+
* })
|
|
17
|
+
*/
|
|
18
|
+
import type { Limits, ParseOptions } from "./ReceivedMessage.js";
|
|
19
|
+
export interface PasswordAuth {
|
|
20
|
+
user: string;
|
|
21
|
+
pass: string;
|
|
22
|
+
}
|
|
23
|
+
export interface TokenAuth {
|
|
24
|
+
user: string;
|
|
25
|
+
/**
|
|
26
|
+
* An OAuth2 access token for XOAUTH2 — or a function that answers one. The
|
|
27
|
+
* function is called on every connection, so a token that expired is
|
|
28
|
+
* asked for again when the connection is made anew.
|
|
29
|
+
*/
|
|
30
|
+
accessToken: string | (() => string | Promise<string>);
|
|
31
|
+
}
|
|
32
|
+
export interface MailboxConfig {
|
|
33
|
+
host: string;
|
|
34
|
+
/** Default 993 with `secure`, 143 with `starttls`. */
|
|
35
|
+
port?: number;
|
|
36
|
+
/**
|
|
37
|
+
* TLS from the first byte (port 993). The default, unless `starttls` is
|
|
38
|
+
* set. Turn it off only for `starttls`, or — on a development machine —
|
|
39
|
+
* with `allowInsecure`.
|
|
40
|
+
*/
|
|
41
|
+
secure?: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Upgrade a plaintext connection with STARTTLS (port 143, `secure` off by
|
|
44
|
+
* itself), and refuse to go on if the server does not offer it. Without this, a client upgrades only when
|
|
45
|
+
* the server says it can — and an attacker on the path can remove that.
|
|
46
|
+
*/
|
|
47
|
+
starttls?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Allow a connection without TLS, or without verifying the server's
|
|
50
|
+
* certificate. For a local test server only.
|
|
51
|
+
*/
|
|
52
|
+
allowInsecure?: boolean;
|
|
53
|
+
/** Passed to Node's TLS: `ca` for a private CA, `servername`… */
|
|
54
|
+
tls?: {
|
|
55
|
+
ca?: string | Buffer;
|
|
56
|
+
servername?: string;
|
|
57
|
+
rejectUnauthorized?: boolean;
|
|
58
|
+
};
|
|
59
|
+
auth: PasswordAuth | TokenAuth;
|
|
60
|
+
}
|
|
61
|
+
export interface InboxConfig<Mailboxes extends Record<string, MailboxConfig>> {
|
|
62
|
+
/** The mailbox `inbox.mailbox()` returns with no argument. */
|
|
63
|
+
mailbox: keyof Mailboxes & string;
|
|
64
|
+
mailboxes: Mailboxes;
|
|
65
|
+
/** Bounds applied to every message read; see {@link Limits}. */
|
|
66
|
+
limits?: Limits;
|
|
67
|
+
/**
|
|
68
|
+
* Check DKIM, SPF and DMARC on every message read. Default `true`; see
|
|
69
|
+
* `message.authenticity`.
|
|
70
|
+
*/
|
|
71
|
+
authenticate?: boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Remote content in `message.html`: `"block"` (the default) or
|
|
74
|
+
* `"allow"`; see {@link ParseOptions}.
|
|
75
|
+
*/
|
|
76
|
+
remoteContent?: ParseOptions["remoteContent"];
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Type helper for `config/inbox.ts`. Refuses, at boot: no mailbox, a default
|
|
80
|
+
* that is not declared, and a mailbox that would talk without TLS or without
|
|
81
|
+
* checking the server's certificate unless `allowInsecure` says so.
|
|
82
|
+
*/
|
|
83
|
+
export declare function defineConfig<Mailboxes extends Record<string, MailboxConfig>>(config: InboxConfig<Mailboxes>): InboxConfig<Mailboxes>;
|
|
84
|
+
export declare function assertSecure(name: string, mailbox: MailboxConfig): void;
|
|
85
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAGH,OAAO,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAEjE,MAAM,WAAW,YAAY;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,SAAS;IACzB,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,WAAW,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;CACvD;AAED,MAAM,WAAW,aAAa;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,sDAAsD;IACtD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,iEAAiE;IACjE,GAAG,CAAC,EAAE;QACL,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;QACrB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,kBAAkB,CAAC,EAAE,OAAO,CAAC;KAC7B,CAAC;IACF,IAAI,EAAE,YAAY,GAAG,SAAS,CAAC;CAC/B;AAED,MAAM,WAAW,WAAW,CAAC,SAAS,SAAS,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC;IAC3E,8DAA8D;IAC9D,OAAO,EAAE,MAAM,SAAS,GAAG,MAAM,CAAC;IAClC,SAAS,EAAE,SAAS,CAAC;IACrB,gEAAgE;IAChE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;OAGG;IACH,aAAa,CAAC,EAAE,YAAY,CAAC,eAAe,CAAC,CAAC;CAC9C;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,SAAS,SAAS,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,EAC3E,MAAM,EAAE,WAAW,CAAC,SAAS,CAAC,GAC5B,WAAW,CAAC,SAAS,CAAC,CAkBxB;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,GAAG,IAAI,CAoBvE"}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shape `config/inbox.ts` returns: the mailboxes the application reads.
|
|
3
|
+
*
|
|
4
|
+
* export default defineConfig({
|
|
5
|
+
* mailbox: 'support',
|
|
6
|
+
* mailboxes: {
|
|
7
|
+
* support: {
|
|
8
|
+
* host: 'imap.example.com',
|
|
9
|
+
* auth: { user: env.get('IMAP_USER'), pass: env.get('IMAP_PASSWORD') },
|
|
10
|
+
* },
|
|
11
|
+
* billing: {
|
|
12
|
+
* host: 'outlook.office365.com',
|
|
13
|
+
* auth: { user: 'billing@example.com', accessToken: () => tokens.for('billing') },
|
|
14
|
+
* },
|
|
15
|
+
* },
|
|
16
|
+
* })
|
|
17
|
+
*/
|
|
18
|
+
import { GoldstoneError } from "./errors.js";
|
|
19
|
+
/**
|
|
20
|
+
* Type helper for `config/inbox.ts`. Refuses, at boot: no mailbox, a default
|
|
21
|
+
* that is not declared, and a mailbox that would talk without TLS or without
|
|
22
|
+
* checking the server's certificate unless `allowInsecure` says so.
|
|
23
|
+
*/
|
|
24
|
+
export function defineConfig(config) {
|
|
25
|
+
const names = Object.keys(config.mailboxes);
|
|
26
|
+
if (names.length === 0) {
|
|
27
|
+
throw new GoldstoneError("E_GOLDSTONE_INVALID_CONFIG", "[inbox] config declares no mailbox");
|
|
28
|
+
}
|
|
29
|
+
if (!names.includes(config.mailbox)) {
|
|
30
|
+
throw new GoldstoneError("E_GOLDSTONE_INVALID_CONFIG", `[inbox] default mailbox "${config.mailbox}" is not declared — got ${names.join(", ")}`);
|
|
31
|
+
}
|
|
32
|
+
for (const [name, mailbox] of Object.entries(config.mailboxes)) {
|
|
33
|
+
assertSecure(name, mailbox);
|
|
34
|
+
}
|
|
35
|
+
return config;
|
|
36
|
+
}
|
|
37
|
+
export function assertSecure(name, mailbox) {
|
|
38
|
+
if (mailbox.secure === true && mailbox.starttls === true) {
|
|
39
|
+
throw new GoldstoneError("E_GOLDSTONE_INVALID_CONFIG", `[inbox] mailbox "${name}" asks for TLS from the first byte and for STARTTLS — keep one: starttls: true alone upgrades a plaintext connection`);
|
|
40
|
+
}
|
|
41
|
+
if (mailbox.allowInsecure === true)
|
|
42
|
+
return;
|
|
43
|
+
if (mailbox.secure === false && mailbox.starttls !== true) {
|
|
44
|
+
throw new GoldstoneError("E_GOLDSTONE_INSECURE_CONNECTION", `[inbox] mailbox "${name}" would connect without TLS — set starttls: true, or allowInsecure for a local test server`);
|
|
45
|
+
}
|
|
46
|
+
if (mailbox.tls?.rejectUnauthorized === false) {
|
|
47
|
+
throw new GoldstoneError("E_GOLDSTONE_INSECURE_CONNECTION", `[inbox] mailbox "${name}" would not check the server's certificate — give its CA in tls.ca, or allowInsecure for a local test server`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAkE7C;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAC3B,MAA8B;IAE9B,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;IAC5C,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxB,MAAM,IAAI,cAAc,CACvB,4BAA4B,EAC5B,oCAAoC,CACpC,CAAC;IACH,CAAC;IACD,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC;QACrC,MAAM,IAAI,cAAc,CACvB,4BAA4B,EAC5B,4BAA4B,MAAM,CAAC,OAAO,2BAA2B,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CACvF,CAAC;IACH,CAAC;IACD,KAAK,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC;QAChE,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IAC7B,CAAC;IACD,OAAO,MAAM,CAAC;AACf,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,IAAY,EAAE,OAAsB;IAChE,IAAI,OAAO,CAAC,MAAM,KAAK,IAAI,IAAI,OAAO,CAAC,QAAQ,KAAK,IAAI,EAAE,CAAC;QAC1D,MAAM,IAAI,cAAc,CACvB,4BAA4B,EAC5B,oBAAoB,IAAI,sHAAsH,CAC9I,CAAC;IACH,CAAC;IACD,IAAI,OAAO,CAAC,aAAa,KAAK,IAAI;QAAE,OAAO;IAC3C,IAAI,OAAO,CAAC,MAAM,KAAK,KAAK,IAAI,OAAO,CAAC,QAAQ,KAAK,IAAI,EAAE,CAAC;QAC3D,MAAM,IAAI,cAAc,CACvB,iCAAiC,EACjC,oBAAoB,IAAI,4FAA4F,CACpH,CAAC;IACH,CAAC;IACD,IAAI,OAAO,CAAC,GAAG,EAAE,kBAAkB,KAAK,KAAK,EAAE,CAAC;QAC/C,MAAM,IAAI,cAAc,CACvB,iCAAiC,EACjC,oBAAoB,IAAI,8GAA8G,CACtI,CAAC;IACH,CAAC;AACF,CAAC"}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Goldstone errors. No upstream package exists, so every code carries the
|
|
3
|
+
* namespace.
|
|
4
|
+
*/
|
|
5
|
+
export declare class GoldstoneError extends Error {
|
|
6
|
+
readonly code: string;
|
|
7
|
+
constructor(code: string, message: string, options?: ErrorOptions);
|
|
8
|
+
}
|
|
9
|
+
/** The engine's `CODE: message`, back as a coded error. */
|
|
10
|
+
export declare function fromEngine(error: unknown): GoldstoneError;
|
|
11
|
+
/** Raised when parsing needs the Rust engine and it is not there. */
|
|
12
|
+
export declare class GoldstoneNativeRequiredError extends GoldstoneError {
|
|
13
|
+
constructor(reason: string);
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,qBAAa,cAAe,SAAQ,KAAK;IACxC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;gBAEV,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY;CAKjE;AAqBD,2DAA2D;AAC3D,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,cAAc,CAUzD;AAED,qEAAqE;AACrE,qBAAa,4BAA6B,SAAQ,cAAc;gBACnD,MAAM,EAAE,MAAM;CAO1B"}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Goldstone errors. No upstream package exists, so every code carries the
|
|
3
|
+
* namespace.
|
|
4
|
+
*/
|
|
5
|
+
export class GoldstoneError extends Error {
|
|
6
|
+
code;
|
|
7
|
+
constructor(code, message, options) {
|
|
8
|
+
super(message, options);
|
|
9
|
+
this.name = new.target.name;
|
|
10
|
+
this.code = code;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/** The codes the engine raises, `E_GOLDSTONE_<REASON>: <message>`. */
|
|
14
|
+
const ENGINE_CODES = new Set([
|
|
15
|
+
"E_GOLDSTONE_MESSAGE_TOO_LARGE",
|
|
16
|
+
"E_GOLDSTONE_NOT_A_MESSAGE",
|
|
17
|
+
"E_GOLDSTONE_TOO_MANY_PARTS",
|
|
18
|
+
"E_GOLDSTONE_TOO_DEEP",
|
|
19
|
+
"E_GOLDSTONE_TOO_MANY_HEADERS",
|
|
20
|
+
"E_GOLDSTONE_HEADERS_TOO_LARGE",
|
|
21
|
+
"E_GOLDSTONE_MISSING_BOUNDARIES",
|
|
22
|
+
"E_GOLDSTONE_BOUNDARY_TOO_LONG",
|
|
23
|
+
"E_GOLDSTONE_TOO_MANY_ATTACHMENTS",
|
|
24
|
+
"E_GOLDSTONE_ATTACHMENT_TOO_LARGE",
|
|
25
|
+
"E_GOLDSTONE_HTML_TOO_LARGE",
|
|
26
|
+
"E_GOLDSTONE_HTML_TOO_DEEP",
|
|
27
|
+
"E_GOLDSTONE_HTML_TOO_COMPLEX",
|
|
28
|
+
"E_GOLDSTONE_DNS_UNAVAILABLE",
|
|
29
|
+
"E_GOLDSTONE_INVALID_ENVELOPE",
|
|
30
|
+
]);
|
|
31
|
+
/** The engine's `CODE: message`, back as a coded error. */
|
|
32
|
+
export function fromEngine(error) {
|
|
33
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
34
|
+
const separator = message.indexOf(": ");
|
|
35
|
+
const code = separator > 0 ? message.slice(0, separator) : "";
|
|
36
|
+
if (ENGINE_CODES.has(code)) {
|
|
37
|
+
return new GoldstoneError(code, message.slice(separator + 2), {
|
|
38
|
+
cause: error,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return new GoldstoneError("E_GOLDSTONE_ENGINE", message, { cause: error });
|
|
42
|
+
}
|
|
43
|
+
/** Raised when parsing needs the Rust engine and it is not there. */
|
|
44
|
+
export class GoldstoneNativeRequiredError extends GoldstoneError {
|
|
45
|
+
constructor(reason) {
|
|
46
|
+
super("E_GOLDSTONE_NATIVE_REQUIRED", `The Rust mail engine is required but not loaded — ${reason}.\n` +
|
|
47
|
+
"Install the prebuilt binary for this platform, or build it with `pnpm build:napi`.");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,OAAO,cAAe,SAAQ,KAAK;IAC/B,IAAI,CAAS;IAEtB,YAAY,IAAY,EAAE,OAAe,EAAE,OAAsB;QAChE,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QACxB,IAAI,CAAC,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC;QAC5B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IAClB,CAAC;CACD;AAED,sEAAsE;AACtE,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC;IAC5B,+BAA+B;IAC/B,2BAA2B;IAC3B,4BAA4B;IAC5B,sBAAsB;IACtB,8BAA8B;IAC9B,+BAA+B;IAC/B,gCAAgC;IAChC,+BAA+B;IAC/B,kCAAkC;IAClC,kCAAkC;IAClC,4BAA4B;IAC5B,2BAA2B;IAC3B,8BAA8B;IAC9B,6BAA6B;IAC7B,8BAA8B;CAC9B,CAAC,CAAC;AAEH,2DAA2D;AAC3D,MAAM,UAAU,UAAU,CAAC,KAAc;IACxC,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACvE,MAAM,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACxC,MAAM,IAAI,GAAG,SAAS,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAC9D,IAAI,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;QAC5B,OAAO,IAAI,cAAc,CAAC,IAAI,EAAE,OAAO,CAAC,KAAK,CAAC,SAAS,GAAG,CAAC,CAAC,EAAE;YAC7D,KAAK,EAAE,KAAK;SACZ,CAAC,CAAC;IACJ,CAAC;IACD,OAAO,IAAI,cAAc,CAAC,oBAAoB,EAAE,OAAO,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;AAC5E,CAAC;AAED,qEAAqE;AACrE,MAAM,OAAO,4BAA6B,SAAQ,cAAc;IAC/D,YAAY,MAAc;QACzB,KAAK,CACJ,6BAA6B,EAC7B,qDAAqD,MAAM,KAAK;YAC/D,oFAAoF,CACrF,CAAC;IACH,CAAC;CACD"}
|