@maestro-js/mail 1.0.0-alpha.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 +360 -0
- package/dist/index.d.ts +478 -0
- package/dist/index.js +554 -0
- package/package.json +45 -0
package/README.md
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
# @maestro-js/mail
|
|
2
|
+
|
|
3
|
+
Driver-based email sending with queued delivery, scheduled sending, and reusable mailable templates. Follows the Maestro Provider pattern with pluggable drivers (SendGrid, SES, MailTrap, Stub).
|
|
4
|
+
|
|
5
|
+
## Quick Setup
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { Mail } from '@maestro-js/mail'
|
|
9
|
+
import { Queue } from '@maestro-js/queue'
|
|
10
|
+
import { Log } from '@maestro-js/log'
|
|
11
|
+
|
|
12
|
+
// Create a logger for mail records
|
|
13
|
+
const mailLogger = Log.createLogger<[Mail.LogMessage]>({ transports: [Log.transports.console()] })
|
|
14
|
+
|
|
15
|
+
// Create and register the mail service
|
|
16
|
+
const mailService = Mail.Provider.create({
|
|
17
|
+
driver: Mail.drivers.sendGrid(sendGridClient),
|
|
18
|
+
name: 'default',
|
|
19
|
+
globalFrom: { email: 'noreply@example.com', name: 'MyApp' },
|
|
20
|
+
logger: mailLogger,
|
|
21
|
+
queueService: Queue.provider('default')
|
|
22
|
+
})
|
|
23
|
+
Mail.Provider.register('default', mailService)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Drivers
|
|
27
|
+
|
|
28
|
+
### SendGrid
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import sgMail from '@sendgrid/mail'
|
|
32
|
+
sgMail.setApiKey(process.env.SENDGRID_API_KEY)
|
|
33
|
+
const driver = Mail.drivers.sendGrid(sgMail)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The SendGrid driver accepts the `@sendgrid/mail` client. It passes `envelope.extras` as `customArgs` and supports webhook event normalization via `driver.toMailEvent(sgEvent)`.
|
|
37
|
+
|
|
38
|
+
### SES
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { SESv2Client, SendRawEmailCommand } from '@aws-sdk/client-sesv2'
|
|
42
|
+
const sesClient = new SESv2Client({ region: 'us-east-1' })
|
|
43
|
+
const driver = Mail.drivers.ses(sesClient, 'my-configuration-set')
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The SES driver requires a client adapter with a `send()` method and a configuration set name. It uses `nodemailer/MailComposer` to build raw MIME messages, sends per-recipient with retry (5 attempts, 3s delay), and passes `envelope.extras` as SES message tags. Supports webhook event normalization via `driver.toMailEvent(sesEvent)`.
|
|
47
|
+
|
|
48
|
+
### MailTrap
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import { MailtrapClient } from 'mailtrap'
|
|
52
|
+
const mailtrapClient = new MailtrapClient({ token: process.env.MAILTRAP_TOKEN })
|
|
53
|
+
const driver = Mail.drivers.mailTrap(mailtrapClient)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The MailTrap driver retries up to 10 attempts with 10s delay. It passes `envelope.extras` as `custom_variables`. Supports webhook event normalization via `driver.toMailEvent(mtEvent)`.
|
|
57
|
+
|
|
58
|
+
### Stub
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
const driver = Mail.drivers.stub()
|
|
62
|
+
// Or with logging:
|
|
63
|
+
const driver = Mail.drivers.stub({ logger: myLogger })
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The Stub driver returns success with random UUIDs as message IDs. Use for testing and development. Optionally accepts a logger to record sent envelopes.
|
|
67
|
+
|
|
68
|
+
## API Reference
|
|
69
|
+
|
|
70
|
+
### Mail.Provider.create(config)
|
|
71
|
+
|
|
72
|
+
Create a mail service instance. Config fields:
|
|
73
|
+
|
|
74
|
+
| Field | Type | Description |
|
|
75
|
+
|---|---|---|
|
|
76
|
+
| `driver` | `Mail.Driver` | The mail driver instance |
|
|
77
|
+
| `name` | `string` | Instance name, used for queue naming (`Maestro.Mail.{name}`) |
|
|
78
|
+
| `globalFrom` | `Mail.Address` | Default sender, overridden by `envelope.from` |
|
|
79
|
+
| `logger` | `Log.Logger<[Mail.LogMessage]>` | Logger for envelope and event records |
|
|
80
|
+
| `queueService` | `Queue.QueueService` | Queue service for async delivery |
|
|
81
|
+
| `onMessageSendFailure?` | `(envelope, error) => unknown` | Optional callback on send failure |
|
|
82
|
+
|
|
83
|
+
### Mail.Provider.register(name, service)
|
|
84
|
+
|
|
85
|
+
Register a service instance in the named registry.
|
|
86
|
+
|
|
87
|
+
### Mail.send(envelope)
|
|
88
|
+
|
|
89
|
+
Send an email immediately. Returns `{ success: boolean, messages: { id: string, email: string }[] }`. Addresses are trimmed, lowercased, and deduplicated automatically.
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
await Mail.send({
|
|
93
|
+
to: 'alice@example.com',
|
|
94
|
+
subject: 'Order Confirmed',
|
|
95
|
+
content: { html: '<h1>Thank you!</h1>' }
|
|
96
|
+
})
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Mail.queue(envelope)
|
|
100
|
+
|
|
101
|
+
Enqueue an email for immediate async processing by the queue worker. Does not block the current request.
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
Mail.queue({
|
|
105
|
+
to: 'bob@example.com',
|
|
106
|
+
subject: 'Password Reset',
|
|
107
|
+
content: { html: '<p>Click the link to reset your password.</p>' }
|
|
108
|
+
})
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Mail.later(envelope, scheduledDate)
|
|
112
|
+
|
|
113
|
+
Enqueue an email for delivery at a future date. The `scheduledDate` is an `Iso.Instant`.
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
Mail.later(
|
|
117
|
+
{ to: 'user@example.com', subject: 'Reminder', content: { text: 'Your trial ends tomorrow.' } },
|
|
118
|
+
'2025-01-15T09:00:00Z' as Iso.Instant
|
|
119
|
+
)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Mail.provider(key)
|
|
123
|
+
|
|
124
|
+
Resolve a named mail service instance. Returns `{ send, queue, later }`.
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
const transactional = Mail.provider('transactional')
|
|
128
|
+
await transactional.send({ to: '...', subject: '...', content: { html: '...' } })
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Mail.mailable(name, renderFn)
|
|
132
|
+
|
|
133
|
+
Register a reusable mailable template. The render function receives args and returns an `Envelope` or `Mail.EMPTY_ENVELOPE` to skip sending.
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
const OrderConfirmation = Mail.mailable('OrderConfirmation', async (args: { orderId: string }) => {
|
|
137
|
+
const order = await db.query('SELECT * FROM orders WHERE id = ?', [args.orderId])
|
|
138
|
+
return {
|
|
139
|
+
to: order.email,
|
|
140
|
+
subject: `Order #${order.id} Confirmed`,
|
|
141
|
+
content: { html: `<h1>Thank you, ${order.name}!</h1>` }
|
|
142
|
+
}
|
|
143
|
+
})
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
#### Rendered Mailable Methods
|
|
147
|
+
|
|
148
|
+
After calling `.render(args)`, the returned object provides:
|
|
149
|
+
|
|
150
|
+
- **`send()`** -- Send immediately
|
|
151
|
+
- **`queue()`** -- Enqueue for async processing
|
|
152
|
+
- **`later(scheduledDate)`** -- Schedule for future delivery
|
|
153
|
+
- **`content()`** -- Retrieve the generated content without sending
|
|
154
|
+
- **`provider(key)`** -- Target a specific named mail service
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
// Send immediately
|
|
158
|
+
await OrderConfirmation.render({ orderId: '1042' }).send()
|
|
159
|
+
|
|
160
|
+
// Queue for async processing
|
|
161
|
+
await OrderConfirmation.render({ orderId: '1042' }).queue()
|
|
162
|
+
|
|
163
|
+
// Schedule for later
|
|
164
|
+
await OrderConfirmation.render({ orderId: '1042' }).later('2025-06-01T12:00:00Z' as Iso.Instant)
|
|
165
|
+
|
|
166
|
+
// Get content without sending
|
|
167
|
+
const content = await OrderConfirmation.render({ orderId: '1042' }).content()
|
|
168
|
+
|
|
169
|
+
// Use a specific provider
|
|
170
|
+
await OrderConfirmation.render({ orderId: '1042' }).provider('transactional').send()
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Mail.EMPTY_ENVELOPE
|
|
174
|
+
|
|
175
|
+
Return from a mailable render function to skip sending. Useful for conditional emails.
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
const WeeklyDigest = Mail.mailable('WeeklyDigest', async (args: { userId: string }) => {
|
|
179
|
+
const updates = await getUpdates(args.userId)
|
|
180
|
+
if (updates.length === 0) return Mail.EMPTY_ENVELOPE
|
|
181
|
+
return { to: '...', subject: 'Weekly Digest', content: { html: '...' } }
|
|
182
|
+
})
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Mail.attachments.ics(options)
|
|
186
|
+
|
|
187
|
+
Generate an ICS calendar attachment from structured options.
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
const attachment = Mail.attachments.ics({
|
|
191
|
+
title: 'Team Standup',
|
|
192
|
+
start: '2025-03-01T10:00:00Z' as Iso.Instant,
|
|
193
|
+
end: 'PT1H' as Iso.Duration, // 1-hour duration
|
|
194
|
+
location: 'Conference Room A',
|
|
195
|
+
organizer: { name: 'Alice', email: 'alice@example.com' },
|
|
196
|
+
attendees: [{ name: 'Bob', email: 'bob@example.com', rsvp: true }],
|
|
197
|
+
status: 'CONFIRMED',
|
|
198
|
+
alarms: [{ action: 'display', description: 'Standup in 15 min', trigger: '-PT15M' as Iso.Duration }]
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
await Mail.send({
|
|
202
|
+
to: 'bob@example.com',
|
|
203
|
+
subject: 'Team Standup Invite',
|
|
204
|
+
content: { html: '<p>You are invited to standup.</p>' },
|
|
205
|
+
attachments: [attachment]
|
|
206
|
+
})
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
ICS options support `start` and `end` as `Iso.Instant`, `Iso.ZonedDateTime`, or `Iso.DateTime`. The `end` field also accepts `Iso.Duration`. Additional fields: `description`, `geo`, `url`, `busyStatus`, `categories`, `uid`, `method`, `recurrenceRule`, `sequence`, `calName`, `classification`, `created`, `lastModified`, `htmlContent`, `alarms`.
|
|
210
|
+
|
|
211
|
+
## Envelope Type
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
interface Envelope {
|
|
215
|
+
to: Mail.Address | Mail.Address[]
|
|
216
|
+
from?: Mail.Address // overrides globalFrom
|
|
217
|
+
cc?: Mail.Address | Mail.Address[]
|
|
218
|
+
bcc?: Mail.Address | Mail.Address[]
|
|
219
|
+
attachments?: Mail.Attachment[]
|
|
220
|
+
subject: string | null
|
|
221
|
+
content: { text: string } | { html: string }
|
|
222
|
+
headers?: {
|
|
223
|
+
listUnsubscribe?: string
|
|
224
|
+
listUnsubscribePost?: string
|
|
225
|
+
replyTo?: Mail.Address
|
|
226
|
+
}
|
|
227
|
+
category?: string | null
|
|
228
|
+
extras?: Record<string, string | null>
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
The `Mail.Address` type is either a plain email string or `{ email: string, name?: string }`.
|
|
233
|
+
|
|
234
|
+
## Common Patterns
|
|
235
|
+
|
|
236
|
+
### Sending with Attachments
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
await Mail.send({
|
|
240
|
+
to: 'user@example.com',
|
|
241
|
+
subject: 'Your Invoice',
|
|
242
|
+
content: { html: '<p>Please find your invoice attached.</p>' },
|
|
243
|
+
attachments: [{
|
|
244
|
+
content: Buffer.from(pdfData).toString('base64'),
|
|
245
|
+
filename: 'invoice.pdf',
|
|
246
|
+
type: 'application/pdf',
|
|
247
|
+
disposition: 'attachment'
|
|
248
|
+
}]
|
|
249
|
+
})
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Unsubscribe Headers
|
|
253
|
+
|
|
254
|
+
```ts
|
|
255
|
+
await Mail.send({
|
|
256
|
+
to: 'subscriber@example.com',
|
|
257
|
+
subject: 'Newsletter',
|
|
258
|
+
content: { html: '...' },
|
|
259
|
+
headers: {
|
|
260
|
+
listUnsubscribe: '<https://example.com/unsubscribe?id=123>',
|
|
261
|
+
listUnsubscribePost: 'List-Unsubscribe=One-Click'
|
|
262
|
+
},
|
|
263
|
+
category: 'newsletter'
|
|
264
|
+
})
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### Processing Webhook Events
|
|
268
|
+
|
|
269
|
+
SendGrid, SES, and MailTrap drivers expose `toMailEvent()` to normalize provider-specific webhook payloads into a unified `Mail.Event` type.
|
|
270
|
+
|
|
271
|
+
```ts
|
|
272
|
+
// SendGrid webhook handler
|
|
273
|
+
const sgDriver = Mail.drivers.sendGrid(sgMail) as SendGridMailDriver
|
|
274
|
+
const event = sgDriver.toMailEvent(rawSendGridEvent) // returns MailEvent | null
|
|
275
|
+
|
|
276
|
+
// SES webhook handler
|
|
277
|
+
const sesDriver = Mail.drivers.ses(sesClient, 'config-set') as SesMailDriver
|
|
278
|
+
const event = sesDriver.toMailEvent(rawSesEvent) // returns MailEvent | null
|
|
279
|
+
|
|
280
|
+
// MailTrap webhook handler
|
|
281
|
+
const mtDriver = Mail.drivers.mailTrap(mtClient) as MailTrapMailDriver
|
|
282
|
+
const event = mtDriver.toMailEvent(rawMailTrapEvent) // returns MailEvent
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
The normalized `Mail.Event` includes: `eventId`, `event` (delivery, bounce, softBounce, open, click, unsubscribe, spamComplaint, reject, suspension), `email`, `messageId`, `timestamp`, `category`, `extras`, `driverName`.
|
|
286
|
+
|
|
287
|
+
### Conditional Mailable with EMPTY_ENVELOPE
|
|
288
|
+
|
|
289
|
+
```ts
|
|
290
|
+
const InactivityReminder = Mail.mailable('InactivityReminder', async (args: { userId: string }) => {
|
|
291
|
+
const user = await getUser(args.userId)
|
|
292
|
+
if (user.lastActiveAt > thirtyDaysAgo) return Mail.EMPTY_ENVELOPE
|
|
293
|
+
return {
|
|
294
|
+
to: user.email,
|
|
295
|
+
subject: 'We miss you!',
|
|
296
|
+
content: { html: `<p>Hi ${user.name}, it has been a while...</p>` }
|
|
297
|
+
}
|
|
298
|
+
})
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### Error Handling with onMessageSendFailure
|
|
302
|
+
|
|
303
|
+
```ts
|
|
304
|
+
Mail.Provider.create({
|
|
305
|
+
driver: Mail.drivers.sendGrid(sgMail),
|
|
306
|
+
name: 'default',
|
|
307
|
+
globalFrom: { email: 'noreply@example.com' },
|
|
308
|
+
logger: mailLogger,
|
|
309
|
+
queueService: Queue.provider('default'),
|
|
310
|
+
onMessageSendFailure(envelope, error) {
|
|
311
|
+
Log.error({ message: 'Mail send failed', to: envelope.to, error })
|
|
312
|
+
}
|
|
313
|
+
})
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
## Testing
|
|
317
|
+
|
|
318
|
+
Use the Stub driver in tests to avoid real email delivery.
|
|
319
|
+
|
|
320
|
+
```ts
|
|
321
|
+
const stubDriver = Mail.drivers.stub()
|
|
322
|
+
|
|
323
|
+
const mailService = Mail.Provider.create({
|
|
324
|
+
driver: stubDriver,
|
|
325
|
+
name: 'default',
|
|
326
|
+
globalFrom: { email: 'test@example.com' },
|
|
327
|
+
logger: testLogger,
|
|
328
|
+
queueService: testQueueService
|
|
329
|
+
})
|
|
330
|
+
Mail.Provider.register('default', mailService)
|
|
331
|
+
|
|
332
|
+
// Emails go through the full pipeline but are not delivered
|
|
333
|
+
await Mail.send({ to: 'user@example.com', subject: 'Test', content: { text: 'Hello' } })
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
To inspect sent envelopes, pass a logger to the stub driver:
|
|
337
|
+
|
|
338
|
+
```ts
|
|
339
|
+
const sent: Mail.Envelope[] = []
|
|
340
|
+
const stubDriver = Mail.drivers.stub({
|
|
341
|
+
logger: { info: (envelope) => sent.push(envelope) } as any
|
|
342
|
+
})
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
## Cross-Package Integration
|
|
346
|
+
|
|
347
|
+
- **@maestro-js/service-registry** -- Provides the Provider pattern (`create`, `register`, `provider`, facade)
|
|
348
|
+
- **@maestro-js/log** -- Used for structured mail logging (envelope records and webhook events)
|
|
349
|
+
- **@maestro-js/queue** -- Powers `Mail.queue()` and `Mail.later()` for async and scheduled delivery. Queue names follow the pattern `Maestro.Mail.{name}` and `Maestro.Mailable.{name}`
|
|
350
|
+
- **@maestro-js/helpers** -- Used by SES and MailTrap drivers for retry logic (`Helpers.retry()`)
|
|
351
|
+
|
|
352
|
+
## Key Source Files
|
|
353
|
+
|
|
354
|
+
- `packages/mail/src/index.ts` -- Main module, service creation, mailable system, facade export
|
|
355
|
+
- `packages/mail/src/mail-types.ts` -- Type definitions (Envelope, Address, Content, Attachment, Event, Driver)
|
|
356
|
+
- `packages/mail/src/send-grid-mail-driver.ts` -- SendGrid driver with webhook event mapping
|
|
357
|
+
- `packages/mail/src/ses-mail-driver.ts` -- SES driver with raw MIME composition and retry
|
|
358
|
+
- `packages/mail/src/mail-trap-mail-driver.ts` -- MailTrap driver with retry and webhook events
|
|
359
|
+
- `packages/mail/src/stub-mail-driver.ts` -- Stub driver for testing
|
|
360
|
+
- `packages/mail/src/ics-attachment.ts` -- ICS calendar attachment generator
|