@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.
Files changed (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +238 -2
  3. package/dist/GoldstoneProvider.d.ts +33 -0
  4. package/dist/GoldstoneProvider.d.ts.map +1 -0
  5. package/dist/GoldstoneProvider.js +55 -0
  6. package/dist/GoldstoneProvider.js.map +1 -0
  7. package/dist/InboxManager.d.ts +17 -0
  8. package/dist/InboxManager.d.ts.map +1 -0
  9. package/dist/InboxManager.js +39 -0
  10. package/dist/InboxManager.js.map +1 -0
  11. package/dist/Mailbox.d.ts +83 -0
  12. package/dist/Mailbox.d.ts.map +1 -0
  13. package/dist/Mailbox.js +326 -0
  14. package/dist/Mailbox.js.map +1 -0
  15. package/dist/ReceivedMessage.d.ts +152 -0
  16. package/dist/ReceivedMessage.d.ts.map +1 -0
  17. package/dist/ReceivedMessage.js +179 -0
  18. package/dist/ReceivedMessage.js.map +1 -0
  19. package/dist/augmentations.d.ts +18 -0
  20. package/dist/augmentations.d.ts.map +1 -0
  21. package/dist/augmentations.js +10 -0
  22. package/dist/augmentations.js.map +1 -0
  23. package/dist/authenticity.d.ts +81 -0
  24. package/dist/authenticity.d.ts.map +1 -0
  25. package/dist/authenticity.js +102 -0
  26. package/dist/authenticity.js.map +1 -0
  27. package/dist/config.d.ts +85 -0
  28. package/dist/config.d.ts.map +1 -0
  29. package/dist/config.js +50 -0
  30. package/dist/config.js.map +1 -0
  31. package/dist/errors.d.ts +15 -0
  32. package/dist/errors.d.ts.map +1 -0
  33. package/dist/errors.js +50 -0
  34. package/dist/errors.js.map +1 -0
  35. package/dist/inbound/context.d.ts +43 -0
  36. package/dist/inbound/context.d.ts.map +1 -0
  37. package/dist/inbound/context.js +56 -0
  38. package/dist/inbound/context.js.map +1 -0
  39. package/dist/inbound/mailgun.d.ts +57 -0
  40. package/dist/inbound/mailgun.d.ts.map +1 -0
  41. package/dist/inbound/mailgun.js +132 -0
  42. package/dist/inbound/mailgun.js.map +1 -0
  43. package/dist/index.d.ts +11 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +11 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/native/generated.d.ts +106 -0
  48. package/dist/native/generated.d.ts.map +1 -0
  49. package/dist/native/generated.js +7 -0
  50. package/dist/native/generated.js.map +1 -0
  51. package/dist/native.d.ts +23 -0
  52. package/dist/native.d.ts.map +1 -0
  53. package/dist/native.js +45 -0
  54. package/dist/native.js.map +1 -0
  55. package/dist/services/main.d.ts +22 -0
  56. package/dist/services/main.d.ts.map +1 -0
  57. package/dist/services/main.js +43 -0
  58. package/dist/services/main.js.map +1 -0
  59. package/dist/vendor/nativeBinary.d.ts +67 -0
  60. package/dist/vendor/nativeBinary.d.ts.map +1 -0
  61. package/dist/vendor/nativeBinary.js +156 -0
  62. package/dist/vendor/nativeBinary.js.map +1 -0
  63. package/index.darwin-arm64.node +0 -0
  64. package/index.darwin-x64.node +0 -0
  65. package/index.linux-arm64-gnu.node +0 -0
  66. package/index.linux-x64-gnu.node +0 -0
  67. package/index.win32-x64-msvc.node +0 -0
  68. package/package.json +67 -3
  69. package/src/GoldstoneProvider.ts +75 -0
  70. package/src/InboxManager.ts +51 -0
  71. package/src/Mailbox.ts +433 -0
  72. package/src/ReceivedMessage.ts +287 -0
  73. package/src/augmentations.ts +22 -0
  74. package/src/authenticity.ts +183 -0
  75. package/src/config.ts +132 -0
  76. package/src/errors.ts +57 -0
  77. package/src/inbound/context.ts +92 -0
  78. package/src/inbound/mailgun.ts +201 -0
  79. package/src/index.ts +53 -0
  80. package/src/native/generated.ts +141 -0
  81. package/src/native.ts +62 -0
  82. package/src/services/main.ts +51 -0
  83. package/src/vendor/nativeBinary.ts +186 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 C9up
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,239 @@
1
- # Temporary Holding Version
1
+ # @c9up/goldstone
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Reading mail for the Ream framework.
4
+
5
+ Goldstone reads IMAP mailboxes — in real time with IDLE — and receives the
6
+ inbound mail that providers post to a webhook. Every message it hands over has
7
+ been parsed as the hostile input it is: HTML sanitized for display, attachments
8
+ bounded, file names cleaned, and the sender's DKIM, SPF and DMARC verdict
9
+ attached.
10
+
11
+ Sending stays with `@c9up/rover`.
12
+
13
+ ## Status
14
+
15
+ Here: reading IMAP mailboxes, receiving Mailgun's inbound mail, parsing what
16
+ arrives, and checking who sent it (DKIM, SPF, DMARC, ARC).
17
+
18
+ ## Reading a mailbox
19
+
20
+ ```ts
21
+ // config/inbox.ts
22
+ import { defineConfig } from "@c9up/goldstone"
23
+
24
+ export default defineConfig({
25
+ mailbox: "support",
26
+ mailboxes: {
27
+ support: {
28
+ host: "imap.example.com",
29
+ auth: { user: env.get("IMAP_USER"), pass: env.get("IMAP_PASSWORD") },
30
+ },
31
+ billing: {
32
+ host: "outlook.office365.com",
33
+ // XOAUTH2. Called on every connection, so an expired token is asked
34
+ // for again when the connection is made anew.
35
+ auth: { user: "billing@example.com", accessToken: () => tokens.for("billing") },
36
+ },
37
+ },
38
+ })
39
+
40
+ // reamrc.ts
41
+ providers: [() => import("@c9up/goldstone/provider")]
42
+ ```
43
+
44
+ ```ts
45
+ import inbox from "@c9up/goldstone/services/main"
46
+
47
+ const support = inbox.mailbox() // the default; inbox.mailbox("billing")
48
+
49
+ for (const uid of await support.search({ seen: false })) {
50
+ const message = await support.fetch(uid) // parsed, see below
51
+ await support.markSeen(uid)
52
+ }
53
+ await support.move(uid, "Archive")
54
+
55
+ const stop = support.watch(async (message, { uid }) => {
56
+ // every new message, as it arrives
57
+ })
58
+ ```
59
+
60
+ - **TLS is required**: implicit TLS by default (993), or `starttls: true`
61
+ (143, plaintext upgraded: `secure` need not be set), which refuses to go on
62
+ when the server does not offer STARTTLS. `secure: true` with
63
+ `starttls: true` is refused at `defineConfig`. Plaintext
64
+ or an unchecked certificate need `allowInsecure`, for a local test server.
65
+ - A mailbox opens on first use. `watch()` keeps its own connection in IDLE;
66
+ when it is lost, it reconnects with a growing wait and delivers what
67
+ arrived meanwhile — messages are followed by UID, so none is skipped and
68
+ none delivered twice. A handler that throws is reported to `onError` and
69
+ the watch goes on.
70
+ - A message's size is asked of the server before it is downloaded: one over
71
+ `limits.maxBytes` is refused without being fetched. The answer is only a
72
+ claim, so the connection also refuses any message literal over
73
+ `maxBytes` (and caps every response) before buffering it: a server that
74
+ lies about the size, or leaves it out, is cut off with
75
+ `E_GOLDSTONE_MESSAGE_TOO_LARGE` instead of pushing a gigabyte into memory.
76
+ - The provider logs out of every mailbox on shutdown, so an IDLE connection
77
+ does not keep the process alive.
78
+
79
+ ## Receiving inbound mail (Mailgun)
80
+
81
+ ```ts
82
+ import { createMailgunInboundHandler } from "@c9up/goldstone"
83
+
84
+ router.post("/inbound/mailgun/mime", createMailgunInboundHandler({
85
+ signingKey: env.get("MAILGUN_SIGNING_KEY"),
86
+ async onMessage(message, { recipient, sender }) {
87
+ // parsed, checked (message.authenticity)
88
+ },
89
+ }))
90
+ ```
91
+
92
+ Point the Mailgun route's *forward* action at a URL ending in `mime`: that is
93
+ what makes Mailgun send the message as received.
94
+
95
+ The handler reads the fields from `request.body()`, which Ream fills only
96
+ when its body parser has read the request: the route must run behind
97
+ `BodyParserMiddleware` (registered globally in a scaffolded app). Mailgun
98
+ posts `multipart/form-data`, or urlencoded; raise both limits above the
99
+ largest message you accept (25 MiB by default; the parser's defaults are
100
+ 1 MB for a form and 20 MB for multipart), in `config/bodyparser.ts` so the
101
+ server applies them too:
102
+
103
+ ```ts
104
+ // config/bodyparser.ts
105
+ export default {
106
+ form: { limit: "30mb" },
107
+ multipart: { limit: "30mb" },
108
+ }
109
+ ```
110
+
111
+ Without the parser, every delivery is answered 401, its fields missing.
112
+
113
+ Mailgun signs a timestamp and a token — not the message. So the handler
114
+ refuses a timestamp older than two minutes (`maxAgeSeconds`, an integer
115
+ from 1 to 86 400 — anything else is refused when the handler is created,
116
+ since a `NaN` read from an unset variable would turn the check off) and
117
+ handles each token once,
118
+ claimed before handling so two racing requests cannot both get through; a
119
+ delivery whose handler throws gives its token back, and Mailgun's retry is
120
+ handled. Fields are read from the request body only, never the query string.
121
+ The tokens live in this process's memory; with several instances, pass a
122
+ shared `replayGuard`. A message the parser refuses is answered 406, which
123
+ stops Mailgun from retrying it.
124
+
125
+ ## Parsing a received message
126
+
127
+ ```ts
128
+ import { parseMessage } from "@c9up/goldstone"
129
+
130
+ const message = await parseMessage(raw) // raw RFC 5322 bytes
131
+
132
+ message.subject
133
+ message.from // [{ name, address }]
134
+ message.date // Date
135
+ message.text
136
+ message.html // sanitized: safe to render, loads nothing remote
137
+ message.attachments // [{ fileName, originalName, contentType, size, content, … }]
138
+ message.header("List-Unsubscribe")
139
+ ```
140
+
141
+ Parsing runs in Rust ([mail-parser](https://github.com/stalwartlabs/mail-parser)),
142
+ in one call and off the JavaScript thread: the bytes go in, the message comes
143
+ out, and the event loop keeps serving requests meanwhile. A received mail is
144
+ input from whoever sent it, so:
145
+
146
+ - **the size is checked before a byte is parsed**, then the structure is
147
+ bounded — parts, nesting depth, header fields and header size, number and
148
+ size of attachments. Parts and nesting count through the messages a message
149
+ carries (message/rfc822), encoded or not. Parts and headers are bounded
150
+ before the parse too: the message is first walked the way the parser walks
151
+ it, keeping only counts, so a message made of millions of tiny parts or
152
+ headers is refused before it costs any memory.
153
+ Each limit has a default and can be set per call
154
+ (`parseMessage(raw, { limits: { maxBytes } })`); over one, a `GoldstoneError` names it
155
+ (`E_GOLDSTONE_MESSAGE_TOO_LARGE`, `…_TOO_MANY_PARTS`, `…_TOO_DEEP`,
156
+ `…_TOO_MANY_HEADERS`, `…_HEADERS_TOO_LARGE`, `…_TOO_MANY_ATTACHMENTS`,
157
+ `…_ATTACHMENT_TOO_LARGE`). A part starts only at a delimiter of the
158
+ boundary its multipart declared, or after the header of a carried message:
159
+ a `-- ` signature, a pasted log or a large attachment is body, whatever it
160
+ says. The search for those delimiters is bounded too: a multipart whose
161
+ boundary never occurs is searched for up to the end of the message, so
162
+ past four such scans of a `maxBytes` message the message is refused
163
+ (`E_GOLDSTONE_MISSING_BOUNDARIES`), and so is a boundary over 256 bytes
164
+ (`E_GOLDSTONE_BOUNDARY_TOO_LONG`; RFC 2046 allows 70);
165
+ - **`html` is sanitized** — no script, no event handler, no `javascript:` URL,
166
+ and nothing a browser would load by itself: remote images, backgrounds and
167
+ posters are removed, because loading one tells the sender the message was
168
+ opened, when and from where. `cid:` references to the message's own parts
169
+ stay, and so do links, which are only followed when clicked. An application
170
+ that shows remote content on purpose passes `remoteContent: "allow"` (to
171
+ `parseMessage`, `readMessage`, the inbox config or the Mailgun handler).
172
+ The HTML as received is only reachable through `unsanitizedHtml()`, for what
173
+ has to see the original; never render it;
174
+ - **the HTML is bounded before it is sanitized**: over `maxHtmlBytes`
175
+ (512 KiB by default), nested deeper than 1 000 elements, or building a tree
176
+ out of proportion to its markup, it is not sanitized at all — the HTML
177
+ standard's tree building costs more than linear time on such markup.
178
+ `html` is then absent and `htmlError` says why
179
+ (`E_GOLDSTONE_HTML_TOO_LARGE`, `…_HTML_TOO_DEEP`, `…_HTML_TOO_COMPLEX`);
180
+ the message, its text and its attachments are read as usual;
181
+ - **`fileName` is safe to save under**, on any system: the last path segment
182
+ of what the sender wrote, with control, invisible and bidi characters and
183
+ the characters Windows refuses (`< > : " | ? *` — a `:` would write into an
184
+ NTFS stream) replaced by `_`, without leading dots, trailing dots or
185
+ spaces, or a Windows device name (`CON`, `COM1`, `CONIN$`…). A right-to-left
186
+ override cannot dress `fdp.exe` up as `exe.pdf`. What the sender wrote stays in `originalName`, for display only.
187
+
188
+ The engine is a native binary, prebuilt for Linux, macOS and Windows; there is
189
+ no JavaScript fallback.
190
+
191
+ ## Who sent it
192
+
193
+ The From header is whatever the sender typed. A message read from a mailbox
194
+ carries the verdict on `message.authenticity`; for raw bytes, `readMessage()`
195
+ parses and checks, `authenticate()` only checks:
196
+
197
+ ```ts
198
+ import { authenticate, readMessage } from "@c9up/goldstone"
199
+
200
+ const message = await readMessage(raw)
201
+ message.authenticity.summary // "pass" | "fail" | "none" | "temperror"
202
+ message.authenticity.dkim // [{ result, domain, selector, reason }]
203
+ message.authenticity.dmarc // and dmarcPolicy, dmarcDomain, spf, arc
204
+
205
+ // SPF needs to know who delivered the message — a webhook usually says:
206
+ await authenticate(raw, { ip: "203.0.113.7", helo: "mx.sender.test", mailFrom: "bounces@sender.test" })
207
+ ```
208
+
209
+ - `summary` is `pass` only when the From domain's DMARC passes, through a
210
+ DKIM signature or an SPF result aligned with that domain. A message signed
211
+ by another domain than its From does not pass.
212
+ - The verdict and the message come from one parse, and a pass speaks for the
213
+ From the message shows: exactly one From header, holding exactly one
214
+ address, whose domain is `authenticity.fromDomain`. A repeated From, a From
215
+ with several addresses, or a header allowed once (Subject, Date, To,
216
+ Message-ID…) that appears twice is a `fail`: an unsigned copy added to a
217
+ signed message is how a phisher rides on someone else's signature.
218
+ - Only the address's domain is checked, never the display name:
219
+ `"ceo@bank.test" <a@evil.test>` can pass — for `evil.test`. Show
220
+ `fromDomain`, or the address, next to the verdict.
221
+ - A repeated header is read from its first copy everywhere: `message.from`,
222
+ `message.subject` and `message.header("from")` agree, and every copy stays
223
+ in `message.headers`.
224
+ - A DKIM signature made with rsa-sha1 (RFC 8301), or whose `h=` leaves out
225
+ From (RFC 6376 §6.1.1), is a `permerror` whatever its cryptography says,
226
+ and DMARC cannot pass on it: SHA-1 is broken, and a From the signature does
227
+ not cover can be rewritten at will.
228
+ - A check that cannot complete — DNS down, too slow — is `temperror`,
229
+ never a pass. The whole check has a deadline (10 s by default), at most 5
230
+ DKIM signatures are verified (each is a lookup; the refused ones above are
231
+ not counted), and DNS answers are cached.
232
+ - Read over IMAP, a message usually does not carry who delivered it
233
+ reliably, so SPF is `none` there and DMARC rests on DKIM.
234
+ - Turn it off with `authenticate: false` in `config/inbox.ts`, or
235
+ `readMessage(raw, { authenticate: false })`; `parseMessage()` never checks.
236
+
237
+ ## License
238
+
239
+ MIT
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Wires `config/inbox.ts` into the container as `'inbox'`, and seats the
3
+ * manager on `@c9up/goldstone/services/main`.
4
+ *
5
+ * // reamrc.ts
6
+ * providers: [() => import('@c9up/goldstone/provider')]
7
+ *
8
+ * The host is duck-typed — this package stays publishable without importing
9
+ * `@c9up/ream`. `register` opens no connection: mailboxes open on first use.
10
+ */
11
+ import "./augmentations.js";
12
+ interface GoldstoneContainer {
13
+ singleton(token: unknown, factory: () => unknown): void;
14
+ }
15
+ interface GoldstoneConfigStore {
16
+ get(key: string): unknown;
17
+ }
18
+ export interface GoldstoneAppContext {
19
+ container: GoldstoneContainer;
20
+ config: GoldstoneConfigStore;
21
+ }
22
+ export default class GoldstoneProvider {
23
+ #private;
24
+ constructor(app: GoldstoneAppContext);
25
+ register(): void;
26
+ /**
27
+ * Log out of every mailbox and stop every watch. Without it, an IDLE
28
+ * connection keeps the process alive and the server looks hung on stop.
29
+ */
30
+ shutdown(): Promise<void>;
31
+ }
32
+ export {};
33
+ //# sourceMappingURL=GoldstoneProvider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GoldstoneProvider.d.ts","sourceRoot":"","sources":["../src/GoldstoneProvider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,oBAAoB,CAAC;AAK5B,UAAU,kBAAkB;IAC3B,SAAS,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,OAAO,GAAG,IAAI,CAAC;CACxD;AACD,UAAU,oBAAoB;IAC7B,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CAC1B;AACD,MAAM,WAAW,mBAAmB;IACnC,SAAS,EAAE,kBAAkB,CAAC;IAC9B,MAAM,EAAE,oBAAoB,CAAC;CAC7B;AAcD,MAAM,CAAC,OAAO,OAAO,iBAAiB;;gBAIzB,GAAG,EAAE,mBAAmB;IAIpC,QAAQ,IAAI,IAAI;IAgBhB;;;OAGG;IACG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;CAO/B"}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Wires `config/inbox.ts` into the container as `'inbox'`, and seats the
3
+ * manager on `@c9up/goldstone/services/main`.
4
+ *
5
+ * // reamrc.ts
6
+ * providers: [() => import('@c9up/goldstone/provider')]
7
+ *
8
+ * The host is duck-typed — this package stays publishable without importing
9
+ * `@c9up/ream`. `register` opens no connection: mailboxes open on first use.
10
+ */
11
+ import "./augmentations.js";
12
+ import { InboxManager } from "./InboxManager.js";
13
+ import { clearInbox, setInbox } from "./services/main.js";
14
+ function isInboxConfig(value) {
15
+ if (typeof value !== "object" || value === null)
16
+ return false;
17
+ const mailboxes = Reflect.get(value, "mailboxes");
18
+ return (typeof Reflect.get(value, "mailbox") === "string" &&
19
+ typeof mailboxes === "object" &&
20
+ mailboxes !== null);
21
+ }
22
+ export default class GoldstoneProvider {
23
+ #app;
24
+ #manager;
25
+ constructor(app) {
26
+ this.#app = app;
27
+ }
28
+ register() {
29
+ const raw = this.#app.config.get("inbox");
30
+ if (raw === undefined)
31
+ throw new Error("[inbox] missing config/inbox.ts");
32
+ if (!isInboxConfig(raw)) {
33
+ throw new Error("[inbox] config/inbox.ts must export defineConfig({ mailbox, mailboxes }).");
34
+ }
35
+ const manager = new InboxManager(raw);
36
+ this.#manager = manager;
37
+ setInbox(manager);
38
+ this.#app.container.singleton(InboxManager, () => manager);
39
+ this.#app.container.singleton("goldstone.inbox", () => manager);
40
+ this.#app.container.singleton("inbox", () => manager);
41
+ }
42
+ /**
43
+ * Log out of every mailbox and stop every watch. Without it, an IDLE
44
+ * connection keeps the process alive and the server looks hung on stop.
45
+ */
46
+ async shutdown() {
47
+ const manager = this.#manager;
48
+ if (!manager)
49
+ return;
50
+ await manager.closeAll();
51
+ clearInbox(manager);
52
+ this.#manager = undefined;
53
+ }
54
+ }
55
+ //# sourceMappingURL=GoldstoneProvider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GoldstoneProvider.js","sourceRoot":"","sources":["../src/GoldstoneProvider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,oBAAoB,CAAC;AAE5B,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAa1D,SAAS,aAAa,CACrB,KAAc;IAEd,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAC9D,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC;IAClD,OAAO,CACN,OAAO,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,SAAS,CAAC,KAAK,QAAQ;QACjD,OAAO,SAAS,KAAK,QAAQ;QAC7B,SAAS,KAAK,IAAI,CAClB,CAAC;AACH,CAAC;AAED,MAAM,CAAC,OAAO,OAAO,iBAAiB;IAC5B,IAAI,CAAsB;IACnC,QAAQ,CAA2B;IAEnC,YAAY,GAAwB;QACnC,IAAI,CAAC,IAAI,GAAG,GAAG,CAAC;IACjB,CAAC;IAED,QAAQ;QACP,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QAC1C,IAAI,GAAG,KAAK,SAAS;YAAE,MAAM,IAAI,KAAK,CAAC,iCAAiC,CAAC,CAAC;QAC1E,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,EAAE,CAAC;YACzB,MAAM,IAAI,KAAK,CACd,2EAA2E,CAC3E,CAAC;QACH,CAAC;QACD,MAAM,OAAO,GAAG,IAAI,YAAY,CAAC,GAAG,CAAC,CAAC;QACtC,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC;QACxB,QAAQ,CAAC,OAAO,CAAC,CAAC;QAClB,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,YAAY,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,CAAC;QAC3D,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,iBAAiB,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,CAAC;QAChE,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,CAAC;IACvD,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,QAAQ;QACb,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC;QAC9B,IAAI,CAAC,OAAO;YAAE,OAAO;QACrB,MAAM,OAAO,CAAC,QAAQ,EAAE,CAAC;QACzB,UAAU,CAAC,OAAO,CAAC,CAAC;QACpB,IAAI,CAAC,QAAQ,GAAG,SAAS,CAAC;IAC3B,CAAC;CACD"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The configured mailboxes, each opened on first use.
3
+ *
4
+ * inbox.mailbox() // the default one
5
+ * inbox.mailbox('billing')
6
+ */
7
+ import type { InboxConfig, MailboxConfig } from "./config.js";
8
+ import { Mailbox } from "./Mailbox.js";
9
+ export declare class InboxManager<Mailboxes extends Record<string, MailboxConfig> = Record<string, MailboxConfig>> {
10
+ #private;
11
+ constructor(config: InboxConfig<Mailboxes>);
12
+ /** A mailbox by name; the default one without. Opens nothing yet. */
13
+ mailbox(name?: keyof Mailboxes & string): Mailbox;
14
+ /** Stop every watch and log out of every mailbox. */
15
+ closeAll(): Promise<void>;
16
+ }
17
+ //# sourceMappingURL=InboxManager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"InboxManager.d.ts","sourceRoot":"","sources":["../src/InboxManager.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE9D,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAEvC,qBAAa,YAAY,CACxB,SAAS,SAAS,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,GAAG,MAAM,CACvD,MAAM,EACN,aAAa,CACb;;gBAKW,MAAM,EAAE,WAAW,CAAC,SAAS,CAAC;IAI1C,qEAAqE;IACrE,OAAO,CAAC,IAAI,GAAE,MAAM,SAAS,GAAG,MAA6B,GAAG,OAAO;IAmBvE,qDAAqD;IAC/C,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;CAK/B"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * The configured mailboxes, each opened on first use.
3
+ *
4
+ * inbox.mailbox() // the default one
5
+ * inbox.mailbox('billing')
6
+ */
7
+ import { GoldstoneError } from "./errors.js";
8
+ import { Mailbox } from "./Mailbox.js";
9
+ export class InboxManager {
10
+ #config;
11
+ #open = new Map();
12
+ constructor(config) {
13
+ this.#config = config;
14
+ }
15
+ /** A mailbox by name; the default one without. Opens nothing yet. */
16
+ mailbox(name = this.#config.mailbox) {
17
+ const existing = this.#open.get(name);
18
+ if (existing)
19
+ return existing;
20
+ const config = this.#config.mailboxes[name];
21
+ if (config === undefined) {
22
+ throw new GoldstoneError("E_GOLDSTONE_UNKNOWN_MAILBOX", `[inbox] no mailbox "${name}" — got ${Object.keys(this.#config.mailboxes).join(", ")}`);
23
+ }
24
+ const mailbox = new Mailbox(name, config, {
25
+ limits: this.#config.limits,
26
+ authenticate: this.#config.authenticate,
27
+ remoteContent: this.#config.remoteContent,
28
+ });
29
+ this.#open.set(name, mailbox);
30
+ return mailbox;
31
+ }
32
+ /** Stop every watch and log out of every mailbox. */
33
+ async closeAll() {
34
+ const mailboxes = [...this.#open.values()];
35
+ this.#open.clear();
36
+ await Promise.all(mailboxes.map((mailbox) => mailbox.close()));
37
+ }
38
+ }
39
+ //# sourceMappingURL=InboxManager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"InboxManager.js","sourceRoot":"","sources":["../src/InboxManager.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAEvC,MAAM,OAAO,YAAY;IAMf,OAAO,CAAyB;IAChC,KAAK,GAAG,IAAI,GAAG,EAAmB,CAAC;IAE5C,YAAY,MAA8B;QACzC,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC;IACvB,CAAC;IAED,qEAAqE;IACrE,OAAO,CAAC,OAAiC,IAAI,CAAC,OAAO,CAAC,OAAO;QAC5D,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACtC,IAAI,QAAQ;YAAE,OAAO,QAAQ,CAAC;QAC9B,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;QAC5C,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YAC1B,MAAM,IAAI,cAAc,CACvB,6BAA6B,EAC7B,uBAAuB,IAAI,WAAW,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CACtF,CAAC;QACH,CAAC;QACD,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE;YACzC,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,MAAM;YAC3B,YAAY,EAAE,IAAI,CAAC,OAAO,CAAC,YAAY;YACvC,aAAa,EAAE,IAAI,CAAC,OAAO,CAAC,aAAa;SACzC,CAAC,CAAC;QACH,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QAC9B,OAAO,OAAO,CAAC;IAChB,CAAC;IAED,qDAAqD;IACrD,KAAK,CAAC,QAAQ;QACb,MAAM,SAAS,GAAG,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;QAC3C,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;QACnB,MAAM,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;IAChE,CAAC;CACD"}
@@ -0,0 +1,83 @@
1
+ /**
2
+ * One IMAP mailbox: its connection, opened on first use, and what the
3
+ * application does with it.
4
+ *
5
+ * const support = inbox.mailbox('support')
6
+ * for (const uid of await support.search({ seen: false })) {
7
+ * const message = await support.fetch(uid)
8
+ * }
9
+ * const stop = support.watch(async (message, { uid }) => { … })
10
+ *
11
+ * Every message read goes through {@link readMessage}: its size is asked of
12
+ * the server first, so one over the limit is refused without being
13
+ * downloaded; it is then parsed, and its sender checked (DKIM, SPF, DMARC)
14
+ * unless `authenticate: false`.
15
+ */
16
+ import { type SearchObject } from "imapflow";
17
+ import { type MailboxConfig } from "./config.js";
18
+ import { type Limits, type ParseOptions, type ReceivedMessage } from "./ReceivedMessage.js";
19
+ export type SearchCriteria = SearchObject;
20
+ export interface WatchOptions {
21
+ /** Default `INBOX`. */
22
+ folder?: string;
23
+ /**
24
+ * Deliver messages after this UID. Default: none of those already there —
25
+ * only what arrives once watching started.
26
+ */
27
+ afterUid?: number;
28
+ /**
29
+ * Told about what does not stop the watch: a lost connection (it
30
+ * reconnects), a message refused by the limits, a handler that threw, a
31
+ * mailbox whose UIDs were reset. Default: `console.error`.
32
+ */
33
+ onError?: (error: unknown) => void;
34
+ }
35
+ /** Wait before reconnect attempt `n`: 0.5 s, 1 s, 2 s… up to 30 s. */
36
+ export declare function reconnectDelay(attempt: number): number;
37
+ export interface MailboxOptions {
38
+ /** Bounds on every message read. */
39
+ limits?: Limits;
40
+ /**
41
+ * Check DKIM, SPF and DMARC on every message read — the verdict lands on
42
+ * `message.authenticity`. Default `true`: it costs DNS lookups (cached),
43
+ * and a From nobody checked is the From a phisher wrote.
44
+ */
45
+ authenticate?: boolean;
46
+ /** Remote content in `message.html`; see {@link ParseOptions}. Default `"block"`. */
47
+ remoteContent?: ParseOptions["remoteContent"];
48
+ }
49
+ export declare class Mailbox {
50
+ #private;
51
+ readonly name: string;
52
+ constructor(name: string, config: MailboxConfig, options?: MailboxOptions);
53
+ /** Every folder's path, as the server names it. */
54
+ folders(): Promise<string[]>;
55
+ /** UIDs of the messages that match, in `folder` (default `INBOX`). */
56
+ search(criteria: SearchCriteria, folder?: string): Promise<number[]>;
57
+ /**
58
+ * One message, parsed — or `undefined` when no message has that UID.
59
+ * Refused with `E_GOLDSTONE_MESSAGE_TOO_LARGE` before download when the
60
+ * server says it is over the limit.
61
+ */
62
+ fetch(uid: number, folder?: string): Promise<ReceivedMessage | undefined>;
63
+ markSeen(uids: number | number[], folder?: string): Promise<void>;
64
+ markUnseen(uids: number | number[], folder?: string): Promise<void>;
65
+ move(uids: number | number[], to: string, folder?: string): Promise<void>;
66
+ delete(uids: number | number[], folder?: string): Promise<void>;
67
+ /**
68
+ * Call `handler` for every new message, as it arrives (IDLE), on a
69
+ * connection of its own. A lost connection is reopened with a growing
70
+ * wait, and what arrived meanwhile is delivered on return: messages are
71
+ * followed by UID, so none is skipped and none delivered twice.
72
+ *
73
+ * A handler that throws does not stop the watch, and the message is not
74
+ * offered again — a message that always fails would otherwise stall the
75
+ * mailbox. Resolves the returned function once the watch has stopped.
76
+ */
77
+ watch(handler: (message: ReceivedMessage, meta: {
78
+ uid: number;
79
+ }) => unknown, options?: WatchOptions): () => Promise<void>;
80
+ /** Stop every watch and log out. */
81
+ close(): Promise<void>;
82
+ }
83
+ //# sourceMappingURL=Mailbox.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Mailbox.d.ts","sourceRoot":"","sources":["../src/Mailbox.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAY,KAAK,YAAY,EAAE,MAAM,UAAU,CAAC;AACvD,OAAO,EAAgB,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAE/D,OAAO,EACN,KAAK,MAAM,EACX,KAAK,YAAY,EACjB,KAAK,eAAe,EAEpB,MAAM,sBAAsB,CAAC;AAE9B,MAAM,MAAM,cAAc,GAAG,YAAY,CAAC;AAE1C,MAAM,WAAW,YAAY;IAC5B,uBAAuB;IACvB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CACnC;AAED,sEAAsE;AACtE,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEtD;AAyBD,MAAM,WAAW,cAAc;IAC9B,oCAAoC;IACpC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,qFAAqF;IACrF,aAAa,CAAC,EAAE,YAAY,CAAC,eAAe,CAAC,CAAC;CAC9C;AAED,qBAAa,OAAO;;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;gBAUrB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,aAAa,EACrB,OAAO,GAAE,cAAmB;IAU7B,mDAAmD;IAC7C,OAAO,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IAKlC,sEAAsE;IAChE,MAAM,CAAC,QAAQ,EAAE,cAAc,EAAE,MAAM,SAAU,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAO3E;;;;OAIG;IACG,KAAK,CACV,GAAG,EAAE,MAAM,EACX,MAAM,SAAU,GACd,OAAO,CAAC,eAAe,GAAG,SAAS,CAAC;IAIjC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,EAAE,MAAM,SAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAOlE,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,EAAE,MAAM,SAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAOpE,IAAI,CACT,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,EACvB,EAAE,EAAE,MAAM,EACV,MAAM,SAAU,GACd,OAAO,CAAC,IAAI,CAAC;IAOV,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,EAAE,MAAM,SAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAOtE;;;;;;;;;OASG;IACH,KAAK,CACJ,OAAO,EAAE,CAAC,OAAO,EAAE,eAAe,EAAE,IAAI,EAAE;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,EACrE,OAAO,GAAE,YAAiB,GACxB,MAAM,OAAO,CAAC,IAAI,CAAC;IA8GtB,oCAAoC;IAC9B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAqI5B"}