@nxgt/mail 0.1.0 → 0.3.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 +120 -5
- package/dist/chunks/{index-0f7kdb8k.js → index-nkzwt8vn.js} +80 -3
- package/dist/chunks/index-nkzwt8vn.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 +7 -1
- package/dist/memory.d.ts.map +1 -1
- package/dist/message.d.ts +7 -1
- package/dist/message.d.ts.map +1 -1
- package/dist/types.d.ts +33 -0
- package/dist/types.d.ts.map +1 -1
- package/docs/README.md +3 -3
- package/docs/guide/sending.md +178 -3
- package/docs/guide/testing.md +125 -4
- package/docs/guide/transports.md +129 -13
- package/docs/roadmap.md +21 -1
- package/docs/troubleshooting.md +324 -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,82 @@ 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
|
+
|
|
157
|
+
### Idempotency — a retry that delivers once
|
|
158
|
+
|
|
159
|
+
`idempotencyKey` names a send, so sending it again — a retry after a timeout,
|
|
160
|
+
a job run twice — delivers it once where the transport can deduplicate.
|
|
161
|
+
Derive it from what the e-mail is about, never from the time or a random
|
|
162
|
+
value. The memory mailer honours it, so a test can prove a retry is safe:
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
import { expect, it } from 'bun:test';
|
|
166
|
+
import { createMemoryMailer } from '@nxgt/mail';
|
|
167
|
+
|
|
168
|
+
it('sends one receipt, however often the job runs', async () => {
|
|
169
|
+
const mailer = createMemoryMailer();
|
|
170
|
+
const receipt = {
|
|
171
|
+
to: 'ada@example.com',
|
|
172
|
+
subject: 'Your receipt',
|
|
173
|
+
html: '<p>Thank you for your order.</p>',
|
|
174
|
+
text: 'Thank you for your order.',
|
|
175
|
+
idempotencyKey: 'order-42/receipt', // 1 to 256 visible ASCII characters
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const first = await mailer.send(receipt);
|
|
179
|
+
const again = await mailer.send(receipt); // the retry
|
|
180
|
+
|
|
181
|
+
expect(again).toEqual(first); // { messageId: 'memory-1' }
|
|
182
|
+
expect(mailer.sent).toHaveLength(1); // delivered once
|
|
183
|
+
expect(mailer.attempts).toBe(2);
|
|
184
|
+
});
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
A different message under a key already delivered is refused with
|
|
188
|
+
`MailRefused` — a key names one e-mail — and a failed send leaves its key
|
|
189
|
+
free, so its retry delivers. `@nxgt/mail-resend`
|
|
190
|
+
sends the key as Resend's `Idempotency-Key`, which Resend keeps for 24 hours;
|
|
191
|
+
`@nxgt/mail-smtp` ignores it — SMTP has no such mechanism, and a message sent
|
|
192
|
+
twice is delivered twice. A key that is not 1 to 256 visible ASCII characters
|
|
193
|
+
is refused with `MailRefused`. See
|
|
194
|
+
[Sending — idempotency](docs/guide/sending.md#idempotency--sending-once).
|
|
195
|
+
|
|
120
196
|
### Errors — switch on `code`
|
|
121
197
|
|
|
122
198
|
Both errors extend `MailError`, whose `code` is a union a `switch` exhausts.
|
|
@@ -201,14 +277,32 @@ peer, and passes the conformance suite:
|
|
|
201
277
|
```ts
|
|
202
278
|
import { checkMessage, MailFailure, MailRefused, type Mailer, recipientsOf } from '@nxgt/mail';
|
|
203
279
|
|
|
280
|
+
// JSON has no bytes: an attachment travels as base64, read in slices.
|
|
281
|
+
function base64Of(bytes: Uint8Array): string {
|
|
282
|
+
let binary = '';
|
|
283
|
+
for (let start = 0; start < bytes.length; start += 0x8000) {
|
|
284
|
+
binary += String.fromCharCode(...bytes.subarray(start, start + 0x8000));
|
|
285
|
+
}
|
|
286
|
+
return btoa(binary);
|
|
287
|
+
}
|
|
288
|
+
|
|
204
289
|
export function createHttpMailer(endpoint: string, apiKey: string): Mailer {
|
|
205
290
|
return {
|
|
206
291
|
async send(message) {
|
|
207
292
|
checkMessage(message); // MailRefused, naming where, never the value
|
|
293
|
+
const { idempotencyKey, ...fields } = message; // names the send: never in the body
|
|
294
|
+
const attachments = message.attachments?.length
|
|
295
|
+
? message.attachments.map((file) => ({ ...file, content: base64Of(file.content) }))
|
|
296
|
+
: undefined; // an empty list is none
|
|
208
297
|
const response = await fetch(endpoint, {
|
|
209
298
|
method: 'POST',
|
|
210
|
-
headers: {
|
|
211
|
-
|
|
299
|
+
headers: {
|
|
300
|
+
authorization: `Bearer ${apiKey}`,
|
|
301
|
+
'content-type': 'application/json',
|
|
302
|
+
// if the provider deduplicates; a transport whose provider cannot ignores the key
|
|
303
|
+
...(idempotencyKey === undefined ? {} : { 'idempotency-key': idempotencyKey }),
|
|
304
|
+
},
|
|
305
|
+
body: JSON.stringify({ ...fields, to: recipientsOf(message), attachments }),
|
|
212
306
|
}).catch((cause: unknown) => {
|
|
213
307
|
throw new MailFailure('send: the provider could not be reached', { cause });
|
|
214
308
|
});
|
|
@@ -271,6 +365,11 @@ with `MailRefused`; write `{ name: 'Ada', address: 'ada@example.com' }`.
|
|
|
271
365
|
into an unhandled rejection and the user into someone waiting for an e-mail
|
|
272
366
|
that never comes. `await` it, or hand it to a queue that does.
|
|
273
367
|
|
|
368
|
+
**An attachment is bytes you already hold.** Read the file first
|
|
369
|
+
(`await readFile(path)`, `new Uint8Array(await response.arrayBuffer())`);
|
|
370
|
+
a `path` or a URL is a compile error and a `MailRefused`. Past a few
|
|
371
|
+
megabytes, send a signed link instead.
|
|
372
|
+
|
|
274
373
|
**A custom header cannot set an address.** `headers: { Bcc: '…' }` would add
|
|
275
374
|
a recipient no check saw: `checkMessage` refuses `To`, `Cc`, `Bcc`, `From`,
|
|
276
375
|
`Sender`, `Reply-To`, `Return-Path`, `Subject`, `MIME-Version` and
|
|
@@ -280,6 +379,10 @@ a recipient no check saw: `checkMessage` refuses `To`, `Cc`, `Bcc`, `From`,
|
|
|
280
379
|
again unchanged. Nothing in this package retries a `MAIL_FAILED`: a retry is
|
|
281
380
|
your decision, made where you can see it.
|
|
282
381
|
|
|
382
|
+
**An idempotency key from the clock or a random value protects nothing.**
|
|
383
|
+
``idempotencyKey: `receipt-${Date.now()}` `` gives the retry a new key, and
|
|
384
|
+
the e-mail goes out twice; write ``idempotencyKey: `order-${order.id}/receipt` ``.
|
|
385
|
+
|
|
283
386
|
**The locale is the recipient's, not the request's.** An administrator who
|
|
284
387
|
invites a user sends the invitation in the *user's* locale:
|
|
285
388
|
`pickLocale(invitee.locale, supported, fallback)`.
|
|
@@ -293,7 +396,7 @@ gives a test file `describe` and `it` as bare identifiers, not on `globalThis`.
|
|
|
293
396
|
|
|
294
397
|
## Type safety, counted
|
|
295
398
|
|
|
296
|
-
**
|
|
399
|
+
**22 plausible mistakes, 22 refused** at compile time, each measured by a
|
|
297
400
|
`@ts-expect-error` in
|
|
298
401
|
[`test/types/refusals.ts`](https://github.com/softistx/nxgt-mail/blob/develop/packages/mail/test/types/refusals.ts)
|
|
299
402
|
that fails the typecheck the moment it stops holding:
|
|
@@ -323,6 +426,18 @@ The name written as a literal and the variables at the call, as usual:
|
|
|
323
426
|
16. The variables left out altogether, for an e-mail that takes some.
|
|
324
427
|
17. A number for a URL variable: a URL is a string.
|
|
325
428
|
|
|
429
|
+
And an attachment:
|
|
430
|
+
|
|
431
|
+
18. Its `content` as a string: an attachment is bytes, a `Uint8Array`.
|
|
432
|
+
19. A `path` instead of the bytes: no transport reads a file for you.
|
|
433
|
+
20. No `contentType`: nothing guesses it from the file name.
|
|
434
|
+
21. One attachment, not in a list.
|
|
435
|
+
|
|
436
|
+
And the idempotency key:
|
|
437
|
+
|
|
438
|
+
22. A number (`idempotencyKey: order.id`): the key is a string, as
|
|
439
|
+
`order-42/receipt`.
|
|
440
|
+
|
|
326
441
|
The same file holds the calls that must keep compiling: a refusal that refuses
|
|
327
442
|
the correct call is a bug.
|
|
328
443
|
|
|
@@ -8,6 +8,11 @@ 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;
|
|
15
|
+
var IDEMPOTENCY_KEY = /^[\x21-\x7E]{1,256}$/;
|
|
11
16
|
function recipientsOf2(message) {
|
|
12
17
|
const to = Array.isArray(message.to) ? message.to : [message.to];
|
|
13
18
|
return to.map(addressOf2);
|
|
@@ -32,6 +37,20 @@ function checkAddress(address, where) {
|
|
|
32
37
|
throw new MailRefused2(`send: ${where}.name must be a string without a line break`);
|
|
33
38
|
}
|
|
34
39
|
}
|
|
40
|
+
function checkAttachment(attachment, where) {
|
|
41
|
+
if (typeof attachment !== "object" || attachment === null) {
|
|
42
|
+
throw new MailRefused2(`send: ${where} must be an object, as { filename, content, contentType }`);
|
|
43
|
+
}
|
|
44
|
+
if (!(attachment.content instanceof Uint8Array)) {
|
|
45
|
+
throw new MailRefused2(`send: ${where}.content must be a Uint8Array — the file's bytes, never a path or a URL`);
|
|
46
|
+
}
|
|
47
|
+
if (typeof attachment.filename !== "string" || attachment.filename === "" || DOT_NAME.test(attachment.filename) || FILENAME_REFUSED.test(attachment.filename)) {
|
|
48
|
+
throw new MailRefused2(`send: ${where}.filename must be a file name — not empty, not . or .., without / or \\, a line break or a control character`);
|
|
49
|
+
}
|
|
50
|
+
if (typeof attachment.contentType !== "string" || !CONTENT_TYPE.test(attachment.contentType) || CONTAINER_TYPE.test(attachment.contentType)) {
|
|
51
|
+
throw new MailRefused2(`send: ${where}.contentType must be a file's type/subtype, as application/pdf — never multipart/* or message/*`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
35
54
|
function checkMessage2(message) {
|
|
36
55
|
if (typeof message !== "object" || message === null) {
|
|
37
56
|
throw new MailRefused2("send: the message must be an object");
|
|
@@ -69,14 +88,60 @@ function checkMessage2(message) {
|
|
|
69
88
|
throw new MailRefused2(`send: header ${name} must be a string without a line break`);
|
|
70
89
|
}
|
|
71
90
|
}
|
|
91
|
+
if (message.attachments !== undefined) {
|
|
92
|
+
if (!Array.isArray(message.attachments)) {
|
|
93
|
+
throw new MailRefused2("send: attachments must be an array");
|
|
94
|
+
}
|
|
95
|
+
for (let index = 0;index < message.attachments.length; index++) {
|
|
96
|
+
checkAttachment(message.attachments[index], `attachments[${index}]`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (message.idempotencyKey !== undefined && (typeof message.idempotencyKey !== "string" || !IDEMPOTENCY_KEY.test(message.idempotencyKey))) {
|
|
100
|
+
throw new MailRefused2("send: idempotencyKey must be 1 to 256 visible ASCII characters, as order-42/receipt");
|
|
101
|
+
}
|
|
72
102
|
}
|
|
73
103
|
|
|
74
104
|
// src/memory.ts
|
|
105
|
+
function copyOf(message) {
|
|
106
|
+
const { attachments, ...rest } = message;
|
|
107
|
+
const copy = structuredClone(rest);
|
|
108
|
+
if (attachments === undefined)
|
|
109
|
+
return copy;
|
|
110
|
+
return {
|
|
111
|
+
...copy,
|
|
112
|
+
attachments: attachments.map((attachment) => ({
|
|
113
|
+
filename: attachment.filename,
|
|
114
|
+
content: new Uint8Array(attachment.content),
|
|
115
|
+
contentType: attachment.contentType
|
|
116
|
+
}))
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
var hexOf = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
120
|
+
function fingerprintOf(message) {
|
|
121
|
+
const address = (value) => typeof value === "object" ? [value.name, value.address] : value;
|
|
122
|
+
const to = Array.isArray(message.to) ? message.to : [message.to];
|
|
123
|
+
const headers = Object.entries(message.headers ?? {}).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
124
|
+
return JSON.stringify([
|
|
125
|
+
to.map(address),
|
|
126
|
+
address(message.from),
|
|
127
|
+
address(message.replyTo),
|
|
128
|
+
message.subject,
|
|
129
|
+
message.html,
|
|
130
|
+
message.text,
|
|
131
|
+
headers,
|
|
132
|
+
(message.attachments ?? []).map((file) => [
|
|
133
|
+
file.filename,
|
|
134
|
+
file.contentType,
|
|
135
|
+
hexOf(file.content)
|
|
136
|
+
])
|
|
137
|
+
]);
|
|
138
|
+
}
|
|
75
139
|
function createMemoryMailer2() {
|
|
76
140
|
let sent = [];
|
|
77
141
|
let failures = [];
|
|
78
142
|
let attempts = 0;
|
|
79
143
|
let counter = 0;
|
|
144
|
+
let keys = new Map;
|
|
80
145
|
return {
|
|
81
146
|
get sent() {
|
|
82
147
|
return sent.map((mail) => structuredClone(mail));
|
|
@@ -93,6 +158,7 @@ function createMemoryMailer2() {
|
|
|
93
158
|
sent = [];
|
|
94
159
|
failures = [];
|
|
95
160
|
attempts = 0;
|
|
161
|
+
keys = new Map;
|
|
96
162
|
},
|
|
97
163
|
async send(message) {
|
|
98
164
|
checkMessage2(message);
|
|
@@ -100,9 +166,20 @@ function createMemoryMailer2() {
|
|
|
100
166
|
const failure = failures.shift();
|
|
101
167
|
if (failure !== undefined)
|
|
102
168
|
throw failure;
|
|
169
|
+
const key = message.idempotencyKey;
|
|
170
|
+
const fingerprint = key === undefined ? "" : fingerprintOf(message);
|
|
171
|
+
const delivered = key === undefined ? undefined : keys.get(key);
|
|
172
|
+
if (delivered !== undefined) {
|
|
173
|
+
if (delivered.fingerprint !== fingerprint) {
|
|
174
|
+
throw new MailRefused2("send: idempotencyKey was already used for a different message — a key names one e-mail");
|
|
175
|
+
}
|
|
176
|
+
return { messageId: delivered.messageId };
|
|
177
|
+
}
|
|
103
178
|
counter += 1;
|
|
104
179
|
const messageId = `memory-${counter}`;
|
|
105
|
-
sent.push({ ...
|
|
180
|
+
sent.push({ ...copyOf(message), messageId });
|
|
181
|
+
if (key !== undefined)
|
|
182
|
+
keys.set(key, { messageId, fingerprint });
|
|
106
183
|
return { messageId };
|
|
107
184
|
}
|
|
108
185
|
};
|
|
@@ -110,5 +187,5 @@ function createMemoryMailer2() {
|
|
|
110
187
|
|
|
111
188
|
export { recipientsOf2, addressOf2, checkMessage2, createMemoryMailer2 };
|
|
112
189
|
|
|
113
|
-
//# debugId=
|
|
114
|
-
//# sourceMappingURL=index-
|
|
190
|
+
//# debugId=1A74D07F2680AEF664756E2164756E21
|
|
191
|
+
//# sourceMappingURL=index-nkzwt8vn.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// Written into a header by the transports that use it (Resend's\n// `Idempotency-Key`): visible ASCII only, so no line break, and Resend's length.\nconst IDEMPOTENCY_KEY = /^[\\x21-\\x7E]{1,256}$/;\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 * - `idempotencyKey`, when present, is 1 to 256 visible ASCII characters.\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\tif (\n\t\tmessage.idempotencyKey !== undefined &&\n\t\t(typeof message.idempotencyKey !== 'string' ||\n\t\t\t!IDEMPOTENCY_KEY.test(message.idempotencyKey))\n\t) {\n\t\tthrow new MailRefused(\n\t\t\t'send: idempotencyKey must be 1 to 256 visible ASCII characters, as order-42/receipt',\n\t\t);\n\t}\n}\n",
|
|
6
|
+
"import { type MailError, MailFailure, MailRefused } from './errors';\nimport { checkMessage } from './message';\nimport type { Address, 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 *\n * It honours `idempotencyKey`, as Resend does: the same message again under a\n * key it already delivered resolves with that delivery's `messageId`, and is\n * not delivered again; a different message under that key is a\n * {@link MailRefused}. A send that failed delivered nothing, so its key stays\n * free.\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, any queued failure, and the idempotency keys. */\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/** Bytes as hex: short to compare, whatever holds them. */\nconst hexOf = (bytes: Uint8Array) =>\n\tArray.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('');\n\n/**\n * What makes two messages the same one, for an idempotency key: the fields of\n * the port, read by name as `checkMessage` reads them — so a field on a\n * prototype counts and a field the port does not know does not — in one\n * form, however the object was written: `to` as one address or a list of\n * one, no `headers` or `attachments` or an empty one, headers in any order,\n * bytes in a `Buffer` or a plain `Uint8Array`.\n */\nfunction fingerprintOf(message: MailMessage): string {\n\tconst address = (value: Address | undefined) =>\n\t\ttypeof value === 'object' ? [value.name, value.address] : value;\n\tconst to = Array.isArray(message.to) ? message.to : [message.to];\n\tconst headers = Object.entries(message.headers ?? {}).sort(([a], [b]) =>\n\t\ta < b ? -1 : a > b ? 1 : 0,\n\t);\n\treturn JSON.stringify([\n\t\tto.map(address),\n\t\taddress(message.from),\n\t\taddress(message.replyTo),\n\t\tmessage.subject,\n\t\tmessage.html,\n\t\tmessage.text,\n\t\theaders,\n\t\t(message.attachments ?? []).map((file) => [\n\t\t\tfile.filename,\n\t\t\tfile.contentType,\n\t\t\thexOf(file.content),\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\tlet keys = new Map<string, { messageId: string; fingerprint: string }>();\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\tkeys = new Map();\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\tconst key = message.idempotencyKey;\n\t\t\tconst fingerprint = key === undefined ? '' : fingerprintOf(message);\n\t\t\tconst delivered = key === undefined ? undefined : keys.get(key);\n\t\t\tif (delivered !== undefined) {\n\t\t\t\tif (delivered.fingerprint !== fingerprint) {\n\t\t\t\t\tthrow new MailRefused(\n\t\t\t\t\t\t'send: idempotencyKey was already used for a different message — a key names one e-mail',\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\treturn { messageId: delivered.messageId };\n\t\t\t}\n\n\t\t\tcounter += 1;\n\t\t\tconst messageId = `memory-${counter}`;\n\t\t\tsent.push({ ...copyOf(message), messageId });\n\t\t\tif (key !== undefined) keys.set(key, { messageId, fingerprint });\n\t\t\treturn { messageId };\n\t\t},\n\t};\n}\n"
|
|
7
|
+
],
|
|
8
|
+
"mappings": "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",
|
|
9
|
+
"debugId": "1A74D07F2680AEF664756E2164756E21",
|
|
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-nkzwt8vn.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"}
|