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