@shaferllc/keel 0.66.0 → 0.68.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/AGENTS.md +167 -0
- package/README.md +30 -1
- package/bin/keel-mcp.mjs +9 -0
- package/dist/core/application.d.ts +5 -5
- package/dist/core/application.js +2 -2
- package/dist/core/cache.d.ts +82 -5
- package/dist/core/cache.js +181 -23
- package/dist/core/cli/stubs.d.ts +12 -0
- package/dist/core/cli/stubs.js +120 -0
- package/dist/core/events.d.ts +129 -5
- package/dist/core/events.js +165 -7
- package/dist/core/health.d.ts +141 -0
- package/dist/core/health.js +226 -0
- package/dist/core/helpers.d.ts +9 -3
- package/dist/core/helpers.js +11 -3
- package/dist/core/index.d.ts +15 -10
- package/dist/core/index.js +7 -4
- package/dist/core/lock.d.ts +139 -0
- package/dist/core/lock.js +215 -0
- package/dist/core/logger.d.ts +82 -4
- package/dist/core/logger.js +141 -23
- package/dist/core/mail.d.ts +128 -7
- package/dist/core/mail.js +264 -16
- package/dist/core/queue.d.ts +134 -9
- package/dist/core/queue.js +304 -14
- package/dist/core/storage.d.ts +159 -6
- package/dist/core/storage.js +287 -7
- package/dist/mcp/server.d.ts +19 -0
- package/dist/mcp/server.js +355 -0
- package/docs/ai-manifest.json +2472 -0
- package/docs/ai.md +128 -0
- package/docs/architecture.md +331 -0
- package/docs/authentication.md +453 -0
- package/docs/authorization.md +167 -0
- package/docs/broadcasting.md +137 -0
- package/docs/broker.md +500 -0
- package/docs/cache.md +558 -0
- package/docs/configuration.md +311 -0
- package/docs/console.md +356 -0
- package/docs/container.md +467 -0
- package/docs/controllers.md +265 -0
- package/docs/cors.md +51 -0
- package/docs/database.md +530 -0
- package/docs/debugging.md +129 -0
- package/docs/decorators.md +127 -0
- package/docs/errors.md +395 -0
- package/docs/events.md +496 -0
- package/docs/examples/architecture-app.ts +27 -0
- package/docs/examples/authentication.ts +61 -0
- package/docs/examples/authorization.ts +79 -0
- package/docs/examples/broadcasting.ts +60 -0
- package/docs/examples/broker-cache-validate.ts +34 -0
- package/docs/examples/broker-fault-tolerance.ts +29 -0
- package/docs/examples/broker-middleware.ts +27 -0
- package/docs/examples/broker.ts +203 -0
- package/docs/examples/cache.ts +222 -0
- package/docs/examples/configuration.ts +81 -0
- package/docs/examples/container.ts +134 -0
- package/docs/examples/controllers.ts +86 -0
- package/docs/examples/database.ts +118 -0
- package/docs/examples/debugging.ts +41 -0
- package/docs/examples/decorators.ts +40 -0
- package/docs/examples/errors.ts +121 -0
- package/docs/examples/events.ts +204 -0
- package/docs/examples/factories.ts +84 -0
- package/docs/examples/hashing.ts +71 -0
- package/docs/examples/health.ts +94 -0
- package/docs/examples/helpers.ts +171 -0
- package/docs/examples/hooks.ts +54 -0
- package/docs/examples/inertia.ts +81 -0
- package/docs/examples/locks.ts +120 -0
- package/docs/examples/logger.ts +92 -0
- package/docs/examples/mail.ts +160 -0
- package/docs/examples/middleware.ts +119 -0
- package/docs/examples/migrations.ts +126 -0
- package/docs/examples/models.ts +239 -0
- package/docs/examples/notification.ts +124 -0
- package/docs/examples/providers.ts +123 -0
- package/docs/examples/queues.ts +254 -0
- package/docs/examples/rate-limiting.ts +42 -0
- package/docs/examples/redis.ts +99 -0
- package/docs/examples/request-response.ts +197 -0
- package/docs/examples/routing.ts +186 -0
- package/docs/examples/scheduling.ts +62 -0
- package/docs/examples/sessions.ts +102 -0
- package/docs/examples/static-files.ts +63 -0
- package/docs/examples/storage.ts +132 -0
- package/docs/examples/templates.ts +58 -0
- package/docs/examples/testing.ts +66 -0
- package/docs/examples/transformer.ts +141 -0
- package/docs/examples/transformers.ts +49 -0
- package/docs/examples/url-builder.ts +86 -0
- package/docs/examples/validation.ts +102 -0
- package/docs/examples/views.tsx +62 -0
- package/docs/examples/vite.ts +106 -0
- package/docs/factories.md +166 -0
- package/docs/getting-started.md +290 -0
- package/docs/hashing.md +259 -0
- package/docs/health.md +225 -0
- package/docs/helpers.md +347 -0
- package/docs/hono.md +186 -0
- package/docs/hooks.md +118 -0
- package/docs/inertia.md +241 -0
- package/docs/locks.md +323 -0
- package/docs/logger.md +290 -0
- package/docs/mail.md +678 -0
- package/docs/middleware.md +425 -0
- package/docs/migrations.md +476 -0
- package/docs/models.md +810 -0
- package/docs/notifications.md +474 -0
- package/docs/providers.md +363 -0
- package/docs/queues.md +679 -0
- package/docs/rate-limiting.md +155 -0
- package/docs/redis.md +178 -0
- package/docs/request-response.md +953 -0
- package/docs/routing.md +804 -0
- package/docs/scheduling.md +110 -0
- package/docs/security.md +85 -0
- package/docs/sessions.md +354 -0
- package/docs/social-auth.md +174 -0
- package/docs/static-files.md +211 -0
- package/docs/storage.md +450 -0
- package/docs/templates.md +315 -0
- package/docs/testing.md +125 -0
- package/docs/transformers.md +381 -0
- package/docs/url-builder.md +295 -0
- package/docs/validation.md +288 -0
- package/docs/views.md +267 -0
- package/docs/vite.md +434 -0
- package/llms-full.txt +17694 -0
- package/llms.txt +116 -0
- package/package.json +26 -7
package/docs/mail.md
ADDED
|
@@ -0,0 +1,678 @@
|
|
|
1
|
+
# Mail
|
|
2
|
+
|
|
3
|
+
Send email through a pluggable **transport**. Compose a message with a fluent
|
|
4
|
+
builder and dispatch it — the API mirrors the database layer (`setMailer` /
|
|
5
|
+
`mail()` are to mail what `setConnection` / `db()` are to the database). The core
|
|
6
|
+
imports no SDK: the built-in transports use `fetch`, `console`, or memory, so it
|
|
7
|
+
runs on Node and the edge.
|
|
8
|
+
|
|
9
|
+
## Sending
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { mail } from "@shaferllc/keel/core";
|
|
13
|
+
|
|
14
|
+
await mail()
|
|
15
|
+
.to("ada@example.com")
|
|
16
|
+
.subject("Welcome aboard")
|
|
17
|
+
.html("<h1>Hi Ada</h1>")
|
|
18
|
+
.send();
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Every setter is chainable, and several accept multiple values:
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
await mail()
|
|
25
|
+
.to("a@x.com", "b@x.com")
|
|
26
|
+
.cc("team@x.com")
|
|
27
|
+
.bcc("audit@x.com")
|
|
28
|
+
.replyTo("support@x.com")
|
|
29
|
+
.from("hello@x.com") // optional if a default is configured
|
|
30
|
+
.subject("Report")
|
|
31
|
+
.text("Plain-text body")
|
|
32
|
+
.html("<p>HTML body</p>")
|
|
33
|
+
.header("X-Campaign", "weekly")
|
|
34
|
+
.send();
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Seed several fields at once with `fill()`:
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
await mail().fill({ to: "a@x.com", subject: "Hi", text: "body" }).send();
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`send()` resolves to the **finalized message** — the same object the transport
|
|
44
|
+
received, with the default `from` already applied. Handy for logging or
|
|
45
|
+
assertions:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
const sent = await mail().to("ada@example.com").subject("Hi").text("hey").send();
|
|
49
|
+
sent.from; // the resolved from address
|
|
50
|
+
sent.to; // ["ada@example.com"]
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Validation & error behavior
|
|
54
|
+
|
|
55
|
+
A message needs at least one recipient, a subject, a body (`text` or `html`),
|
|
56
|
+
and a `from` — `send()` throws a clear `Error` otherwise, before the transport is
|
|
57
|
+
ever called. The checks run in this order:
|
|
58
|
+
|
|
59
|
+
| Missing | Message |
|
|
60
|
+
|---------|---------|
|
|
61
|
+
| `to` (empty) | `Mail: at least one recipient (to) is required.` |
|
|
62
|
+
| `subject` | `Mail: a subject is required.` |
|
|
63
|
+
| `text` **and** `html` | `Mail: a text or html body is required.` |
|
|
64
|
+
| `from` (and no default) | `Mail: a from address is required (set one or a default).` |
|
|
65
|
+
|
|
66
|
+
The `from` default from `setMailer(..., { from })` is applied first, so a
|
|
67
|
+
configured default satisfies the last check without any per-message `from`.
|
|
68
|
+
|
|
69
|
+
## Configuring the transport
|
|
70
|
+
|
|
71
|
+
Register a default transport once (typically in a service provider):
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
import { setMailer, fetchTransport } from "@shaferllc/keel/core";
|
|
75
|
+
|
|
76
|
+
setMailer(
|
|
77
|
+
fetchTransport({
|
|
78
|
+
url: "https://api.resend.com/emails",
|
|
79
|
+
headers: { Authorization: `Bearer ${env("RESEND_API_KEY")}` },
|
|
80
|
+
body: (m) => ({ from: m.from, to: m.to, subject: m.subject, html: m.html }),
|
|
81
|
+
}),
|
|
82
|
+
{ from: "hello@myapp.com" }, // default `from` for messages that omit one
|
|
83
|
+
);
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`fetchTransport` POSTs JSON to any provider API (Resend, Postmark, Mailgun, …).
|
|
87
|
+
The optional `body` mapper shapes the request for that provider; without it the
|
|
88
|
+
message is sent as-is. A non-2xx response throws
|
|
89
|
+
`Mail: transport responded <status> <statusText>`.
|
|
90
|
+
|
|
91
|
+
## Built-in transports
|
|
92
|
+
|
|
93
|
+
| Transport | Use |
|
|
94
|
+
|-----------|-----|
|
|
95
|
+
| `ArrayTransport` | Collects messages in `.sent` — the default, and ideal for tests |
|
|
96
|
+
| `LogTransport` | Logs each message via the logger instead of delivering — local dev |
|
|
97
|
+
| `fetchTransport(opts)` | POSTs to a provider HTTP API via `fetch` — production |
|
|
98
|
+
|
|
99
|
+
Until you call `setMailer`, the default mailer is a fresh `ArrayTransport` — so
|
|
100
|
+
`mail()` never throws for want of a transport, it just buffers in memory.
|
|
101
|
+
|
|
102
|
+
## Writing your own transport
|
|
103
|
+
|
|
104
|
+
A transport is one method:
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
import type { Transport } from "@shaferllc/keel/core";
|
|
108
|
+
|
|
109
|
+
const transport: Transport = {
|
|
110
|
+
async send(message) {
|
|
111
|
+
// hand `message` to any SDK or API you like
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
setMailer(transport, { from: "hello@myapp.com" });
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
The `message` your `send` receives is already validated and has `from` resolved,
|
|
118
|
+
so a transport can trust every required field is present.
|
|
119
|
+
|
|
120
|
+
## Queueing: `sendLater()`
|
|
121
|
+
|
|
122
|
+
Sending is slow and it fails. Holding a request open for an SMTP round trip means
|
|
123
|
+
the user waits on your provider, and a provider hiccup turns "sign up" into an
|
|
124
|
+
error page. Put the message on the [queue](./queues.md) instead:
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
await mail().to(user.email).subject("Welcome").html(body).sendLater();
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
The request returns immediately, and a failed send **retries on the queue** rather
|
|
131
|
+
than failing the user's action. Everything else is identical — same builder, same
|
|
132
|
+
transport.
|
|
133
|
+
|
|
134
|
+
The message is **validated at the call site**, not on the worker: a missing
|
|
135
|
+
recipient throws where you composed it, where the stack trace means something,
|
|
136
|
+
rather than surfacing in a worker log an hour later.
|
|
137
|
+
|
|
138
|
+
With the default `SyncDriver` this still sends inline (nothing is deferred until
|
|
139
|
+
you register a real driver), so `sendLater()` is safe to adopt before you have a
|
|
140
|
+
queue.
|
|
141
|
+
|
|
142
|
+
## Attachments
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
await mail()
|
|
146
|
+
.to("ada@example.com")
|
|
147
|
+
.subject("Your invoice")
|
|
148
|
+
.html('<p>Attached. <img src="cid:logo"></p>')
|
|
149
|
+
.attach("invoice.pdf", pdfBytes) // content type inferred: application/pdf
|
|
150
|
+
.attach("data.csv", "a,b,c", "text/csv") // ...or set it
|
|
151
|
+
.embed("logo", logoBytes, "logo.png") // inline, referenced as cid:logo
|
|
152
|
+
.send();
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
`attach(filename, content, contentType?)` takes a string or `Uint8Array`; the
|
|
156
|
+
content type is inferred from the extension when you don't give one.
|
|
157
|
+
|
|
158
|
+
`embed(cid, content, filename?, contentType?)` is the same thing with a **content
|
|
159
|
+
id**, so the HTML body can display it inline via `<img src="cid:logo">` instead of
|
|
160
|
+
linking out to a hosted image.
|
|
161
|
+
|
|
162
|
+
## Class-based mails
|
|
163
|
+
|
|
164
|
+
A one-liner is fine until the email has real content. `BaseMail` is to mail what
|
|
165
|
+
`Job` is to the queue — a reusable, testable class:
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
import { BaseMail, type PendingMail } from "@shaferllc/keel/core";
|
|
169
|
+
|
|
170
|
+
export class WelcomeEmail extends BaseMail {
|
|
171
|
+
constructor(private user: User) {
|
|
172
|
+
super();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
build(message: PendingMail) {
|
|
176
|
+
message
|
|
177
|
+
.to(this.user.email)
|
|
178
|
+
.subject(`Welcome, ${this.user.name}`)
|
|
179
|
+
.html(`<h1>Hi ${this.user.name}</h1>`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
import { send, sendLater } from "@shaferllc/keel/core";
|
|
186
|
+
|
|
187
|
+
await send(new WelcomeEmail(user));
|
|
188
|
+
await sendLater(new WelcomeEmail(user)); // ...or queue it
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
`build()` may be async, so it can render a template or fetch what it needs.
|
|
192
|
+
|
|
193
|
+
## Multiple mailers
|
|
194
|
+
|
|
195
|
+
Register mailers by name — a transactional provider and a marketing one, say — and
|
|
196
|
+
pick one with `mail(name)`:
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
setMailer(postmark, { from: "hi@app.com" }); // the default
|
|
200
|
+
setMailer(resend, { from: "news@app.com" }, "marketing");
|
|
201
|
+
|
|
202
|
+
await mail().to(user.email).subject("Receipt").text(body).send();
|
|
203
|
+
await mail("marketing").to(user.email).subject("This month").html(body).send();
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
`send(email, name)` and `sendLater(email, name)` take a mailer name too.
|
|
207
|
+
|
|
208
|
+
## In tests
|
|
209
|
+
|
|
210
|
+
`fakeMail()` swaps the mailer for one that **records instead of delivering**, so
|
|
211
|
+
tests never talk to a provider. `restoreMail()` puts the real one back.
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
import { fakeMail, restoreMail } from "@shaferllc/keel/core";
|
|
215
|
+
|
|
216
|
+
const mailer = fakeMail();
|
|
217
|
+
|
|
218
|
+
await registerUser();
|
|
219
|
+
|
|
220
|
+
mailer.assertSent();
|
|
221
|
+
mailer.assertSent((m) => m.subject === "Welcome");
|
|
222
|
+
mailer.assertSentCount(1);
|
|
223
|
+
mailer.assertQueued((m) => m.to.includes("ada@example.com")); // sent with sendLater()
|
|
224
|
+
mailer.assertNotSent((m) => m.subject === "Password reset");
|
|
225
|
+
mailer.assertNothingSent();
|
|
226
|
+
|
|
227
|
+
restoreMail();
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
The fake keeps **sent** and **queued** separate — `assertSent` only matches
|
|
231
|
+
`send()`, `assertQueued` only `sendLater()` — so a test can tell "we emailed them"
|
|
232
|
+
from "we queued an email". A faked `sendLater()` doesn't touch the real queue
|
|
233
|
+
either; recording the intent is the point.
|
|
234
|
+
|
|
235
|
+
It still **validates** the message, so a fake can't paper over a message the real
|
|
236
|
+
mailer would reject.
|
|
237
|
+
|
|
238
|
+
`mailer.sent()` and `mailer.queued()` return the raw messages if you'd rather
|
|
239
|
+
assert by hand.
|
|
240
|
+
|
|
241
|
+
If you want the transport-level view instead, `ArrayTransport` still works:
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
const transport = new ArrayTransport();
|
|
245
|
+
setMailer(transport, { from: "hi@app.com" });
|
|
246
|
+
|
|
247
|
+
await mail().to("ada@example.com").subject("Welcome").text("hi").send();
|
|
248
|
+
|
|
249
|
+
assert.equal(transport.sent[0].subject, "Welcome");
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
You can also hold your own `Mailer` instead of the global one — construct it with
|
|
253
|
+
a transport and reuse it, leaving the process-wide `mail()` untouched:
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
import { Mailer, ArrayTransport } from "@shaferllc/keel/core";
|
|
257
|
+
|
|
258
|
+
const mailer = new Mailer(new ArrayTransport(), { from: "hi@app.com" });
|
|
259
|
+
await mailer.message().to("ada@example.com").subject("Hi").text("hey").send();
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
## Events
|
|
263
|
+
|
|
264
|
+
Every send fires [events](./events.md), so logging, metrics, and auditing can hang
|
|
265
|
+
off mail without touching the mailer:
|
|
266
|
+
|
|
267
|
+
| Event | When |
|
|
268
|
+
|-------|------|
|
|
269
|
+
| `mail.sending` | before the transport is called |
|
|
270
|
+
| `mail.sent` | after it returns |
|
|
271
|
+
| `mail.queued` | a `sendLater()` message reached the queue |
|
|
272
|
+
|
|
273
|
+
Each carries the final `Message` — after defaults are applied.
|
|
274
|
+
|
|
275
|
+
```ts
|
|
276
|
+
listen("mail.sent", (message) => logger().info("mail sent", { subject: message.subject }));
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
## Related
|
|
280
|
+
|
|
281
|
+
The mail layer stands alone, but the [database](./database.md) builder shares its
|
|
282
|
+
shape (`setConnection`/`db` mirror `setMailer`/`mail`) — the same register-once,
|
|
283
|
+
call-anywhere pattern.
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
## API reference
|
|
288
|
+
|
|
289
|
+
### `mail()`
|
|
290
|
+
|
|
291
|
+
`mail(): PendingMail`
|
|
292
|
+
|
|
293
|
+
Starts composing a message on the default (global) mailer.
|
|
294
|
+
|
|
295
|
+
```ts
|
|
296
|
+
await mail().to("ada@example.com").subject("Hi").text("hey").send();
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
**Notes:** a thin shortcut for `getMailer().message()`. Uses whatever transport
|
|
300
|
+
and options were last passed to `setMailer` (an in-memory `ArrayTransport` if you
|
|
301
|
+
never called it).
|
|
302
|
+
|
|
303
|
+
### `setMailer(transport, options?)`
|
|
304
|
+
|
|
305
|
+
`setMailer(transport: Transport, options?: MailerOptions): Mailer`
|
|
306
|
+
|
|
307
|
+
Replaces the global mailer with a new one built from `transport` and `options`,
|
|
308
|
+
and returns it.
|
|
309
|
+
|
|
310
|
+
```ts
|
|
311
|
+
setMailer(fetchTransport({ url }), { from: "hello@myapp.com" });
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
**Notes:** global — the last call wins. Returns the constructed `Mailer` if you
|
|
315
|
+
want a direct handle. `options` defaults to `{}` (no default `from`).
|
|
316
|
+
|
|
317
|
+
### `getMailer()`
|
|
318
|
+
|
|
319
|
+
`getMailer(): Mailer`
|
|
320
|
+
|
|
321
|
+
Returns the current global `Mailer` instance.
|
|
322
|
+
|
|
323
|
+
```ts
|
|
324
|
+
const mailer = getMailer();
|
|
325
|
+
await mailer.message().to("ada@example.com").subject("Hi").text("hey").send();
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
**Notes:** before any `setMailer` call this is a `Mailer` wrapping a fresh
|
|
329
|
+
`ArrayTransport`.
|
|
330
|
+
|
|
331
|
+
### `fetchTransport(options)`
|
|
332
|
+
|
|
333
|
+
`fetchTransport(options: FetchTransportOptions): Transport`
|
|
334
|
+
|
|
335
|
+
Builds a `Transport` that POSTs each message as JSON to a provider HTTP API via
|
|
336
|
+
`fetch`.
|
|
337
|
+
|
|
338
|
+
```ts
|
|
339
|
+
const transport = fetchTransport({
|
|
340
|
+
url: "https://api.resend.com/emails",
|
|
341
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
342
|
+
body: (m) => ({ from: m.from, to: m.to, subject: m.subject, html: m.html }),
|
|
343
|
+
});
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
**Notes:** always sets `Content-Type: application/json`; your `headers` merge on
|
|
347
|
+
top. Without a `body` mapper the raw `Message` is serialized. Throws
|
|
348
|
+
`Mail: transport responded <status> <statusText>` on any non-`ok` response.
|
|
349
|
+
|
|
350
|
+
### `Mailer`
|
|
351
|
+
|
|
352
|
+
The engine that validates a message, applies defaults, and hands it to the
|
|
353
|
+
transport. Construct one directly (`new Mailer(transport, options?)`) for a
|
|
354
|
+
scoped mailer, or reach the global one via `getMailer()` / `setMailer()`.
|
|
355
|
+
|
|
356
|
+
#### `new Mailer(transport, options?)`
|
|
357
|
+
|
|
358
|
+
`new Mailer(transport: Transport, options?: MailerOptions)`
|
|
359
|
+
|
|
360
|
+
Wraps a transport and its options.
|
|
361
|
+
|
|
362
|
+
```ts
|
|
363
|
+
const mailer = new Mailer(new ArrayTransport(), { from: "hi@app.com" });
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
**Notes:** `options` defaults to `{}`. The transport is fixed for this instance —
|
|
367
|
+
build a new `Mailer` to swap it.
|
|
368
|
+
|
|
369
|
+
#### `message()`
|
|
370
|
+
|
|
371
|
+
`message(): PendingMail`
|
|
372
|
+
|
|
373
|
+
Starts a new `PendingMail` bound to this mailer.
|
|
374
|
+
|
|
375
|
+
```ts
|
|
376
|
+
const pending = mailer.message();
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
**Notes:** each call returns a fresh builder; nothing is shared between messages.
|
|
380
|
+
|
|
381
|
+
#### `send(message)`
|
|
382
|
+
|
|
383
|
+
`send(message: Message): Promise<Message>`
|
|
384
|
+
|
|
385
|
+
Applies the default `from`, validates the message, dispatches it through the
|
|
386
|
+
transport, and resolves to the finalized message.
|
|
387
|
+
|
|
388
|
+
```ts
|
|
389
|
+
const sent = await mailer.send({ to: ["ada@x.com"], subject: "Hi", text: "hey" });
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
**Notes:** throws (before touching the transport) if `to` is empty, or `subject`,
|
|
393
|
+
a body, or `from` is missing — see [Validation](#validation--error-behavior).
|
|
394
|
+
`PendingMail.send()` funnels through here. The returned object is a shallow copy
|
|
395
|
+
with `from` resolved.
|
|
396
|
+
|
|
397
|
+
### `PendingMail`
|
|
398
|
+
|
|
399
|
+
The fluent builder. You get one from `mail()` or `mailer.message()`, never
|
|
400
|
+
`new`. Every setter returns `this`, so calls chain in any order; nothing is sent
|
|
401
|
+
until `send()`.
|
|
402
|
+
|
|
403
|
+
#### `to(...addresses)`
|
|
404
|
+
|
|
405
|
+
`to(...addresses: string[]): this`
|
|
406
|
+
|
|
407
|
+
Appends one or more recipients.
|
|
408
|
+
|
|
409
|
+
```ts
|
|
410
|
+
mail().to("a@x.com", "b@x.com");
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
**Notes:** additive — repeated calls accumulate recipients rather than replace.
|
|
414
|
+
|
|
415
|
+
#### `from(address)`
|
|
416
|
+
|
|
417
|
+
`from(address: string): this`
|
|
418
|
+
|
|
419
|
+
Sets the sender, overriding the mailer's default `from`.
|
|
420
|
+
|
|
421
|
+
```ts
|
|
422
|
+
mail().from("hello@x.com");
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
**Notes:** a single value (not variadic). Optional when a default `from` is
|
|
426
|
+
configured on the mailer.
|
|
427
|
+
|
|
428
|
+
#### `cc(...addresses)` / `bcc(...addresses)`
|
|
429
|
+
|
|
430
|
+
`cc(...addresses: string[]): this`
|
|
431
|
+
`bcc(...addresses: string[]): this`
|
|
432
|
+
|
|
433
|
+
Append carbon-copy / blind-carbon-copy recipients.
|
|
434
|
+
|
|
435
|
+
```ts
|
|
436
|
+
mail().cc("team@x.com").bcc("audit@x.com");
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
**Notes:** both additive, like `to`. The underlying arrays are created lazily on
|
|
440
|
+
first use.
|
|
441
|
+
|
|
442
|
+
#### `replyTo(address)`
|
|
443
|
+
|
|
444
|
+
`replyTo(address: string): this`
|
|
445
|
+
|
|
446
|
+
Sets the `Reply-To` address.
|
|
447
|
+
|
|
448
|
+
```ts
|
|
449
|
+
mail().replyTo("support@x.com");
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
**Notes:** a single value; a later call replaces the prior one.
|
|
453
|
+
|
|
454
|
+
#### `subject(subject)`
|
|
455
|
+
|
|
456
|
+
`subject(subject: string): this`
|
|
457
|
+
|
|
458
|
+
Sets the subject line.
|
|
459
|
+
|
|
460
|
+
```ts
|
|
461
|
+
mail().subject("Welcome aboard");
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
**Notes:** required — `send()` throws if it's empty. A later call replaces it.
|
|
465
|
+
|
|
466
|
+
#### `text(text)` / `html(html)`
|
|
467
|
+
|
|
468
|
+
`text(text: string): this`
|
|
469
|
+
`html(html: string): this`
|
|
470
|
+
|
|
471
|
+
Set the plain-text / HTML body. At least one is required.
|
|
472
|
+
|
|
473
|
+
```ts
|
|
474
|
+
mail().text("Plain body").html("<p>Rich body</p>");
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
**Notes:** you can set both (a multipart message); `send()` throws only if
|
|
478
|
+
*neither* is present. Each later call replaces its body.
|
|
479
|
+
|
|
480
|
+
#### `header(name, value)`
|
|
481
|
+
|
|
482
|
+
`header(name: string, value: string): this`
|
|
483
|
+
|
|
484
|
+
Adds a custom header.
|
|
485
|
+
|
|
486
|
+
```ts
|
|
487
|
+
mail().header("X-Campaign", "weekly");
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
**Notes:** additive per name — repeated calls with distinct names accumulate;
|
|
491
|
+
the same name overwrites. The `headers` object is created lazily.
|
|
492
|
+
|
|
493
|
+
#### `fill(partial)`
|
|
494
|
+
|
|
495
|
+
`fill(partial: Partial<{ to: string | string[]; cc: string | string[]; bcc: string | string[] } & Omit<Message, "to" | "cc" | "bcc">>): this`
|
|
496
|
+
|
|
497
|
+
Seeds several fields at once, merging into whatever's been chained.
|
|
498
|
+
|
|
499
|
+
```ts
|
|
500
|
+
mail().fill({ to: ["a@x.com", "b@x.com"], subject: "Hi", text: "body" });
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
**Notes:** `to`/`cc`/`bcc` accept a single string or an array and are **appended**
|
|
504
|
+
to any existing recipients. The other fields (`from`, `subject`, `text`, `html`,
|
|
505
|
+
`replyTo`, `headers`) are assigned, **replacing** prior values — passing
|
|
506
|
+
`headers` here overwrites the whole header map rather than merging.
|
|
507
|
+
|
|
508
|
+
#### `send()`
|
|
509
|
+
|
|
510
|
+
`send(): Promise<Message>`
|
|
511
|
+
|
|
512
|
+
Hands the composed message to the mailer and resolves to the finalized message.
|
|
513
|
+
|
|
514
|
+
```ts
|
|
515
|
+
const sent = await mail().to("ada@x.com").subject("Hi").text("hey").send();
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
**Notes:** delegates to `Mailer.send`, so the same validation and default-`from`
|
|
519
|
+
handling apply; it throws on a missing required field.
|
|
520
|
+
|
|
521
|
+
### `ArrayTransport`
|
|
522
|
+
|
|
523
|
+
An in-memory transport that records every message. The default transport, and
|
|
524
|
+
the one to use in tests.
|
|
525
|
+
|
|
526
|
+
#### `new ArrayTransport()`
|
|
527
|
+
|
|
528
|
+
`new ArrayTransport()`
|
|
529
|
+
|
|
530
|
+
Creates a transport with an empty `sent` array.
|
|
531
|
+
|
|
532
|
+
```ts
|
|
533
|
+
const transport = new ArrayTransport();
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
#### `sent`
|
|
537
|
+
|
|
538
|
+
`readonly sent: Message[]`
|
|
539
|
+
|
|
540
|
+
The messages this transport has received, in order.
|
|
541
|
+
|
|
542
|
+
```ts
|
|
543
|
+
const transport = new ArrayTransport();
|
|
544
|
+
setMailer(transport);
|
|
545
|
+
// ...after sending...
|
|
546
|
+
transport.sent.length; // number of messages queued
|
|
547
|
+
transport.sent[0]?.subject; // first message's subject
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
**Notes:** `readonly` binding but the array is mutated on each `send` — assert on
|
|
551
|
+
`.length` and elements.
|
|
552
|
+
|
|
553
|
+
#### `send(message)`
|
|
554
|
+
|
|
555
|
+
`send(message: Message): Promise<void>`
|
|
556
|
+
|
|
557
|
+
Pushes the message onto `sent`.
|
|
558
|
+
|
|
559
|
+
```ts
|
|
560
|
+
await new ArrayTransport().send(message);
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
**Notes:** never throws; delivers nothing. Called for you by `Mailer.send`.
|
|
564
|
+
|
|
565
|
+
### `LogTransport`
|
|
566
|
+
|
|
567
|
+
A transport that logs each message (to, from, subject) via the framework logger
|
|
568
|
+
instead of delivering it — for local development.
|
|
569
|
+
|
|
570
|
+
#### `new LogTransport()`
|
|
571
|
+
|
|
572
|
+
`new LogTransport()`
|
|
573
|
+
|
|
574
|
+
Creates the transport.
|
|
575
|
+
|
|
576
|
+
```ts
|
|
577
|
+
setMailer(new LogTransport(), { from: "dev@localhost" });
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
#### `send(message)`
|
|
581
|
+
|
|
582
|
+
`send(message: Message): Promise<void>`
|
|
583
|
+
|
|
584
|
+
Logs `to`, `from`, and `subject` at info level; sends nothing.
|
|
585
|
+
|
|
586
|
+
```ts
|
|
587
|
+
await new LogTransport().send(message);
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
**Notes:** the body is not logged, only the envelope fields.
|
|
591
|
+
|
|
592
|
+
### Interfaces & types
|
|
593
|
+
|
|
594
|
+
#### `Message`
|
|
595
|
+
|
|
596
|
+
```ts
|
|
597
|
+
interface Message {
|
|
598
|
+
to: string[];
|
|
599
|
+
from?: string;
|
|
600
|
+
cc?: string[];
|
|
601
|
+
bcc?: string[];
|
|
602
|
+
replyTo?: string;
|
|
603
|
+
subject: string;
|
|
604
|
+
text?: string;
|
|
605
|
+
html?: string;
|
|
606
|
+
headers?: Record<string, string>;
|
|
607
|
+
}
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
The normalized, ready-to-send message. The builder produces one; a `Transport`
|
|
611
|
+
receives one (already validated, with `from` resolved). You can also build one by
|
|
612
|
+
hand and pass it to `Mailer.send`.
|
|
613
|
+
|
|
614
|
+
```ts
|
|
615
|
+
const message: Message = {
|
|
616
|
+
to: ["ada@x.com"],
|
|
617
|
+
from: "hi@app.com",
|
|
618
|
+
subject: "Hi",
|
|
619
|
+
text: "hey",
|
|
620
|
+
};
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
#### `Transport`
|
|
624
|
+
|
|
625
|
+
```ts
|
|
626
|
+
interface Transport {
|
|
627
|
+
send(message: Message): Promise<void>;
|
|
628
|
+
}
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
The seam between the mailer and your email provider — one method. Implement it to
|
|
632
|
+
bridge any SDK or API; register it with `setMailer`.
|
|
633
|
+
|
|
634
|
+
```ts
|
|
635
|
+
const transport: Transport = {
|
|
636
|
+
async send(message) {
|
|
637
|
+
await myProviderSdk.emails.send(message);
|
|
638
|
+
},
|
|
639
|
+
};
|
|
640
|
+
setMailer(transport, { from: "hi@app.com" });
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
#### `MailerOptions`
|
|
644
|
+
|
|
645
|
+
```ts
|
|
646
|
+
interface MailerOptions {
|
|
647
|
+
from?: string;
|
|
648
|
+
}
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
Options for a `Mailer`. Currently just a default `from` applied to messages that
|
|
652
|
+
don't set one.
|
|
653
|
+
|
|
654
|
+
```ts
|
|
655
|
+
setMailer(transport, { from: "hello@myapp.com" });
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
#### `FetchTransportOptions`
|
|
659
|
+
|
|
660
|
+
```ts
|
|
661
|
+
interface FetchTransportOptions {
|
|
662
|
+
url: string;
|
|
663
|
+
headers?: Record<string, string>;
|
|
664
|
+
body?: (message: Message) => unknown;
|
|
665
|
+
}
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
Configuration for `fetchTransport`. `url` is the provider endpoint; `headers`
|
|
669
|
+
merge over the automatic `Content-Type: application/json`; `body` maps a
|
|
670
|
+
`Message` to the provider's request shape (defaults to the message itself).
|
|
671
|
+
|
|
672
|
+
```ts
|
|
673
|
+
const opts: FetchTransportOptions = {
|
|
674
|
+
url: "https://api.resend.com/emails",
|
|
675
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
676
|
+
body: (m) => ({ from: m.from, to: m.to, subject: m.subject, html: m.html }),
|
|
677
|
+
};
|
|
678
|
+
```
|