@nxgt/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.
Files changed (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +341 -0
  3. package/dist/chunks/index-0f7kdb8k.js +114 -0
  4. package/dist/chunks/index-0f7kdb8k.js.map +11 -0
  5. package/dist/chunks/index-vq4e9n8f.js +39 -0
  6. package/dist/chunks/index-vq4e9n8f.js.map +10 -0
  7. package/dist/chunks/index-we4n5yfz.js +28 -0
  8. package/dist/chunks/index-we4n5yfz.js.map +10 -0
  9. package/dist/conformance/assert.d.ts +12 -0
  10. package/dist/conformance/assert.d.ts.map +1 -0
  11. package/dist/conformance/cases/failure.d.ts +4 -0
  12. package/dist/conformance/cases/failure.d.ts.map +1 -0
  13. package/dist/conformance/cases/index.d.ts +6 -0
  14. package/dist/conformance/cases/index.d.ts.map +1 -0
  15. package/dist/conformance/cases/send.d.ts +4 -0
  16. package/dist/conformance/cases/send.d.ts.map +1 -0
  17. package/dist/conformance/describe.d.ts +37 -0
  18. package/dist/conformance/describe.d.ts.map +1 -0
  19. package/dist/conformance/index.d.ts +20 -0
  20. package/dist/conformance/index.d.ts.map +1 -0
  21. package/dist/conformance/index.js +297 -0
  22. package/dist/conformance/index.js.map +16 -0
  23. package/dist/conformance/reference.d.ts +7 -0
  24. package/dist/conformance/reference.d.ts.map +1 -0
  25. package/dist/conformance/sample.d.ts +4 -0
  26. package/dist/conformance/sample.d.ts.map +1 -0
  27. package/dist/conformance/types.d.ts +74 -0
  28. package/dist/conformance/types.d.ts.map +1 -0
  29. package/dist/errors.d.ts +68 -0
  30. package/dist/errors.d.ts.map +1 -0
  31. package/dist/index.d.ts +21 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +29 -0
  34. package/dist/index.js.map +9 -0
  35. package/dist/locale.d.ts +33 -0
  36. package/dist/locale.d.ts.map +1 -0
  37. package/dist/memory.d.ts +34 -0
  38. package/dist/memory.d.ts.map +1 -0
  39. package/dist/message.d.ts +23 -0
  40. package/dist/message.d.ts.map +1 -0
  41. package/dist/renderer.d.ts +80 -0
  42. package/dist/renderer.d.ts.map +1 -0
  43. package/dist/renderer.js +169 -0
  44. package/dist/renderer.js.map +10 -0
  45. package/dist/types.d.ts +69 -0
  46. package/dist/types.d.ts.map +1 -0
  47. package/docs/README.md +16 -0
  48. package/docs/guide/locales.md +141 -0
  49. package/docs/guide/rendering.md +523 -0
  50. package/docs/guide/sending.md +325 -0
  51. package/docs/guide/testing.md +175 -0
  52. package/docs/guide/transports.md +451 -0
  53. package/docs/roadmap.md +112 -0
  54. package/docs/troubleshooting.md +1330 -0
  55. package/package.json +68 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Steve Tsala
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 ADDED
@@ -0,0 +1,341 @@
1
+ # @nxgt/mail
2
+
3
+ The run-time side of transactional e-mail: the renderer that fills a Maizzle
4
+ build made with `@nxgt/mail-i18n`, the `Mailer` port a transport implements,
5
+ the shape it sends, the two errors it throws, a memory transport for tests, and
6
+ locale selection. **No dependency.**
7
+
8
+ ```ts
9
+ import { createMemoryMailer } from '@nxgt/mail';
10
+ import { createMailRenderer } from '@nxgt/mail/renderer';
11
+
12
+ const mails = createMailRenderer({ dir: 'dist' }); // the folder `maizzle build` wrote
13
+ const mailer = createMemoryMailer(); // in production, a transport's mailer — see below
14
+
15
+ const { messageId } = await mailer.send({
16
+ to: { name: 'Ada Lovelace', address: 'ada@example.com' },
17
+ from: 'noreply@example.com',
18
+ ...mails.render('verify-email', { name: 'Ada', link: 'https://app.example.com/verify?token=abc' }),
19
+ }); // 'memory-1' — or it throws
20
+ ```
21
+
22
+ In production, `mailer` comes from a transport:
23
+ [`@nxgt/mail-smtp`](https://github.com/softistx/nxgt-mail/tree/develop/packages/mail-smtp)
24
+ on your nodemailer, or
25
+ [`@nxgt/mail-resend`](https://github.com/softistx/nxgt-mail/tree/develop/packages/mail-resend)
26
+ over `fetch`.
27
+
28
+ > **0.x.** A minor version may still change the surface; the changelog says how.
29
+
30
+ ## Install
31
+
32
+ ```sh
33
+ bun add @nxgt/mail
34
+ ```
35
+
36
+ No runtime dependency. `typescript` (6) is a required peer. Your tsconfig
37
+ resolves as a bundler does (`"moduleResolution": "bundler"`): the declarations
38
+ import without extensions, so `nodenext` is not supported.
39
+
40
+ ## Subpaths
41
+
42
+ | Import | What it holds |
43
+ | --- | --- |
44
+ | `@nxgt/mail` | The port (`Mailer`, `MailMessage`, `Rendered`, `SentMail`, `Address`), the errors (`MailError`, `MailFailure`, `MailRefused`), `createMemoryMailer`, `pickLocale` and `parseAcceptLanguage`, and what a transport calls first: `checkMessage`, `recipientsOf`, `addressOf`. No Node built-in: it runs anywhere |
45
+ | `@nxgt/mail/renderer` | The renderer: `createMailRenderer`, `MailRenderer`, `MailRendererOptions`, `RenderOptions`, `MailVariables`, and the types that type it with a build's `MailEmails` (`MailEmailsOf`, `AnyMailEmails`, `RenderArguments`). Reads the build with `node:fs` |
46
+ | `@nxgt/mail/conformance` | **For transport authors**: `describeMailer`, its cases as data, `runMailerCase`, and the memory mailer's harness as a worked example |
47
+
48
+ ## Usage
49
+
50
+ ### Rendering — `createMailRenderer`
51
+
52
+ Build the project with `maizzle build` and the `i18n()` plugin of
53
+ `@nxgt/mail-i18n`, deploy its output folder with your server, and create one
54
+ renderer at start-up. It reads `mail-manifest.json` and every built file once,
55
+ so a missing build fails there, not at the first send:
56
+
57
+ ```ts
58
+ import { type Mailer, pickLocale } from '@nxgt/mail';
59
+ import { createMailRenderer } from '@nxgt/mail/renderer';
60
+ import type { MailEmails } from './generated/mail'; // written by the build, committed
61
+
62
+ export const mails = createMailRenderer<MailEmails>({ dir: 'dist' }); // throws now if dist/ is missing
63
+
64
+ export async function sendVerification(
65
+ mailer: Mailer,
66
+ user: { email: string; name: string; locale: string | null },
67
+ link: string,
68
+ ): Promise<void> {
69
+ const locale = pickLocale(user.locale, mails.locales, 'en'); // 'fr-CA' renders 'fr'
70
+ await mailer.send({ to: user.email, ...mails.render('verify-email', { name: user.name, link }, { locale }) });
71
+ }
72
+ ```
73
+
74
+ Each value is HTML-escaped in `html` and written as is in `text` and the
75
+ subject; line breaks in the subject become a space. A variable that starts an
76
+ `href` or a `src` must be an `http:`, `https:` or `mailto:` URL, or `render`
77
+ throws `MailRefused`. A missing or unknown variable, e-mail or locale throws an
78
+ `Error`.
79
+
80
+ `<MailEmails>` is optional. `@nxgt/mail-i18n` writes it after each build, in
81
+ `generated/mail.ts`, from the manifest; commit it. With it, the compiler
82
+ refuses what `render` would throw: an e-mail the build does not have, a
83
+ variable missing or unknown, and a number for a URL variable, as
84
+ `Argument of type '"verify-emial"' is not assignable to parameter of type
85
+ '"sign-in-code" | "verify-email"'`. Without it, any name and any
86
+ `MailVariables` compile, and the same mistakes throw at run time. See
87
+ [Rendering](docs/guide/rendering.md) for the options, typing the renderer,
88
+ the locale chosen through `getLanguage`, and every error.
89
+
90
+ ### Sending — the port and `MailMessage`
91
+
92
+ A `MailMessage` is a rendered e-mail — `subject`, `html`, `text` — plus its
93
+ addresses. `Rendered` is what `mails.render(…)` answers, and any function
94
+ answering the same shape fits, so an e-mail can also be written by hand:
95
+
96
+ ```ts
97
+ import type { Mailer, Rendered, SentMail } from '@nxgt/mail';
98
+
99
+ function passwordChanged(): Rendered {
100
+ return {
101
+ subject: 'Your password was changed',
102
+ html: '<p>Your password was changed. If it was not you, reset it now.</p>',
103
+ text: 'Your password was changed. If it was not you, reset it now.',
104
+ };
105
+ }
106
+
107
+ export function notifyPasswordChanged(mailer: Mailer, to: string): Promise<SentMail> {
108
+ return mailer.send({
109
+ ...passwordChanged(),
110
+ to,
111
+ replyTo: { name: 'Support', address: 'support@example.com' },
112
+ headers: { 'X-Entity-Ref-ID': 'password-changed' },
113
+ });
114
+ }
115
+ ```
116
+
117
+ `SentMail` is `{ messageId: string | null }`: `null` when the transport gives no
118
+ id — an absence, not a failure. See [Sending](docs/guide/sending.md).
119
+
120
+ ### Errors — switch on `code`
121
+
122
+ Both errors extend `MailError`, whose `code` is a union a `switch` exhausts.
123
+ `MailError` is abstract: catch it, but throw `MailFailure` or `MailRefused`.
124
+
125
+ ```ts
126
+ import { MailError, type MailErrorCode, type Mailer, type MailMessage } from '@nxgt/mail';
127
+
128
+ function statusOf(code: MailErrorCode): number {
129
+ switch (code) {
130
+ case 'MAIL_FAILED':
131
+ return 503; // nothing is known to have been sent: retry later, or say it failed
132
+ case 'MAIL_REFUSED':
133
+ return 422; // the e-mail itself is malformed: sending it again fails again
134
+ }
135
+ }
136
+
137
+ export async function sendOrRespond(mailer: Mailer, message: MailMessage): Promise<Response> {
138
+ try {
139
+ await mailer.send(message);
140
+ return new Response(null, { status: 202 });
141
+ } catch (error) {
142
+ if (!(error instanceof MailError)) throw error;
143
+ return Response.json({ code: error.code }, { status: statusOf(error.code) });
144
+ }
145
+ }
146
+ ```
147
+
148
+ `MailFailure` carries the transport's own error as `cause`. An error's `message`
149
+ names **where** the problem is, never the value: never an address, a subject or
150
+ a link. See [Sending — errors](docs/guide/sending.md#errors).
151
+
152
+ ### Testing — the memory mailer
153
+
154
+ `createMemoryMailer()` refuses what every transport refuses, keeps an outbox,
155
+ and can be told to fail:
156
+
157
+ ```ts
158
+ import { expect, it } from 'bun:test';
159
+ import { createMemoryMailer, MailFailure } from '@nxgt/mail';
160
+
161
+ it('says so when the e-mail could not be sent', async () => {
162
+ const mailer = createMemoryMailer();
163
+ mailer.failNext(); // the next send rejects with MailFailure, as an outage would
164
+
165
+ const error = await mailer
166
+ .send({ to: 'ada@example.com', subject: 'Hi', html: '<p>Hi</p>', text: 'Hi' })
167
+ .then(() => null, (e: unknown) => e);
168
+
169
+ expect(error).toBeInstanceOf(MailFailure);
170
+ expect(mailer.sent).toEqual([]); // the outbox
171
+ expect(mailer.attempts).toBe(1); // nothing retried in secret
172
+ });
173
+ ```
174
+
175
+ See [Testing](docs/guide/testing.md).
176
+
177
+ ### Locales — `pickLocale` and `parseAcceptLanguage`
178
+
179
+ The locale of an e-mail is the **recipient's**: their stored preference first,
180
+ then, if the recipient is the visitor, their `Accept-Language`:
181
+
182
+ ```ts
183
+ import { parseAcceptLanguage, pickLocale } from '@nxgt/mail';
184
+
185
+ const locale = pickLocale(
186
+ ['fr-CA', ...parseAcceptLanguage('de;q=0.9,en;q=0.8')],
187
+ ['en', 'fr'],
188
+ 'en',
189
+ ); // 'fr' — typed 'en' | 'fr'
190
+ ```
191
+
192
+ `fr-CA` matches `fr`; nothing matching answers the fallback. See
193
+ [Locales](docs/guide/locales.md).
194
+
195
+ ### Writing a transport — `checkMessage` and `describeMailer`
196
+
197
+ A transport calls `checkMessage` first, quotes or encodes a recipient's name
198
+ itself (a name is free text), throws the classes imported from its `@nxgt/mail`
199
+ peer, and passes the conformance suite:
200
+
201
+ ```ts
202
+ import { checkMessage, MailFailure, MailRefused, type Mailer, recipientsOf } from '@nxgt/mail';
203
+
204
+ export function createHttpMailer(endpoint: string, apiKey: string): Mailer {
205
+ return {
206
+ async send(message) {
207
+ checkMessage(message); // MailRefused, naming where, never the value
208
+ const response = await fetch(endpoint, {
209
+ method: 'POST',
210
+ headers: { authorization: `Bearer ${apiKey}`, 'content-type': 'application/json' },
211
+ body: JSON.stringify({ ...message, to: recipientsOf(message) }),
212
+ }).catch((cause: unknown) => {
213
+ throw new MailFailure('send: the provider could not be reached', { cause });
214
+ });
215
+ const cause = new Error(`the provider answered HTTP ${response.status}`);
216
+ if (response.status === 400 || response.status === 422) {
217
+ throw new MailRefused('send: the provider refused the message', { cause });
218
+ }
219
+ if (!response.ok) throw new MailFailure('send: the provider failed', { cause });
220
+ return { messageId: response.headers.get('x-message-id') }; // null when absent
221
+ },
222
+ };
223
+ }
224
+ ```
225
+
226
+ ```ts
227
+ import { describe, it } from 'bun:test';
228
+ import { describeMailer, referenceMailerHarness } from '@nxgt/mail/conformance';
229
+
230
+ describeMailer({
231
+ name: 'the memory mailer',
232
+ harness: referenceMailerHarness(), // yours: open() a fresh mailer, read back what it delivered
233
+ runner: { describe, it },
234
+ });
235
+ ```
236
+
237
+ See [Writing a transport](docs/guide/transports.md) for the harness, faults and
238
+ skips.
239
+
240
+ ## Traps
241
+
242
+ **A failure throws; never map it to "sent".** A mailer that could not hand an
243
+ e-mail over rejects with `MailFailure`; it never answers `false`, and it never
244
+ logs and resolves. A caller that reports a failed send as sent has told a user
245
+ to check an inbox that will stay empty. The conformance suite fails a transport
246
+ that breaks the rule.
247
+
248
+ **Deploy the build with the server, and point `dir` at it.** `dir` is read
249
+ from the working directory; resolve it from the module when the process may
250
+ start elsewhere: `fileURLToPath(new URL('../mails/dist', import.meta.url))`.
251
+
252
+ **The renderer needs a file system.** `@nxgt/mail/renderer` imports
253
+ `node:fs`: it runs on Node, Bun and Deno, not on an edge runtime without `fs`.
254
+ `@nxgt/mail` itself imports no Node built-in.
255
+
256
+ **Create the renderer once.** It reads the whole build when created; one per
257
+ request reads it every time, and a rebuild is only seen by a new renderer.
258
+
259
+ **`{ locale }` must be one of `mails.locales`, spelled the same.** `'fr-CA'`
260
+ throws where the build has `fr`; pass
261
+ `pickLocale(user.locale, mails.locales, 'en')`.
262
+
263
+ **A URL variable is refused unless it is `http:`, `https:` or `mailto:`.**
264
+ `render` throws `MailRefused` before anything is sent, so keep it inside the
265
+ `try` that handles `MailError`.
266
+
267
+ **A string address is only an address.** `'Ada <ada@example.com>'` is refused
268
+ with `MailRefused`; write `{ name: 'Ada', address: 'ada@example.com' }`.
269
+
270
+ **Never fire and forget a send.** `void mailer.send(message)` turns a failure
271
+ into an unhandled rejection and the user into someone waiting for an e-mail
272
+ that never comes. `await` it, or hand it to a queue that does.
273
+
274
+ **A custom header cannot set an address.** `headers: { Bcc: '…' }` would add
275
+ a recipient no check saw: `checkMessage` refuses `To`, `Cc`, `Bcc`, `From`,
276
+ `Sender`, `Reply-To`, `Return-Path`, `Subject`, `MIME-Version` and
277
+ `Content-*` among `headers`, in any case. Use `to`, `from` and `replyTo`.
278
+
279
+ **A refusal is not worth retrying; a failure may be.** `MAIL_REFUSED` fails
280
+ again unchanged. Nothing in this package retries a `MAIL_FAILED`: a retry is
281
+ your decision, made where you can see it.
282
+
283
+ **The locale is the recipient's, not the request's.** An administrator who
284
+ invites a user sends the invitation in the *user's* locale:
285
+ `pickLocale(invitee.locale, supported, fallback)`.
286
+
287
+ **A transport defines no error class.** It throws `MailFailure` and
288
+ `MailRefused` from its `@nxgt/mail` peer; its own copy fails `instanceof`, and
289
+ the conformance suite with it.
290
+
291
+ **Under `bun test`, pass `runner: { describe, it }` to `describeMailer`.** Bun
292
+ gives a test file `describe` and `it` as bare identifiers, not on `globalThis`.
293
+
294
+ ## Type safety, counted
295
+
296
+ **17 plausible mistakes, 17 refused** at compile time, each measured by a
297
+ `@ts-expect-error` in
298
+ [`test/types/refusals.ts`](https://github.com/softistx/nxgt-mail/blob/develop/packages/mail/test/types/refusals.ts)
299
+ that fails the typecheck the moment it stops holding:
300
+
301
+ 1. A `MailMessage` without `to`.
302
+ 2. A `MailMessage` without a `text` part.
303
+ 3. An address object without its `address`.
304
+ 4. A `Mailer` without `send`.
305
+ 5. A `Mailer` whose `send` answers a boolean.
306
+ 6. A `SentMail` whose `messageId` is `undefined` rather than `null`.
307
+ 7. A `pickLocale` fallback that is not one of the supported locales.
308
+ 8. A `MailErrorCode` the union does not declare.
309
+ 9. A bare `new MailError(…)` — it is abstract, so nothing throws an error that
310
+ passes a `code` check and fails `instanceof MailFailure`.
311
+ 10. A `createMailRenderer` without `dir`, the build's output folder.
312
+ 11. A `getLanguage` given as a locale rather than a function answering one.
313
+ 12. A `render` variable that is neither a string nor a number (a `URL`).
314
+
315
+ With the renderer given the build's `MailEmails`
316
+ (`createMailRenderer<MailEmails>(…)`):
317
+
318
+ The name written as a literal and the variables at the call, as usual:
319
+
320
+ 13. An e-mail the build does not have (`render('verify-emial', …)`).
321
+ 14. A variable the e-mail does not take.
322
+ 15. A variable the e-mail takes, left out.
323
+ 16. The variables left out altogether, for an e-mail that takes some.
324
+ 17. A number for a URL variable: a URL is a string.
325
+
326
+ The same file holds the calls that must keep compiling: a refusal that refuses
327
+ the correct call is a bug.
328
+
329
+ ## Documentation
330
+
331
+ - [The guides](docs/README.md) — one page per area, with every option and error.
332
+ - [Troubleshooting](docs/troubleshooting.md) — an error message, its cause and
333
+ its fix.
334
+ - [Roadmap](docs/roadmap.md) — what is next, and what is deliberately not
335
+ planned.
336
+ - [Vocabulary](https://github.com/softistx/nxgt-mail/blob/develop/docs/vocabulary.md)
337
+ — the words these pages use, defined once.
338
+
339
+ ## Licence
340
+
341
+ MIT
@@ -0,0 +1,114 @@
1
+ import {
2
+ MailFailure2,
3
+ MailRefused2
4
+ } from "./index-we4n5yfz.js";
5
+
6
+ // src/message.ts
7
+ var LINE_BREAK = /[\r\n]/;
8
+ var ADDRESS = /^[^\s@<>,;:]+@[^\s@<>,;:]+$/;
9
+ var HEADER_NAME = /^[A-Za-z0-9-]+$/;
10
+ var RESERVED_HEADER = /^(?:to|cc|bcc|from|sender|reply-to|return-path|subject|mime-version|content-.*)$/i;
11
+ function recipientsOf2(message) {
12
+ const to = Array.isArray(message.to) ? message.to : [message.to];
13
+ return to.map(addressOf2);
14
+ }
15
+ function addressOf2(address) {
16
+ return typeof address === "string" ? address : address.address;
17
+ }
18
+ function checkAddress(address, where) {
19
+ if (typeof address === "string") {
20
+ if (!ADDRESS.test(address)) {
21
+ throw new MailRefused2(`send: ${where} is not an e-mail address`);
22
+ }
23
+ return;
24
+ }
25
+ if (typeof address !== "object" || address === null) {
26
+ throw new MailRefused2(`send: ${where} is not an e-mail address`);
27
+ }
28
+ if (typeof address.address !== "string" || !ADDRESS.test(address.address)) {
29
+ throw new MailRefused2(`send: ${where}.address is not an e-mail address`);
30
+ }
31
+ if (typeof address.name !== "string" || LINE_BREAK.test(address.name)) {
32
+ throw new MailRefused2(`send: ${where}.name must be a string without a line break`);
33
+ }
34
+ }
35
+ function checkMessage2(message) {
36
+ if (typeof message !== "object" || message === null) {
37
+ throw new MailRefused2("send: the message must be an object");
38
+ }
39
+ if (message.to === undefined || message.to === null) {
40
+ throw new MailRefused2("send: to must hold at least one address");
41
+ }
42
+ const to = Array.isArray(message.to) ? message.to : [message.to];
43
+ if (to.length === 0) {
44
+ throw new MailRefused2("send: to must hold at least one address");
45
+ }
46
+ to.forEach((address, index) => {
47
+ checkAddress(address, Array.isArray(message.to) ? `to[${index}]` : "to");
48
+ });
49
+ if (message.from !== undefined)
50
+ checkAddress(message.from, "from");
51
+ if (message.replyTo !== undefined)
52
+ checkAddress(message.replyTo, "replyTo");
53
+ for (const part of ["subject", "html", "text"]) {
54
+ if (typeof message[part] !== "string") {
55
+ throw new MailRefused2(`send: ${part} must be a string`);
56
+ }
57
+ }
58
+ if (LINE_BREAK.test(message.subject)) {
59
+ throw new MailRefused2("send: subject must not hold a line break");
60
+ }
61
+ for (const [name, value] of Object.entries(message.headers ?? {})) {
62
+ if (!HEADER_NAME.test(name)) {
63
+ throw new MailRefused2("send: a header name must be letters, digits and hyphens");
64
+ }
65
+ if (RESERVED_HEADER.test(name)) {
66
+ throw new MailRefused2(`send: header ${name} is reserved — addresses, the subject and the MIME structure are never custom headers`);
67
+ }
68
+ if (typeof value !== "string" || LINE_BREAK.test(value)) {
69
+ throw new MailRefused2(`send: header ${name} must be a string without a line break`);
70
+ }
71
+ }
72
+ }
73
+
74
+ // src/memory.ts
75
+ function createMemoryMailer2() {
76
+ let sent = [];
77
+ let failures = [];
78
+ let attempts = 0;
79
+ let counter = 0;
80
+ return {
81
+ get sent() {
82
+ return sent.map((mail) => structuredClone(mail));
83
+ },
84
+ get attempts() {
85
+ return attempts;
86
+ },
87
+ failNext(error) {
88
+ failures.push(error ?? new MailFailure2("send: the memory mailer was told to fail this send", {
89
+ cause: new Error("memory mailer: failNext")
90
+ }));
91
+ },
92
+ clear() {
93
+ sent = [];
94
+ failures = [];
95
+ attempts = 0;
96
+ },
97
+ async send(message) {
98
+ checkMessage2(message);
99
+ attempts += 1;
100
+ const failure = failures.shift();
101
+ if (failure !== undefined)
102
+ throw failure;
103
+ counter += 1;
104
+ const messageId = `memory-${counter}`;
105
+ sent.push({ ...structuredClone(message), messageId });
106
+ return { messageId };
107
+ }
108
+ };
109
+ }
110
+
111
+ export { recipientsOf2, addressOf2, checkMessage2, createMemoryMailer2 };
112
+
113
+ //# debugId=44B7D62837EFA8BB64756E2164756E21
114
+ //# sourceMappingURL=index-0f7kdb8k.js.map
@@ -0,0 +1,11 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/message.ts", "../src/memory.ts"],
4
+ "sourcesContent": [
5
+ "import { MailRefused } from './errors';\nimport type { Address, MailMessage } from './types';\n\nconst LINE_BREAK = /[\\r\\n]/;\n// Deliberately loose: one `@`, something on each side, and none of what an\n// address list parser reads as structure — whitespace, `<` `>` (a display\n// name), `,` `;` (a second address), `:` (a group). A provider that parses\n// the string then finds one mailbox, the one checked. Whether the mailbox\n// exists is the receiving server's question.\nconst ADDRESS = /^[^\\s@<>,;:]+@[^\\s@<>,;:]+$/;\nconst HEADER_NAME = /^[A-Za-z0-9-]+$/;\n// The headers a transport writes from the message: the addresses, the subject\n// and the MIME structure. Set through `headers`, a Bcc reaches an SMTP\n// envelope unchecked, and a Content-Type rewrites how the parts are read.\nconst RESERVED_HEADER =\n\t/^(?:to|cc|bcc|from|sender|reply-to|return-path|subject|mime-version|content-.*)$/i;\n\n/** Every recipient of a message, as bare addresses, in order. */\nexport function recipientsOf(message: MailMessage): string[] {\n\tconst to = Array.isArray(message.to) ? message.to : [message.to];\n\treturn to.map(addressOf);\n}\n\n/** The bare address of an {@link Address}. */\nexport function addressOf(address: Address): string {\n\treturn typeof address === 'string' ? address : address.address;\n}\n\n/** Refuses `address` unless it is an {@link Address}. `undefined` is refused too. */\nfunction checkAddress(address: Address | undefined, where: string): void {\n\tif (typeof address === 'string') {\n\t\tif (!ADDRESS.test(address)) {\n\t\t\tthrow new MailRefused(`send: ${where} is not an e-mail address`);\n\t\t}\n\t\treturn;\n\t}\n\tif (typeof address !== 'object' || address === null) {\n\t\tthrow new MailRefused(`send: ${where} is not an e-mail address`);\n\t}\n\tif (typeof address.address !== 'string' || !ADDRESS.test(address.address)) {\n\t\tthrow new MailRefused(`send: ${where}.address is not an e-mail address`);\n\t}\n\tif (typeof address.name !== 'string' || LINE_BREAK.test(address.name)) {\n\t\tthrow new MailRefused(\n\t\t\t`send: ${where}.name must be a string without a line break`,\n\t\t);\n\t}\n}\n\n/**\n * Refuses a message no transport should hand over, with a {@link MailRefused}\n * that names **where** the problem is and never the value.\n *\n * A transport calls it first thing in `send`, so the refusals are the same\n * whichever transport is wired. It checks:\n *\n * - at least one recipient, each one an address;\n * - `from` and `replyTo`, when present, are addresses;\n * - `subject`, `html` and `text` are strings, and `subject` holds no line\n * break — a line break in a subject is a header injection;\n * - every header name is letters, digits and hyphens, none names what the\n * transport writes from the message (`To`, `Cc`, `Bcc`, `From`, `Sender`,\n * `Reply-To`, `Return-Path`, `Subject`, `MIME-Version`, `Content-*`, in\n * any case), and no header value holds a line break.\n */\nexport function checkMessage(message: MailMessage): void {\n\tif (typeof message !== 'object' || message === null) {\n\t\tthrow new MailRefused('send: the message must be an object');\n\t}\n\tif (message.to === undefined || message.to === null) {\n\t\tthrow new MailRefused('send: to must hold at least one address');\n\t}\n\tconst to = Array.isArray(message.to) ? message.to : [message.to];\n\tif (to.length === 0) {\n\t\tthrow new MailRefused('send: to must hold at least one address');\n\t}\n\tto.forEach((address, index) => {\n\t\tcheckAddress(address, Array.isArray(message.to) ? `to[${index}]` : 'to');\n\t});\n\tif (message.from !== undefined) checkAddress(message.from, 'from');\n\tif (message.replyTo !== undefined) checkAddress(message.replyTo, 'replyTo');\n\n\tfor (const part of ['subject', 'html', 'text'] as const) {\n\t\tif (typeof message[part] !== 'string') {\n\t\t\tthrow new MailRefused(`send: ${part} must be a string`);\n\t\t}\n\t}\n\tif (LINE_BREAK.test(message.subject)) {\n\t\tthrow new MailRefused('send: subject must not hold a line break');\n\t}\n\n\tfor (const [name, value] of Object.entries(message.headers ?? {})) {\n\t\tif (!HEADER_NAME.test(name)) {\n\t\t\tthrow new MailRefused(\n\t\t\t\t'send: a header name must be letters, digits and hyphens',\n\t\t\t);\n\t\t}\n\t\tif (RESERVED_HEADER.test(name)) {\n\t\t\tthrow new MailRefused(\n\t\t\t\t`send: header ${name} is reserved — addresses, the subject and the MIME structure are never custom headers`,\n\t\t\t);\n\t\t}\n\t\tif (typeof value !== 'string' || LINE_BREAK.test(value)) {\n\t\t\tthrow new MailRefused(\n\t\t\t\t`send: header ${name} must be a string without a line break`,\n\t\t\t);\n\t\t}\n\t}\n}\n",
6
+ "import { type MailError, MailFailure } from './errors';\nimport { checkMessage } from './message';\nimport type { Mailer, MailMessage, SentMail } from './types';\n\n/** One message the memory mailer accepted, with the id it gave it. */\nexport interface MemoryMail extends MailMessage {\n\treadonly messageId: string;\n}\n\n/**\n * The reference transport: it keeps what it sends in memory, for tests.\n *\n * It refuses exactly what every transport refuses (it calls\n * {@link checkMessage}), and it can be told to fail, so a test can prove what\n * the application does when a send throws.\n */\nexport interface MemoryMailer extends Mailer {\n\t/** Every message accepted so far, oldest first. A copy: mutating it changes nothing. */\n\treadonly sent: readonly MemoryMail[];\n\t/**\n\t * How many sends reached the hand-over, failed ones included. A message\n\t * refused as malformed never reaches it. A caller that retries in secret\n\t * shows up here.\n\t */\n\treadonly attempts: number;\n\t/**\n\t * Makes the next send that reaches the hand-over reject with `error`, by\n\t * default a {@link MailFailure} as an outage would. Calls queue: two calls\n\t * fail the next two sends.\n\t */\n\tfailNext(error?: MailError): void;\n\t/** Forgets what was sent, the attempts, and any queued failure. */\n\tclear(): void;\n}\n\n/** Creates a {@link MemoryMailer}. Message ids are `memory-1`, `memory-2`, … */\nexport function createMemoryMailer(): MemoryMailer {\n\tlet sent: MemoryMail[] = [];\n\tlet failures: MailError[] = [];\n\tlet attempts = 0;\n\tlet counter = 0;\n\n\treturn {\n\t\tget sent() {\n\t\t\treturn sent.map((mail) => structuredClone(mail));\n\t\t},\n\t\tget attempts() {\n\t\t\treturn attempts;\n\t\t},\n\t\tfailNext(error) {\n\t\t\tfailures.push(\n\t\t\t\terror ??\n\t\t\t\t\tnew MailFailure(\n\t\t\t\t\t\t'send: the memory mailer was told to fail this send',\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tcause: new Error('memory mailer: failNext'),\n\t\t\t\t\t\t},\n\t\t\t\t\t),\n\t\t\t);\n\t\t},\n\t\tclear() {\n\t\t\tsent = [];\n\t\t\tfailures = [];\n\t\t\tattempts = 0;\n\t\t},\n\t\tasync send(message): Promise<SentMail> {\n\t\t\tcheckMessage(message);\n\t\t\tattempts += 1;\n\t\t\tconst failure = failures.shift();\n\t\t\tif (failure !== undefined) throw failure;\n\n\t\t\tcounter += 1;\n\t\t\tconst messageId = `memory-${counter}`;\n\t\t\tsent.push({ ...structuredClone(message), messageId });\n\t\t\treturn { messageId };\n\t\t},\n\t};\n}\n"
7
+ ],
8
+ "mappings": ";;;;;;AAGA,IAAM,aAAa;AAMnB,IAAM,UAAU;AAChB,IAAM,cAAc;AAIpB,IAAM,kBACL;AAGM,SAAS,aAAY,CAAC,SAAgC;AAAA,EAC5D,MAAM,KAAK,MAAM,QAAQ,QAAQ,EAAE,IAAI,QAAQ,KAAK,CAAC,QAAQ,EAAE;AAAA,EAC/D,OAAO,GAAG,IAAI,UAAS;AAAA;AAIjB,SAAS,UAAS,CAAC,SAA0B;AAAA,EACnD,OAAO,OAAO,YAAY,WAAW,UAAU,QAAQ;AAAA;AAIxD,SAAS,YAAY,CAAC,SAA8B,OAAqB;AAAA,EACxE,IAAI,OAAO,YAAY,UAAU;AAAA,IAChC,IAAI,CAAC,QAAQ,KAAK,OAAO,GAAG;AAAA,MAC3B,MAAM,IAAI,aAAY,SAAS,gCAAgC;AAAA,IAChE;AAAA,IACA;AAAA,EACD;AAAA,EACA,IAAI,OAAO,YAAY,YAAY,YAAY,MAAM;AAAA,IACpD,MAAM,IAAI,aAAY,SAAS,gCAAgC;AAAA,EAChE;AAAA,EACA,IAAI,OAAO,QAAQ,YAAY,YAAY,CAAC,QAAQ,KAAK,QAAQ,OAAO,GAAG;AAAA,IAC1E,MAAM,IAAI,aAAY,SAAS,wCAAwC;AAAA,EACxE;AAAA,EACA,IAAI,OAAO,QAAQ,SAAS,YAAY,WAAW,KAAK,QAAQ,IAAI,GAAG;AAAA,IACtE,MAAM,IAAI,aACT,SAAS,kDACV;AAAA,EACD;AAAA;AAmBM,SAAS,aAAY,CAAC,SAA4B;AAAA,EACxD,IAAI,OAAO,YAAY,YAAY,YAAY,MAAM;AAAA,IACpD,MAAM,IAAI,aAAY,qCAAqC;AAAA,EAC5D;AAAA,EACA,IAAI,QAAQ,OAAO,aAAa,QAAQ,OAAO,MAAM;AAAA,IACpD,MAAM,IAAI,aAAY,yCAAyC;AAAA,EAChE;AAAA,EACA,MAAM,KAAK,MAAM,QAAQ,QAAQ,EAAE,IAAI,QAAQ,KAAK,CAAC,QAAQ,EAAE;AAAA,EAC/D,IAAI,GAAG,WAAW,GAAG;AAAA,IACpB,MAAM,IAAI,aAAY,yCAAyC;AAAA,EAChE;AAAA,EACA,GAAG,QAAQ,CAAC,SAAS,UAAU;AAAA,IAC9B,aAAa,SAAS,MAAM,QAAQ,QAAQ,EAAE,IAAI,MAAM,WAAW,IAAI;AAAA,GACvE;AAAA,EACD,IAAI,QAAQ,SAAS;AAAA,IAAW,aAAa,QAAQ,MAAM,MAAM;AAAA,EACjE,IAAI,QAAQ,YAAY;AAAA,IAAW,aAAa,QAAQ,SAAS,SAAS;AAAA,EAE1E,WAAW,QAAQ,CAAC,WAAW,QAAQ,MAAM,GAAY;AAAA,IACxD,IAAI,OAAO,QAAQ,UAAU,UAAU;AAAA,MACtC,MAAM,IAAI,aAAY,SAAS,uBAAuB;AAAA,IACvD;AAAA,EACD;AAAA,EACA,IAAI,WAAW,KAAK,QAAQ,OAAO,GAAG;AAAA,IACrC,MAAM,IAAI,aAAY,0CAA0C;AAAA,EACjE;AAAA,EAEA,YAAY,MAAM,UAAU,OAAO,QAAQ,QAAQ,WAAW,CAAC,CAAC,GAAG;AAAA,IAClE,IAAI,CAAC,YAAY,KAAK,IAAI,GAAG;AAAA,MAC5B,MAAM,IAAI,aACT,yDACD;AAAA,IACD;AAAA,IACA,IAAI,gBAAgB,KAAK,IAAI,GAAG;AAAA,MAC/B,MAAM,IAAI,aACT,gBAAgB,2FACjB;AAAA,IACD;AAAA,IACA,IAAI,OAAO,UAAU,YAAY,WAAW,KAAK,KAAK,GAAG;AAAA,MACxD,MAAM,IAAI,aACT,gBAAgB,4CACjB;AAAA,IACD;AAAA,EACD;AAAA;;;ACvEM,SAAS,mBAAkB,GAAiB;AAAA,EAClD,IAAI,OAAqB,CAAC;AAAA,EAC1B,IAAI,WAAwB,CAAC;AAAA,EAC7B,IAAI,WAAW;AAAA,EACf,IAAI,UAAU;AAAA,EAEd,OAAO;AAAA,QACF,IAAI,GAAG;AAAA,MACV,OAAO,KAAK,IAAI,CAAC,SAAS,gBAAgB,IAAI,CAAC;AAAA;AAAA,QAE5C,QAAQ,GAAG;AAAA,MACd,OAAO;AAAA;AAAA,IAER,QAAQ,CAAC,OAAO;AAAA,MACf,SAAS,KACR,SACC,IAAI,aACH,sDACA;AAAA,QACC,OAAO,IAAI,MAAM,yBAAyB;AAAA,MAC3C,CACD,CACF;AAAA;AAAA,IAED,KAAK,GAAG;AAAA,MACP,OAAO,CAAC;AAAA,MACR,WAAW,CAAC;AAAA,MACZ,WAAW;AAAA;AAAA,SAEN,KAAI,CAAC,SAA4B;AAAA,MACtC,cAAa,OAAO;AAAA,MACpB,YAAY;AAAA,MACZ,MAAM,UAAU,SAAS,MAAM;AAAA,MAC/B,IAAI,YAAY;AAAA,QAAW,MAAM;AAAA,MAEjC,WAAW;AAAA,MACX,MAAM,YAAY,UAAU;AAAA,MAC5B,KAAK,KAAK,KAAK,gBAAgB,OAAO,GAAG,UAAU,CAAC;AAAA,MACpD,OAAO,EAAE,UAAU;AAAA;AAAA,EAErB;AAAA;",
9
+ "debugId": "44B7D62837EFA8BB64756E2164756E21",
10
+ "names": []
11
+ }
@@ -0,0 +1,39 @@
1
+ // src/locale.ts
2
+ var normalise = (locale) => locale.trim().replace(/_/g, "-").toLowerCase();
3
+ var languageOf = (locale) => normalise(locale).split("-")[0] ?? "";
4
+ function pickLocale2(wanted, supported, fallback) {
5
+ if (supported.length === 0) {
6
+ throw new TypeError("pickLocale: supported must hold at least one locale");
7
+ }
8
+ if (!supported.includes(fallback)) {
9
+ throw new TypeError("pickLocale: fallback must be one of supported");
10
+ }
11
+ const list = Array.isArray(wanted) ? wanted : [wanted];
12
+ for (const locale of list) {
13
+ if (typeof locale !== "string" || locale.trim() === "")
14
+ continue;
15
+ const exact = supported.find((s) => normalise(s) === normalise(locale));
16
+ if (exact !== undefined)
17
+ return exact;
18
+ const language = languageOf(locale);
19
+ const sameLanguage = supported.find((s) => normalise(s) === language) ?? supported.find((s) => languageOf(s) === language);
20
+ if (sameLanguage !== undefined)
21
+ return sameLanguage;
22
+ }
23
+ return fallback;
24
+ }
25
+ function parseAcceptLanguage2(header) {
26
+ if (typeof header !== "string")
27
+ return [];
28
+ return header.split(",").map((entry, index) => {
29
+ const [tag = "", ...params] = entry.split(";").map((p) => p.trim());
30
+ const q = params.map((p) => /^q=([0-9.]+)$/i.exec(p)?.[1]).find((v) => v !== undefined);
31
+ const weight = q === undefined ? 1 : Number(q);
32
+ return { tag, weight: Number.isNaN(weight) ? 0 : weight, index };
33
+ }).filter(({ tag, weight }) => tag !== "" && tag !== "*" && weight > 0).sort((a, b) => b.weight - a.weight || a.index - b.index).map(({ tag }) => tag);
34
+ }
35
+
36
+ export { pickLocale2, parseAcceptLanguage2 };
37
+
38
+ //# debugId=17ED638EE74DEAA764756E2164756E21
39
+ //# sourceMappingURL=index-vq4e9n8f.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/locale.ts"],
4
+ "sourcesContent": [
5
+ "/** What {@link pickLocale} accepts as the wanted locales. */\nexport type WantedLocales =\n\t| string\n\t| null\n\t| undefined\n\t| readonly (string | null | undefined)[];\n\nconst normalise = (locale: string) =>\n\tlocale.trim().replace(/_/g, '-').toLowerCase();\nconst languageOf = (locale: string) => normalise(locale).split('-')[0] ?? '';\n\n/**\n * The locale to render an e-mail in: the first wanted locale this build\n * supports, or `fallback`.\n *\n * `wanted` is in order of preference — typically the recipient's stored\n * locale, then their `Accept-Language` (see {@link parseAcceptLanguage}).\n * For each wanted locale in turn, an exact match wins (case and `_` or `-`\n * do not matter), then a match on the language alone: `fr-CA` picks `fr`,\n * and `fr` picks `fr-CA` when that is the only French supported. Nothing\n * matching, or nothing wanted, answers `fallback`.\n *\n * Pure, with no request context: **the locale of an e-mail is the\n * recipient's**, usually a field of the user, and not the language of the\n * request that triggered the send.\n *\n * Throws a `TypeError` when `supported` is empty or does not hold `fallback`.\n * That is a wiring mistake, not a request's.\n */\nexport function pickLocale<const L extends string>(\n\twanted: WantedLocales,\n\tsupported: readonly L[],\n\tfallback: NoInfer<L>,\n): L {\n\tif (supported.length === 0) {\n\t\tthrow new TypeError('pickLocale: supported must hold at least one locale');\n\t}\n\tif (!supported.includes(fallback)) {\n\t\tthrow new TypeError('pickLocale: fallback must be one of supported');\n\t}\n\n\tconst list = Array.isArray(wanted) ? wanted : [wanted];\n\tfor (const locale of list) {\n\t\tif (typeof locale !== 'string' || locale.trim() === '') continue;\n\t\tconst exact = supported.find((s) => normalise(s) === normalise(locale));\n\t\tif (exact !== undefined) return exact;\n\t\tconst language = languageOf(locale);\n\t\tconst sameLanguage =\n\t\t\tsupported.find((s) => normalise(s) === language) ??\n\t\t\tsupported.find((s) => languageOf(s) === language);\n\t\tif (sameLanguage !== undefined) return sameLanguage;\n\t}\n\treturn fallback;\n}\n\n/**\n * The locales of an `Accept-Language` header, most wanted first.\n *\n * Entries are ordered by their `q` weight, ties keeping the header's order;\n * `q=0` entries and `*` are dropped. A missing or empty header answers `[]`.\n *\n * ```ts\n * parseAcceptLanguage('fr-CA,fr;q=0.9,en;q=0.8'); // ['fr-CA', 'fr', 'en']\n * ```\n */\nexport function parseAcceptLanguage(\n\theader: string | null | undefined,\n): string[] {\n\tif (typeof header !== 'string') return [];\n\treturn header\n\t\t.split(',')\n\t\t.map((entry, index) => {\n\t\t\tconst [tag = '', ...params] = entry.split(';').map((p) => p.trim());\n\t\t\tconst q = params\n\t\t\t\t.map((p) => /^q=([0-9.]+)$/i.exec(p)?.[1])\n\t\t\t\t.find((v) => v !== undefined);\n\t\t\tconst weight = q === undefined ? 1 : Number(q);\n\t\t\treturn { tag, weight: Number.isNaN(weight) ? 0 : weight, index };\n\t\t})\n\t\t.filter(({ tag, weight }) => tag !== '' && tag !== '*' && weight > 0)\n\t\t.sort((a, b) => b.weight - a.weight || a.index - b.index)\n\t\t.map(({ tag }) => tag);\n}\n"
6
+ ],
7
+ "mappings": ";AAOA,IAAM,YAAY,CAAC,WAClB,OAAO,KAAK,EAAE,QAAQ,MAAM,GAAG,EAAE,YAAY;AAC9C,IAAM,aAAa,CAAC,WAAmB,UAAU,MAAM,EAAE,MAAM,GAAG,EAAE,MAAM;AAoBnE,SAAS,WAAkC,CACjD,QACA,WACA,UACI;AAAA,EACJ,IAAI,UAAU,WAAW,GAAG;AAAA,IAC3B,MAAM,IAAI,UAAU,qDAAqD;AAAA,EAC1E;AAAA,EACA,IAAI,CAAC,UAAU,SAAS,QAAQ,GAAG;AAAA,IAClC,MAAM,IAAI,UAAU,+CAA+C;AAAA,EACpE;AAAA,EAEA,MAAM,OAAO,MAAM,QAAQ,MAAM,IAAI,SAAS,CAAC,MAAM;AAAA,EACrD,WAAW,UAAU,MAAM;AAAA,IAC1B,IAAI,OAAO,WAAW,YAAY,OAAO,KAAK,MAAM;AAAA,MAAI;AAAA,IACxD,MAAM,QAAQ,UAAU,KAAK,CAAC,MAAM,UAAU,CAAC,MAAM,UAAU,MAAM,CAAC;AAAA,IACtE,IAAI,UAAU;AAAA,MAAW,OAAO;AAAA,IAChC,MAAM,WAAW,WAAW,MAAM;AAAA,IAClC,MAAM,eACL,UAAU,KAAK,CAAC,MAAM,UAAU,CAAC,MAAM,QAAQ,KAC/C,UAAU,KAAK,CAAC,MAAM,WAAW,CAAC,MAAM,QAAQ;AAAA,IACjD,IAAI,iBAAiB;AAAA,MAAW,OAAO;AAAA,EACxC;AAAA,EACA,OAAO;AAAA;AAaD,SAAS,oBAAmB,CAClC,QACW;AAAA,EACX,IAAI,OAAO,WAAW;AAAA,IAAU,OAAO,CAAC;AAAA,EACxC,OAAO,OACL,MAAM,GAAG,EACT,IAAI,CAAC,OAAO,UAAU;AAAA,IACtB,OAAO,MAAM,OAAO,UAAU,MAAM,MAAM,GAAG,EAAE,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC;AAAA,IAClE,MAAM,IAAI,OACR,IAAI,CAAC,MAAM,iBAAiB,KAAK,CAAC,IAAI,EAAE,EACxC,KAAK,CAAC,MAAM,MAAM,SAAS;AAAA,IAC7B,MAAM,SAAS,MAAM,YAAY,IAAI,OAAO,CAAC;AAAA,IAC7C,OAAO,EAAE,KAAK,QAAQ,OAAO,MAAM,MAAM,IAAI,IAAI,QAAQ,MAAM;AAAA,GAC/D,EACA,OAAO,GAAG,KAAK,aAAa,QAAQ,MAAM,QAAQ,OAAO,SAAS,CAAC,EACnE,KAAK,CAAC,GAAG,MAAM,EAAE,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE,KAAK,EACvD,IAAI,GAAG,UAAU,GAAG;AAAA;",
8
+ "debugId": "17ED638EE74DEAA764756E2164756E21",
9
+ "names": []
10
+ }
@@ -0,0 +1,28 @@
1
+ // src/errors.ts
2
+ class MailError2 extends Error {
3
+ constructor(message, options) {
4
+ super(message, { cause: options?.cause });
5
+ this.name = "MailError";
6
+ }
7
+ }
8
+
9
+ class MailFailure2 extends MailError2 {
10
+ constructor() {
11
+ super(...arguments);
12
+ this.name = "MailFailure";
13
+ this.code = "MAIL_FAILED";
14
+ }
15
+ }
16
+
17
+ class MailRefused2 extends MailError2 {
18
+ constructor() {
19
+ super(...arguments);
20
+ this.name = "MailRefused";
21
+ this.code = "MAIL_REFUSED";
22
+ }
23
+ }
24
+
25
+ export { MailError2, MailFailure2, MailRefused2 };
26
+
27
+ //# debugId=5753AA1D988C668964756E2164756E21
28
+ //# sourceMappingURL=index-we4n5yfz.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/errors.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * What a transport refuses, as a string a caller can switch on.\n *\n * Every code is a **refusal at call time**. A refusal that can only come from\n * how the application was wired — a bad option passed to a factory — is a\n * bare `TypeError` instead: no handler should ever answer one.\n *\n * The codes are `SCREAMING_SNAKE` because they are data values, not API\n * identifiers. Every key in this package is `camelCase`.\n */\nexport type MailErrorCode =\n\t/**\n\t * The transport could not hand the message over: a refused connection, a\n\t * timeout, a 5xx from the provider, an expired credential. The transport's\n\t * own error is the `cause`.\n\t *\n\t * **Nothing is known to have been sent** — after a timeout or a dropped\n\t * connection, the provider may have taken it all the same. Retry later,\n\t * or tell the user it failed. Never report it as sent.\n\t */\n\t| 'MAIL_FAILED'\n\t/**\n\t * The message itself was refused, before or by the transport: no\n\t * recipient, something that is not an address, a line break in the\n\t * subject or a header, a provider answering that the message is\n\t * malformed, or — from `@nxgt/mail/renderer` — a URL variable that is not\n\t * an `http:`, `https:` or `mailto:` URL. Sending it again unchanged fails\n\t * again.\n\t */\n\t| 'MAIL_REFUSED';\n\n/** Options every error of this package accepts. */\nexport interface MailErrorOptions {\n\t/** The error that caused this one, typically the transport's. */\n\treadonly cause?: unknown;\n}\n\n/**\n * The base class of every error this package throws at call time. It is\n * abstract: a transport throws {@link MailFailure} or {@link MailRefused}.\n *\n * **There is exactly one definition of this class.** A transport defines no\n * error class of its own and throws these, imported from its `@nxgt/mail`\n * peer, so `error instanceof MailFailure` holds whatever transport threw it.\n *\n * A message reports **a shape, never a value**: never a recipient address,\n * never a subject, never a link — the link in a verification e-mail is a\n * credential.\n */\nexport abstract class MailError extends Error {\n\toverride name = 'MailError';\n\t/**\n\t * Abstract, so a transport cannot throw a bare `MailError` that passes a\n\t * `code` check and fails `instanceof MailFailure`: it throws one of the two\n\t * subclasses.\n\t */\n\tabstract readonly code: MailErrorCode;\n\n\tconstructor(message: string, options?: MailErrorOptions) {\n\t\tsuper(message, { cause: options?.cause });\n\t}\n}\n\n/** The transport could not hand the message over. Code `MAIL_FAILED`. */\nexport class MailFailure extends MailError {\n\toverride name = 'MailFailure';\n\toverride readonly code = 'MAIL_FAILED' as const;\n}\n\n/** The message was refused as malformed. Code `MAIL_REFUSED`. */\nexport class MailRefused extends MailError {\n\toverride name = 'MailRefused';\n\toverride readonly code = 'MAIL_REFUSED' as const;\n}\n"
6
+ ],
7
+ "mappings": ";AAiDO,MAAe,mBAAkB,MAAM;AAAA,EAS7C,WAAW,CAAC,SAAiB,SAA4B;AAAA,IACxD,MAAM,SAAS,EAAE,OAAO,SAAS,MAAM,CAAC;AAAA,IAThC,YAAO;AAAA;AAWjB;AAAA;AAGO,MAAM,qBAAoB,WAAU;AAAA;AAAA;AAAA,IACjC,YAAO;AAAA,IACE,YAAO;AAAA;AAC1B;AAAA;AAGO,MAAM,qBAAoB,WAAU;AAAA;AAAA;AAAA,IACjC,YAAO;AAAA,IACE,YAAO;AAAA;AAC1B;",
8
+ "debugId": "5753AA1D988C668964756E2164756E21",
9
+ "names": []
10
+ }
@@ -0,0 +1,12 @@
1
+ import type { MailerCaseContext } from './types';
2
+ /** Throws when `condition` is false. The suite depends on no assertion library. */
3
+ export declare function check(condition: boolean, what: string): asserts condition;
4
+ export declare const same: (a: unknown, b: unknown) => boolean;
5
+ /**
6
+ * Settles an expected rejection where it is created, and answers the error —
7
+ * or throws when the promise resolved.
8
+ */
9
+ export declare function rejection(promise: Promise<unknown>, what: string): Promise<unknown>;
10
+ /** Throws when the receiving end got anything. */
11
+ export declare function nothingDelivered(context: MailerCaseContext, what: string): Promise<void>;
12
+ //# sourceMappingURL=assert.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assert.d.ts","sourceRoot":"","sources":["../../src/conformance/assert.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAEjD,mFAAmF;AACnF,wBAAgB,KAAK,CAAC,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAEzE;AAED,eAAO,MAAM,IAAI,GAAI,GAAG,OAAO,EAAE,GAAG,OAAO,YACH,CAAC;AAEzC;;;GAGG;AACH,wBAAsB,SAAS,CAC9B,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,EACzB,IAAI,EAAE,MAAM,GACV,OAAO,CAAC,OAAO,CAAC,CAOlB;AAED,kDAAkD;AAClD,wBAAsB,gBAAgB,CACrC,OAAO,EAAE,iBAAiB,EAC1B,IAAI,EAAE,MAAM,iBAMZ"}
@@ -0,0 +1,4 @@
1
+ import type { MailerCase } from '../types';
2
+ /** What a send must do when the transport fails. Needs {@link MailerFaults}. */
3
+ export declare const failureCases: readonly MailerCase[];
4
+ //# sourceMappingURL=failure.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"failure.d.ts","sourceRoot":"","sources":["../../../src/conformance/cases/failure.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAE3C,gFAAgF;AAChF,eAAO,MAAM,YAAY,EAAE,SAAS,UAAU,EAqF7C,CAAC"}
@@ -0,0 +1,6 @@
1
+ import type { MailerCase } from '../types';
2
+ export { failureCases } from './failure';
3
+ export { sendCases } from './send';
4
+ /** Every case, in the order they are described. */
5
+ export declare const allMailerCases: readonly MailerCase[];
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/conformance/cases/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAI3C,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AAEnC,mDAAmD;AACnD,eAAO,MAAM,cAAc,EAAE,SAAS,UAAU,EAG/C,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { MailerCase } from '../types';
2
+ /** What every send must do, with no fault injected. */
3
+ export declare const sendCases: readonly MailerCase[];
4
+ //# sourceMappingURL=send.d.ts.map