@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/README.md ADDED
@@ -0,0 +1,682 @@
1
+ # @igniter-js/mail
2
+
3
+ [![NPM Version](https://img.shields.io/npm/v/@igniter-js/mail.svg)](https://www.npmjs.com/package/@igniter-js/mail)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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