@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
|
@@ -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
CHANGED
|
@@ -10,6 +10,12 @@ export interface MemoryMail extends MailMessage {
|
|
|
10
10
|
* It refuses exactly what every transport refuses (it calls
|
|
11
11
|
* {@link checkMessage}), and it can be told to fail, so a test can prove what
|
|
12
12
|
* the application does when a send throws.
|
|
13
|
+
*
|
|
14
|
+
* It honours `idempotencyKey`, as Resend does: the same message again under a
|
|
15
|
+
* key it already delivered resolves with that delivery's `messageId`, and is
|
|
16
|
+
* not delivered again; a different message under that key is a
|
|
17
|
+
* {@link MailRefused}. A send that failed delivered nothing, so its key stays
|
|
18
|
+
* free.
|
|
13
19
|
*/
|
|
14
20
|
export interface MemoryMailer extends Mailer {
|
|
15
21
|
/** Every message accepted so far, oldest first. A copy: mutating it changes nothing. */
|
|
@@ -26,7 +32,7 @@ export interface MemoryMailer extends Mailer {
|
|
|
26
32
|
* fail the next two sends.
|
|
27
33
|
*/
|
|
28
34
|
failNext(error?: MailError): void;
|
|
29
|
-
/** Forgets what was sent, the attempts,
|
|
35
|
+
/** Forgets what was sent, the attempts, any queued failure, and the idempotency keys. */
|
|
30
36
|
clear(): void;
|
|
31
37
|
}
|
|
32
38
|
/** Creates a {@link MemoryMailer}. Message ids are `memory-1`, `memory-2`, … */
|
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,
|
|
1
|
+
{"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../src/memory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAA4B,MAAM,UAAU,CAAC;AAEpE,OAAO,KAAK,EAAW,MAAM,EAAE,WAAW,EAAY,MAAM,SAAS,CAAC;AAEtE,sEAAsE;AACtE,MAAM,WAAW,UAAW,SAAQ,WAAW;IAC9C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;;;;;;;GAYG;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,yFAAyF;IACzF,KAAK,IAAI,IAAI,CAAC;CACd;AAyDD,gFAAgF;AAChF,wBAAgB,kBAAkB,IAAI,YAAY,CAwDjD"}
|
package/dist/message.d.ts
CHANGED
|
@@ -17,7 +17,13 @@ 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/*`;
|
|
26
|
+
* - `idempotencyKey`, when present, is 1 to 256 visible ASCII characters.
|
|
21
27
|
*/
|
|
22
28
|
export declare function checkMessage(message: MailMessage): void;
|
|
23
29
|
//# 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;AAmCpE,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;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI,CA+DvD"}
|
package/dist/types.d.ts
CHANGED
|
@@ -24,6 +24,21 @@ export interface Rendered {
|
|
|
24
24
|
readonly html: string;
|
|
25
25
|
readonly text: string;
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* A file sent with an e-mail, **as bytes**: never a path or a URL for the
|
|
29
|
+
* transport to read, never a stream. A large or sensitive file is a signed
|
|
30
|
+
* link in the template instead — a URL variable.
|
|
31
|
+
*
|
|
32
|
+
* `filename` is what the recipient's mail client shows and saves it as: no
|
|
33
|
+
* path (`/`, `\`, `.`, `..`), no line break, no control or format character.
|
|
34
|
+
* `contentType` is a bare `type/subtype`, as `application/pdf`, without
|
|
35
|
+
* parameters, and never a MIME container (`multipart/*`, `message/*`).
|
|
36
|
+
*/
|
|
37
|
+
export interface MailAttachment {
|
|
38
|
+
readonly filename: string;
|
|
39
|
+
readonly content: Uint8Array;
|
|
40
|
+
readonly contentType: string;
|
|
41
|
+
}
|
|
27
42
|
/**
|
|
28
43
|
* A rendered e-mail, addressed. What a {@link Mailer} sends.
|
|
29
44
|
*
|
|
@@ -42,6 +57,24 @@ export interface MailMessage extends Rendered {
|
|
|
42
57
|
* case — is refused.
|
|
43
58
|
*/
|
|
44
59
|
readonly headers?: Readonly<Record<string, string>>;
|
|
60
|
+
/**
|
|
61
|
+
* Files sent with the e-mail, in order. An empty list is the same as none.
|
|
62
|
+
* Each is bytes, checked by `checkMessage`: see {@link MailAttachment}.
|
|
63
|
+
*/
|
|
64
|
+
readonly attachments?: readonly MailAttachment[];
|
|
65
|
+
/**
|
|
66
|
+
* Names this send, so sending it again — a retry after a timeout, a job run
|
|
67
|
+
* twice — delivers it once. 1 to 256 visible ASCII characters, as
|
|
68
|
+
* `order-42/receipt`; derive it from what the e-mail is about, never from
|
|
69
|
+
* the time or a random value, or a retry carries a new one.
|
|
70
|
+
*
|
|
71
|
+
* **A transport that can deduplicate uses it; one that cannot ignores it.**
|
|
72
|
+
* Resend keeps a key for 24 hours; SMTP has no such thing, and ignores it.
|
|
73
|
+
* The memory mailer answers the same message under a key it already
|
|
74
|
+
* delivered with the same `messageId`, and delivers nothing more; a
|
|
75
|
+
* different message under that key is a `MailRefused`, as Resend's `409`.
|
|
76
|
+
*/
|
|
77
|
+
readonly idempotencyKey?: string;
|
|
45
78
|
}
|
|
46
79
|
/** What a transport answers once it has handed a message over. */
|
|
47
80
|
export interface SentMail {
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,MAAM,MAAM,OAAO,GAChB,MAAM,GACN;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAEvD;;;;;GAKG;AACH,MAAM,WAAW,QAAQ;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACtB;AAED;;;;;GAKG;AACH,MAAM,WAAW,WAAY,SAAQ,QAAQ;IAC5C,QAAQ,CAAC,EAAE,EAAE,OAAO,GAAG,SAAS,OAAO,EAAE,CAAC;IAC1C,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B;;;;;;OAMG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,MAAM,MAAM,OAAO,GAChB,MAAM,GACN;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAEvD;;;;;GAKG;AACH,MAAM,WAAW,QAAQ;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,cAAc;IAC9B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAC;IAC7B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC7B;AAED;;;;;GAKG;AACH,MAAM,WAAW,WAAY,SAAQ,QAAQ;IAC5C,QAAQ,CAAC,EAAE,EAAE,OAAO,GAAG,SAAS,OAAO,EAAE,CAAC;IAC1C,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B;;;;;;OAMG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACpD;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,cAAc,EAAE,CAAC;IACjD;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;CACjC;AAED,kEAAkE;AAClE,MAAM,WAAW,QAAQ;IACxB;;;OAGG;IACH,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAClC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,MAAM;IACtB,IAAI,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CAC9C"}
|
package/docs/README.md
CHANGED
|
@@ -8,9 +8,9 @@ mailer, transport, hand-over, refusal, failure — are defined once, in the
|
|
|
8
8
|
| Page | Read it when |
|
|
9
9
|
| --- | --- |
|
|
10
10
|
| [Rendering](guide/rendering.md) | You are turning a Maizzle build of `@nxgt/mail-i18n` into an e-mail with `createMailRenderer`: its options, typing it with the build's `MailEmails`, choosing the locale, escaping, URL variables, deploying the build, and every error |
|
|
11
|
-
| [Sending](guide/sending.md) | You are calling `mailer.send`: the `MailMessage` shape, addresses, headers, what `send` answers, and turning `MailFailure` and `MailRefused` into a response |
|
|
12
|
-
| [Testing](guide/testing.md) | You are testing code that sends e-mail with `createMemoryMailer`: reading the outbox, making a send fail, counting attempts |
|
|
11
|
+
| [Sending](guide/sending.md) | You are calling `mailer.send`: the `MailMessage` shape, addresses, headers, attachments, the idempotency key, what `send` answers, and turning `MailFailure` and `MailRefused` into a response |
|
|
12
|
+
| [Testing](guide/testing.md) | You are testing code that sends e-mail with `createMemoryMailer`: reading the outbox and its attachments, making a send fail, counting attempts, a retry under an idempotency key |
|
|
13
13
|
| [Locales](guide/locales.md) | You are choosing the locale an e-mail is rendered in, with `pickLocale` and `parseAcceptLanguage` |
|
|
14
|
-
| [Writing a transport](guide/transports.md) | You are implementing the `Mailer` port for a provider
|
|
14
|
+
| [Writing a transport](guide/transports.md) | You are implementing the `Mailer` port for a provider — the idempotency key included — and running `@nxgt/mail/conformance` against it |
|
|
15
15
|
| [Troubleshooting](troubleshooting.md) | You have an error message and want its cause and its fix |
|
|
16
16
|
| [Roadmap](roadmap.md) | You want to know what is coming, what shipped, and what is deliberately not planned |
|
package/docs/guide/sending.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# Sending
|
|
2
2
|
|
|
3
3
|
This page is for calling `mailer.send`: the shape of what it takes, the
|
|
4
|
-
addresses and
|
|
4
|
+
addresses, headers and attachments it accepts, the idempotency key that makes
|
|
5
|
+
a retry safe, what it answers, and what it throws.
|
|
5
6
|
|
|
6
7
|
```ts
|
|
7
8
|
import { createMemoryMailer } from '@nxgt/mail';
|
|
@@ -73,6 +74,14 @@ interface MailMessage extends Rendered {
|
|
|
73
74
|
readonly from?: Address;
|
|
74
75
|
readonly replyTo?: Address;
|
|
75
76
|
readonly headers?: Readonly<Record<string, string>>;
|
|
77
|
+
readonly attachments?: readonly MailAttachment[];
|
|
78
|
+
readonly idempotencyKey?: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
interface MailAttachment {
|
|
82
|
+
readonly filename: string;
|
|
83
|
+
readonly content: Uint8Array;
|
|
84
|
+
readonly contentType: string;
|
|
76
85
|
}
|
|
77
86
|
```
|
|
78
87
|
|
|
@@ -85,6 +94,8 @@ interface MailMessage extends Rendered {
|
|
|
85
94
|
| `from` | `Address` | no | The sender. `checkMessage` does not require one: a transport is usually wired with a default sender, and one without a default may refuse a message without `from` — see its documentation |
|
|
86
95
|
| `replyTo` | `Address` | no | Where replies go |
|
|
87
96
|
| `headers` | `Record<string, string>` | no | Extra headers, such as `List-Unsubscribe` |
|
|
97
|
+
| `attachments` | `readonly MailAttachment[]` | no | Files sent with the e-mail, in order, as bytes — see [Attachments](#attachments). An empty list is the same as none |
|
|
98
|
+
| `idempotencyKey` | `string` | no | Names this send, so sending it again delivers it once where the transport can deduplicate — see [Idempotency](#idempotency--sending-once). 1 to 256 visible ASCII characters |
|
|
88
99
|
|
|
89
100
|
`Rendered` is what the renderer answers — `mails.render('verify-email', { name, link })`
|
|
90
101
|
fills the values only known at send time into a built Maizzle template — and a
|
|
@@ -214,6 +225,170 @@ const message: MailMessage = {
|
|
|
214
225
|
| `{ Bcc: 'eve@example.com' }` | `MailRefused`: `send: header Bcc is reserved — addresses, the subject and the MIME structure are never custom headers` |
|
|
215
226
|
| `{ 'content-type': 'text/plain' }` | `MailRefused`: `send: header content-type is reserved — …` |
|
|
216
227
|
|
|
228
|
+
## Attachments
|
|
229
|
+
|
|
230
|
+
An attachment is a file's **bytes**, its name and its type:
|
|
231
|
+
|
|
232
|
+
| Field | Type | Effect |
|
|
233
|
+
| --- | --- | --- |
|
|
234
|
+
| `filename` | `string` | The name the recipient's mail client shows and saves it as. Not empty, not `.` or `..`; no `/` or `\`, no line break, no control or format character. Accents and spaces are fine: the transport encodes the name |
|
|
235
|
+
| `content` | `Uint8Array` | The bytes, sent as they are. A Node `Buffer` is a `Uint8Array` |
|
|
236
|
+
| `contentType` | `string` | A bare `type/subtype`, as `application/pdf` or `text/calendar` — no parameters, and never `multipart/*` or `message/*`, which are not files. Nothing guesses it from the file name |
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
import { readFile } from 'node:fs/promises';
|
|
240
|
+
import type { Mailer } from '@nxgt/mail';
|
|
241
|
+
import { createMailRenderer } from '@nxgt/mail/renderer';
|
|
242
|
+
|
|
243
|
+
const mails = createMailRenderer({ dir: 'dist' });
|
|
244
|
+
|
|
245
|
+
export async function sendInvoice(mailer: Mailer, to: string, name: string, number: string): Promise<void> {
|
|
246
|
+
const pdf = await readFile(`invoices/${number}.pdf`); // your storage: a Buffer
|
|
247
|
+
await mailer.send({
|
|
248
|
+
to,
|
|
249
|
+
...mails.render('invoice', { name, number }),
|
|
250
|
+
attachments: [{ filename: `invoice-${number}.pdf`, content: pdf, contentType: 'application/pdf' }],
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Bytes from anywhere fit — a file read with `readFile`, a PDF your code just
|
|
256
|
+
generated, what `fetch` answered (`new Uint8Array(await response.arrayBuffer())`),
|
|
257
|
+
or text you encoded (`new TextEncoder().encode(csv)`).
|
|
258
|
+
|
|
259
|
+
**There is no `path`, no URL and no stream.** A transport never reads a file
|
|
260
|
+
from disk or fetches a URL to attach it: a value that came from outside —
|
|
261
|
+
a file name in a request, a link in a database — can then never make an
|
|
262
|
+
e-mail carry a file it should not. The SMTP transport tells nodemailer so
|
|
263
|
+
explicitly. Read the file yourself, where you decide which files may be read.
|
|
264
|
+
|
|
265
|
+
**A large or sensitive file is a link.** Every provider caps the whole
|
|
266
|
+
message — about 25 MB sending through Gmail, 40 MB at Resend once encoded —
|
|
267
|
+
and base64, which every transport uses on the way, makes a file a third
|
|
268
|
+
larger. A file in an e-mail also stays in an inbox forever, forwarded or not.
|
|
269
|
+
Put a signed, expiring URL in the template instead; a
|
|
270
|
+
[URL variable](rendering.md) is already checked (`http:`, `https:` or
|
|
271
|
+
`mailto:` only):
|
|
272
|
+
|
|
273
|
+
```ts
|
|
274
|
+
import type { Mailer } from '@nxgt/mail';
|
|
275
|
+
import { createMailRenderer } from '@nxgt/mail/renderer';
|
|
276
|
+
|
|
277
|
+
declare function signedUrl(key: string, expiresInSeconds: number): Promise<string>; // your storage
|
|
278
|
+
|
|
279
|
+
const mails = createMailRenderer({ dir: 'dist' });
|
|
280
|
+
|
|
281
|
+
export async function sendExport(mailer: Mailer, to: string, key: string): Promise<void> {
|
|
282
|
+
const link = await signedUrl(key, 24 * 3600);
|
|
283
|
+
await mailer.send({ to, ...mails.render('export-ready', { link }) });
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Inline images — a `cid:` the HTML points at — are not supported yet: a
|
|
288
|
+
logo belongs on an `https:` URL, which is what the templates of
|
|
289
|
+
`@nxgt/mail-ui` already use.
|
|
290
|
+
|
|
291
|
+
`checkMessage` refuses, naming where and never the file's name:
|
|
292
|
+
|
|
293
|
+
| Written | Answer |
|
|
294
|
+
| --- | --- |
|
|
295
|
+
| `attachments: pdf` — one, not in a list | a compile error; at run time `MailRefused`: `send: attachments must be an array` |
|
|
296
|
+
| `[null]` | `MailRefused`: `send: attachments[0] must be an object, as { filename, content, contentType }` |
|
|
297
|
+
| `{ filename, content: '%PDF-1.7', contentType }`, or `{ filename, path, contentType }` | a compile error; at run time `MailRefused`: `send: attachments[0].content must be a Uint8Array — the file's bytes, never a path or a URL` |
|
|
298
|
+
| `filename: 'invoices/42.pdf'`, `'..\\42.pdf'`, `'..'`, `''`, or one holding a line break or a right-to-left override | `MailRefused`: `send: attachments[0].filename must be a file name — not empty, not . or .., without / or \, a line break or a control character` |
|
|
299
|
+
| `contentType: 'text/plain; charset=utf-8'`, `'pdf'`, or `'message/rfc822'` | `MailRefused`: `send: attachments[0].contentType must be a file's type/subtype, as application/pdf — never multipart/* or message/*` |
|
|
300
|
+
| `attachments: []` | accepted: the same as none |
|
|
301
|
+
|
|
302
|
+
A message the provider refuses — too large, or an attachment it will not
|
|
303
|
+
carry — is a `MailRefused` from the transport (an SMTP `552`; a Resend `400`,
|
|
304
|
+
`413` or `422`), and sending it again unchanged fails again: send a link
|
|
305
|
+
instead.
|
|
306
|
+
|
|
307
|
+
## Idempotency — sending once
|
|
308
|
+
|
|
309
|
+
A send that fails with `MailFailure` may still have reached the provider: a
|
|
310
|
+
timeout, a dropped connection. Retrying it can deliver the e-mail twice.
|
|
311
|
+
`idempotencyKey` names the send, so a transport that can deduplicate delivers
|
|
312
|
+
it once however often it is sent:
|
|
313
|
+
|
|
314
|
+
```ts
|
|
315
|
+
import type { Mailer, Rendered } from '@nxgt/mail';
|
|
316
|
+
|
|
317
|
+
export async function sendReceipt(mailer: Mailer, order: { id: string; email: string }, rendered: Rendered): Promise<void> {
|
|
318
|
+
await mailer.send({
|
|
319
|
+
...rendered,
|
|
320
|
+
to: order.email,
|
|
321
|
+
idempotencyKey: `order-${order.id}/receipt`, // the same for every retry of this receipt
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
**Derive the key from what the e-mail is about** — an order id, a user id
|
|
327
|
+
and a purpose (`user-7/welcome`, `invitation-19`) — **never from the time or
|
|
328
|
+
a random value**: a retry would carry a new key, and deliver again. **A key
|
|
329
|
+
names one e-mail**: two different e-mails about the same thing need two keys
|
|
330
|
+
(`order-42/receipt`, `order-42/shipped`) — a transport that deduplicates
|
|
331
|
+
refuses a different message under a key it already delivered.
|
|
332
|
+
|
|
333
|
+
```ts
|
|
334
|
+
declare const order: { id: string };
|
|
335
|
+
declare const userId: string;
|
|
336
|
+
declare const resetRequestId: string;
|
|
337
|
+
|
|
338
|
+
const receipt = `order-${order.id}/receipt`; // one receipt per order
|
|
339
|
+
const welcome = `user-${userId}/welcome`; // one welcome per user
|
|
340
|
+
const reset = `password-reset/${resetRequestId}`; // per request, not per user: a second request is a second e-mail
|
|
341
|
+
|
|
342
|
+
const wrong = `receipt-${Date.now()}`; // a retry gets a new key, and delivers again
|
|
343
|
+
const wrongToo = crypto.randomUUID(); // the same
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
**What each transport does with it:**
|
|
347
|
+
|
|
348
|
+
| Transport | Effect |
|
|
349
|
+
| --- | --- |
|
|
350
|
+
| `createMemoryMailer()` | The same message under a key it already delivered answers that delivery's `messageId`, and delivers nothing more; a different message under that key is a `MailRefused`. A send that failed leaves its key free. See [Testing — idempotency](testing.md#idempotencykey--a-retry-delivers-once) |
|
|
351
|
+
| `@nxgt/mail-resend` | Sent as Resend's `Idempotency-Key` header. Resend keeps a key for 24 hours: a retry within them answers the first send's id and delivers nothing more; the same key with a different message is refused (`MailRefused`) |
|
|
352
|
+
| `@nxgt/mail-smtp` | Ignored: SMTP has no such mechanism, so a message sent twice is delivered twice |
|
|
353
|
+
|
|
354
|
+
A transport that cannot deduplicate ignores the key; it never refuses the
|
|
355
|
+
message for carrying one. So a key is always safe to set, and only makes a
|
|
356
|
+
retry safe where the transport honours it. It is never written into the
|
|
357
|
+
e-mail itself.
|
|
358
|
+
|
|
359
|
+
`checkMessage` refuses a key that is not 1 to 256 visible ASCII characters —
|
|
360
|
+
empty, too long, holding a space, a line break or an accented letter, or not
|
|
361
|
+
a string — without quoting it:
|
|
362
|
+
|
|
363
|
+
| Written | Answer |
|
|
364
|
+
| --- | --- |
|
|
365
|
+
| `'order-42/receipt'`, `'a:b_c.d~e'`, `'k'.repeat(256)` | accepted |
|
|
366
|
+
| `''`, `'k'.repeat(257)`, `'order 42'`, `'commande-42-reçu'`, `'order-42\r\nX-Evil: 1'`, `42` | `MailRefused`: `send: idempotencyKey must be 1 to 256 visible ASCII characters, as order-42/receipt` |
|
|
367
|
+
|
|
368
|
+
A transport that deduplicates also refuses **a different message** under a
|
|
369
|
+
key it already delivered, with `MailRefused` — the memory mailer with
|
|
370
|
+
`send: idempotencyKey was already used for a different message — a key names one e-mail`,
|
|
371
|
+
Resend's transport with `send: Resend refused the message` on its `409`.
|
|
372
|
+
Sending it again fails again: give that e-mail its own key.
|
|
373
|
+
|
|
374
|
+
A retry from a queue, with the key the job carries:
|
|
375
|
+
|
|
376
|
+
```ts
|
|
377
|
+
import { MailFailure, type Mailer, type MailMessage } from '@nxgt/mail';
|
|
378
|
+
|
|
379
|
+
// Yours: the queue that runs a job again later.
|
|
380
|
+
declare function retryLater(job: { message: MailMessage }, delaySeconds: number): Promise<void>;
|
|
381
|
+
|
|
382
|
+
export async function runSendJob(mailer: Mailer, job: { message: MailMessage }): Promise<void> {
|
|
383
|
+
try {
|
|
384
|
+
await mailer.send(job.message); // job.message.idempotencyKey was set when the job was queued
|
|
385
|
+
} catch (error) {
|
|
386
|
+
if (error instanceof MailFailure) return retryLater(job, 60); // the same key: delivered once, where the transport deduplicates
|
|
387
|
+
throw error; // MailRefused: sending it again fails again
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
```
|
|
391
|
+
|
|
217
392
|
## Errors
|
|
218
393
|
|
|
219
394
|
```ts
|
|
@@ -237,8 +412,8 @@ class MailRefused extends MailError {
|
|
|
237
412
|
|
|
238
413
|
| Code | Class | When | Sending it again |
|
|
239
414
|
| --- | --- | --- | --- |
|
|
240
|
-
| `MAIL_FAILED` | `MailFailure` | The transport could not hand the e-mail over: a refused connection, a timeout, a 5xx from the provider, an expired credential. The transport's error is the `cause`. **Nothing is known to have been sent**: after a timeout or a dropped connection the provider may have taken it all the same | May work later. Never report it as sent |
|
|
241
|
-
| `MAIL_REFUSED` | `MailRefused` | The e-mail itself was refused, before or by the transport: no recipient, something that is not an address, a line break in the subject or a header, a reserved header, or the provider answering that the message is malformed | Fails again, unchanged |
|
|
415
|
+
| `MAIL_FAILED` | `MailFailure` | The transport could not hand the e-mail over: a refused connection, a timeout, a 5xx from the provider, an expired credential. The transport's error is the `cause`. **Nothing is known to have been sent**: after a timeout or a dropped connection the provider may have taken it all the same | May work later — with an [`idempotencyKey`](#idempotency--sending-once), without a second delivery where the transport deduplicates. Never report it as sent |
|
|
416
|
+
| `MAIL_REFUSED` | `MailRefused` | The e-mail itself was refused, before or by the transport: no recipient, something that is not an address, a line break in the subject or a header, a reserved header, an attachment that is not bytes or is badly named, a malformed idempotency key or one already used for a different message, or the provider answering that the message is malformed or too large | Fails again, unchanged |
|
|
242
417
|
|
|
243
418
|
`MailError` is **abstract**: catch it, test `instanceof MailError`, but
|
|
244
419
|
`new MailError(…)` does not compile — a bare one would pass a `code` check and
|
package/docs/guide/testing.md
CHANGED
|
@@ -56,6 +56,38 @@ headers['X-Ref'] = 'changed';
|
|
|
56
56
|
mailer.sent[0]?.headers; // { 'X-Ref': 'a' }
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
+
### Attachments in the outbox
|
|
60
|
+
|
|
61
|
+
An attachment is kept with its bytes **copied** when it is sent: a caller that
|
|
62
|
+
reuses or fills its buffer afterwards does not change what the outbox holds,
|
|
63
|
+
and each read of `sent` hands out a fresh copy again. A Node `Buffer` comes
|
|
64
|
+
back as a plain `Uint8Array` of the same bytes — compare the bytes, not the
|
|
65
|
+
class:
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
import { expect, it } from 'bun:test';
|
|
69
|
+
import { createMemoryMailer } from '@nxgt/mail';
|
|
70
|
+
|
|
71
|
+
it('attaches the invoice', async () => {
|
|
72
|
+
const mailer = createMemoryMailer();
|
|
73
|
+
const pdf = Buffer.from('%PDF-1.7');
|
|
74
|
+
|
|
75
|
+
await mailer.send({
|
|
76
|
+
to: 'ada@example.com',
|
|
77
|
+
subject: 'Your invoice',
|
|
78
|
+
html: '<p>Your invoice is attached.</p>',
|
|
79
|
+
text: 'Your invoice is attached.',
|
|
80
|
+
attachments: [{ filename: 'invoice-42.pdf', content: pdf, contentType: 'application/pdf' }],
|
|
81
|
+
});
|
|
82
|
+
pdf.fill(0); // changes nothing in the outbox
|
|
83
|
+
|
|
84
|
+
const [file] = mailer.sent[0]?.attachments ?? [];
|
|
85
|
+
expect(file?.filename).toBe('invoice-42.pdf');
|
|
86
|
+
expect(file?.contentType).toBe('application/pdf');
|
|
87
|
+
expect(new TextDecoder().decode(file?.content)).toBe('%PDF-1.7');
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
59
91
|
## `failNext(error?)` — making a send fail
|
|
60
92
|
|
|
61
93
|
The next send that reaches the hand-over rejects with `error` — by default a
|
|
@@ -111,19 +143,108 @@ await mailer
|
|
|
111
143
|
mailer.attempts; // 0 — and the failure is still queued for the next well-formed send
|
|
112
144
|
```
|
|
113
145
|
|
|
146
|
+
## `idempotencyKey` — a retry delivers once
|
|
147
|
+
|
|
148
|
+
The memory mailer honours a message's
|
|
149
|
+
[`idempotencyKey`](sending.md#idempotency--sending-once) as Resend does:
|
|
150
|
+
|
|
151
|
+
- the **same message** under a key it already delivered answers that
|
|
152
|
+
delivery's `messageId`, and nothing more reaches `sent`;
|
|
153
|
+
- a **different message** under that key — another subject, another
|
|
154
|
+
recipient, other attachment bytes, any field — is refused with
|
|
155
|
+
`MailRefused`: `send: idempotencyKey was already used for a different message — a key names one e-mail`,
|
|
156
|
+
as Resend answers `409 invalid_idempotent_request`;
|
|
157
|
+
- a send that **failed** delivered nothing, so its key stays free, and the
|
|
158
|
+
retry delivers;
|
|
159
|
+
- each send still counts in `attempts`, the answered duplicate and the
|
|
160
|
+
refused reuse included: both reached the hand-over;
|
|
161
|
+
- `clear()` forgets the keys.
|
|
162
|
+
|
|
163
|
+
A test proving that a job run twice e-mails once:
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
import { expect, it } from 'bun:test';
|
|
167
|
+
import { createMemoryMailer, type Mailer, MailRefused } from '@nxgt/mail';
|
|
168
|
+
|
|
169
|
+
// The code under test: a job that may run more than once for the same order.
|
|
170
|
+
async function sendReceipt(mailer: Mailer, orderId: string): Promise<string | null> {
|
|
171
|
+
const { messageId } = await mailer.send({
|
|
172
|
+
to: 'ada@example.com',
|
|
173
|
+
subject: 'Your receipt',
|
|
174
|
+
html: '<p>Thank you for your order.</p>',
|
|
175
|
+
text: 'Thank you for your order.',
|
|
176
|
+
idempotencyKey: `order-${orderId}/receipt`,
|
|
177
|
+
});
|
|
178
|
+
return messageId;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
it('sends one receipt, however often the job runs', async () => {
|
|
182
|
+
const mailer = createMemoryMailer();
|
|
183
|
+
|
|
184
|
+
const first = await sendReceipt(mailer, '42');
|
|
185
|
+
const again = await sendReceipt(mailer, '42');
|
|
186
|
+
|
|
187
|
+
expect(again).toBe(first); // 'memory-1', both times
|
|
188
|
+
expect(mailer.sent).toHaveLength(1);
|
|
189
|
+
expect(mailer.attempts).toBe(2);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('refuses another e-mail under the same key', async () => {
|
|
193
|
+
const mailer = createMemoryMailer();
|
|
194
|
+
await sendReceipt(mailer, '42');
|
|
195
|
+
|
|
196
|
+
const error = await mailer
|
|
197
|
+
.send({
|
|
198
|
+
to: 'ada@example.com',
|
|
199
|
+
subject: 'Your order has shipped', // another e-mail, the receipt's key
|
|
200
|
+
html: '<p>Your order has shipped.</p>',
|
|
201
|
+
text: 'Your order has shipped.',
|
|
202
|
+
idempotencyKey: 'order-42/receipt',
|
|
203
|
+
})
|
|
204
|
+
.then(() => null, (e: unknown) => e);
|
|
205
|
+
|
|
206
|
+
expect(error).toBeInstanceOf(MailRefused); // give it its own key: order-42/shipped
|
|
207
|
+
expect(mailer.sent).toHaveLength(1);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('delivers the retry of a send that failed', async () => {
|
|
211
|
+
const mailer = createMemoryMailer();
|
|
212
|
+
mailer.failNext();
|
|
213
|
+
|
|
214
|
+
await sendReceipt(mailer, '42').then(() => null, (e: unknown) => e); // MailFailure
|
|
215
|
+
await sendReceipt(mailer, '42');
|
|
216
|
+
|
|
217
|
+
expect(mailer.sent).toHaveLength(1);
|
|
218
|
+
expect(mailer.sent[0]?.idempotencyKey).toBe('order-42/receipt');
|
|
219
|
+
});
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
"The same message" is what it would deliver: every field of `MailMessage`,
|
|
223
|
+
read by name, the attachments by their bytes. How the object was written
|
|
224
|
+
does not count — the order of its fields or of its headers, `to` as one
|
|
225
|
+
address or a list of one, no `headers` or `attachments` or an empty one,
|
|
226
|
+
the bytes in a `Buffer` or a plain `Uint8Array` — and neither does a field
|
|
227
|
+
`MailMessage` does not have. SMTP ignores
|
|
228
|
+
the key altogether, so this is the behaviour of a deduplicating transport,
|
|
229
|
+
not of every one. A queued `failNext` fails the next send even when its key
|
|
230
|
+
was already delivered.
|
|
231
|
+
|
|
114
232
|
## `clear()`
|
|
115
233
|
|
|
116
|
-
Forgets the outbox, the attempts,
|
|
117
|
-
going, so an id is never reused within one mailer.
|
|
234
|
+
Forgets the outbox, the attempts, any queued failure, and the idempotency
|
|
235
|
+
keys. The id counter keeps going, so an id is never reused within one mailer.
|
|
118
236
|
|
|
119
237
|
## What it refuses
|
|
120
238
|
|
|
121
239
|
Exactly what every transport refuses, because it calls
|
|
122
240
|
[`checkMessage`](transports.md#checkmessage-first) first: no recipient, something
|
|
123
241
|
that is not an address, a line break in a name, the subject or a header, a
|
|
124
|
-
missing part
|
|
242
|
+
missing part, an attachment that is not bytes or whose file name or type is
|
|
243
|
+
malformed, or an idempotency key that is not 1 to 256 visible ASCII
|
|
244
|
+
characters. A test that passes against the memory mailer does not pass by
|
|
125
245
|
accident a message a real transport would refuse. The full list is in
|
|
126
|
-
[Sending](sending.md#addresses)
|
|
246
|
+
[Sending](sending.md#addresses) and
|
|
247
|
+
[Sending — attachments](sending.md#attachments).
|
|
127
248
|
|
|
128
249
|
## A realistic case — the failure path of a service
|
|
129
250
|
|