@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/dist/index.mjs ADDED
@@ -0,0 +1,856 @@
1
+ import { IgniterError } from '@igniter-js/core';
2
+ import { Resend } from 'resend';
3
+ import nodemailer from 'nodemailer';
4
+ import React from 'react';
5
+ import { render } from '@react-email/components';
6
+
7
+ // src/errors/igniter-mail.error.ts
8
+ var IgniterMailError = class _IgniterMailError extends IgniterError {
9
+ constructor(payload) {
10
+ super({
11
+ code: payload.code,
12
+ message: payload.message,
13
+ statusCode: payload.statusCode,
14
+ causer: "@igniter-js/mail",
15
+ details: payload.details,
16
+ metadata: payload.metadata,
17
+ logger: payload.logger
18
+ });
19
+ this.name = "IgniterMailError";
20
+ }
21
+ /**
22
+ * Type guard utility.
23
+ */
24
+ static is(error) {
25
+ return error instanceof _IgniterMailError;
26
+ }
27
+ };
28
+
29
+ // src/adapters/postmark.adapter.ts
30
+ var PostmarkMailAdapterBuilder = class _PostmarkMailAdapterBuilder {
31
+ /** Creates a new builder instance. */
32
+ static create() {
33
+ return new _PostmarkMailAdapterBuilder();
34
+ }
35
+ /** Sets the Postmark Server Token. */
36
+ withSecret(secret) {
37
+ this.secret = secret;
38
+ return this;
39
+ }
40
+ /** Sets the default FROM address used when sending emails via Postmark. */
41
+ withFrom(from) {
42
+ this.from = from;
43
+ return this;
44
+ }
45
+ /** Builds the adapter instance. */
46
+ build() {
47
+ if (!this.secret) {
48
+ throw new IgniterMailError({
49
+ code: "MAIL_ADAPTER_CONFIGURATION_INVALID",
50
+ message: "Postmark adapter secret is required"
51
+ });
52
+ }
53
+ if (!this.from) {
54
+ throw new IgniterMailError({
55
+ code: "MAIL_ADAPTER_CONFIGURATION_INVALID",
56
+ message: "Postmark adapter from is required"
57
+ });
58
+ }
59
+ const token = this.secret;
60
+ const from = this.from;
61
+ return {
62
+ /** Sends an email using Postmark (HTTP API). */
63
+ send: async ({ to, subject, html, text }) => {
64
+ const response = await fetch("https://api.postmarkapp.com/email", {
65
+ method: "POST",
66
+ headers: {
67
+ Accept: "application/json",
68
+ "Content-Type": "application/json",
69
+ "X-Postmark-Server-Token": token
70
+ },
71
+ body: JSON.stringify({
72
+ From: from,
73
+ To: to,
74
+ Subject: subject,
75
+ HtmlBody: html,
76
+ TextBody: text
77
+ })
78
+ });
79
+ if (!response.ok) {
80
+ const body = await response.text().catch(() => "");
81
+ throw new IgniterMailError({
82
+ code: "MAIL_PROVIDER_SEND_FAILED",
83
+ message: "Postmark send failed",
84
+ metadata: {
85
+ status: response.status,
86
+ body
87
+ }
88
+ });
89
+ }
90
+ }
91
+ };
92
+ }
93
+ };
94
+ var postmarkAdapter = (options) => PostmarkMailAdapterBuilder.create().withSecret(options.secret).withFrom(options.from).build();
95
+ var ResendMailAdapterBuilder = class _ResendMailAdapterBuilder {
96
+ /** Creates a new builder instance. */
97
+ static create() {
98
+ return new _ResendMailAdapterBuilder();
99
+ }
100
+ /** Sets the Resend API key. */
101
+ withSecret(secret) {
102
+ this.secret = secret;
103
+ return this;
104
+ }
105
+ /** Sets the default FROM address used when sending emails via Resend. */
106
+ withFrom(from) {
107
+ this.from = from;
108
+ return this;
109
+ }
110
+ /** Builds the adapter instance. */
111
+ build() {
112
+ if (!this.secret) {
113
+ throw new IgniterMailError({
114
+ code: "MAIL_ADAPTER_CONFIGURATION_INVALID",
115
+ message: "Resend adapter secret is required"
116
+ });
117
+ }
118
+ if (!this.from) {
119
+ throw new IgniterMailError({
120
+ code: "MAIL_ADAPTER_CONFIGURATION_INVALID",
121
+ message: "Resend adapter from is required"
122
+ });
123
+ }
124
+ const resend = new Resend(this.secret);
125
+ const from = this.from;
126
+ return {
127
+ /**
128
+ * Sends an email using Resend.
129
+ *
130
+ * Note: Resend accepts `scheduledAt` as an ISO string.
131
+ */
132
+ send: async ({ to, subject, html, text, scheduledAt }) => {
133
+ await resend.emails.create({
134
+ to,
135
+ from,
136
+ subject,
137
+ html,
138
+ text,
139
+ scheduledAt: scheduledAt?.toISOString()
140
+ });
141
+ }
142
+ };
143
+ }
144
+ };
145
+ var resendAdapter = (options) => ResendMailAdapterBuilder.create().withSecret(options.secret).withFrom(options.from).build();
146
+
147
+ // src/adapters/sendgrid.adapter.ts
148
+ var SendGridMailAdapterBuilder = class _SendGridMailAdapterBuilder {
149
+ /** Creates a new builder instance. */
150
+ static create() {
151
+ return new _SendGridMailAdapterBuilder();
152
+ }
153
+ /** Sets the SendGrid API key. */
154
+ withSecret(secret) {
155
+ this.secret = secret;
156
+ return this;
157
+ }
158
+ /** Sets the default FROM address used when sending emails via SendGrid. */
159
+ withFrom(from) {
160
+ this.from = from;
161
+ return this;
162
+ }
163
+ /** Builds the adapter instance. */
164
+ build() {
165
+ if (!this.secret) {
166
+ throw new IgniterMailError({
167
+ code: "MAIL_ADAPTER_CONFIGURATION_INVALID",
168
+ message: "SendGrid adapter secret is required"
169
+ });
170
+ }
171
+ if (!this.from) {
172
+ throw new IgniterMailError({
173
+ code: "MAIL_ADAPTER_CONFIGURATION_INVALID",
174
+ message: "SendGrid adapter from is required"
175
+ });
176
+ }
177
+ const apiKey = this.secret;
178
+ const from = this.from;
179
+ return {
180
+ /** Sends an email using SendGrid (HTTP API). */
181
+ send: async ({ to, subject, html, text }) => {
182
+ const response = await fetch("https://api.sendgrid.com/v3/mail/send", {
183
+ method: "POST",
184
+ headers: {
185
+ Authorization: `Bearer ${apiKey}`,
186
+ "Content-Type": "application/json"
187
+ },
188
+ body: JSON.stringify({
189
+ personalizations: [{
190
+ to: [{ email: to }]
191
+ }],
192
+ from: { email: from },
193
+ subject,
194
+ content: [
195
+ { type: "text/plain", value: text },
196
+ { type: "text/html", value: html }
197
+ ]
198
+ })
199
+ });
200
+ if (!response.ok) {
201
+ const body = await response.text().catch(() => "");
202
+ throw new IgniterMailError({
203
+ code: "MAIL_PROVIDER_SEND_FAILED",
204
+ message: "SendGrid send failed",
205
+ metadata: {
206
+ status: response.status,
207
+ body
208
+ }
209
+ });
210
+ }
211
+ }
212
+ };
213
+ }
214
+ };
215
+ var sendgridAdapter = (options) => SendGridMailAdapterBuilder.create().withSecret(options.secret).withFrom(options.from).build();
216
+ var SmtpMailAdapterBuilder = class _SmtpMailAdapterBuilder {
217
+ /** Creates a new builder instance. */
218
+ static create() {
219
+ return new _SmtpMailAdapterBuilder();
220
+ }
221
+ /** Sets the SMTP connection URL (e.g. `smtps://user:pass@host:port`). */
222
+ withSecret(secret) {
223
+ this.secret = secret;
224
+ return this;
225
+ }
226
+ /** Sets the default FROM address used when sending emails via SMTP. */
227
+ withFrom(from) {
228
+ this.from = from;
229
+ return this;
230
+ }
231
+ /** Builds the adapter instance. */
232
+ build() {
233
+ if (!this.secret) {
234
+ throw new IgniterMailError({
235
+ code: "MAIL_ADAPTER_CONFIGURATION_INVALID",
236
+ message: "SMTP adapter secret is required"
237
+ });
238
+ }
239
+ if (!this.from) {
240
+ throw new IgniterMailError({
241
+ code: "MAIL_ADAPTER_CONFIGURATION_INVALID",
242
+ message: "SMTP adapter from is required"
243
+ });
244
+ }
245
+ const smtpUrl = this.secret;
246
+ const from = this.from;
247
+ const createTransporter = () => {
248
+ return nodemailer.createTransport(smtpUrl, {
249
+ tls: {
250
+ rejectUnauthorized: false
251
+ },
252
+ connectionTimeout: 1e4,
253
+ greetingTimeout: 5e3,
254
+ socketTimeout: 1e4
255
+ });
256
+ };
257
+ return {
258
+ /** Sends an email using Nodemailer over SMTP. */
259
+ send: async ({ to, subject, html, text }) => {
260
+ const transport = createTransporter();
261
+ const mailOptions = {
262
+ from,
263
+ to,
264
+ subject,
265
+ html,
266
+ text
267
+ };
268
+ try {
269
+ await transport.sendMail(mailOptions);
270
+ transport.close();
271
+ } catch (error) {
272
+ transport.close();
273
+ throw error;
274
+ }
275
+ }
276
+ };
277
+ }
278
+ };
279
+ var smtpAdapter = (options) => SmtpMailAdapterBuilder.create().withSecret(options.secret).withFrom(options.from).build();
280
+
281
+ // src/adapters/test.adapter.ts
282
+ function createTestMailAdapter(options = {}) {
283
+ const sent = [];
284
+ const logger = options.logger ?? console;
285
+ const silent = options.silent ?? false;
286
+ return {
287
+ sent,
288
+ reset: () => {
289
+ sent.length = 0;
290
+ },
291
+ last: () => sent.at(-1),
292
+ send: async (params) => {
293
+ sent.push({ ...params, at: /* @__PURE__ */ new Date() });
294
+ if (!silent) {
295
+ logger.info(
296
+ `[TestMailAdapter] to=${params.to} subject=${params.subject} html=${params.html.length}B text=${params.text.length}B`
297
+ );
298
+ }
299
+ }
300
+ };
301
+ }
302
+
303
+ // src/adapters/webhook.adapter.ts
304
+ var WebhookMailAdapterBuilder = class _WebhookMailAdapterBuilder {
305
+ /** Creates a new builder instance. */
306
+ static create() {
307
+ return new _WebhookMailAdapterBuilder();
308
+ }
309
+ /**
310
+ * Sets the webhook URL.
311
+ *
312
+ * Note: when using `IgniterMailBuilder.withAdapter('webhook', secret)`, the `secret`
313
+ * is treated as the webhook URL.
314
+ */
315
+ withUrl(url) {
316
+ this.url = url;
317
+ return this;
318
+ }
319
+ /** Sets the default FROM address. */
320
+ withFrom(from) {
321
+ this.from = from;
322
+ return this;
323
+ }
324
+ /** Builds the adapter instance. */
325
+ build() {
326
+ if (!this.url) {
327
+ throw new IgniterMailError({
328
+ code: "MAIL_ADAPTER_CONFIGURATION_INVALID",
329
+ message: "Webhook adapter url is required"
330
+ });
331
+ }
332
+ if (!this.from) {
333
+ throw new IgniterMailError({
334
+ code: "MAIL_ADAPTER_CONFIGURATION_INVALID",
335
+ message: "Webhook adapter from is required"
336
+ });
337
+ }
338
+ const url = this.url;
339
+ const from = this.from;
340
+ return {
341
+ /** Sends an email by POST-ing to the configured webhook URL. */
342
+ send: async ({ to, subject, html, text, scheduledAt }) => {
343
+ const response = await fetch(url, {
344
+ method: "POST",
345
+ headers: {
346
+ "Content-Type": "application/json"
347
+ },
348
+ body: JSON.stringify({
349
+ to,
350
+ from,
351
+ subject,
352
+ html,
353
+ text,
354
+ scheduledAt: scheduledAt?.toISOString()
355
+ })
356
+ });
357
+ if (!response.ok) {
358
+ const body = await response.text().catch(() => "");
359
+ throw new IgniterMailError({
360
+ code: "MAIL_PROVIDER_SEND_FAILED",
361
+ message: "Webhook send failed",
362
+ metadata: {
363
+ status: response.status,
364
+ body
365
+ }
366
+ });
367
+ }
368
+ }
369
+ };
370
+ }
371
+ };
372
+ var webhookAdapter = (options) => WebhookMailAdapterBuilder.create().withUrl(options.secret).withFrom(options.from).build();
373
+
374
+ // src/builders/igniter-mail.builder.ts
375
+ var IgniterMailBuilder = class _IgniterMailBuilder {
376
+ constructor(factory) {
377
+ this.templates = {};
378
+ this.factory = factory;
379
+ }
380
+ /**
381
+ * Creates a new builder.
382
+ */
383
+ static create(factory) {
384
+ return new _IgniterMailBuilder(factory);
385
+ }
386
+ /** Sets the default FROM address. */
387
+ withFrom(from) {
388
+ this.from = from;
389
+ return this;
390
+ }
391
+ /** Attaches a logger instance. */
392
+ withLogger(logger) {
393
+ this.logger = logger;
394
+ return this;
395
+ }
396
+ /**
397
+ * Enables queue delivery.
398
+ *
399
+ * If configured, `IgniterMail.schedule()` will enqueue jobs instead of using `setTimeout`.
400
+ */
401
+ withQueue(adapter, options) {
402
+ this.queue = { adapter, options };
403
+ return this;
404
+ }
405
+ withAdapter(adapterOrProvider, secret) {
406
+ if (typeof adapterOrProvider === "string") {
407
+ if (!secret) {
408
+ throw new IgniterMailError({
409
+ code: "MAIL_PROVIDER_ADAPTER_SECRET_REQUIRED",
410
+ message: "MAIL_PROVIDER_ADAPTER_SECRET_REQUIRED",
411
+ logger: this.logger
412
+ });
413
+ }
414
+ this.adapter = {
415
+ kind: "provider",
416
+ provider: adapterOrProvider,
417
+ secret
418
+ };
419
+ return this;
420
+ }
421
+ this.adapter = {
422
+ kind: "adapter",
423
+ adapter: adapterOrProvider
424
+ };
425
+ return this;
426
+ }
427
+ /**
428
+ * Registers a template.
429
+ */
430
+ addTemplate(key, template) {
431
+ this.templates[key] = template;
432
+ return this;
433
+ }
434
+ /** Hook invoked before sending. */
435
+ withOnSendStarted(onSendStarted) {
436
+ this.onSendStarted = onSendStarted;
437
+ return this;
438
+ }
439
+ /** Hook invoked on error. */
440
+ withOnSendError(onSendError) {
441
+ this.onSendError = onSendError;
442
+ return this;
443
+ }
444
+ /** Hook invoked on success. */
445
+ withOnSendSuccess(onSendSuccess) {
446
+ this.onSendSuccess = onSendSuccess;
447
+ return this;
448
+ }
449
+ /**
450
+ * Builds the {@link IgniterMail} instance.
451
+ */
452
+ build() {
453
+ if (!this.from) {
454
+ throw new IgniterMailError({
455
+ code: "MAIL_PROVIDER_FROM_REQUIRED",
456
+ message: "MAIL_PROVIDER_FROM_REQUIRED",
457
+ logger: this.logger
458
+ });
459
+ }
460
+ if (!this.adapter) {
461
+ throw new IgniterMailError({
462
+ code: "MAIL_PROVIDER_ADAPTER_REQUIRED",
463
+ message: "MAIL_PROVIDER_ADAPTER_REQUIRED",
464
+ logger: this.logger
465
+ });
466
+ }
467
+ const resolvedAdapter = this.adapter.kind === "adapter" ? this.adapter.adapter : (() => {
468
+ switch (this.adapter.provider) {
469
+ case "resend":
470
+ return ResendMailAdapterBuilder.create().withSecret(this.adapter.secret).withFrom(this.from).build();
471
+ case "smtp":
472
+ return SmtpMailAdapterBuilder.create().withSecret(this.adapter.secret).withFrom(this.from).build();
473
+ case "postmark":
474
+ return PostmarkMailAdapterBuilder.create().withSecret(this.adapter.secret).withFrom(this.from).build();
475
+ case "sendgrid":
476
+ return SendGridMailAdapterBuilder.create().withSecret(this.adapter.secret).withFrom(this.from).build();
477
+ case "webhook":
478
+ return WebhookMailAdapterBuilder.create().withUrl(this.adapter.secret).withFrom(this.from).build();
479
+ default:
480
+ throw new IgniterMailError({
481
+ code: "MAIL_PROVIDER_ADAPTER_NOT_FOUND",
482
+ message: `MAIL_PROVIDER_ADAPTER_NOT_FOUND: ${this.adapter.provider}`,
483
+ logger: this.logger,
484
+ metadata: {
485
+ provider: this.adapter.provider
486
+ }
487
+ });
488
+ }
489
+ })();
490
+ return this.factory({
491
+ from: this.from,
492
+ adapter: resolvedAdapter,
493
+ templates: this.templates,
494
+ onSendStarted: this.onSendStarted,
495
+ onSendError: this.onSendError,
496
+ onSendSuccess: this.onSendSuccess,
497
+ logger: this.logger,
498
+ queue: this.queue ? {
499
+ adapter: this.queue.adapter,
500
+ id: `${this.queue.options?.namespace ?? "mail"}.${this.queue.options?.task ?? "send"}`,
501
+ options: this.queue.options
502
+ } : void 0
503
+ });
504
+ }
505
+ };
506
+
507
+ // src/builders/mail-template.builder.ts
508
+ var MailTemplateBuilder = class _MailTemplateBuilder {
509
+ /** Creates a new builder instance. */
510
+ static create() {
511
+ return new _MailTemplateBuilder();
512
+ }
513
+ /** Sets the default subject for the template. */
514
+ withSubject(subject) {
515
+ this.subject = subject;
516
+ return this;
517
+ }
518
+ /** Attaches the schema used to validate and infer payload types. */
519
+ withSchema(schema) {
520
+ this.schema = schema;
521
+ return this;
522
+ }
523
+ /** Sets the React Email render function for the template. */
524
+ withRender(render2) {
525
+ this.render = render2;
526
+ return this;
527
+ }
528
+ /** Builds the template definition. */
529
+ build() {
530
+ if (!this.subject) {
531
+ throw new IgniterMailError({
532
+ code: "MAIL_TEMPLATE_CONFIGURATION_INVALID",
533
+ message: "Mail template subject is required"
534
+ });
535
+ }
536
+ if (!this.schema) {
537
+ throw new IgniterMailError({
538
+ code: "MAIL_TEMPLATE_CONFIGURATION_INVALID",
539
+ message: "Mail template schema is required"
540
+ });
541
+ }
542
+ if (!this.render) {
543
+ throw new IgniterMailError({
544
+ code: "MAIL_TEMPLATE_CONFIGURATION_INVALID",
545
+ message: "Mail template render is required"
546
+ });
547
+ }
548
+ return {
549
+ subject: this.subject,
550
+ schema: this.schema,
551
+ render: this.render
552
+ };
553
+ }
554
+ };
555
+
556
+ // src/utils/validate-standard-schema-input.ts
557
+ async function validateStandardSchemaInput(schema, input) {
558
+ const standard = schema?.["~standard"];
559
+ if (!standard?.validate) {
560
+ return input;
561
+ }
562
+ const result = await standard.validate(input);
563
+ if (result?.issues?.length) {
564
+ throw new IgniterMailError({
565
+ code: "MAIL_PROVIDER_TEMPLATE_DATA_INVALID",
566
+ message: "Invalid mail template payload",
567
+ statusCode: 400,
568
+ details: result.issues
569
+ });
570
+ }
571
+ return result?.value ?? input;
572
+ }
573
+
574
+ // src/core/igniter-mail.tsx
575
+ var _IgniterMail = class _IgniterMail {
576
+ constructor(options) {
577
+ this.queueJobRegistered = false;
578
+ /**
579
+ * Type inference helper.
580
+ * Access via `typeof mail.$Infer` (type-level only).
581
+ */
582
+ this.$Infer = void 0;
583
+ const { adapter, templates, logger, queue, ...rest } = options;
584
+ if (!adapter) {
585
+ throw new IgniterMailError({
586
+ code: "MAIL_PROVIDER_ADAPTER_REQUIRED",
587
+ message: "MAIL_PROVIDER_ADAPTER_REQUIRED",
588
+ logger
589
+ });
590
+ }
591
+ if (!templates) {
592
+ throw new IgniterMailError({
593
+ code: "MAIL_PROVIDER_TEMPLATES_REQUIRED",
594
+ message: "MAIL_PROVIDER_TEMPLATES_REQUIRED",
595
+ logger
596
+ });
597
+ }
598
+ this.adapter = adapter;
599
+ this.templates = templates;
600
+ this.logger = logger;
601
+ this.queue = queue;
602
+ this.options = rest;
603
+ }
604
+ async ensureQueueJobRegistered() {
605
+ const queue = this.queue;
606
+ if (!queue) return;
607
+ if (this.queueJobRegistered) return;
608
+ if (this.queueJobRegistering) {
609
+ await this.queueJobRegistering;
610
+ return;
611
+ }
612
+ this.queueJobRegistering = (async () => {
613
+ const queueOptions = queue.options;
614
+ const name = queueOptions?.name ?? "send";
615
+ const passthroughSchema = {
616
+ "~standard": {
617
+ vendor: "@igniter-js/mail",
618
+ version: 1,
619
+ validate: async (value) => ({ value })
620
+ }
621
+ };
622
+ const definition = queue.adapter.register({
623
+ name,
624
+ input: passthroughSchema,
625
+ handler: async ({ input }) => {
626
+ await this.send(input);
627
+ },
628
+ queue: queueOptions?.queue,
629
+ attempts: queueOptions?.attempts,
630
+ priority: queueOptions?.priority,
631
+ removeOnComplete: queueOptions?.removeOnComplete,
632
+ removeOnFail: queueOptions?.removeOnFail,
633
+ metadata: queueOptions?.metadata,
634
+ limiter: queueOptions?.limiter
635
+ });
636
+ await queue.adapter.bulkRegister({
637
+ [queue.id]: definition
638
+ });
639
+ this.queueJobRegistered = true;
640
+ })();
641
+ try {
642
+ await this.queueJobRegistering;
643
+ } finally {
644
+ this.queueJobRegistering = void 0;
645
+ }
646
+ }
647
+ async validateTemplateData(template, data) {
648
+ try {
649
+ return await validateStandardSchemaInput(
650
+ template.schema,
651
+ data
652
+ );
653
+ } catch (error) {
654
+ const normalizedError = IgniterMailError.is(error) ? error : new IgniterMailError({
655
+ code: "MAIL_PROVIDER_TEMPLATE_DATA_INVALID",
656
+ message: "MAIL_PROVIDER_TEMPLATE_DATA_INVALID",
657
+ cause: error,
658
+ logger: this.logger,
659
+ metadata: { subject: String(template.subject) }
660
+ });
661
+ throw normalizedError;
662
+ }
663
+ }
664
+ /**
665
+ * Sends an email immediately.
666
+ */
667
+ async send(params) {
668
+ try {
669
+ this.logger?.debug("IgniterMail.send started", {
670
+ to: params.to,
671
+ template: String(params.template)
672
+ });
673
+ await this.onSendStarted(params);
674
+ const template = this.templates[params.template];
675
+ if (!template) {
676
+ throw new IgniterMailError({
677
+ code: "MAIL_PROVIDER_TEMPLATE_NOT_FOUND",
678
+ message: "MAIL_PROVIDER_TEMPLATE_NOT_FOUND",
679
+ logger: this.logger,
680
+ metadata: {
681
+ template: String(params.template)
682
+ }
683
+ });
684
+ }
685
+ const validatedData = await this.validateTemplateData(
686
+ template,
687
+ params.data
688
+ );
689
+ const MailTemplate = template.render;
690
+ const html = await render(/* @__PURE__ */ React.createElement(MailTemplate, { ...validatedData }));
691
+ const text = await render(/* @__PURE__ */ React.createElement(MailTemplate, { ...validatedData }), {
692
+ plainText: true
693
+ });
694
+ await this.adapter.send({
695
+ to: params.to,
696
+ subject: params.subject || template.subject,
697
+ html,
698
+ text
699
+ });
700
+ await this.onSendSuccess(params);
701
+ this.logger?.info("IgniterMail.send success", {
702
+ to: params.to,
703
+ template: String(params.template)
704
+ });
705
+ } catch (error) {
706
+ const normalizedError = IgniterMailError.is(error) ? error : new IgniterMailError({
707
+ code: "MAIL_PROVIDER_SEND_FAILED",
708
+ message: "MAIL_PROVIDER_SEND_FAILED",
709
+ cause: error,
710
+ logger: this.logger,
711
+ metadata: {
712
+ to: params.to,
713
+ template: String(params.template)
714
+ }
715
+ });
716
+ this.logger?.error("IgniterMail.send failed", normalizedError);
717
+ await this.onSendError(params, normalizedError);
718
+ throw normalizedError;
719
+ }
720
+ }
721
+ /**
722
+ * Schedules an email for a future date.
723
+ *
724
+ * If a queue is configured, this method enqueues a job.
725
+ * Otherwise, it uses a best-effort `setTimeout`.
726
+ */
727
+ async schedule(params, date) {
728
+ if (date.getTime() <= Date.now()) {
729
+ throw new IgniterMailError({
730
+ code: "MAIL_PROVIDER_SCHEDULE_DATE_INVALID",
731
+ message: "MAIL_PROVIDER_SCHEDULE_DATE_INVALID",
732
+ logger: this.logger
733
+ });
734
+ }
735
+ if (this.queue) {
736
+ try {
737
+ await this.ensureQueueJobRegistered();
738
+ const delay = Math.max(0, date.getTime() - Date.now());
739
+ this.logger?.info("IgniterMail.schedule enqueued", {
740
+ id: this.queue.id,
741
+ to: params.to,
742
+ template: String(params.template),
743
+ delay
744
+ });
745
+ await this.queue.adapter.invoke({
746
+ id: this.queue.id,
747
+ input: params,
748
+ delay
749
+ });
750
+ return;
751
+ } catch (error) {
752
+ const normalizedError = IgniterMailError.is(error) ? error : new IgniterMailError({
753
+ code: "MAIL_PROVIDER_SCHEDULE_FAILED",
754
+ message: "MAIL_PROVIDER_SCHEDULE_FAILED",
755
+ cause: error,
756
+ logger: this.logger,
757
+ metadata: {
758
+ to: params.to,
759
+ template: String(params.template),
760
+ date: date.toISOString()
761
+ }
762
+ });
763
+ this.logger?.error("IgniterMail.schedule failed", normalizedError);
764
+ throw normalizedError;
765
+ }
766
+ }
767
+ const timeout = date.getTime() - Date.now();
768
+ setTimeout(() => {
769
+ this.send(params).catch((error) => {
770
+ console.error("Failed to send scheduled email:", error);
771
+ });
772
+ }, timeout);
773
+ }
774
+ async onSendStarted(params) {
775
+ await this.options.onSendStarted?.(params);
776
+ }
777
+ async onSendError(params, error) {
778
+ await this.options.onSendError?.(params, error);
779
+ }
780
+ async onSendSuccess(params) {
781
+ await this.options.onSendSuccess?.(params);
782
+ }
783
+ };
784
+ /** Helper to declare adapter factories. */
785
+ _IgniterMail.adapter = (adapter) => adapter;
786
+ /** Helper to declare templates with inferred payload types. */
787
+ _IgniterMail.template = (template) => template;
788
+ /**
789
+ * Creates a new builder instance.
790
+ */
791
+ _IgniterMail.create = () => IgniterMailBuilder.create((options) => new _IgniterMail(options));
792
+ /**
793
+ * Initializes (singleton) instance.
794
+ *
795
+ * Prefer using {@link IgniterMail.create} for new code.
796
+ */
797
+ _IgniterMail.initialize = (options) => {
798
+ if (_IgniterMail.instance) {
799
+ return _IgniterMail.instance;
800
+ }
801
+ const adapter = options.adapter;
802
+ if (typeof adapter === "function") {
803
+ const legacyOptions = options;
804
+ _IgniterMail.instance = new _IgniterMail({
805
+ from: legacyOptions.from,
806
+ templates: legacyOptions.templates,
807
+ adapter: legacyOptions.adapter(legacyOptions),
808
+ onSendStarted: legacyOptions.onSendStarted,
809
+ onSendError: legacyOptions.onSendError,
810
+ onSendSuccess: legacyOptions.onSendSuccess
811
+ });
812
+ return _IgniterMail.instance;
813
+ }
814
+ _IgniterMail.instance = new _IgniterMail(
815
+ options
816
+ );
817
+ return _IgniterMail.instance;
818
+ };
819
+ var IgniterMail = _IgniterMail;
820
+
821
+ // src/types/provider.ts
822
+ function createPassthroughSchema() {
823
+ return {
824
+ "~standard": {
825
+ vendor: "@igniter-js/mail",
826
+ version: 1,
827
+ validate: async (value) => ({ value })
828
+ }
829
+ };
830
+ }
831
+
832
+ // src/utils/get-adapter.ts
833
+ var getAdapter = (adapter) => {
834
+ switch (adapter) {
835
+ case "resend":
836
+ return resendAdapter;
837
+ case "smtp":
838
+ return smtpAdapter;
839
+ case "postmark":
840
+ return postmarkAdapter;
841
+ case "sendgrid":
842
+ return sendgridAdapter;
843
+ case "webhook":
844
+ return webhookAdapter;
845
+ default:
846
+ throw new IgniterMailError({
847
+ code: "MAIL_PROVIDER_ADAPTER_NOT_FOUND",
848
+ message: `MAIL_PROVIDER_ADAPTER_NOT_FOUND: ${adapter}`,
849
+ metadata: { adapter }
850
+ });
851
+ }
852
+ };
853
+
854
+ export { IgniterMail, IgniterMailBuilder, IgniterMailError, MailTemplateBuilder, PostmarkMailAdapterBuilder, ResendMailAdapterBuilder, SendGridMailAdapterBuilder, SmtpMailAdapterBuilder, WebhookMailAdapterBuilder, createPassthroughSchema, createTestMailAdapter, getAdapter, postmarkAdapter, resendAdapter, sendgridAdapter, smtpAdapter, validateStandardSchemaInput, webhookAdapter };
855
+ //# sourceMappingURL=index.mjs.map
856
+ //# sourceMappingURL=index.mjs.map