@igniter-js/mail 0.1.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 +394 -0
- package/CHANGELOG.md +95 -0
- package/README.md +682 -0
- package/dist/index.d.mts +510 -0
- package/dist/index.d.ts +510 -0
- package/dist/index.js +880 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +856 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +90 -0
package/README.md
ADDED
|
@@ -0,0 +1,682 @@
|
|
|
1
|
+
# @igniter-js/mail
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@igniter-js/mail)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
|
|
6
|
+
Type-safe email library for Igniter.js applications with React Email templates and multiple provider adapters. Send transactional emails with confidence using compile-time type safety and runtime validation.
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- ✅ **Type-Safe Templates** - Full TypeScript inference with template payload validation
|
|
11
|
+
- ✅ **React Email** - Build beautiful emails with React components
|
|
12
|
+
- ✅ **Multiple Providers** - Resend, Postmark, SendGrid, SMTP, Webhook
|
|
13
|
+
- ✅ **Schema Validation** - Runtime validation with StandardSchema support
|
|
14
|
+
- ✅ **Queue Integration** - Schedule emails with BullMQ or custom queues
|
|
15
|
+
- ✅ **Lifecycle Hooks** - React to send events (started, success, error)
|
|
16
|
+
- ✅ **Builder Pattern** - Fluent API for configuration
|
|
17
|
+
- ✅ **Server-First** - Built for Node.js, Bun, Deno (no browser dependencies)
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# npm
|
|
23
|
+
npm install @igniter-js/mail @react-email/components react
|
|
24
|
+
|
|
25
|
+
# pnpm
|
|
26
|
+
pnpm add @igniter-js/mail @react-email/components react
|
|
27
|
+
|
|
28
|
+
# yarn
|
|
29
|
+
yarn add @igniter-js/mail @react-email/components react
|
|
30
|
+
|
|
31
|
+
# bun
|
|
32
|
+
bun add @igniter-js/mail @react-email/components react
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Provider Dependencies
|
|
36
|
+
|
|
37
|
+
Install the adapter you need:
|
|
38
|
+
|
|
39
|
+
**Resend:**
|
|
40
|
+
```bash
|
|
41
|
+
npm install resend
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**Postmark:**
|
|
45
|
+
```bash
|
|
46
|
+
npm install postmark
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**SendGrid:**
|
|
50
|
+
```bash
|
|
51
|
+
npm install sendgrid
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**SMTP:**
|
|
55
|
+
```bash
|
|
56
|
+
npm install nodemailer @types/nodemailer
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Quick Start
|
|
60
|
+
|
|
61
|
+
### 1. Create Email Templates
|
|
62
|
+
|
|
63
|
+
Use React Email components to build your templates:
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
// src/emails/welcome.tsx
|
|
67
|
+
import { Button, Html, Text } from '@react-email/components'
|
|
68
|
+
|
|
69
|
+
export interface WelcomeEmailProps {
|
|
70
|
+
name: string
|
|
71
|
+
verifyUrl: string
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function WelcomeEmail({ name, verifyUrl }: WelcomeEmailProps) {
|
|
75
|
+
return (
|
|
76
|
+
<Html>
|
|
77
|
+
<Text>Hi {name},</Text>
|
|
78
|
+
<Text>Welcome to our platform! Click below to verify your email:</Text>
|
|
79
|
+
<Button href={verifyUrl}>Verify Email</Button>
|
|
80
|
+
</Html>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### 2. Initialize Mail Service
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
import { IgniterMail } from '@igniter-js/mail'
|
|
89
|
+
import { z } from 'zod'
|
|
90
|
+
import { WelcomeEmail } from './emails/welcome'
|
|
91
|
+
|
|
92
|
+
// Create mail instance with builder
|
|
93
|
+
export const mail = IgniterMail.create()
|
|
94
|
+
.withFrom('no-reply@example.com')
|
|
95
|
+
.withAdapter('resend', process.env.RESEND_API_KEY!)
|
|
96
|
+
.addTemplate('welcome', {
|
|
97
|
+
subject: 'Welcome to Our Platform',
|
|
98
|
+
schema: z.object({
|
|
99
|
+
name: z.string(),
|
|
100
|
+
verifyUrl: z.string().url(),
|
|
101
|
+
}),
|
|
102
|
+
render: WelcomeEmail,
|
|
103
|
+
})
|
|
104
|
+
.build()
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### 3. Send Emails
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
// Send immediately
|
|
111
|
+
await mail.send({
|
|
112
|
+
to: 'user@example.com',
|
|
113
|
+
template: 'welcome',
|
|
114
|
+
data: {
|
|
115
|
+
name: 'John Doe',
|
|
116
|
+
verifyUrl: 'https://example.com/verify?token=abc123',
|
|
117
|
+
},
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
// Schedule for later
|
|
121
|
+
await mail.schedule(
|
|
122
|
+
{
|
|
123
|
+
to: 'user@example.com',
|
|
124
|
+
template: 'welcome',
|
|
125
|
+
data: {
|
|
126
|
+
name: 'John Doe',
|
|
127
|
+
verifyUrl: 'https://example.com/verify?token=abc123',
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
new Date(Date.now() + 24 * 60 * 60 * 1000) // Send in 24 hours
|
|
131
|
+
)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Core Concepts
|
|
135
|
+
|
|
136
|
+
### Templates
|
|
137
|
+
|
|
138
|
+
Templates combine React Email components with schema validation:
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
import { IgniterMail } from '@igniter-js/mail'
|
|
142
|
+
import { z } from 'zod'
|
|
143
|
+
|
|
144
|
+
const mail = IgniterMail.create()
|
|
145
|
+
.withFrom('no-reply@example.com')
|
|
146
|
+
.withAdapter('resend', process.env.RESEND_API_KEY!)
|
|
147
|
+
.addTemplate('resetPassword', {
|
|
148
|
+
subject: 'Reset Your Password',
|
|
149
|
+
schema: z.object({
|
|
150
|
+
name: z.string(),
|
|
151
|
+
resetLink: z.string().url(),
|
|
152
|
+
expiresAt: z.date(),
|
|
153
|
+
}),
|
|
154
|
+
render: ({ name, resetLink, expiresAt }) => (
|
|
155
|
+
<Html>
|
|
156
|
+
<Text>Hi {name},</Text>
|
|
157
|
+
<Text>Click below to reset your password:</Text>
|
|
158
|
+
<Button href={resetLink}>Reset Password</Button>
|
|
159
|
+
<Text>This link expires at {expiresAt.toLocaleString()}</Text>
|
|
160
|
+
</Html>
|
|
161
|
+
),
|
|
162
|
+
})
|
|
163
|
+
.build()
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Type Safety
|
|
167
|
+
|
|
168
|
+
The library provides end-to-end type safety:
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
// ✅ TypeScript knows 'welcome' template exists
|
|
172
|
+
await mail.send({
|
|
173
|
+
to: 'user@example.com',
|
|
174
|
+
template: 'welcome',
|
|
175
|
+
data: {
|
|
176
|
+
name: 'John Doe',
|
|
177
|
+
verifyUrl: 'https://example.com/verify',
|
|
178
|
+
},
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
// ❌ TypeScript error: unknown template
|
|
182
|
+
await mail.send({
|
|
183
|
+
to: 'user@example.com',
|
|
184
|
+
template: 'unknown', // Error: Type '"unknown"' is not assignable to type '"welcome"'
|
|
185
|
+
data: {},
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
// ❌ TypeScript error: invalid data shape
|
|
189
|
+
await mail.send({
|
|
190
|
+
to: 'user@example.com',
|
|
191
|
+
template: 'welcome',
|
|
192
|
+
data: {
|
|
193
|
+
invalidProp: true, // Error: Object literal may only specify known properties
|
|
194
|
+
},
|
|
195
|
+
})
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Schema Validation
|
|
199
|
+
|
|
200
|
+
Templates support StandardSchema for runtime validation:
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
import { z } from 'zod'
|
|
204
|
+
import { IgniterMail } from '@igniter-js/mail'
|
|
205
|
+
|
|
206
|
+
const mail = IgniterMail.create()
|
|
207
|
+
.addTemplate('notification', {
|
|
208
|
+
subject: 'New Notification',
|
|
209
|
+
schema: z.object({
|
|
210
|
+
message: z.string().min(1).max(500),
|
|
211
|
+
priority: z.enum(['low', 'medium', 'high']),
|
|
212
|
+
}),
|
|
213
|
+
render: ({ message, priority }) => (
|
|
214
|
+
<Html>
|
|
215
|
+
<Text>Priority: {priority}</Text>
|
|
216
|
+
<Text>{message}</Text>
|
|
217
|
+
</Html>
|
|
218
|
+
),
|
|
219
|
+
})
|
|
220
|
+
.build()
|
|
221
|
+
|
|
222
|
+
// ✅ Valid data
|
|
223
|
+
await mail.send({
|
|
224
|
+
to: 'user@example.com',
|
|
225
|
+
template: 'notification',
|
|
226
|
+
data: {
|
|
227
|
+
message: 'Your order has shipped!',
|
|
228
|
+
priority: 'high',
|
|
229
|
+
},
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
// ❌ Runtime validation error
|
|
233
|
+
await mail.send({
|
|
234
|
+
to: 'user@example.com',
|
|
235
|
+
template: 'notification',
|
|
236
|
+
data: {
|
|
237
|
+
message: '', // Error: String must contain at least 1 character(s)
|
|
238
|
+
priority: 'urgent', // Error: Invalid enum value
|
|
239
|
+
},
|
|
240
|
+
})
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## Adapters
|
|
244
|
+
|
|
245
|
+
### Resend
|
|
246
|
+
|
|
247
|
+
```typescript
|
|
248
|
+
import { IgniterMail } from '@igniter-js/mail'
|
|
249
|
+
import { ResendMailAdapterBuilder } from '@igniter-js/mail'
|
|
250
|
+
|
|
251
|
+
// Using builder shorthand
|
|
252
|
+
const mail = IgniterMail.create()
|
|
253
|
+
.withFrom('no-reply@example.com')
|
|
254
|
+
.withAdapter('resend', process.env.RESEND_API_KEY!)
|
|
255
|
+
.build()
|
|
256
|
+
|
|
257
|
+
// Or using adapter builder
|
|
258
|
+
const adapter = ResendMailAdapterBuilder.create()
|
|
259
|
+
.withSecret(process.env.RESEND_API_KEY!)
|
|
260
|
+
.withFrom('no-reply@example.com')
|
|
261
|
+
.build()
|
|
262
|
+
|
|
263
|
+
const mail = IgniterMail.create()
|
|
264
|
+
.withFrom('no-reply@example.com')
|
|
265
|
+
.withAdapter(adapter)
|
|
266
|
+
.build()
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### Postmark
|
|
270
|
+
|
|
271
|
+
```typescript
|
|
272
|
+
import { PostmarkMailAdapterBuilder } from '@igniter-js/mail'
|
|
273
|
+
|
|
274
|
+
const mail = IgniterMail.create()
|
|
275
|
+
.withFrom('no-reply@example.com')
|
|
276
|
+
.withAdapter('postmark', process.env.POSTMARK_SERVER_TOKEN!)
|
|
277
|
+
.build()
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### SendGrid
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
const mail = IgniterMail.create()
|
|
284
|
+
.withFrom('no-reply@example.com')
|
|
285
|
+
.withAdapter('sendgrid', process.env.SENDGRID_API_KEY!)
|
|
286
|
+
.build()
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### SMTP
|
|
290
|
+
|
|
291
|
+
```typescript
|
|
292
|
+
const mail = IgniterMail.create()
|
|
293
|
+
.withFrom('no-reply@example.com')
|
|
294
|
+
.withAdapter('smtp', JSON.stringify({
|
|
295
|
+
host: 'smtp.gmail.com',
|
|
296
|
+
port: 587,
|
|
297
|
+
secure: false,
|
|
298
|
+
auth: {
|
|
299
|
+
user: process.env.SMTP_USER,
|
|
300
|
+
pass: process.env.SMTP_PASS,
|
|
301
|
+
},
|
|
302
|
+
}))
|
|
303
|
+
.build()
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### Webhook
|
|
307
|
+
|
|
308
|
+
For testing or custom integrations:
|
|
309
|
+
|
|
310
|
+
```typescript
|
|
311
|
+
const mail = IgniterMail.create()
|
|
312
|
+
.withFrom('no-reply@example.com')
|
|
313
|
+
.withAdapter('webhook', 'https://webhook.site/your-unique-url')
|
|
314
|
+
.build()
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
### Test Adapter
|
|
318
|
+
|
|
319
|
+
For unit tests:
|
|
320
|
+
|
|
321
|
+
```typescript
|
|
322
|
+
import { TestMailAdapter } from '@igniter-js/mail'
|
|
323
|
+
|
|
324
|
+
const adapter = new TestMailAdapter()
|
|
325
|
+
|
|
326
|
+
const mail = IgniterMail.create()
|
|
327
|
+
.withFrom('no-reply@example.com')
|
|
328
|
+
.withAdapter(adapter)
|
|
329
|
+
.build()
|
|
330
|
+
|
|
331
|
+
// Send email
|
|
332
|
+
await mail.send({ ... })
|
|
333
|
+
|
|
334
|
+
// Verify in tests
|
|
335
|
+
expect(adapter.sent).toHaveLength(1)
|
|
336
|
+
expect(adapter.sent[0].to).toBe('user@example.com')
|
|
337
|
+
expect(adapter.sent[0].html).toContain('Welcome')
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
## Queue Integration
|
|
341
|
+
|
|
342
|
+
Integrate with BullMQ or custom job queues for async email delivery:
|
|
343
|
+
|
|
344
|
+
```typescript
|
|
345
|
+
import { IgniterMail } from '@igniter-js/mail'
|
|
346
|
+
import { createBullMQAdapter } from '@igniter-js/adapter-bullmq'
|
|
347
|
+
|
|
348
|
+
const queueAdapter = createBullMQAdapter({
|
|
349
|
+
connection: {
|
|
350
|
+
host: process.env.REDIS_HOST,
|
|
351
|
+
port: Number(process.env.REDIS_PORT),
|
|
352
|
+
},
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
const mail = IgniterMail.create()
|
|
356
|
+
.withFrom('no-reply@example.com')
|
|
357
|
+
.withAdapter('resend', process.env.RESEND_API_KEY!)
|
|
358
|
+
.withQueue(queueAdapter, {
|
|
359
|
+
namespace: 'mail',
|
|
360
|
+
task: 'send',
|
|
361
|
+
attempts: 3,
|
|
362
|
+
removeOnComplete: true,
|
|
363
|
+
})
|
|
364
|
+
.addTemplate('welcome', { ... })
|
|
365
|
+
.build()
|
|
366
|
+
|
|
367
|
+
// This now enqueues the email instead of sending immediately
|
|
368
|
+
await mail.schedule(
|
|
369
|
+
{
|
|
370
|
+
to: 'user@example.com',
|
|
371
|
+
template: 'welcome',
|
|
372
|
+
data: { name: 'John' },
|
|
373
|
+
},
|
|
374
|
+
new Date(Date.now() + 60000) // Send in 1 minute
|
|
375
|
+
)
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
## Lifecycle Hooks
|
|
379
|
+
|
|
380
|
+
React to email sending events:
|
|
381
|
+
|
|
382
|
+
```typescript
|
|
383
|
+
const mail = IgniterMail.create()
|
|
384
|
+
.withFrom('no-reply@example.com')
|
|
385
|
+
.withAdapter('resend', process.env.RESEND_API_KEY!)
|
|
386
|
+
.withOnSendStarted(async (params) => {
|
|
387
|
+
console.log('Starting to send email:', params.template)
|
|
388
|
+
})
|
|
389
|
+
.withOnSendSuccess(async (params) => {
|
|
390
|
+
console.log('Email sent successfully:', params.template)
|
|
391
|
+
// Log to analytics, update database, etc.
|
|
392
|
+
})
|
|
393
|
+
.withOnSendError(async (params, error) => {
|
|
394
|
+
console.error('Failed to send email:', error)
|
|
395
|
+
// Log error, send alert, etc.
|
|
396
|
+
})
|
|
397
|
+
.build()
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
## API Reference
|
|
401
|
+
|
|
402
|
+
### IgniterMail
|
|
403
|
+
|
|
404
|
+
Main mail client created by the builder.
|
|
405
|
+
|
|
406
|
+
#### Methods
|
|
407
|
+
|
|
408
|
+
##### `send(params)`
|
|
409
|
+
|
|
410
|
+
Sends an email immediately.
|
|
411
|
+
|
|
412
|
+
```typescript
|
|
413
|
+
await mail.send({
|
|
414
|
+
to: string
|
|
415
|
+
template: TemplateKey
|
|
416
|
+
data: TemplatePayload
|
|
417
|
+
subject?: string // Optional subject override
|
|
418
|
+
})
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
##### `schedule(params, date)`
|
|
422
|
+
|
|
423
|
+
Schedules an email for a future date. Uses queue if configured, otherwise `setTimeout`.
|
|
424
|
+
|
|
425
|
+
```typescript
|
|
426
|
+
await mail.schedule(
|
|
427
|
+
{
|
|
428
|
+
to: string
|
|
429
|
+
template: TemplateKey
|
|
430
|
+
data: TemplatePayload
|
|
431
|
+
subject?: string
|
|
432
|
+
},
|
|
433
|
+
date: Date
|
|
434
|
+
)
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
### IgniterMailBuilder
|
|
438
|
+
|
|
439
|
+
Fluent API for configuring the mail service.
|
|
440
|
+
|
|
441
|
+
#### Methods
|
|
442
|
+
|
|
443
|
+
##### `create()`
|
|
444
|
+
|
|
445
|
+
Creates a new builder instance.
|
|
446
|
+
|
|
447
|
+
```typescript
|
|
448
|
+
const builder = IgniterMail.create()
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
##### `withFrom(from)`
|
|
452
|
+
|
|
453
|
+
Sets the default FROM address.
|
|
454
|
+
|
|
455
|
+
```typescript
|
|
456
|
+
builder.withFrom('no-reply@example.com')
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
##### `withAdapter(adapter)`
|
|
460
|
+
|
|
461
|
+
Sets the mail adapter (instance or provider + secret).
|
|
462
|
+
|
|
463
|
+
```typescript
|
|
464
|
+
// With provider string + secret
|
|
465
|
+
builder.withAdapter('resend', process.env.RESEND_API_KEY!)
|
|
466
|
+
|
|
467
|
+
// With adapter instance
|
|
468
|
+
builder.withAdapter(adapterInstance)
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
##### `withLogger(logger)`
|
|
472
|
+
|
|
473
|
+
Attaches a logger for debugging.
|
|
474
|
+
|
|
475
|
+
```typescript
|
|
476
|
+
builder.withLogger(logger)
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
##### `withQueue(adapter, options?)`
|
|
480
|
+
|
|
481
|
+
Enables queue-based delivery.
|
|
482
|
+
|
|
483
|
+
```typescript
|
|
484
|
+
builder.withQueue(queueAdapter, {
|
|
485
|
+
namespace: 'mail',
|
|
486
|
+
task: 'send',
|
|
487
|
+
attempts: 3,
|
|
488
|
+
})
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
##### `addTemplate(key, template)`
|
|
492
|
+
|
|
493
|
+
Registers an email template.
|
|
494
|
+
|
|
495
|
+
```typescript
|
|
496
|
+
builder.addTemplate('welcome', {
|
|
497
|
+
subject: 'Welcome',
|
|
498
|
+
schema: z.object({ name: z.string() }),
|
|
499
|
+
render: ({ name }) => <Html>...</Html>,
|
|
500
|
+
})
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
##### `withOnSendStarted(hook)`
|
|
504
|
+
|
|
505
|
+
Registers a hook for send start events.
|
|
506
|
+
|
|
507
|
+
```typescript
|
|
508
|
+
builder.withOnSendStarted(async (params) => {
|
|
509
|
+
console.log('Sending:', params.template)
|
|
510
|
+
})
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
##### `withOnSendSuccess(hook)`
|
|
514
|
+
|
|
515
|
+
Registers a hook for send success events.
|
|
516
|
+
|
|
517
|
+
```typescript
|
|
518
|
+
builder.withOnSendSuccess(async (params) => {
|
|
519
|
+
console.log('Sent:', params.template)
|
|
520
|
+
})
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
##### `withOnSendError(hook)`
|
|
524
|
+
|
|
525
|
+
Registers a hook for send error events.
|
|
526
|
+
|
|
527
|
+
```typescript
|
|
528
|
+
builder.withOnSendError(async (params, error) => {
|
|
529
|
+
console.error('Failed:', error)
|
|
530
|
+
})
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
##### `build()`
|
|
534
|
+
|
|
535
|
+
Builds the mail instance.
|
|
536
|
+
|
|
537
|
+
```typescript
|
|
538
|
+
const mail = builder.build()
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
## Error Handling
|
|
542
|
+
|
|
543
|
+
All errors are instances of `IgniterMailError` with stable error codes:
|
|
544
|
+
|
|
545
|
+
```typescript
|
|
546
|
+
try {
|
|
547
|
+
await mail.send({ ... })
|
|
548
|
+
} catch (error) {
|
|
549
|
+
if (error instanceof IgniterMailError) {
|
|
550
|
+
console.error('Code:', error.code)
|
|
551
|
+
console.error('Metadata:', error.metadata)
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
**Common Error Codes:**
|
|
557
|
+
- `MAIL_PROVIDER_ADAPTER_REQUIRED` - No adapter configured
|
|
558
|
+
- `MAIL_PROVIDER_TEMPLATES_REQUIRED` - No templates registered
|
|
559
|
+
- `MAIL_PROVIDER_TEMPLATE_NOT_FOUND` - Template key doesn't exist
|
|
560
|
+
- `MAIL_PROVIDER_TEMPLATE_DATA_INVALID` - Schema validation failed
|
|
561
|
+
- `MAIL_PROVIDER_SEND_FAILED` - Failed to send email
|
|
562
|
+
- `MAIL_PROVIDER_SCHEDULE_DATE_INVALID` - Schedule date is in the past
|
|
563
|
+
- `MAIL_PROVIDER_SCHEDULE_FAILED` - Failed to schedule email
|
|
564
|
+
- `MAIL_ADAPTER_CONFIGURATION_INVALID` - Adapter configuration error
|
|
565
|
+
|
|
566
|
+
## TypeScript Support
|
|
567
|
+
|
|
568
|
+
Full TypeScript support with compile-time type inference:
|
|
569
|
+
|
|
570
|
+
```typescript
|
|
571
|
+
const mail = IgniterMail.create()
|
|
572
|
+
.addTemplate('welcome', {
|
|
573
|
+
subject: 'Welcome',
|
|
574
|
+
schema: z.object({
|
|
575
|
+
name: z.string(),
|
|
576
|
+
email: z.string().email(),
|
|
577
|
+
}),
|
|
578
|
+
render: (props) => <Html>...</Html>,
|
|
579
|
+
})
|
|
580
|
+
.build()
|
|
581
|
+
|
|
582
|
+
// Type inference works!
|
|
583
|
+
type Templates = typeof mail.$Infer.Templates // 'welcome'
|
|
584
|
+
type WelcomePayload = typeof mail.$Infer.Payloads['welcome'] // { name: string; email: string }
|
|
585
|
+
type SendInput = typeof mail.$Infer.SendInput // Union of all send params
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
## Best Practices
|
|
589
|
+
|
|
590
|
+
1. **Centralize Templates** - Define all templates in one place for consistency
|
|
591
|
+
2. **Use Schema Validation** - Always provide schemas for runtime safety
|
|
592
|
+
3. **Leverage Hooks** - Use hooks for logging, analytics, and error tracking
|
|
593
|
+
4. **Queue Heavy Loads** - Use queue integration for high-volume sending
|
|
594
|
+
5. **Test Adapter First** - Use TestAdapter in unit tests before deploying
|
|
595
|
+
6. **Environment Variables** - Store API keys and secrets in environment variables
|
|
596
|
+
7. **Preview Emails** - Use React Email's preview feature during development
|
|
597
|
+
|
|
598
|
+
## Examples
|
|
599
|
+
|
|
600
|
+
### Password Reset Email
|
|
601
|
+
|
|
602
|
+
```tsx
|
|
603
|
+
import { Button, Html, Text } from '@react-email/components'
|
|
604
|
+
import { IgniterMail } from '@igniter-js/mail'
|
|
605
|
+
import { z } from 'zod'
|
|
606
|
+
|
|
607
|
+
const mail = IgniterMail.create()
|
|
608
|
+
.withFrom('security@example.com')
|
|
609
|
+
.withAdapter('resend', process.env.RESEND_API_KEY!)
|
|
610
|
+
.addTemplate('resetPassword', {
|
|
611
|
+
subject: 'Reset Your Password',
|
|
612
|
+
schema: z.object({
|
|
613
|
+
name: z.string(),
|
|
614
|
+
resetLink: z.string().url(),
|
|
615
|
+
expiresIn: z.number(),
|
|
616
|
+
}),
|
|
617
|
+
render: ({ name, resetLink, expiresIn }) => (
|
|
618
|
+
<Html>
|
|
619
|
+
<Text>Hi {name},</Text>
|
|
620
|
+
<Text>You requested to reset your password.</Text>
|
|
621
|
+
<Button href={resetLink}>Reset Password</Button>
|
|
622
|
+
<Text>This link expires in {expiresIn} minutes.</Text>
|
|
623
|
+
</Html>
|
|
624
|
+
),
|
|
625
|
+
})
|
|
626
|
+
.build()
|
|
627
|
+
|
|
628
|
+
// Usage
|
|
629
|
+
await mail.send({
|
|
630
|
+
to: 'user@example.com',
|
|
631
|
+
template: 'resetPassword',
|
|
632
|
+
data: {
|
|
633
|
+
name: 'John Doe',
|
|
634
|
+
resetLink: 'https://example.com/reset?token=abc',
|
|
635
|
+
expiresIn: 15,
|
|
636
|
+
},
|
|
637
|
+
})
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
### Order Confirmation with Queue
|
|
641
|
+
|
|
642
|
+
```typescript
|
|
643
|
+
import { IgniterMail } from '@igniter-js/mail'
|
|
644
|
+
import { createBullMQAdapter } from '@igniter-js/adapter-bullmq'
|
|
645
|
+
|
|
646
|
+
const queueAdapter = createBullMQAdapter({ ... })
|
|
647
|
+
|
|
648
|
+
const mail = IgniterMail.create()
|
|
649
|
+
.withFrom('orders@example.com')
|
|
650
|
+
.withAdapter('postmark', process.env.POSTMARK_TOKEN!)
|
|
651
|
+
.withQueue(queueAdapter, {
|
|
652
|
+
namespace: 'mail',
|
|
653
|
+
task: 'send',
|
|
654
|
+
attempts: 5,
|
|
655
|
+
priority: 10,
|
|
656
|
+
})
|
|
657
|
+
.addTemplate('orderConfirmation', { ... })
|
|
658
|
+
.build()
|
|
659
|
+
|
|
660
|
+
// Sends via queue
|
|
661
|
+
await mail.send({
|
|
662
|
+
to: 'customer@example.com',
|
|
663
|
+
template: 'orderConfirmation',
|
|
664
|
+
data: { orderNumber: '12345', items: [...] },
|
|
665
|
+
})
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
## Contributing
|
|
669
|
+
|
|
670
|
+
Contributions are welcome! Please see the main [CONTRIBUTING.md](https://github.com/felipebarcelospro/igniter-js/blob/main/CONTRIBUTING.md) for details.
|
|
671
|
+
|
|
672
|
+
## License
|
|
673
|
+
|
|
674
|
+
MIT License - see [LICENSE](https://github.com/felipebarcelospro/igniter-js/blob/main/LICENSE) for details.
|
|
675
|
+
|
|
676
|
+
## Links
|
|
677
|
+
|
|
678
|
+
- **Documentation:** https://igniterjs.com/docs/mail
|
|
679
|
+
- **GitHub:** https://github.com/felipebarcelospro/igniter-js
|
|
680
|
+
- **NPM:** https://www.npmjs.com/package/@igniter-js/mail
|
|
681
|
+
- **Issues:** https://github.com/felipebarcelospro/igniter-js/issues
|
|
682
|
+
- **React Email:** https://react.email
|