@nxgt/mail 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -4
- package/dist/chunks/{index-0f7kdb8k.js → index-4h39j3n7.js} +43 -3
- package/dist/chunks/index-4h39j3n7.js.map +11 -0
- package/dist/chunks/index-we4n5yfz.js.map +1 -1
- package/dist/conformance/cases/send.d.ts.map +1 -1
- package/dist/conformance/index.d.ts +1 -1
- package/dist/conformance/index.d.ts.map +1 -1
- package/dist/conformance/index.js +44 -3
- package/dist/conformance/index.js.map +5 -5
- package/dist/conformance/reference.d.ts.map +1 -1
- package/dist/conformance/sample.d.ts +6 -1
- package/dist/conformance/sample.d.ts.map +1 -1
- package/dist/conformance/types.d.ts +11 -1
- package/dist/conformance/types.d.ts.map +1 -1
- package/dist/errors.d.ts +4 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/memory.d.ts.map +1 -1
- package/dist/message.d.ts +6 -1
- package/dist/message.d.ts.map +1 -1
- package/dist/types.d.ts +20 -0
- package/dist/types.d.ts.map +1 -1
- package/docs/README.md +2 -2
- package/docs/guide/sending.md +90 -2
- package/docs/guide/testing.md +36 -2
- package/docs/guide/transports.md +69 -12
- package/docs/roadmap.md +12 -1
- package/docs/troubleshooting.md +233 -5
- package/package.json +1 -1
- package/dist/chunks/index-0f7kdb8k.js.map +0 -11
package/README.md
CHANGED
|
@@ -41,9 +41,9 @@ import without extensions, so `nodenext` is not supported.
|
|
|
41
41
|
|
|
42
42
|
| Import | What it holds |
|
|
43
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 |
|
|
44
|
+
| `@nxgt/mail` | The port (`Mailer`, `MailMessage`, `Rendered`, `SentMail`, `Address`, `MailAttachment`), 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
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 |
|
|
46
|
+
| `@nxgt/mail/conformance` | **For transport authors**: `describeMailer`, its cases as data, `runMailerCase`, the messages they send (`sampleMessage`, `sampleAttachment`), and the memory mailer's harness as a worked example |
|
|
47
47
|
|
|
48
48
|
## Usage
|
|
49
49
|
|
|
@@ -117,6 +117,43 @@ export function notifyPasswordChanged(mailer: Mailer, to: string): Promise<SentM
|
|
|
117
117
|
`SentMail` is `{ messageId: string | null }`: `null` when the transport gives no
|
|
118
118
|
id — an absence, not a failure. See [Sending](docs/guide/sending.md).
|
|
119
119
|
|
|
120
|
+
### Attachments — bytes, never a path
|
|
121
|
+
|
|
122
|
+
A file goes with the e-mail as `attachments`: its name, its bytes as a
|
|
123
|
+
`Uint8Array` (a Node `Buffer` is one), and its type:
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
import { readFile } from 'node:fs/promises';
|
|
127
|
+
import type { Mailer, Rendered } from '@nxgt/mail';
|
|
128
|
+
|
|
129
|
+
export async function sendInvoice(mailer: Mailer, to: string, rendered: Rendered, pdfPath: string): Promise<void> {
|
|
130
|
+
await mailer.send({
|
|
131
|
+
...rendered,
|
|
132
|
+
to,
|
|
133
|
+
attachments: [
|
|
134
|
+
{ filename: 'invoice-2026-09.pdf', content: await readFile(pdfPath), contentType: 'application/pdf' },
|
|
135
|
+
],
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
There is no `path`, no URL and no stream: a transport never reads a file or
|
|
141
|
+
fetches a URL for you, so a value from outside can never make it attach one.
|
|
142
|
+
`checkMessage` refuses a hole in the list, content that is not a `Uint8Array`,
|
|
143
|
+
a file name that is empty, `.` or `..`, or holds `/`, `\`, a line break, a
|
|
144
|
+
control character or a format character (a right-to-left override), and a
|
|
145
|
+
content type that is not a bare `type/subtype` or is a MIME container
|
|
146
|
+
(`multipart/*`, `message/*`). An empty list is the same as none.
|
|
147
|
+
|
|
148
|
+
**A large or sensitive file is a link, not an attachment.** Put a signed,
|
|
149
|
+
expiring URL in the template as a URL variable —
|
|
150
|
+
`mails.render('invoice-ready', { link: signedUrl })` — and the file never
|
|
151
|
+
sits in an inbox. Providers cap the whole message — about 25 MB sending
|
|
152
|
+
through Gmail, 40 MB at Resend once encoded — and base64 makes a file a third
|
|
153
|
+
larger on the way.
|
|
154
|
+
Inline images (`cid:`) are not supported yet. See
|
|
155
|
+
[Sending — attachments](docs/guide/sending.md#attachments).
|
|
156
|
+
|
|
120
157
|
### Errors — switch on `code`
|
|
121
158
|
|
|
122
159
|
Both errors extend `MailError`, whose `code` is a union a `switch` exhausts.
|
|
@@ -201,14 +238,26 @@ peer, and passes the conformance suite:
|
|
|
201
238
|
```ts
|
|
202
239
|
import { checkMessage, MailFailure, MailRefused, type Mailer, recipientsOf } from '@nxgt/mail';
|
|
203
240
|
|
|
241
|
+
// JSON has no bytes: an attachment travels as base64, read in slices.
|
|
242
|
+
function base64Of(bytes: Uint8Array): string {
|
|
243
|
+
let binary = '';
|
|
244
|
+
for (let start = 0; start < bytes.length; start += 0x8000) {
|
|
245
|
+
binary += String.fromCharCode(...bytes.subarray(start, start + 0x8000));
|
|
246
|
+
}
|
|
247
|
+
return btoa(binary);
|
|
248
|
+
}
|
|
249
|
+
|
|
204
250
|
export function createHttpMailer(endpoint: string, apiKey: string): Mailer {
|
|
205
251
|
return {
|
|
206
252
|
async send(message) {
|
|
207
253
|
checkMessage(message); // MailRefused, naming where, never the value
|
|
254
|
+
const attachments = message.attachments?.length
|
|
255
|
+
? message.attachments.map((file) => ({ ...file, content: base64Of(file.content) }))
|
|
256
|
+
: undefined; // an empty list is none
|
|
208
257
|
const response = await fetch(endpoint, {
|
|
209
258
|
method: 'POST',
|
|
210
259
|
headers: { authorization: `Bearer ${apiKey}`, 'content-type': 'application/json' },
|
|
211
|
-
body: JSON.stringify({ ...message, to: recipientsOf(message) }),
|
|
260
|
+
body: JSON.stringify({ ...message, to: recipientsOf(message), attachments }),
|
|
212
261
|
}).catch((cause: unknown) => {
|
|
213
262
|
throw new MailFailure('send: the provider could not be reached', { cause });
|
|
214
263
|
});
|
|
@@ -271,6 +320,11 @@ with `MailRefused`; write `{ name: 'Ada', address: 'ada@example.com' }`.
|
|
|
271
320
|
into an unhandled rejection and the user into someone waiting for an e-mail
|
|
272
321
|
that never comes. `await` it, or hand it to a queue that does.
|
|
273
322
|
|
|
323
|
+
**An attachment is bytes you already hold.** Read the file first
|
|
324
|
+
(`await readFile(path)`, `new Uint8Array(await response.arrayBuffer())`);
|
|
325
|
+
a `path` or a URL is a compile error and a `MailRefused`. Past a few
|
|
326
|
+
megabytes, send a signed link instead.
|
|
327
|
+
|
|
274
328
|
**A custom header cannot set an address.** `headers: { Bcc: '…' }` would add
|
|
275
329
|
a recipient no check saw: `checkMessage` refuses `To`, `Cc`, `Bcc`, `From`,
|
|
276
330
|
`Sender`, `Reply-To`, `Return-Path`, `Subject`, `MIME-Version` and
|
|
@@ -293,7 +347,7 @@ gives a test file `describe` and `it` as bare identifiers, not on `globalThis`.
|
|
|
293
347
|
|
|
294
348
|
## Type safety, counted
|
|
295
349
|
|
|
296
|
-
**
|
|
350
|
+
**21 plausible mistakes, 21 refused** at compile time, each measured by a
|
|
297
351
|
`@ts-expect-error` in
|
|
298
352
|
[`test/types/refusals.ts`](https://github.com/softistx/nxgt-mail/blob/develop/packages/mail/test/types/refusals.ts)
|
|
299
353
|
that fails the typecheck the moment it stops holding:
|
|
@@ -323,6 +377,13 @@ The name written as a literal and the variables at the call, as usual:
|
|
|
323
377
|
16. The variables left out altogether, for an e-mail that takes some.
|
|
324
378
|
17. A number for a URL variable: a URL is a string.
|
|
325
379
|
|
|
380
|
+
And an attachment:
|
|
381
|
+
|
|
382
|
+
18. Its `content` as a string: an attachment is bytes, a `Uint8Array`.
|
|
383
|
+
19. A `path` instead of the bytes: no transport reads a file for you.
|
|
384
|
+
20. No `contentType`: nothing guesses it from the file name.
|
|
385
|
+
21. One attachment, not in a list.
|
|
386
|
+
|
|
326
387
|
The same file holds the calls that must keep compiling: a refusal that refuses
|
|
327
388
|
the correct call is a bug.
|
|
328
389
|
|
|
@@ -8,6 +8,10 @@ var LINE_BREAK = /[\r\n]/;
|
|
|
8
8
|
var ADDRESS = /^[^\s@<>,;:]+@[^\s@<>,;:]+$/;
|
|
9
9
|
var HEADER_NAME = /^[A-Za-z0-9-]+$/;
|
|
10
10
|
var RESERVED_HEADER = /^(?:to|cc|bcc|from|sender|reply-to|return-path|subject|mime-version|content-.*)$/i;
|
|
11
|
+
var FILENAME_REFUSED = /[/\\\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u;
|
|
12
|
+
var DOT_NAME = /^\.\.?$/;
|
|
13
|
+
var CONTENT_TYPE = /^[!#$%&'*+.^_`{|}~0-9A-Za-z-]+\/[!#$%&'*+.^_`{|}~0-9A-Za-z-]+$/;
|
|
14
|
+
var CONTAINER_TYPE = /^(?:multipart|message)\//i;
|
|
11
15
|
function recipientsOf2(message) {
|
|
12
16
|
const to = Array.isArray(message.to) ? message.to : [message.to];
|
|
13
17
|
return to.map(addressOf2);
|
|
@@ -32,6 +36,20 @@ function checkAddress(address, where) {
|
|
|
32
36
|
throw new MailRefused2(`send: ${where}.name must be a string without a line break`);
|
|
33
37
|
}
|
|
34
38
|
}
|
|
39
|
+
function checkAttachment(attachment, where) {
|
|
40
|
+
if (typeof attachment !== "object" || attachment === null) {
|
|
41
|
+
throw new MailRefused2(`send: ${where} must be an object, as { filename, content, contentType }`);
|
|
42
|
+
}
|
|
43
|
+
if (!(attachment.content instanceof Uint8Array)) {
|
|
44
|
+
throw new MailRefused2(`send: ${where}.content must be a Uint8Array — the file's bytes, never a path or a URL`);
|
|
45
|
+
}
|
|
46
|
+
if (typeof attachment.filename !== "string" || attachment.filename === "" || DOT_NAME.test(attachment.filename) || FILENAME_REFUSED.test(attachment.filename)) {
|
|
47
|
+
throw new MailRefused2(`send: ${where}.filename must be a file name — not empty, not . or .., without / or \\, a line break or a control character`);
|
|
48
|
+
}
|
|
49
|
+
if (typeof attachment.contentType !== "string" || !CONTENT_TYPE.test(attachment.contentType) || CONTAINER_TYPE.test(attachment.contentType)) {
|
|
50
|
+
throw new MailRefused2(`send: ${where}.contentType must be a file's type/subtype, as application/pdf — never multipart/* or message/*`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
35
53
|
function checkMessage2(message) {
|
|
36
54
|
if (typeof message !== "object" || message === null) {
|
|
37
55
|
throw new MailRefused2("send: the message must be an object");
|
|
@@ -69,9 +87,31 @@ function checkMessage2(message) {
|
|
|
69
87
|
throw new MailRefused2(`send: header ${name} must be a string without a line break`);
|
|
70
88
|
}
|
|
71
89
|
}
|
|
90
|
+
if (message.attachments !== undefined) {
|
|
91
|
+
if (!Array.isArray(message.attachments)) {
|
|
92
|
+
throw new MailRefused2("send: attachments must be an array");
|
|
93
|
+
}
|
|
94
|
+
for (let index = 0;index < message.attachments.length; index++) {
|
|
95
|
+
checkAttachment(message.attachments[index], `attachments[${index}]`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
72
98
|
}
|
|
73
99
|
|
|
74
100
|
// src/memory.ts
|
|
101
|
+
function copyOf(message) {
|
|
102
|
+
const { attachments, ...rest } = message;
|
|
103
|
+
const copy = structuredClone(rest);
|
|
104
|
+
if (attachments === undefined)
|
|
105
|
+
return copy;
|
|
106
|
+
return {
|
|
107
|
+
...copy,
|
|
108
|
+
attachments: attachments.map((attachment) => ({
|
|
109
|
+
filename: attachment.filename,
|
|
110
|
+
content: new Uint8Array(attachment.content),
|
|
111
|
+
contentType: attachment.contentType
|
|
112
|
+
}))
|
|
113
|
+
};
|
|
114
|
+
}
|
|
75
115
|
function createMemoryMailer2() {
|
|
76
116
|
let sent = [];
|
|
77
117
|
let failures = [];
|
|
@@ -102,7 +142,7 @@ function createMemoryMailer2() {
|
|
|
102
142
|
throw failure;
|
|
103
143
|
counter += 1;
|
|
104
144
|
const messageId = `memory-${counter}`;
|
|
105
|
-
sent.push({ ...
|
|
145
|
+
sent.push({ ...copyOf(message), messageId });
|
|
106
146
|
return { messageId };
|
|
107
147
|
}
|
|
108
148
|
};
|
|
@@ -110,5 +150,5 @@ function createMemoryMailer2() {
|
|
|
110
150
|
|
|
111
151
|
export { recipientsOf2, addressOf2, checkMessage2, createMemoryMailer2 };
|
|
112
152
|
|
|
113
|
-
//# debugId=
|
|
114
|
-
//# sourceMappingURL=index-
|
|
153
|
+
//# debugId=49A96A07CA93F79964756E2164756E21
|
|
154
|
+
//# sourceMappingURL=index-4h39j3n7.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, MailAttachment, 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// A file name is shown and saved by the recipient's mail client: no path\n// separator and no `.` or `..` (a client that saves it as is writes\n// elsewhere), no line break or other control character, C1 included (a header\n// could be split on one), and no format character — a right-to-left override\n// disguises `fdp.exe` as `exe.pdf`.\nconst FILENAME_REFUSED = /[/\\\\\\p{Cc}\\p{Cf}\\p{Zl}\\p{Zp}]/u;\nconst DOT_NAME = /^\\.\\.?$/;\n// RFC 2045: type \"/\" subtype, each a token — any printable ASCII but space\n// and the tspecials ()<>@,;:\\\"/[]?=. No parameters: a charset or a name\n// there would be a second, unchecked place to write the file's name.\nconst CONTENT_TYPE =\n\t/^[!#$%&'*+.^_`{|}~0-9A-Za-z-]+\\/[!#$%&'*+.^_`{|}~0-9A-Za-z-]+$/;\n// multipart/* and message/* are MIME containers, not files: nodemailer writes\n// them unencoded, and the receiving end reads back no attachment at all.\nconst CONTAINER_TYPE = /^(?:multipart|message)\\//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/** Refuses `attachment` unless it is a {@link MailAttachment}. */\nfunction checkAttachment(attachment: MailAttachment, where: string): void {\n\tif (typeof attachment !== 'object' || attachment === null) {\n\t\tthrow new MailRefused(\n\t\t\t`send: ${where} must be an object, as { filename, content, contentType }`,\n\t\t);\n\t}\n\tif (!(attachment.content instanceof Uint8Array)) {\n\t\tthrow new MailRefused(\n\t\t\t`send: ${where}.content must be a Uint8Array — the file's bytes, never a path or a URL`,\n\t\t);\n\t}\n\tif (\n\t\ttypeof attachment.filename !== 'string' ||\n\t\tattachment.filename === '' ||\n\t\tDOT_NAME.test(attachment.filename) ||\n\t\tFILENAME_REFUSED.test(attachment.filename)\n\t) {\n\t\tthrow new MailRefused(\n\t\t\t`send: ${where}.filename must be a file name — not empty, not . or .., without / or \\\\, a line break or a control character`,\n\t\t);\n\t}\n\tif (\n\t\ttypeof attachment.contentType !== 'string' ||\n\t\t!CONTENT_TYPE.test(attachment.contentType) ||\n\t\tCONTAINER_TYPE.test(attachment.contentType)\n\t) {\n\t\tthrow new MailRefused(\n\t\t\t`send: ${where}.contentType must be a file's type/subtype, as application/pdf — never multipart/* or message/*`,\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 * - `attachments`, when present, is an array without holes — empty is the\n * same as absent — and each entry has its bytes as a `Uint8Array`, a\n * `filename` that is not empty, `.` or `..` and holds no `/`, `\\`, line\n * break, control or format character, and a `contentType` that is a bare\n * `type/subtype`, never `multipart/*` or `message/*`.\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\tif (message.attachments !== undefined) {\n\t\tif (!Array.isArray(message.attachments)) {\n\t\t\tthrow new MailRefused('send: attachments must be an array');\n\t\t}\n\t\t// Indexed, not forEach: a hole in the array is refused, not skipped.\n\t\tfor (let index = 0; index < message.attachments.length; index++) {\n\t\t\tcheckAttachment(message.attachments[index], `attachments[${index}]`);\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/**\n * A copy of `message` the caller cannot change afterwards. Each attachment's\n * bytes are copied to a plain `Uint8Array` of their own: a `Buffer` from\n * Node's pool is a view on a larger, shared buffer, which a clone would copy\n * whole.\n */\nfunction copyOf(message: MailMessage): MailMessage {\n\tconst { attachments, ...rest } = message;\n\tconst copy = structuredClone(rest);\n\tif (attachments === undefined) return copy;\n\treturn {\n\t\t...copy,\n\t\tattachments: attachments.map((attachment) => ({\n\t\t\tfilename: attachment.filename,\n\t\t\tcontent: new Uint8Array(attachment.content),\n\t\t\tcontentType: attachment.contentType,\n\t\t})),\n\t};\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({ ...copyOf(message), messageId });\n\t\t\treturn { messageId };\n\t\t},\n\t};\n}\n"
|
|
7
|
+
],
|
|
8
|
+
"mappings": "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",
|
|
9
|
+
"debugId": "49A96A07CA93F79964756E2164756E21",
|
|
10
|
+
"names": []
|
|
11
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/errors.ts"],
|
|
4
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
|
|
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, an attachment that is not bytes or is badly named,\n\t * a provider answering that the message is malformed or too large, or —\n\t * from `@nxgt/mail/renderer` — a URL variable that is not an `http:`,\n\t * `https:` or `mailto:` URL. Sending it again unchanged fails 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
6
|
],
|
|
7
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
8
|
"debugId": "5753AA1D988C668964756E2164756E21",
|
|
@@ -1 +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,
|
|
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,EAqO1C,CAAC"}
|
|
@@ -15,6 +15,6 @@
|
|
|
15
15
|
export { allMailerCases, failureCases, sendCases } from './cases/index';
|
|
16
16
|
export { describeMailer, MAILER_SKIP_REASONS, runMailerCase, } from './describe';
|
|
17
17
|
export { referenceMailerHarness } from './reference';
|
|
18
|
-
export { sampleMessage } from './sample';
|
|
18
|
+
export { sampleAttachment, sampleMessage } from './sample';
|
|
19
19
|
export type { DeliveredMail, MailerCase, MailerCaseContext, MailerFaults, MailerHarness, MailerRunner, OpenedMailer, } from './types';
|
|
20
20
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +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;
|
|
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,gBAAgB,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAC3D,YAAY,EACX,aAAa,EACb,UAAU,EACV,iBAAiB,EACjB,YAAY,EACZ,aAAa,EACb,YAAY,EACZ,YAAY,GACZ,MAAM,SAAS,CAAC"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
recipientsOf2,
|
|
3
3
|
createMemoryMailer2
|
|
4
|
-
} from "../chunks/index-
|
|
4
|
+
} from "../chunks/index-4h39j3n7.js";
|
|
5
5
|
import {
|
|
6
6
|
MailError2,
|
|
7
7
|
MailFailure2,
|
|
@@ -34,6 +34,11 @@ var sampleMessage = {
|
|
|
34
34
|
réinitialiser : https://example.test/r?t=abc&x=1
|
|
35
35
|
`
|
|
36
36
|
};
|
|
37
|
+
var sampleAttachment = {
|
|
38
|
+
filename: "reçu n° 42.pdf",
|
|
39
|
+
content: Uint8Array.from({ length: 256 }, (_, byte) => byte),
|
|
40
|
+
contentType: "application/pdf"
|
|
41
|
+
};
|
|
37
42
|
|
|
38
43
|
// src/conformance/cases/failure.ts
|
|
39
44
|
var failureCases = [
|
|
@@ -138,6 +143,25 @@ var sendCases = [
|
|
|
138
143
|
check(same(mail?.to, ["ada@example.test"]), "a name let a second recipient through");
|
|
139
144
|
}
|
|
140
145
|
},
|
|
146
|
+
{
|
|
147
|
+
id: "send.attachment",
|
|
148
|
+
title: "an attachment is delivered byte for byte, with its name and its type",
|
|
149
|
+
async run(context) {
|
|
150
|
+
await context.mailer.send({
|
|
151
|
+
...sampleMessage,
|
|
152
|
+
attachments: [sampleAttachment]
|
|
153
|
+
});
|
|
154
|
+
const [mail] = await context.delivered();
|
|
155
|
+
check(mail !== undefined, "the message with an attachment was not delivered");
|
|
156
|
+
check(mail.attachments !== undefined, "the harness's delivered() reads back no attachments — read them from the receiving end, or skip send.attachment with the reason");
|
|
157
|
+
check(mail.attachments.length === 1, `expected 1 delivered attachment, got ${mail.attachments.length}`);
|
|
158
|
+
const [file] = mail.attachments;
|
|
159
|
+
check(file?.filename === sampleAttachment.filename, "the attachment was not delivered with its file name");
|
|
160
|
+
check(file.contentType.toLowerCase() === sampleAttachment.contentType, "the attachment was not delivered with its content type");
|
|
161
|
+
check(file.content instanceof Uint8Array && same([...file.content], [...sampleAttachment.content]), "the attachment was not delivered byte for byte");
|
|
162
|
+
check(mail.text === sampleMessage.text && mail.html === sampleMessage.html, "the parts of a message with an attachment were not delivered as sent");
|
|
163
|
+
}
|
|
164
|
+
},
|
|
141
165
|
{
|
|
142
166
|
id: "send.refusesNoRecipient",
|
|
143
167
|
title: "a message with no recipient is refused with MailRefused, and nothing is sent",
|
|
@@ -173,6 +197,21 @@ Bcc: eve@example.test`
|
|
|
173
197
|
await nothingDelivered(context, "the message was refused");
|
|
174
198
|
}
|
|
175
199
|
},
|
|
200
|
+
{
|
|
201
|
+
id: "send.refusesAttachmentPath",
|
|
202
|
+
title: "an attachment named with a path is refused with MailRefused: a mail client could save it elsewhere",
|
|
203
|
+
async run(context) {
|
|
204
|
+
const error = await rejection(context.mailer.send({
|
|
205
|
+
...sampleMessage,
|
|
206
|
+
attachments: [
|
|
207
|
+
{ ...sampleAttachment, filename: "../secret-7f3a/report.pdf" }
|
|
208
|
+
]
|
|
209
|
+
}), "a send with an attachment named with a path");
|
|
210
|
+
check(error instanceof MailRefused2, "an attachment named with a path must throw MailRefused");
|
|
211
|
+
check(!error.message.includes("secret-7f3a"), "the refusal message holds the refused value");
|
|
212
|
+
await nothingDelivered(context, "the message was refused");
|
|
213
|
+
}
|
|
214
|
+
},
|
|
176
215
|
{
|
|
177
216
|
id: "send.refusesWithoutTheValue",
|
|
178
217
|
title: "a refusal names where the problem is, never the value",
|
|
@@ -262,7 +301,8 @@ function referenceMailerHarness() {
|
|
|
262
301
|
to: recipientsOf2(mail),
|
|
263
302
|
subject: mail.subject,
|
|
264
303
|
html: mail.html,
|
|
265
|
-
text: mail.text
|
|
304
|
+
text: mail.text,
|
|
305
|
+
attachments: mail.attachments ?? []
|
|
266
306
|
}));
|
|
267
307
|
},
|
|
268
308
|
faults: {
|
|
@@ -289,9 +329,10 @@ export {
|
|
|
289
329
|
failureCases,
|
|
290
330
|
referenceMailerHarness,
|
|
291
331
|
runMailerCase,
|
|
332
|
+
sampleAttachment,
|
|
292
333
|
sampleMessage,
|
|
293
334
|
sendCases
|
|
294
335
|
};
|
|
295
336
|
|
|
296
|
-
//# debugId=
|
|
337
|
+
//# debugId=D8FC228C191C6BF264756E2164756E21
|
|
297
338
|
//# sourceMappingURL=index.js.map
|
|
@@ -3,14 +3,14 @@
|
|
|
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
4
|
"sourcesContent": [
|
|
5
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&x=1\">réinitialiser</a></p>',\n\ttext: 'Bonjour Ada 👋,\\n\\nréinitialiser : https://example.test/r?t=abc&x=1\\n',\n};\n",
|
|
6
|
+
"import type { MailAttachment, 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&x=1\">réinitialiser</a></p>',\n\ttext: 'Bonjour Ada 👋,\\n\\nréinitialiser : https://example.test/r?t=abc&x=1\\n',\n};\n\n/**\n * A small binary file, every byte from 0 to 255 once — a NUL, a CR and an LF\n * among them, and bytes that are not UTF-8 — with a name a header must encode.\n */\nexport const sampleAttachment: MailAttachment = {\n\tfilename: 'reçu n° 42.pdf',\n\tcontent: Uint8Array.from({ length: 256 }, (_, byte) => byte),\n\tcontentType: 'application/pdf',\n};\n",
|
|
7
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",
|
|
8
|
+
"import { MailRefused } from '../../errors';\nimport { check, nothingDelivered, rejection, same } from '../assert';\nimport { sampleAttachment, 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.attachment',\n\t\ttitle:\n\t\t\t'an attachment is delivered byte for byte, with its name and its type',\n\t\tasync run(context) {\n\t\t\tawait context.mailer.send({\n\t\t\t\t...sampleMessage,\n\t\t\t\tattachments: [sampleAttachment],\n\t\t\t});\n\t\t\tconst [mail] = await context.delivered();\n\t\t\tcheck(\n\t\t\t\tmail !== undefined,\n\t\t\t\t'the message with an attachment was not delivered',\n\t\t\t);\n\t\t\tcheck(\n\t\t\t\tmail.attachments !== undefined,\n\t\t\t\t\"the harness's delivered() reads back no attachments — read them from the receiving end, or skip send.attachment with the reason\",\n\t\t\t);\n\t\t\tcheck(\n\t\t\t\tmail.attachments.length === 1,\n\t\t\t\t`expected 1 delivered attachment, got ${mail.attachments.length}`,\n\t\t\t);\n\t\t\tconst [file] = mail.attachments;\n\t\t\tcheck(\n\t\t\t\tfile?.filename === sampleAttachment.filename,\n\t\t\t\t'the attachment was not delivered with its file name',\n\t\t\t);\n\t\t\t// A media type is case-insensitive: a parser may lower it.\n\t\t\tcheck(\n\t\t\t\tfile.contentType.toLowerCase() === sampleAttachment.contentType,\n\t\t\t\t'the attachment was not delivered with its content type',\n\t\t\t);\n\t\t\tcheck(\n\t\t\t\tfile.content instanceof Uint8Array &&\n\t\t\t\t\tsame([...file.content], [...sampleAttachment.content]),\n\t\t\t\t'the attachment was not delivered byte for byte',\n\t\t\t);\n\t\t\tcheck(\n\t\t\t\tmail.text === sampleMessage.text && mail.html === sampleMessage.html,\n\t\t\t\t'the parts of a message with an attachment were not delivered as sent',\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.refusesAttachmentPath',\n\t\ttitle:\n\t\t\t'an attachment named with a path is refused with MailRefused: a mail client could save it elsewhere',\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\tattachments: [\n\t\t\t\t\t\t{ ...sampleAttachment, filename: '../secret-7f3a/report.pdf' },\n\t\t\t\t\t],\n\t\t\t\t}),\n\t\t\t\t'a send with an attachment named with a path',\n\t\t\t);\n\t\t\tcheck(\n\t\t\t\terror instanceof MailRefused,\n\t\t\t\t'an attachment named with a path must throw MailRefused',\n\t\t\t);\n\t\t\tcheck(\n\t\t\t\t!error.message.includes('secret-7f3a'),\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
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
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"
|
|
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\tattachments: mail.attachments ?? [],\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
12
|
],
|
|
13
|
-
"mappings": ";;;;;;;;;;;AAGO,SAAS,KAAK,CAAC,WAAoB,MAAiC;AAAA,EAC1E,IAAI,CAAC;AAAA,IAAW,MAAM,IAAI,MAAM,gBAAgB,MAAM;AAAA;AAGhD,IAAM,OAAO,CAAC,GAAY,MAChC,KAAK,UAAU,CAAC,MAAM,KAAK,UAAU,CAAC;AAMvC,eAAsB,SAAS,CAC9B,SACA,MACmB;AAAA,EACnB,OAAO,QAAQ,KACd,MAAM;AAAA,IACL,MAAM,IAAI,MAAM,gBAAgB,+BAA+B;AAAA,KAEhE,CAAC,UAAmB,KACrB;AAAA;AAID,eAAsB,gBAAgB,CACrC,SACA,MACC;AAAA,EACD,OACE,MAAM,QAAQ,UAAU,GAAG,WAAW,GACvC,GAAG,mCACJ;AAAA;;;AC/BM,IAAM,gBAA6B;AAAA,EACzC,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,SAAS;AAAA,EACT,MAAM;AAAA,EACN,MAAM;AAAA;AAAA;AAAA;AACP;;;
|
|
14
|
-
"debugId": "
|
|
13
|
+
"mappings": "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",
|
|
14
|
+
"debugId": "D8FC228C191C6BF264756E2164756E21",
|
|
15
15
|
"names": []
|
|
16
16
|
}
|
|
@@ -1 +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,
|
|
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,CAmCtD"}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import type { MailMessage } from '../types';
|
|
1
|
+
import type { MailAttachment, MailMessage } from '../types';
|
|
2
2
|
/** A message with the characters a transport most often mangles. */
|
|
3
3
|
export declare const sampleMessage: MailMessage;
|
|
4
|
+
/**
|
|
5
|
+
* A small binary file, every byte from 0 to 255 once — a NUL, a CR and an LF
|
|
6
|
+
* among them, and bytes that are not UTF-8 — with a name a header must encode.
|
|
7
|
+
*/
|
|
8
|
+
export declare const sampleAttachment: MailAttachment;
|
|
4
9
|
//# sourceMappingURL=sample.d.ts.map
|
|
@@ -1 +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;
|
|
1
|
+
{"version":3,"file":"sample.d.ts","sourceRoot":"","sources":["../../src/conformance/sample.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAE5D,oEAAoE;AACpE,eAAO,MAAM,aAAa,EAAE,WAM3B,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,gBAAgB,EAAE,cAI9B,CAAC"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Mailer } from '../types';
|
|
1
|
+
import type { MailAttachment, Mailer } from '../types';
|
|
2
2
|
/**
|
|
3
3
|
* What the receiving end got, as the harness reads it back: from the test
|
|
4
4
|
* SMTP server, from the recorded HTTP request, from the memory outbox.
|
|
@@ -9,6 +9,16 @@ export interface DeliveredMail {
|
|
|
9
9
|
readonly subject: string;
|
|
10
10
|
readonly html: string;
|
|
11
11
|
readonly text: string;
|
|
12
|
+
/**
|
|
13
|
+
* The files that arrived with it, in order, each with its bytes, its name
|
|
14
|
+
* and its type as the receiving end read them — `[]` when none did.
|
|
15
|
+
*
|
|
16
|
+
* Optional, so a harness written before attachments still compiles; but
|
|
17
|
+
* **its absence is reported, never passed over**: `send.attachment` fails
|
|
18
|
+
* on a harness that leaves it out, until it reads them back or skips the
|
|
19
|
+
* case with a reason.
|
|
20
|
+
*/
|
|
21
|
+
readonly attachments?: readonly MailAttachment[];
|
|
12
22
|
}
|
|
13
23
|
/**
|
|
14
24
|
* How the suite makes a transport fail **the way its provider fails**: a
|
|
@@ -1 +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;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/conformance/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAEvD;;;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;IACtB;;;;;;;;OAQG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,cAAc,EAAE,CAAC;CACjD;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"}
|
package/dist/errors.d.ts
CHANGED
|
@@ -22,10 +22,10 @@ export type MailErrorCode =
|
|
|
22
22
|
/**
|
|
23
23
|
* The message itself was refused, before or by the transport: no
|
|
24
24
|
* recipient, something that is not an address, a line break in the
|
|
25
|
-
* subject or a header,
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* again.
|
|
25
|
+
* subject or a header, an attachment that is not bytes or is badly named,
|
|
26
|
+
* a provider answering that the message is malformed or too large, or —
|
|
27
|
+
* from `@nxgt/mail/renderer` — a URL variable that is not an `http:`,
|
|
28
|
+
* `https:` or `mailto:` URL. Sending it again unchanged fails again.
|
|
29
29
|
*/
|
|
30
30
|
| 'MAIL_REFUSED';
|
|
31
31
|
/** Options every error of this package accepts. */
|
package/dist/index.d.ts
CHANGED
|
@@ -17,5 +17,5 @@ export { MailError, type MailErrorCode, type MailErrorOptions, MailFailure, Mail
|
|
|
17
17
|
export { parseAcceptLanguage, pickLocale, type WantedLocales } from './locale';
|
|
18
18
|
export { createMemoryMailer, type MemoryMail, type MemoryMailer, } from './memory';
|
|
19
19
|
export { addressOf, checkMessage, recipientsOf } from './message';
|
|
20
|
-
export type { Address, Mailer, MailMessage, Rendered, SentMail } from './types';
|
|
20
|
+
export type { Address, MailAttachment, Mailer, MailMessage, Rendered, SentMail, } from './types';
|
|
21
21
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +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,
|
|
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,EACX,OAAO,EACP,cAAc,EACd,MAAM,EACN,WAAW,EACX,QAAQ,EACR,QAAQ,GACR,MAAM,SAAS,CAAC"}
|
package/dist/index.js
CHANGED
package/dist/memory.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../src/memory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAe,MAAM,UAAU,CAAC;AAEvD,OAAO,KAAK,EAAE,MAAM,EAAE,WAAW,EAAY,MAAM,SAAS,CAAC;AAE7D,sEAAsE;AACtE,MAAM,WAAW,UAAW,SAAQ,WAAW;IAC9C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;GAMG;AACH,MAAM,WAAW,YAAa,SAAQ,MAAM;IAC3C,wFAAwF;IACxF,QAAQ,CAAC,IAAI,EAAE,SAAS,UAAU,EAAE,CAAC;IACrC;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B;;;;OAIG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,GAAG,IAAI,CAAC;IAClC,mEAAmE;IACnE,KAAK,IAAI,IAAI,CAAC;CACd;
|
|
1
|
+
{"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../src/memory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAe,MAAM,UAAU,CAAC;AAEvD,OAAO,KAAK,EAAE,MAAM,EAAE,WAAW,EAAY,MAAM,SAAS,CAAC;AAE7D,sEAAsE;AACtE,MAAM,WAAW,UAAW,SAAQ,WAAW;IAC9C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;GAMG;AACH,MAAM,WAAW,YAAa,SAAQ,MAAM;IAC3C,wFAAwF;IACxF,QAAQ,CAAC,IAAI,EAAE,SAAS,UAAU,EAAE,CAAC;IACrC;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B;;;;OAIG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,GAAG,IAAI,CAAC;IAClC,mEAAmE;IACnE,KAAK,IAAI,IAAI,CAAC;CACd;AAsBD,gFAAgF;AAChF,wBAAgB,kBAAkB,IAAI,YAAY,CAyCjD"}
|
package/dist/message.d.ts
CHANGED
|
@@ -17,7 +17,12 @@ export declare function addressOf(address: Address): string;
|
|
|
17
17
|
* - every header name is letters, digits and hyphens, none names what the
|
|
18
18
|
* transport writes from the message (`To`, `Cc`, `Bcc`, `From`, `Sender`,
|
|
19
19
|
* `Reply-To`, `Return-Path`, `Subject`, `MIME-Version`, `Content-*`, in
|
|
20
|
-
* any case), and no header value holds a line break
|
|
20
|
+
* any case), and no header value holds a line break;
|
|
21
|
+
* - `attachments`, when present, is an array without holes — empty is the
|
|
22
|
+
* same as absent — and each entry has its bytes as a `Uint8Array`, a
|
|
23
|
+
* `filename` that is not empty, `.` or `..` and holds no `/`, `\`, line
|
|
24
|
+
* break, control or format character, and a `contentType` that is a bare
|
|
25
|
+
* `type/subtype`, never `multipart/*` or `message/*`.
|
|
21
26
|
*/
|
|
22
27
|
export declare function checkMessage(message: MailMessage): void;
|
|
23
28
|
//# sourceMappingURL=message.d.ts.map
|
package/dist/message.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"message.d.ts","sourceRoot":"","sources":["../src/message.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,
|
|
1
|
+
{"version":3,"file":"message.d.ts","sourceRoot":"","sources":["../src/message.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAkB,WAAW,EAAE,MAAM,SAAS,CAAC;AAgCpE,iEAAiE;AACjE,wBAAgB,YAAY,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,EAAE,CAG3D;AAED,8CAA8C;AAC9C,wBAAgB,SAAS,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAElD;AAwDD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI,CAqDvD"}
|