@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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"send.d.ts","sourceRoot":"","sources":["../../../src/conformance/cases/send.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAE3C,uDAAuD;AACvD,eAAO,MAAM,SAAS,EAAE,SAAS,UAAU,EAiK1C,CAAC"}
@@ -0,0 +1,37 @@
1
+ import type { MailerCase, MailerHarness, MailerRunner } from './types';
2
+ /** Why a case did not run. A skip is always reported with its reason, never silent. */
3
+ export declare const MAILER_SKIP_REASONS: {
4
+ readonly faults: "faults not provided: the failure contract is not proven for this transport";
5
+ };
6
+ /**
7
+ * Runs one case against a freshly opened transport, and closes it, pass or
8
+ * fail. Answers the reason when the case cannot run on this harness.
9
+ */
10
+ export declare function runMailerCase(mailerCase: MailerCase, harness: MailerHarness): Promise<{
11
+ readonly skipped: string;
12
+ } | {
13
+ readonly passed: true;
14
+ }>;
15
+ /**
16
+ * Describes every case against one transport, under bun:test, vitest or jest.
17
+ *
18
+ * ```ts
19
+ * import { describe, it } from 'bun:test';
20
+ * import { describeMailer } from '@nxgt/mail/conformance';
21
+ *
22
+ * describeMailer({ name: 'my transport', harness, runner: { describe, it } });
23
+ * ```
24
+ *
25
+ * `runner` defaults to the global `describe` and `it`, when the framework
26
+ * defines them.
27
+ */
28
+ export declare function describeMailer(options: {
29
+ readonly name: string;
30
+ readonly harness: MailerHarness;
31
+ readonly runner?: MailerRunner;
32
+ /** Case ids to skip, each with the reason — reported, never silent. */
33
+ readonly skip?: Readonly<Record<string, string>>;
34
+ /** Declared up front, so a missing `faults` is reported before the first case runs. */
35
+ readonly faults?: boolean;
36
+ }): void;
37
+ //# sourceMappingURL=describe.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"describe.d.ts","sourceRoot":"","sources":["../../src/conformance/describe.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEvE,uFAAuF;AACvF,eAAO,MAAM,mBAAmB;;CAGtB,CAAC;AAEX;;;GAGG;AACH,wBAAsB,aAAa,CAClC,UAAU,EAAE,UAAU,EACtB,OAAO,EAAE,aAAa,GACpB,OAAO,CAAC;IAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAA;CAAE,CAAC,CAyBnE;AAYD;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE;IACvC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC;IAChC,QAAQ,CAAC,MAAM,CAAC,EAAE,YAAY,CAAC;IAC/B,uEAAuE;IACvE,QAAQ,CAAC,IAAI,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACjD,uFAAuF;IACvF,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;CAC1B,GAAG,IAAI,CA+BP"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * `@nxgt/mail/conformance` — **what makes the transport contract checkable.**
3
+ *
4
+ * A transport author runs this against their transport, and it fails one that
5
+ * breaks the contract of the port — above all *a failure throws*: an outage
6
+ * that resolves, or a transport that retries in secret, or its own copy of
7
+ * `MailFailure` that `instanceof` rejects.
8
+ *
9
+ * Three layers, and the lowest depends on no test runner:
10
+ *
11
+ * - the cases, as data — `sendCases`, `failureCases`, `allMailerCases`;
12
+ * - `runMailerCase`, which runs one against a harness;
13
+ * - `describeMailer`, which describes them all under bun:test, vitest or jest.
14
+ */
15
+ export { allMailerCases, failureCases, sendCases } from './cases/index';
16
+ export { describeMailer, MAILER_SKIP_REASONS, runMailerCase, } from './describe';
17
+ export { referenceMailerHarness } from './reference';
18
+ export { sampleMessage } from './sample';
19
+ export type { DeliveredMail, MailerCase, MailerCaseContext, MailerFaults, MailerHarness, MailerRunner, OpenedMailer, } from './types';
20
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/conformance/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AACxE,OAAO,EACN,cAAc,EACd,mBAAmB,EACnB,aAAa,GACb,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,YAAY,EACX,aAAa,EACb,UAAU,EACV,iBAAiB,EACjB,YAAY,EACZ,aAAa,EACb,YAAY,EACZ,YAAY,GACZ,MAAM,SAAS,CAAC"}
@@ -0,0 +1,297 @@
1
+ import {
2
+ recipientsOf2,
3
+ createMemoryMailer2
4
+ } from "../chunks/index-0f7kdb8k.js";
5
+ import {
6
+ MailError2,
7
+ MailFailure2,
8
+ MailRefused2
9
+ } from "../chunks/index-we4n5yfz.js";
10
+
11
+ // src/conformance/assert.ts
12
+ function check(condition, what) {
13
+ if (!condition)
14
+ throw new Error(`conformance: ${what}`);
15
+ }
16
+ var same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
17
+ async function rejection(promise, what) {
18
+ return promise.then(() => {
19
+ throw new Error(`conformance: ${what} resolved; it must reject`);
20
+ }, (error) => error);
21
+ }
22
+ async function nothingDelivered(context, what) {
23
+ check((await context.delivered()).length === 0, `${what}, yet something was delivered`);
24
+ }
25
+
26
+ // src/conformance/sample.ts
27
+ var sampleMessage = {
28
+ to: "ada@example.test",
29
+ from: "noreply@example.test",
30
+ subject: "Réinitialisez votre mot de passe — ça expire à 23 h",
31
+ html: '<p>Bonjour Ada \uD83D\uDC4B, <a href="https://example.test/r?t=abc&amp;x=1">réinitialiser</a></p>',
32
+ text: `Bonjour Ada \uD83D\uDC4B,
33
+
34
+ réinitialiser : https://example.test/r?t=abc&x=1
35
+ `
36
+ };
37
+
38
+ // src/conformance/cases/failure.ts
39
+ var failureCases = [
40
+ {
41
+ id: "failure.outage",
42
+ title: "an outage throws MailFailure with the cause, and nothing is retried",
43
+ needs: "faults",
44
+ async run(context) {
45
+ const faults = context.faults;
46
+ check(faults !== null, "faults are required");
47
+ await faults.failNext("outage");
48
+ const error = await rejection(context.mailer.send(sampleMessage), "a send during an outage");
49
+ check(error instanceof MailFailure2, "an outage must throw MailFailure from @nxgt/mail");
50
+ check(error instanceof MailError2, "MailFailure must extend MailError");
51
+ check(error.code === "MAIL_FAILED", "an outage must carry the code MAIL_FAILED");
52
+ check(error.cause !== undefined, "an outage must carry the transport's error as cause");
53
+ check(await faults.attempts() === 1, "the transport retried a failed hand-over");
54
+ await nothingDelivered(context, "the hand-over failed");
55
+ }
56
+ },
57
+ {
58
+ id: "failure.refusal",
59
+ title: "a message the provider refuses throws MailRefused with the cause",
60
+ needs: "faults",
61
+ async run(context) {
62
+ const faults = context.faults;
63
+ check(faults !== null, "faults are required");
64
+ await faults.failNext("refusal");
65
+ const error = await rejection(context.mailer.send(sampleMessage), "a refused send");
66
+ check(error instanceof MailRefused2, "a refusal must throw MailRefused from @nxgt/mail");
67
+ check(error.code === "MAIL_REFUSED", "a refusal must carry the code MAIL_REFUSED");
68
+ check(error.cause !== undefined, "a refusal must carry the transport's error as cause");
69
+ check(await faults.attempts() === 1, "the transport retried a refused message");
70
+ }
71
+ },
72
+ {
73
+ id: "failure.recovers",
74
+ title: "after a failure, the next send goes through",
75
+ needs: "faults",
76
+ async run(context) {
77
+ const faults = context.faults;
78
+ check(faults !== null, "faults are required");
79
+ await faults.failNext("outage");
80
+ await rejection(context.mailer.send(sampleMessage), "a send during an outage");
81
+ await context.mailer.send(sampleMessage);
82
+ check((await context.delivered()).length === 1, "the send after a failure was not delivered");
83
+ }
84
+ }
85
+ ];
86
+
87
+ // src/conformance/cases/send.ts
88
+ var sendCases = [
89
+ {
90
+ id: "send.answersSentMail",
91
+ title: "a send answers SentMail, with a string id or null",
92
+ async run({ mailer }) {
93
+ const sent = await mailer.send(sampleMessage);
94
+ check(typeof sent === "object" && sent !== null && "messageId" in sent, "send did not answer an object with messageId");
95
+ check(sent.messageId === null || typeof sent.messageId === "string" && sent.messageId !== "", "messageId must be a non-empty string or null");
96
+ }
97
+ },
98
+ {
99
+ id: "send.deliversBytes",
100
+ title: "a message is delivered byte for byte: accents, an emoji, a text part",
101
+ async run(context) {
102
+ await context.mailer.send(sampleMessage);
103
+ const delivered = await context.delivered();
104
+ check(delivered.length === 1, `expected 1 delivered message, got ${delivered.length}`);
105
+ const [mail] = delivered;
106
+ check(mail?.subject === sampleMessage.subject, "the subject was not delivered as sent");
107
+ check(mail?.html === sampleMessage.html, "the html part was not delivered as sent");
108
+ check(mail?.text === sampleMessage.text, "the text part was not delivered as sent");
109
+ }
110
+ },
111
+ {
112
+ id: "send.recipients",
113
+ title: "every recipient is delivered to, written as a string or with a name",
114
+ async run(context) {
115
+ await context.mailer.send({
116
+ ...sampleMessage,
117
+ to: [
118
+ "ada@example.test",
119
+ { name: "Grace Hopper", address: "grace@example.test" }
120
+ ]
121
+ });
122
+ const [mail] = await context.delivered();
123
+ check(same(mail?.to, ["ada@example.test", "grace@example.test"]), "the recipients delivered are not the recipients sent");
124
+ }
125
+ },
126
+ {
127
+ id: "send.hostileName",
128
+ title: "a name holding an address and a comma reaches only its own address",
129
+ async run(context) {
130
+ await context.mailer.send({
131
+ ...sampleMessage,
132
+ to: {
133
+ name: 'Ada <mallory@example.test>, "Eve" <eve@example.test>;',
134
+ address: "ada@example.test"
135
+ }
136
+ });
137
+ const [mail] = await context.delivered();
138
+ check(same(mail?.to, ["ada@example.test"]), "a name let a second recipient through");
139
+ }
140
+ },
141
+ {
142
+ id: "send.refusesNoRecipient",
143
+ title: "a message with no recipient is refused with MailRefused, and nothing is sent",
144
+ async run(context) {
145
+ const error = await rejection(context.mailer.send({ ...sampleMessage, to: [] }), "a send with no recipient");
146
+ check(error instanceof MailRefused2, "a send with no recipient must throw MailRefused");
147
+ await nothingDelivered(context, "the message was refused");
148
+ }
149
+ },
150
+ {
151
+ id: "send.refusesLineBreakInSubject",
152
+ title: "a line break in the subject is refused with MailRefused: it is a header injection",
153
+ async run(context) {
154
+ const error = await rejection(context.mailer.send({
155
+ ...sampleMessage,
156
+ subject: `Hello\r
157
+ Bcc: eve@example.test`
158
+ }), "a send with a line break in the subject");
159
+ check(error instanceof MailRefused2, "a line break in the subject must throw MailRefused");
160
+ await nothingDelivered(context, "the message was refused");
161
+ }
162
+ },
163
+ {
164
+ id: "send.refusesAddressHeader",
165
+ title: "a Bcc among the custom headers is refused with MailRefused: it would add an unchecked recipient",
166
+ async run(context) {
167
+ const error = await rejection(context.mailer.send({
168
+ ...sampleMessage,
169
+ headers: { Bcc: "eve@example.test" }
170
+ }), "a send with a Bcc header");
171
+ check(error instanceof MailRefused2, "a Bcc header must throw MailRefused");
172
+ check(!error.message.includes("eve@example.test"), "the refusal message holds the refused value");
173
+ await nothingDelivered(context, "the message was refused");
174
+ }
175
+ },
176
+ {
177
+ id: "send.refusesWithoutTheValue",
178
+ title: "a refusal names where the problem is, never the value",
179
+ async run({ mailer }) {
180
+ const error = await rejection(mailer.send({ ...sampleMessage, to: "not-an-address-7f3a" }), "a send to something that is not an address");
181
+ check(error instanceof MailRefused2, "a malformed address must throw MailRefused");
182
+ check(!error.message.includes("not-an-address-7f3a"), "the refusal message holds the refused value");
183
+ }
184
+ }
185
+ ];
186
+
187
+ // src/conformance/cases/index.ts
188
+ var allMailerCases = [
189
+ ...sendCases,
190
+ ...failureCases
191
+ ];
192
+ // src/conformance/describe.ts
193
+ var MAILER_SKIP_REASONS = {
194
+ faults: "faults not provided: the failure contract is not proven for this transport"
195
+ };
196
+ async function runMailerCase(mailerCase, harness) {
197
+ const opened = await harness.open();
198
+ let outcome;
199
+ try {
200
+ if (mailerCase.needs === "faults" && opened.faults === undefined) {
201
+ outcome = { skipped: MAILER_SKIP_REASONS.faults };
202
+ } else {
203
+ await mailerCase.run({
204
+ mailer: opened.mailer,
205
+ delivered: () => opened.delivered(),
206
+ faults: opened.faults ?? null
207
+ });
208
+ outcome = { passed: true };
209
+ }
210
+ } catch (error) {
211
+ await opened.close?.().then(() => {
212
+ return;
213
+ }, () => {
214
+ return;
215
+ });
216
+ throw error;
217
+ }
218
+ await opened.close?.();
219
+ return outcome;
220
+ }
221
+ function globalRunner() {
222
+ const { describe, it } = globalThis;
223
+ if (typeof describe !== "function" || typeof it !== "function") {
224
+ throw new TypeError("describeMailer: no test runner found — pass runner: { describe, it } from your test framework");
225
+ }
226
+ return { describe, it };
227
+ }
228
+ function describeMailer(options) {
229
+ const runner = options.runner ?? globalRunner();
230
+ const skip = options.skip ?? {};
231
+ for (const id of Object.keys(skip)) {
232
+ if (!allMailerCases.some((c) => c.id === id)) {
233
+ throw new TypeError(`describeMailer: skip names no case: ${id}`);
234
+ }
235
+ }
236
+ runner.describe(`${options.name} — @nxgt/mail conformance`, () => {
237
+ for (const mailerCase of allMailerCases) {
238
+ const title = `${mailerCase.id}: ${mailerCase.title}`;
239
+ const reason = skip[mailerCase.id] ?? (mailerCase.needs === "faults" && options.faults === false ? MAILER_SKIP_REASONS.faults : undefined);
240
+ if (reason !== undefined) {
241
+ runner.it.skip(`${title} (skipped: ${reason})`, async () => {});
242
+ continue;
243
+ }
244
+ runner.it(title, async () => {
245
+ const result = await runMailerCase(mailerCase, options.harness);
246
+ if ("skipped" in result) {
247
+ throw new Error(`conformance: ${mailerCase.id}: ${result.skipped} — pass faults: false to describeMailer to skip it on purpose`);
248
+ }
249
+ });
250
+ }
251
+ });
252
+ }
253
+ // src/conformance/reference.ts
254
+ function referenceMailerHarness() {
255
+ return {
256
+ async open() {
257
+ const mailer = createMemoryMailer2();
258
+ return {
259
+ mailer,
260
+ async delivered() {
261
+ return mailer.sent.map((mail) => ({
262
+ to: recipientsOf2(mail),
263
+ subject: mail.subject,
264
+ html: mail.html,
265
+ text: mail.text
266
+ }));
267
+ },
268
+ faults: {
269
+ async failNext(kind) {
270
+ const cause = new Error(`simulated ${kind}`);
271
+ mailer.failNext(kind === "outage" ? new MailFailure2("send: the transport could not be reached", {
272
+ cause
273
+ }) : new MailRefused2("send: the transport refused the message", {
274
+ cause
275
+ }));
276
+ },
277
+ async attempts() {
278
+ return mailer.attempts;
279
+ }
280
+ }
281
+ };
282
+ }
283
+ };
284
+ }
285
+ export {
286
+ MAILER_SKIP_REASONS,
287
+ allMailerCases,
288
+ describeMailer,
289
+ failureCases,
290
+ referenceMailerHarness,
291
+ runMailerCase,
292
+ sampleMessage,
293
+ sendCases
294
+ };
295
+
296
+ //# debugId=761673AB21F5667764756E2164756E21
297
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,16 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/conformance/assert.ts", "../src/conformance/sample.ts", "../src/conformance/cases/failure.ts", "../src/conformance/cases/send.ts", "../src/conformance/cases/index.ts", "../src/conformance/describe.ts", "../src/conformance/reference.ts"],
4
+ "sourcesContent": [
5
+ "import type { MailerCaseContext } from './types';\n\n/** Throws when `condition` is false. The suite depends on no assertion library. */\nexport function check(condition: boolean, what: string): asserts condition {\n\tif (!condition) throw new Error(`conformance: ${what}`);\n}\n\nexport const same = (a: unknown, b: unknown) =>\n\tJSON.stringify(a) === JSON.stringify(b);\n\n/**\n * Settles an expected rejection where it is created, and answers the error —\n * or throws when the promise resolved.\n */\nexport async function rejection(\n\tpromise: Promise<unknown>,\n\twhat: string,\n): Promise<unknown> {\n\treturn promise.then(\n\t\t() => {\n\t\t\tthrow new Error(`conformance: ${what} resolved; it must reject`);\n\t\t},\n\t\t(error: unknown) => error,\n\t);\n}\n\n/** Throws when the receiving end got anything. */\nexport async function nothingDelivered(\n\tcontext: MailerCaseContext,\n\twhat: string,\n) {\n\tcheck(\n\t\t(await context.delivered()).length === 0,\n\t\t`${what}, yet something was delivered`,\n\t);\n}\n",
6
+ "import type { MailMessage } from '../types';\n\n/** A message with the characters a transport most often mangles. */\nexport const sampleMessage: MailMessage = {\n\tto: 'ada@example.test',\n\tfrom: 'noreply@example.test',\n\tsubject: 'Réinitialisez votre mot de passe — ça expire à 23 h',\n\thtml: '<p>Bonjour Ada 👋, <a href=\"https://example.test/r?t=abc&amp;x=1\">réinitialiser</a></p>',\n\ttext: 'Bonjour Ada 👋,\\n\\nréinitialiser : https://example.test/r?t=abc&x=1\\n',\n};\n",
7
+ "import { MailError, MailFailure, MailRefused } from '../../errors';\nimport { check, nothingDelivered, rejection } from '../assert';\nimport { sampleMessage } from '../sample';\nimport type { MailerCase } from '../types';\n\n/** What a send must do when the transport fails. Needs {@link MailerFaults}. */\nexport const failureCases: readonly MailerCase[] = [\n\t{\n\t\tid: 'failure.outage',\n\t\ttitle:\n\t\t\t'an outage throws MailFailure with the cause, and nothing is retried',\n\t\tneeds: 'faults',\n\t\tasync run(context) {\n\t\t\tconst faults = context.faults;\n\t\t\tcheck(faults !== null, 'faults are required');\n\t\t\tawait faults.failNext('outage');\n\t\t\tconst error = await rejection(\n\t\t\t\tcontext.mailer.send(sampleMessage),\n\t\t\t\t'a send during an outage',\n\t\t\t);\n\t\t\t// The class is the one imported from @nxgt/mail: a transport that\n\t\t\t// defines its own copy fails here.\n\t\t\tcheck(\n\t\t\t\terror instanceof MailFailure,\n\t\t\t\t'an outage must throw MailFailure from @nxgt/mail',\n\t\t\t);\n\t\t\tcheck(error instanceof MailError, 'MailFailure must extend MailError');\n\t\t\tcheck(\n\t\t\t\terror.code === 'MAIL_FAILED',\n\t\t\t\t'an outage must carry the code MAIL_FAILED',\n\t\t\t);\n\t\t\tcheck(\n\t\t\t\terror.cause !== undefined,\n\t\t\t\t\"an outage must carry the transport's error as cause\",\n\t\t\t);\n\t\t\tcheck(\n\t\t\t\t(await faults.attempts()) === 1,\n\t\t\t\t'the transport retried a failed hand-over',\n\t\t\t);\n\t\t\tawait nothingDelivered(context, 'the hand-over failed');\n\t\t},\n\t},\n\t{\n\t\tid: 'failure.refusal',\n\t\ttitle: 'a message the provider refuses throws MailRefused with the cause',\n\t\tneeds: 'faults',\n\t\tasync run(context) {\n\t\t\tconst faults = context.faults;\n\t\t\tcheck(faults !== null, 'faults are required');\n\t\t\tawait faults.failNext('refusal');\n\t\t\tconst error = await rejection(\n\t\t\t\tcontext.mailer.send(sampleMessage),\n\t\t\t\t'a refused send',\n\t\t\t);\n\t\t\tcheck(\n\t\t\t\terror instanceof MailRefused,\n\t\t\t\t'a refusal must throw MailRefused from @nxgt/mail',\n\t\t\t);\n\t\t\tcheck(\n\t\t\t\terror.code === 'MAIL_REFUSED',\n\t\t\t\t'a refusal must carry the code MAIL_REFUSED',\n\t\t\t);\n\t\t\tcheck(\n\t\t\t\terror.cause !== undefined,\n\t\t\t\t\"a refusal must carry the transport's error as cause\",\n\t\t\t);\n\t\t\tcheck(\n\t\t\t\t(await faults.attempts()) === 1,\n\t\t\t\t'the transport retried a refused message',\n\t\t\t);\n\t\t},\n\t},\n\t{\n\t\tid: 'failure.recovers',\n\t\ttitle: 'after a failure, the next send goes through',\n\t\tneeds: 'faults',\n\t\tasync run(context) {\n\t\t\tconst faults = context.faults;\n\t\t\tcheck(faults !== null, 'faults are required');\n\t\t\tawait faults.failNext('outage');\n\t\t\tawait rejection(\n\t\t\t\tcontext.mailer.send(sampleMessage),\n\t\t\t\t'a send during an outage',\n\t\t\t);\n\t\t\tawait context.mailer.send(sampleMessage);\n\t\t\tcheck(\n\t\t\t\t(await context.delivered()).length === 1,\n\t\t\t\t'the send after a failure was not delivered',\n\t\t\t);\n\t\t},\n\t},\n];\n",
8
+ "import { MailRefused } from '../../errors';\nimport { check, nothingDelivered, rejection, same } from '../assert';\nimport { sampleMessage } from '../sample';\nimport type { MailerCase } from '../types';\n\n/** What every send must do, with no fault injected. */\nexport const sendCases: readonly MailerCase[] = [\n\t{\n\t\tid: 'send.answersSentMail',\n\t\ttitle: 'a send answers SentMail, with a string id or null',\n\t\tasync run({ mailer }) {\n\t\t\tconst sent = await mailer.send(sampleMessage);\n\t\t\tcheck(\n\t\t\t\ttypeof sent === 'object' && sent !== null && 'messageId' in sent,\n\t\t\t\t'send did not answer an object with messageId',\n\t\t\t);\n\t\t\tcheck(\n\t\t\t\tsent.messageId === null ||\n\t\t\t\t\t(typeof sent.messageId === 'string' && sent.messageId !== ''),\n\t\t\t\t'messageId must be a non-empty string or null',\n\t\t\t);\n\t\t},\n\t},\n\t{\n\t\tid: 'send.deliversBytes',\n\t\ttitle:\n\t\t\t'a message is delivered byte for byte: accents, an emoji, a text part',\n\t\tasync run(context) {\n\t\t\tawait context.mailer.send(sampleMessage);\n\t\t\tconst delivered = await context.delivered();\n\t\t\tcheck(\n\t\t\t\tdelivered.length === 1,\n\t\t\t\t`expected 1 delivered message, got ${delivered.length}`,\n\t\t\t);\n\t\t\tconst [mail] = delivered;\n\t\t\tcheck(\n\t\t\t\tmail?.subject === sampleMessage.subject,\n\t\t\t\t'the subject was not delivered as sent',\n\t\t\t);\n\t\t\tcheck(\n\t\t\t\tmail?.html === sampleMessage.html,\n\t\t\t\t'the html part was not delivered as sent',\n\t\t\t);\n\t\t\tcheck(\n\t\t\t\tmail?.text === sampleMessage.text,\n\t\t\t\t'the text part was not delivered as sent',\n\t\t\t);\n\t\t},\n\t},\n\t{\n\t\tid: 'send.recipients',\n\t\ttitle:\n\t\t\t'every recipient is delivered to, written as a string or with a name',\n\t\tasync run(context) {\n\t\t\tawait context.mailer.send({\n\t\t\t\t...sampleMessage,\n\t\t\t\tto: [\n\t\t\t\t\t'ada@example.test',\n\t\t\t\t\t{ name: 'Grace Hopper', address: 'grace@example.test' },\n\t\t\t\t],\n\t\t\t});\n\t\t\tconst [mail] = await context.delivered();\n\t\t\tcheck(\n\t\t\t\tsame(mail?.to, ['ada@example.test', 'grace@example.test']),\n\t\t\t\t'the recipients delivered are not the recipients sent',\n\t\t\t);\n\t\t},\n\t},\n\t{\n\t\tid: 'send.hostileName',\n\t\ttitle: 'a name holding an address and a comma reaches only its own address',\n\t\tasync run(context) {\n\t\t\t// A name is free text, and quoting it is the transport's job. One that\n\t\t\t// pastes it into a header unquoted hands mallory a copy.\n\t\t\tawait context.mailer.send({\n\t\t\t\t...sampleMessage,\n\t\t\t\tto: {\n\t\t\t\t\tname: 'Ada <mallory@example.test>, \"Eve\" <eve@example.test>;',\n\t\t\t\t\taddress: 'ada@example.test',\n\t\t\t\t},\n\t\t\t});\n\t\t\tconst [mail] = await context.delivered();\n\t\t\tcheck(\n\t\t\t\tsame(mail?.to, ['ada@example.test']),\n\t\t\t\t'a name let a second recipient through',\n\t\t\t);\n\t\t},\n\t},\n\t{\n\t\tid: 'send.refusesNoRecipient',\n\t\ttitle:\n\t\t\t'a message with no recipient is refused with MailRefused, and nothing is sent',\n\t\tasync run(context) {\n\t\t\tconst error = await rejection(\n\t\t\t\tcontext.mailer.send({ ...sampleMessage, to: [] }),\n\t\t\t\t'a send with no recipient',\n\t\t\t);\n\t\t\tcheck(\n\t\t\t\terror instanceof MailRefused,\n\t\t\t\t'a send with no recipient must throw MailRefused',\n\t\t\t);\n\t\t\tawait nothingDelivered(context, 'the message was refused');\n\t\t},\n\t},\n\t{\n\t\tid: 'send.refusesLineBreakInSubject',\n\t\ttitle:\n\t\t\t'a line break in the subject is refused with MailRefused: it is a header injection',\n\t\tasync run(context) {\n\t\t\tconst error = await rejection(\n\t\t\t\tcontext.mailer.send({\n\t\t\t\t\t...sampleMessage,\n\t\t\t\t\tsubject: 'Hello\\r\\nBcc: eve@example.test',\n\t\t\t\t}),\n\t\t\t\t'a send with a line break in the subject',\n\t\t\t);\n\t\t\tcheck(\n\t\t\t\terror instanceof MailRefused,\n\t\t\t\t'a line break in the subject must throw MailRefused',\n\t\t\t);\n\t\t\tawait nothingDelivered(context, 'the message was refused');\n\t\t},\n\t},\n\t{\n\t\tid: 'send.refusesAddressHeader',\n\t\ttitle:\n\t\t\t'a Bcc among the custom headers is refused with MailRefused: it would add an unchecked recipient',\n\t\tasync run(context) {\n\t\t\t// A custom header named Bcc, To or Cc reaches the envelope of an SMTP\n\t\t\t// transport, and writes a line no address check ever saw.\n\t\t\tconst error = await rejection(\n\t\t\t\tcontext.mailer.send({\n\t\t\t\t\t...sampleMessage,\n\t\t\t\t\t// biome-ignore lint/style/useNamingConvention: a header's name, as a mail client writes it.\n\t\t\t\t\theaders: { Bcc: 'eve@example.test' },\n\t\t\t\t}),\n\t\t\t\t'a send with a Bcc header',\n\t\t\t);\n\t\t\tcheck(\n\t\t\t\terror instanceof MailRefused,\n\t\t\t\t'a Bcc header must throw MailRefused',\n\t\t\t);\n\t\t\tcheck(\n\t\t\t\t!error.message.includes('eve@example.test'),\n\t\t\t\t'the refusal message holds the refused value',\n\t\t\t);\n\t\t\tawait nothingDelivered(context, 'the message was refused');\n\t\t},\n\t},\n\t{\n\t\tid: 'send.refusesWithoutTheValue',\n\t\ttitle: 'a refusal names where the problem is, never the value',\n\t\tasync run({ mailer }) {\n\t\t\tconst error = await rejection(\n\t\t\t\tmailer.send({ ...sampleMessage, to: 'not-an-address-7f3a' }),\n\t\t\t\t'a send to something that is not an address',\n\t\t\t);\n\t\t\tcheck(\n\t\t\t\terror instanceof MailRefused,\n\t\t\t\t'a malformed address must throw MailRefused',\n\t\t\t);\n\t\t\tcheck(\n\t\t\t\t!error.message.includes('not-an-address-7f3a'),\n\t\t\t\t'the refusal message holds the refused value',\n\t\t\t);\n\t\t},\n\t},\n];\n",
9
+ "import type { MailerCase } from '../types';\nimport { failureCases } from './failure';\nimport { sendCases } from './send';\n\nexport { failureCases } from './failure';\nexport { sendCases } from './send';\n\n/** Every case, in the order they are described. */\nexport const allMailerCases: readonly MailerCase[] = [\n\t...sendCases,\n\t...failureCases,\n];\n",
10
+ "import { allMailerCases } from './cases/index';\nimport type { MailerCase, MailerHarness, MailerRunner } from './types';\n\n/** Why a case did not run. A skip is always reported with its reason, never silent. */\nexport const MAILER_SKIP_REASONS = {\n\tfaults:\n\t\t'faults not provided: the failure contract is not proven for this transport',\n} as const;\n\n/**\n * Runs one case against a freshly opened transport, and closes it, pass or\n * fail. Answers the reason when the case cannot run on this harness.\n */\nexport async function runMailerCase(\n\tmailerCase: MailerCase,\n\tharness: MailerHarness,\n): Promise<{ readonly skipped: string } | { readonly passed: true }> {\n\tconst opened = await harness.open();\n\tlet outcome: { readonly skipped: string } | { readonly passed: true };\n\ttry {\n\t\tif (mailerCase.needs === 'faults' && opened.faults === undefined) {\n\t\t\toutcome = { skipped: MAILER_SKIP_REASONS.faults };\n\t\t} else {\n\t\t\tawait mailerCase.run({\n\t\t\t\tmailer: opened.mailer,\n\t\t\t\tdelivered: () => opened.delivered(),\n\t\t\t\tfaults: opened.faults ?? null,\n\t\t\t});\n\t\t\toutcome = { passed: true };\n\t\t}\n\t} catch (error) {\n\t\t// The case's failure is what the author needs to read: a close that\n\t\t// fails too must not replace it.\n\t\tawait opened.close?.().then(\n\t\t\t() => undefined,\n\t\t\t() => undefined,\n\t\t);\n\t\tthrow error;\n\t}\n\tawait opened.close?.();\n\treturn outcome;\n}\n\nfunction globalRunner(): MailerRunner {\n\tconst { describe, it } = globalThis as unknown as Partial<MailerRunner>;\n\tif (typeof describe !== 'function' || typeof it !== 'function') {\n\t\tthrow new TypeError(\n\t\t\t'describeMailer: no test runner found — pass runner: { describe, it } from your test framework',\n\t\t);\n\t}\n\treturn { describe, it };\n}\n\n/**\n * Describes every case against one transport, under bun:test, vitest or jest.\n *\n * ```ts\n * import { describe, it } from 'bun:test';\n * import { describeMailer } from '@nxgt/mail/conformance';\n *\n * describeMailer({ name: 'my transport', harness, runner: { describe, it } });\n * ```\n *\n * `runner` defaults to the global `describe` and `it`, when the framework\n * defines them.\n */\nexport function describeMailer(options: {\n\treadonly name: string;\n\treadonly harness: MailerHarness;\n\treadonly runner?: MailerRunner;\n\t/** Case ids to skip, each with the reason — reported, never silent. */\n\treadonly skip?: Readonly<Record<string, string>>;\n\t/** Declared up front, so a missing `faults` is reported before the first case runs. */\n\treadonly faults?: boolean;\n}): void {\n\tconst runner = options.runner ?? globalRunner();\n\tconst skip = options.skip ?? {};\n\tfor (const id of Object.keys(skip)) {\n\t\tif (!allMailerCases.some((c) => c.id === id)) {\n\t\t\tthrow new TypeError(`describeMailer: skip names no case: ${id}`);\n\t\t}\n\t}\n\n\trunner.describe(`${options.name} — @nxgt/mail conformance`, () => {\n\t\tfor (const mailerCase of allMailerCases) {\n\t\t\tconst title = `${mailerCase.id}: ${mailerCase.title}`;\n\t\t\tconst reason =\n\t\t\t\tskip[mailerCase.id] ??\n\t\t\t\t(mailerCase.needs === 'faults' && options.faults === false\n\t\t\t\t\t? MAILER_SKIP_REASONS.faults\n\t\t\t\t\t: undefined);\n\t\t\tif (reason !== undefined) {\n\t\t\t\trunner.it.skip(`${title} (skipped: ${reason})`, async () => {});\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\trunner.it(title, async () => {\n\t\t\t\tconst result = await runMailerCase(mailerCase, options.harness);\n\t\t\t\tif ('skipped' in result) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`conformance: ${mailerCase.id}: ${result.skipped} — pass faults: false to describeMailer to skip it on purpose`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t});\n}\n",
11
+ "import { MailFailure, MailRefused } from '../errors';\nimport { createMemoryMailer } from '../memory';\nimport { recipientsOf } from '../message';\nimport type { MailerHarness } from './types';\n\n/**\n * The harness of the memory mailer, the transport the suite is proven\n * against. Also a working example of a harness.\n */\nexport function referenceMailerHarness(): MailerHarness {\n\treturn {\n\t\tasync open() {\n\t\t\tconst mailer = createMemoryMailer();\n\t\t\treturn {\n\t\t\t\tmailer,\n\t\t\t\tasync delivered() {\n\t\t\t\t\treturn mailer.sent.map((mail) => ({\n\t\t\t\t\t\tto: recipientsOf(mail),\n\t\t\t\t\t\tsubject: mail.subject,\n\t\t\t\t\t\thtml: mail.html,\n\t\t\t\t\t\ttext: mail.text,\n\t\t\t\t\t}));\n\t\t\t\t},\n\t\t\t\tfaults: {\n\t\t\t\t\tasync failNext(kind) {\n\t\t\t\t\t\tconst cause = new Error(`simulated ${kind}`);\n\t\t\t\t\t\tmailer.failNext(\n\t\t\t\t\t\t\tkind === 'outage'\n\t\t\t\t\t\t\t\t? new MailFailure('send: the transport could not be reached', {\n\t\t\t\t\t\t\t\t\t\tcause,\n\t\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t\t: new MailRefused('send: the transport refused the message', {\n\t\t\t\t\t\t\t\t\t\tcause,\n\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t);\n\t\t\t\t\t},\n\t\t\t\t\tasync attempts() {\n\t\t\t\t\t\treturn mailer.attempts;\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t};\n\t\t},\n\t};\n}\n"
12
+ ],
13
+ "mappings": "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",
14
+ "debugId": "761673AB21F5667764756E2164756E21",
15
+ "names": []
16
+ }
@@ -0,0 +1,7 @@
1
+ import type { MailerHarness } from './types';
2
+ /**
3
+ * The harness of the memory mailer, the transport the suite is proven
4
+ * against. Also a working example of a harness.
5
+ */
6
+ export declare function referenceMailerHarness(): MailerHarness;
7
+ //# sourceMappingURL=reference.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reference.d.ts","sourceRoot":"","sources":["../../src/conformance/reference.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAE7C;;;GAGG;AACH,wBAAgB,sBAAsB,IAAI,aAAa,CAkCtD"}
@@ -0,0 +1,4 @@
1
+ import type { MailMessage } from '../types';
2
+ /** A message with the characters a transport most often mangles. */
3
+ export declare const sampleMessage: MailMessage;
4
+ //# sourceMappingURL=sample.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sample.d.ts","sourceRoot":"","sources":["../../src/conformance/sample.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAE5C,oEAAoE;AACpE,eAAO,MAAM,aAAa,EAAE,WAM3B,CAAC"}
@@ -0,0 +1,74 @@
1
+ import type { Mailer } from '../types';
2
+ /**
3
+ * What the receiving end got, as the harness reads it back: from the test
4
+ * SMTP server, from the recorded HTTP request, from the memory outbox.
5
+ */
6
+ export interface DeliveredMail {
7
+ /** The bare addresses of every recipient, in order. */
8
+ readonly to: readonly string[];
9
+ readonly subject: string;
10
+ readonly html: string;
11
+ readonly text: string;
12
+ }
13
+ /**
14
+ * How the suite makes a transport fail **the way its provider fails**: a
15
+ * refused connection or a 5xx for an outage, a 4xx "malformed message" for a
16
+ * refusal. Not a decorator that throws in front of the transport — that would
17
+ * prove the decorator, and not the transport's translation.
18
+ *
19
+ * Optional, and **its absence is reported, never passed over**: without it,
20
+ * the failure cases are skipped with a reason in their name.
21
+ */
22
+ export interface MailerFaults {
23
+ /** Makes the next hand-over fail as an outage, or as a refusal of the message. */
24
+ failNext(kind: 'outage' | 'refusal'): Promise<void>;
25
+ /**
26
+ * How many hand-overs the receiving end saw, failed ones included. This is
27
+ * what proves a transport does not retry in secret.
28
+ */
29
+ attempts(): Promise<number>;
30
+ }
31
+ /** One transport, opened for one case. */
32
+ export interface OpenedMailer {
33
+ readonly mailer: Mailer;
34
+ /** Every message the receiving end accepted, oldest first. */
35
+ delivered(): Promise<readonly DeliveredMail[]>;
36
+ readonly faults?: MailerFaults;
37
+ /** Called after the case, pass or fail. */
38
+ close?(): Promise<void>;
39
+ }
40
+ /**
41
+ * Opens a **fresh** transport and receiving end — once per case, so no case
42
+ * sees another's messages.
43
+ */
44
+ export interface MailerHarness {
45
+ open(): Promise<OpenedMailer>;
46
+ }
47
+ /** What a case runs against. */
48
+ export interface MailerCaseContext {
49
+ readonly mailer: Mailer;
50
+ delivered(): Promise<readonly DeliveredMail[]>;
51
+ readonly faults: MailerFaults | null;
52
+ }
53
+ /**
54
+ * One conformance case, as **data**: `run` throws on failure and resolves on
55
+ * success, so it runs under any test runner, or none.
56
+ */
57
+ export interface MailerCase {
58
+ /** Unique, stable: `send.deliversBytes`, `failure.outage`. */
59
+ readonly id: string;
60
+ /** What the case proves, as a sentence. */
61
+ readonly title: string;
62
+ /** `'faults'` when the case can only run with {@link MailerFaults}. */
63
+ readonly needs?: 'faults';
64
+ run(context: MailerCaseContext): Promise<void>;
65
+ }
66
+ /** The two functions of a test runner the suite needs: bun:test, vitest and jest all have them. */
67
+ export interface MailerRunner {
68
+ describe(name: string, body: () => void): void;
69
+ it: {
70
+ (name: string, body: () => Promise<void>): void;
71
+ skip(name: string, body: () => Promise<void>): void;
72
+ };
73
+ }
74
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/conformance/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAEvC;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC7B,uDAAuD;IACvD,QAAQ,CAAC,EAAE,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,YAAY;IAC5B,kFAAkF;IAClF,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpD;;;OAGG;IACH,QAAQ,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;CAC5B;AAED,0CAA0C;AAC1C,MAAM,WAAW,YAAY;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,8DAA8D;IAC9D,SAAS,IAAI,OAAO,CAAC,SAAS,aAAa,EAAE,CAAC,CAAC;IAC/C,QAAQ,CAAC,MAAM,CAAC,EAAE,YAAY,CAAC;IAC/B,2CAA2C;IAC3C,KAAK,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC7B,IAAI,IAAI,OAAO,CAAC,YAAY,CAAC,CAAC;CAC9B;AAED,gCAAgC;AAChC,MAAM,WAAW,iBAAiB;IACjC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,SAAS,IAAI,OAAO,CAAC,SAAS,aAAa,EAAE,CAAC,CAAC;IAC/C,QAAQ,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI,CAAC;CACrC;AAED;;;GAGG;AACH,MAAM,WAAW,UAAU;IAC1B,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,2CAA2C;IAC3C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,uEAAuE;IACvE,QAAQ,CAAC,KAAK,CAAC,EAAE,QAAQ,CAAC;IAC1B,GAAG,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/C;AAED,mGAAmG;AACnG,MAAM,WAAW,YAAY;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,IAAI,GAAG,IAAI,CAAC;IAC/C,EAAE,EAAE;QACH,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;QAChD,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;KACpD,CAAC;CACF"}
@@ -0,0 +1,68 @@
1
+ /**
2
+ * What a transport refuses, as a string a caller can switch on.
3
+ *
4
+ * Every code is a **refusal at call time**. A refusal that can only come from
5
+ * how the application was wired — a bad option passed to a factory — is a
6
+ * bare `TypeError` instead: no handler should ever answer one.
7
+ *
8
+ * The codes are `SCREAMING_SNAKE` because they are data values, not API
9
+ * identifiers. Every key in this package is `camelCase`.
10
+ */
11
+ export type MailErrorCode =
12
+ /**
13
+ * The transport could not hand the message over: a refused connection, a
14
+ * timeout, a 5xx from the provider, an expired credential. The transport's
15
+ * own error is the `cause`.
16
+ *
17
+ * **Nothing is known to have been sent** — after a timeout or a dropped
18
+ * connection, the provider may have taken it all the same. Retry later,
19
+ * or tell the user it failed. Never report it as sent.
20
+ */
21
+ 'MAIL_FAILED'
22
+ /**
23
+ * The message itself was refused, before or by the transport: no
24
+ * recipient, something that is not an address, a line break in the
25
+ * subject or a header, a provider answering that the message is
26
+ * malformed, or — from `@nxgt/mail/renderer` — a URL variable that is not
27
+ * an `http:`, `https:` or `mailto:` URL. Sending it again unchanged fails
28
+ * again.
29
+ */
30
+ | 'MAIL_REFUSED';
31
+ /** Options every error of this package accepts. */
32
+ export interface MailErrorOptions {
33
+ /** The error that caused this one, typically the transport's. */
34
+ readonly cause?: unknown;
35
+ }
36
+ /**
37
+ * The base class of every error this package throws at call time. It is
38
+ * abstract: a transport throws {@link MailFailure} or {@link MailRefused}.
39
+ *
40
+ * **There is exactly one definition of this class.** A transport defines no
41
+ * error class of its own and throws these, imported from its `@nxgt/mail`
42
+ * peer, so `error instanceof MailFailure` holds whatever transport threw it.
43
+ *
44
+ * A message reports **a shape, never a value**: never a recipient address,
45
+ * never a subject, never a link — the link in a verification e-mail is a
46
+ * credential.
47
+ */
48
+ export declare abstract class MailError extends Error {
49
+ name: string;
50
+ /**
51
+ * Abstract, so a transport cannot throw a bare `MailError` that passes a
52
+ * `code` check and fails `instanceof MailFailure`: it throws one of the two
53
+ * subclasses.
54
+ */
55
+ abstract readonly code: MailErrorCode;
56
+ constructor(message: string, options?: MailErrorOptions);
57
+ }
58
+ /** The transport could not hand the message over. Code `MAIL_FAILED`. */
59
+ export declare class MailFailure extends MailError {
60
+ name: string;
61
+ readonly code: "MAIL_FAILED";
62
+ }
63
+ /** The message was refused as malformed. Code `MAIL_REFUSED`. */
64
+ export declare class MailRefused extends MailError {
65
+ name: string;
66
+ readonly code: "MAIL_REFUSED";
67
+ }
68
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,MAAM,MAAM,aAAa;AACxB;;;;;;;;GAQG;AACD,aAAa;AACf;;;;;;;GAOG;GACD,cAAc,CAAC;AAElB,mDAAmD;AACnD,MAAM,WAAW,gBAAgB;IAChC,iEAAiE;IACjE,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;CACzB;AAED;;;;;;;;;;;GAWG;AACH,8BAAsB,SAAU,SAAQ,KAAK;IACnC,IAAI,SAAe;IAC5B;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;gBAE1B,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,gBAAgB;CAGvD;AAED,yEAAyE;AACzE,qBAAa,WAAY,SAAQ,SAAS;IAChC,IAAI,SAAiB;IAC9B,SAAkB,IAAI,EAAG,aAAa,CAAU;CAChD;AAED,iEAAiE;AACjE,qBAAa,WAAY,SAAQ,SAAS;IAChC,IAAI,SAAiB;IAC9B,SAAkB,IAAI,EAAG,cAAc,CAAU;CACjD"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * `@nxgt/mail` — the run-time side of transactional e-mail: the `Mailer` port
3
+ * a transport implements, the shapes it sends, the errors it throws, a memory
4
+ * transport for tests, and locale selection. No dependency, and no Node
5
+ * built-in: it runs anywhere. The renderer that fills a build of
6
+ * `@nxgt/mail-i18n` is `@nxgt/mail/renderer`, which reads files.
7
+ *
8
+ * ## The one rule this package is built around
9
+ *
10
+ * **A failure throws.** A transport that could not hand a message over rejects
11
+ * with `MailFailure`; it never answers `false` and never logs and resolves. A
12
+ * caller that maps a failed send to "sent" has told a user to check an inbox
13
+ * that will stay empty. `@nxgt/mail/conformance` fails a transport that breaks
14
+ * it.
15
+ */
16
+ export { MailError, type MailErrorCode, type MailErrorOptions, MailFailure, MailRefused, } from './errors';
17
+ export { parseAcceptLanguage, pickLocale, type WantedLocales } from './locale';
18
+ export { createMemoryMailer, type MemoryMail, type MemoryMailer, } from './memory';
19
+ export { addressOf, checkMessage, recipientsOf } from './message';
20
+ export type { Address, Mailer, MailMessage, Rendered, SentMail } from './types';
21
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EACN,SAAS,EACT,KAAK,aAAa,EAClB,KAAK,gBAAgB,EACrB,WAAW,EACX,WAAW,GACX,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,mBAAmB,EAAE,UAAU,EAAE,KAAK,aAAa,EAAE,MAAM,UAAU,CAAC;AAC/E,OAAO,EACN,kBAAkB,EAClB,KAAK,UAAU,EACf,KAAK,YAAY,GACjB,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAClE,YAAY,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC"}