@gravito/signal 3.0.3 → 3.0.4

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.
Files changed (104) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +89 -60
  3. package/README.zh-TW.md +140 -9
  4. package/dist/MjmlRenderer-IUH663FT.mjs +8 -0
  5. package/dist/ReactMjmlRenderer-C3P5YO5L.mjs +8 -0
  6. package/dist/ReactRenderer-2JFLRVST.mjs +45 -0
  7. package/dist/{ReactRenderer-L5INVYKT.mjs → ReactRenderer-LYEOSYFS.mjs} +9 -8
  8. package/dist/ReactRenderer-V54CUUEI.mjs +45 -0
  9. package/dist/VueMjmlRenderer-4F4CXHDB.mjs +8 -0
  10. package/dist/VueMjmlRenderer-5WZR4CQG.mjs +8 -0
  11. package/dist/VueMjmlRenderer-U5YMWI44.mjs +8 -0
  12. package/dist/VueRenderer-3YBRQXME.mjs +48 -0
  13. package/dist/VueRenderer-46JGXTJ2.mjs +48 -0
  14. package/dist/VueRenderer-5KWD4R3C.mjs +48 -0
  15. package/dist/VueRenderer-C23U4O5E.mjs +48 -0
  16. package/dist/VueRenderer-LEVDFLHP.mjs +31 -0
  17. package/dist/VueRenderer-RNHSCCRI.mjs +48 -0
  18. package/dist/chunk-3WOR3XSL.mjs +82 -0
  19. package/dist/chunk-DBFIVHHG.mjs +79 -0
  20. package/dist/{chunk-6DZX6EAA.mjs → chunk-HEBXNMVQ.mjs} +12 -1
  21. package/dist/chunk-KB7IDDBT.mjs +82 -0
  22. package/dist/chunk-LZL5UUPC.mjs +82 -0
  23. package/dist/chunk-W6LXIJKK.mjs +57 -0
  24. package/dist/chunk-XBIVBJS2.mjs +8 -0
  25. package/dist/index.d.mts +1680 -209
  26. package/dist/index.d.ts +1680 -209
  27. package/dist/index.js +69405 -542
  28. package/dist/index.mjs +993 -110
  29. package/dist/lib-HJTRWKU5.mjs +67788 -0
  30. package/dist/{VueRenderer-Z5PRVBNH.mjs → server-renderer-4IM3P5XZ.mjs} +308 -423
  31. package/dist/server-renderer-7KWFSTPV.mjs +37193 -0
  32. package/dist/{VueRenderer-S65ZARRI.mjs → server-renderer-S5FPSTJ2.mjs} +931 -877
  33. package/dist/server-renderer-X5LUFVWT.mjs +37193 -0
  34. package/doc/OPTIMIZATION_PLAN.md +496 -0
  35. package/package.json +14 -12
  36. package/scripts/check-coverage.ts +64 -0
  37. package/src/Mailable.ts +340 -44
  38. package/src/OrbitSignal.ts +350 -50
  39. package/src/TypedMailable.ts +96 -0
  40. package/src/dev/DevMailbox.ts +89 -33
  41. package/src/dev/DevServer.ts +14 -14
  42. package/src/dev/storage/FileMailboxStorage.ts +66 -0
  43. package/src/dev/storage/MailboxStorage.ts +15 -0
  44. package/src/dev/storage/MemoryMailboxStorage.ts +36 -0
  45. package/src/dev/ui/mailbox.ts +1 -1
  46. package/src/dev/ui/preview.ts +4 -4
  47. package/src/errors.ts +69 -0
  48. package/src/events.ts +72 -0
  49. package/src/index.ts +20 -1
  50. package/src/renderers/HtmlRenderer.ts +20 -18
  51. package/src/renderers/MjmlRenderer.ts +73 -0
  52. package/src/renderers/ReactMjmlRenderer.ts +94 -0
  53. package/src/renderers/ReactRenderer.ts +26 -21
  54. package/src/renderers/Renderer.ts +43 -3
  55. package/src/renderers/TemplateRenderer.ts +48 -15
  56. package/src/renderers/VueMjmlRenderer.ts +99 -0
  57. package/src/renderers/VueRenderer.ts +26 -21
  58. package/src/renderers/mjml-templates.ts +50 -0
  59. package/src/transports/BaseTransport.ts +148 -0
  60. package/src/transports/LogTransport.ts +28 -6
  61. package/src/transports/MemoryTransport.ts +34 -6
  62. package/src/transports/SesTransport.ts +62 -17
  63. package/src/transports/SmtpTransport.ts +123 -27
  64. package/src/transports/Transport.ts +33 -4
  65. package/src/types.ts +172 -3
  66. package/src/utils/html.ts +43 -0
  67. package/src/webhooks/SendGridWebhookDriver.ts +80 -0
  68. package/src/webhooks/SesWebhookDriver.ts +44 -0
  69. package/tests/DevMailbox.test.ts +54 -0
  70. package/tests/FileMailboxStorage.test.ts +56 -0
  71. package/tests/MjmlLayout.test.ts +28 -0
  72. package/tests/MjmlRenderer.test.ts +53 -0
  73. package/tests/OrbitSignalWebhook.test.ts +56 -0
  74. package/tests/ReactMjmlRenderer.test.ts +33 -0
  75. package/tests/SendGridWebhookDriver.test.ts +69 -0
  76. package/tests/SesWebhookDriver.test.ts +46 -0
  77. package/tests/VueMjmlRenderer.test.ts +35 -0
  78. package/tests/dev-server.test.ts +1 -1
  79. package/tests/transports.test.ts +3 -3
  80. package/tsconfig.json +12 -24
  81. package/dist/OrbitMail-2Z7ZTKYA.mjs +0 -7
  82. package/dist/OrbitMail-BGV32HWN.mjs +0 -7
  83. package/dist/OrbitMail-FUYZQSAV.mjs +0 -7
  84. package/dist/OrbitMail-NAPCRK7B.mjs +0 -7
  85. package/dist/OrbitMail-REGJ276B.mjs +0 -7
  86. package/dist/OrbitMail-TCFBJWDT.mjs +0 -7
  87. package/dist/OrbitMail-XZZW6U4N.mjs +0 -7
  88. package/dist/OrbitSignal-IPSA2CDO.mjs +0 -7
  89. package/dist/OrbitSignal-MABW4DDW.mjs +0 -7
  90. package/dist/OrbitSignal-QSW5VQ5M.mjs +0 -7
  91. package/dist/OrbitSignal-R22QHWAA.mjs +0 -7
  92. package/dist/OrbitSignal-ZKKMEC27.mjs +0 -7
  93. package/dist/chunk-3U2CYJO5.mjs +0 -367
  94. package/dist/chunk-3XFC4T6M.mjs +0 -392
  95. package/dist/chunk-456QRYFW.mjs +0 -401
  96. package/dist/chunk-DT3R2TNV.mjs +0 -367
  97. package/dist/chunk-F6MVTUCT.mjs +0 -421
  98. package/dist/chunk-GADWIVC4.mjs +0 -400
  99. package/dist/chunk-HHKFAMSE.mjs +0 -380
  100. package/dist/chunk-NEQCQSZI.mjs +0 -406
  101. package/dist/chunk-OKRNL6PN.mjs +0 -400
  102. package/dist/chunk-ULN3GMY2.mjs +0 -367
  103. package/dist/chunk-XAWO7RSP.mjs +0 -398
  104. package/dist/chunk-YLVDJSED.mjs +0 -431
package/dist/index.mjs CHANGED
@@ -1,11 +1,69 @@
1
- import "./chunk-EBO3CZXG.mjs";
1
+ import {
2
+ MjmlRenderer
3
+ } from "./chunk-W6LXIJKK.mjs";
4
+ import {
5
+ ReactMjmlRenderer
6
+ } from "./chunk-DBFIVHHG.mjs";
7
+ import {
8
+ VueMjmlRenderer
9
+ } from "./chunk-KB7IDDBT.mjs";
10
+ import {
11
+ stripHtml
12
+ } from "./chunk-XBIVBJS2.mjs";
13
+ import "./chunk-HEBXNMVQ.mjs";
2
14
 
3
15
  // src/dev/DevMailbox.ts
4
16
  import { randomUUID } from "crypto";
5
- var DevMailbox = class {
17
+
18
+ // src/dev/storage/MemoryMailboxStorage.ts
19
+ var MemoryMailboxStorage = class {
6
20
  entries = [];
7
- maxEntries = 50;
8
- add(message) {
21
+ async all() {
22
+ return [...this.entries];
23
+ }
24
+ async push(entry) {
25
+ this.entries.unshift(entry);
26
+ }
27
+ async trim(max) {
28
+ if (this.entries.length > max) {
29
+ this.entries = this.entries.slice(0, max);
30
+ }
31
+ }
32
+ async clear() {
33
+ this.entries = [];
34
+ }
35
+ async delete(id) {
36
+ const index = this.entries.findIndex((e) => e.id === id);
37
+ if (index !== -1) {
38
+ this.entries.splice(index, 1);
39
+ return true;
40
+ }
41
+ return false;
42
+ }
43
+ };
44
+
45
+ // src/dev/DevMailbox.ts
46
+ var DevMailbox = class {
47
+ storage;
48
+ _maxEntries = 50;
49
+ /**
50
+ * Creates an instance of DevMailbox.
51
+ *
52
+ * @param maxEntries - Maximum number of emails to store (default: 50)
53
+ * @param storage - Optional custom storage engine (defaults to Memory)
54
+ */
55
+ constructor(maxEntries, storage) {
56
+ if (maxEntries !== void 0) {
57
+ this._maxEntries = maxEntries;
58
+ }
59
+ this.storage = storage ?? new MemoryMailboxStorage();
60
+ }
61
+ /**
62
+ * Adds a new message to the mailbox.
63
+ *
64
+ * If the mailbox exceeds the maximum capacity, the oldest messages are removed.
65
+ */
66
+ async add(message) {
9
67
  const entry = {
10
68
  id: randomUUID(),
11
69
  envelope: {
@@ -22,66 +80,176 @@ var DevMailbox = class {
22
80
  ...message.text ? { text: message.text } : {},
23
81
  sentAt: /* @__PURE__ */ new Date()
24
82
  };
25
- this.entries.unshift(entry);
26
- if (this.entries.length > this.maxEntries) {
27
- this.entries = this.entries.slice(0, this.maxEntries);
28
- }
83
+ await this.storage.push(entry);
84
+ await this.storage.trim(this._maxEntries);
29
85
  return entry;
30
86
  }
31
- list() {
32
- return this.entries;
87
+ /**
88
+ * Sets the maximum number of emails to store.
89
+ *
90
+ * If the current mailbox exceeds the new capacity, the oldest messages are removed.
91
+ */
92
+ async setMaxEntries(count) {
93
+ this._maxEntries = count;
94
+ await this.storage.trim(this._maxEntries);
33
95
  }
34
- get(id) {
35
- return this.entries.find((e) => e.id === id);
96
+ /**
97
+ * Returns the maximum capacity of the mailbox.
98
+ */
99
+ get maxEntries() {
100
+ return this._maxEntries;
36
101
  }
37
- delete(id) {
38
- const index = this.entries.findIndex((e) => e.id === id);
102
+ /**
103
+ * Lists all messages in the mailbox.
104
+ */
105
+ async list() {
106
+ return await this.storage.all();
107
+ }
108
+ /**
109
+ * Retrieves a specific message by ID.
110
+ */
111
+ async get(id) {
112
+ const entries = await this.storage.all();
113
+ return entries.find((e) => e.id === id);
114
+ }
115
+ /**
116
+ * Deletes a specific message by ID.
117
+ */
118
+ async delete(id) {
119
+ if (this.storage.delete) {
120
+ return await this.storage.delete(id);
121
+ }
122
+ const entries = await this.storage.all();
123
+ const index = entries.findIndex((e) => e.id === id);
39
124
  if (index !== -1) {
40
- this.entries.splice(index, 1);
125
+ await this.clear();
126
+ entries.splice(index, 1);
127
+ for (const entry of entries.reverse()) {
128
+ await this.storage.push(entry);
129
+ }
41
130
  return true;
42
131
  }
43
132
  return false;
44
133
  }
45
- clear() {
46
- this.entries = [];
134
+ /**
135
+ * Clears all messages from the mailbox.
136
+ */
137
+ async clear() {
138
+ await this.storage.clear();
139
+ }
140
+ };
141
+
142
+ // src/errors.ts
143
+ var MailErrorCode = /* @__PURE__ */ ((MailErrorCode2) => {
144
+ MailErrorCode2["CONNECTION_FAILED"] = "CONNECTION_FAILED";
145
+ MailErrorCode2["AUTH_FAILED"] = "AUTH_FAILED";
146
+ MailErrorCode2["RECIPIENT_REJECTED"] = "RECIPIENT_REJECTED";
147
+ MailErrorCode2["MESSAGE_REJECTED"] = "MESSAGE_REJECTED";
148
+ MailErrorCode2["RATE_LIMIT"] = "RATE_LIMIT";
149
+ MailErrorCode2["UNKNOWN"] = "UNKNOWN";
150
+ return MailErrorCode2;
151
+ })(MailErrorCode || {});
152
+ var MailTransportError = class _MailTransportError extends Error {
153
+ /**
154
+ * Create a new mail transport error.
155
+ *
156
+ * @param message - Human-readable error message
157
+ * @param code - Categorized error code
158
+ * @param cause - Original error that caused this failure
159
+ *
160
+ * @example
161
+ * ```typescript
162
+ * const error = new MailTransportError('Auth failed', MailErrorCode.AUTH_FAILED);
163
+ * ```
164
+ */
165
+ constructor(message, code, cause) {
166
+ super(message);
167
+ this.code = code;
168
+ this.cause = cause;
169
+ this.name = "MailTransportError";
170
+ if (Error.captureStackTrace) {
171
+ Error.captureStackTrace(this, _MailTransportError);
172
+ }
47
173
  }
48
174
  };
49
175
 
50
176
  // src/renderers/HtmlRenderer.ts
51
177
  var HtmlRenderer = class {
178
+ /**
179
+ * Creates an instance of HtmlRenderer.
180
+ *
181
+ * @param content - The raw HTML string to be rendered.
182
+ */
52
183
  constructor(content) {
53
184
  this.content = content;
54
185
  }
186
+ /**
187
+ * Returns the original HTML and a stripped plain text version.
188
+ *
189
+ * @returns A promise resolving to the rendered content.
190
+ */
55
191
  async render() {
56
192
  return {
57
193
  html: this.content,
58
- text: this.stripHtml(this.content)
194
+ text: stripHtml(this.content)
59
195
  };
60
196
  }
61
- stripHtml(html) {
62
- return html.replace(/<style(?:\s[^>]*)?>[\s\S]*?<\/style>/gi, "").replace(/<script(?:\s[^>]*)?>[\s\S]*?<\/script>/gi, "").replace(/<[^>]+>/g, "").replace(/&nbsp;/g, " ").replace(/\s+/g, " ").trim();
63
- }
64
197
  };
65
198
 
66
199
  // src/renderers/TemplateRenderer.ts
67
- var TemplateRenderer = class {
200
+ var TemplateRenderer = class _TemplateRenderer {
68
201
  template;
69
202
  viewsDir;
203
+ static engineCache = /* @__PURE__ */ new Map();
204
+ /**
205
+ * Creates an instance of TemplateRenderer.
206
+ *
207
+ * @param templateName - The name of the template file (without extension).
208
+ * @param viewsDir - The directory containing template files. Defaults to `src/emails`.
209
+ */
70
210
  constructor(templateName, viewsDir) {
71
211
  this.template = templateName;
72
212
  this.viewsDir = viewsDir || `${process.cwd()}/src/emails`;
73
213
  }
214
+ /**
215
+ * Renders the template with the provided data.
216
+ *
217
+ * This method lazily loads `@gravito/prism` to ensure the core package
218
+ * remains lightweight for users who don't need template rendering.
219
+ *
220
+ * @param data - The data context for template interpolation.
221
+ * @returns A promise resolving to the rendered content.
222
+ * @throws {Error} If the template engine fails to load or rendering fails.
223
+ */
74
224
  async render(data) {
75
225
  const { TemplateEngine } = await import("@gravito/prism");
76
- const engine = new TemplateEngine(this.viewsDir);
226
+ const cached = _TemplateRenderer.engineCache.get(this.viewsDir);
227
+ const engine = cached || new TemplateEngine(this.viewsDir);
228
+ if (!cached) {
229
+ _TemplateRenderer.engineCache.set(this.viewsDir, engine);
230
+ }
77
231
  const html = engine.render(this.template, data, {});
78
232
  return {
79
233
  html,
80
- text: this.stripHtml(html)
234
+ text: stripHtml(html)
81
235
  };
82
236
  }
83
- stripHtml(html) {
84
- return html.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "").replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "").replace(/<[^>]+>/g, "").replace(/&nbsp;/g, " ").replace(/\s+/g, " ").trim();
237
+ /**
238
+ * Clear template engine cache.
239
+ *
240
+ * Useful in development environments to force recompilation of templates
241
+ * after they have been modified on disk.
242
+ *
243
+ * @example
244
+ * ```typescript
245
+ * TemplateRenderer.clearCache();
246
+ * ```
247
+ *
248
+ * @public
249
+ * @since 3.1.0
250
+ */
251
+ static clearCache() {
252
+ _TemplateRenderer.engineCache.clear();
85
253
  }
86
254
  };
87
255
 
@@ -96,7 +264,17 @@ var Mailable = class {
96
264
  /**
97
265
  * Set the sender address for the email.
98
266
  *
99
- * @param address - Email address or Address object.
267
+ * This defines the "From" field in the email envelope. If not called, the default
268
+ * sender from the mail configuration will be used.
269
+ *
270
+ * @param address - The email address or address object containing name and address.
271
+ * @returns The current mailable instance for chaining.
272
+ *
273
+ * @example
274
+ * ```typescript
275
+ * mailable.from('admin@example.com')
276
+ * mailable.from({ name: 'Support', address: 'support@example.com' })
277
+ * ```
100
278
  */
101
279
  from(address) {
102
280
  this.envelope.from = typeof address === "string" ? { address } : address;
@@ -105,7 +283,17 @@ var Mailable = class {
105
283
  /**
106
284
  * Set the primary recipient(s) for the email.
107
285
  *
108
- * @param address - Single address, address object, or array of either.
286
+ * Configures the "To" field. Supports single or multiple recipients in various formats.
287
+ *
288
+ * @param address - A single email string, an address object, or an array of either.
289
+ * @returns The current mailable instance for chaining.
290
+ *
291
+ * @example
292
+ * ```typescript
293
+ * mailable.to('user@example.com')
294
+ * mailable.to(['a@example.com', 'b@example.com'])
295
+ * mailable.to({ name: 'John', address: 'john@example.com' })
296
+ * ```
109
297
  */
110
298
  to(address) {
111
299
  this.envelope.to = this.normalizeAddressArray(address);
@@ -114,7 +302,15 @@ var Mailable = class {
114
302
  /**
115
303
  * Set the carbon copy (CC) recipient(s).
116
304
  *
117
- * @param address - Single address, address object, or array of either.
305
+ * Adds recipients to the "Cc" field of the email.
306
+ *
307
+ * @param address - A single email string, an address object, or an array of either.
308
+ * @returns The current mailable instance for chaining.
309
+ *
310
+ * @example
311
+ * ```typescript
312
+ * mailable.cc('manager@example.com')
313
+ * ```
118
314
  */
119
315
  cc(address) {
120
316
  this.envelope.cc = this.normalizeAddressArray(address);
@@ -123,7 +319,15 @@ var Mailable = class {
123
319
  /**
124
320
  * Set the blind carbon copy (BCC) recipient(s).
125
321
  *
126
- * @param address - Single address, address object, or array of either.
322
+ * Adds recipients to the "Bcc" field. These recipients are hidden from others.
323
+ *
324
+ * @param address - A single email string, an address object, or an array of either.
325
+ * @returns The current mailable instance for chaining.
326
+ *
327
+ * @example
328
+ * ```typescript
329
+ * mailable.bcc('audit@example.com')
330
+ * ```
127
331
  */
128
332
  bcc(address) {
129
333
  this.envelope.bcc = this.normalizeAddressArray(address);
@@ -132,7 +336,15 @@ var Mailable = class {
132
336
  /**
133
337
  * Set the reply-to address.
134
338
  *
135
- * @param address - Email address or Address object.
339
+ * Specifies where replies to this email should be directed.
340
+ *
341
+ * @param address - The email address or address object for replies.
342
+ * @returns The current mailable instance for chaining.
343
+ *
344
+ * @example
345
+ * ```typescript
346
+ * mailable.replyTo('no-reply@example.com')
347
+ * ```
136
348
  */
137
349
  replyTo(address) {
138
350
  this.envelope.replyTo = typeof address === "string" ? { address } : address;
@@ -141,7 +353,15 @@ var Mailable = class {
141
353
  /**
142
354
  * Set the subject line for the email.
143
355
  *
144
- * @param subject - The email subject.
356
+ * Defines the text that appears in the recipient's inbox subject field.
357
+ *
358
+ * @param subject - The subject text.
359
+ * @returns The current mailable instance for chaining.
360
+ *
361
+ * @example
362
+ * ```typescript
363
+ * mailable.subject('Your Order Confirmation')
364
+ * ```
145
365
  */
146
366
  subject(subject) {
147
367
  this.envelope.subject = subject;
@@ -150,7 +370,15 @@ var Mailable = class {
150
370
  /**
151
371
  * Set the email priority.
152
372
  *
153
- * @param level - Priority level ('high', 'normal', or 'low').
373
+ * Hints to the email client how urgent this message is.
374
+ *
375
+ * @param level - The priority level: 'high', 'normal', or 'low'.
376
+ * @returns The current mailable instance for chaining.
377
+ *
378
+ * @example
379
+ * ```typescript
380
+ * mailable.emailPriority('high')
381
+ * ```
154
382
  */
155
383
  emailPriority(level) {
156
384
  this.envelope.priority = level;
@@ -159,7 +387,18 @@ var Mailable = class {
159
387
  /**
160
388
  * Attach a file to the email.
161
389
  *
162
- * @param attachment - Attachment configuration object.
390
+ * Adds a file attachment to the message. Can be called multiple times for multiple files.
391
+ *
392
+ * @param attachment - The attachment configuration including path, content, or filename.
393
+ * @returns The current mailable instance for chaining.
394
+ *
395
+ * @example
396
+ * ```typescript
397
+ * mailable.attach({
398
+ * filename: 'invoice.pdf',
399
+ * path: './storage/invoices/123.pdf'
400
+ * })
401
+ * ```
163
402
  */
164
403
  attach(attachment) {
165
404
  this.envelope.attachments = this.envelope.attachments || [];
@@ -168,9 +407,17 @@ var Mailable = class {
168
407
  }
169
408
  // ===== Content Methods (Renderer Selection) =====
170
409
  /**
171
- * Set the content using raw HTML string.
410
+ * Set the content using a raw HTML string.
411
+ *
412
+ * Use this for simple emails where a full template engine is not required.
172
413
  *
173
- * @param content - The HTML content string.
414
+ * @param content - The raw HTML content.
415
+ * @returns The current mailable instance for chaining.
416
+ *
417
+ * @example
418
+ * ```typescript
419
+ * mailable.html('<h1>Hello</h1><p>Welcome to our platform.</p>')
420
+ * ```
174
421
  */
175
422
  html(content) {
176
423
  this.renderer = new HtmlRenderer(content);
@@ -179,8 +426,17 @@ var Mailable = class {
179
426
  /**
180
427
  * Set the content using an OrbitPrism template.
181
428
  *
182
- * @param template - Template name (relative to viewsDir/emails).
183
- * @param data - Optional data to pass to the template.
429
+ * Renders a template file with the provided data. This is the recommended way
430
+ * to build complex, data-driven emails.
431
+ *
432
+ * @param template - The template name or path relative to the configured views directory.
433
+ * @param data - The data object to be injected into the template.
434
+ * @returns The current mailable instance for chaining.
435
+ *
436
+ * @example
437
+ * ```typescript
438
+ * mailable.view('emails.welcome', { name: 'Alice' })
439
+ * ```
184
440
  */
185
441
  view(template, data) {
186
442
  this.renderer = new TemplateRenderer(template, void 0);
@@ -189,34 +445,105 @@ var Mailable = class {
189
445
  }
190
446
  /**
191
447
  * Set the content using a React component.
192
- * Dynamically imports ReactRenderer to avoid hard dependency errors if React is not installed.
193
448
  *
194
- * @param component - The React component to render.
195
- * @param props - Optional props for the component.
196
- * @param deps - Optional dependencies (React, ReactDOMServer) if not globally available.
449
+ * Leverages React's component model for email design. The renderer is loaded
450
+ * dynamically to keep the core package lightweight.
451
+ *
452
+ * @param component - The React component class or function.
453
+ * @param props - The properties to pass to the component.
454
+ * @param deps - Optional React/ReactDOMServer overrides for custom environments.
455
+ * @returns The current mailable instance for chaining.
456
+ *
457
+ * @example
458
+ * ```typescript
459
+ * mailable.react(WelcomeEmailComponent, { name: 'Alice' })
460
+ * ```
197
461
  */
198
462
  react(component, props, deps) {
199
463
  this.rendererResolver = async () => {
200
- const { ReactRenderer } = await import("./ReactRenderer-KYNA4WKE.mjs");
464
+ const { ReactRenderer } = await import("./ReactRenderer-V54CUUEI.mjs");
201
465
  return new ReactRenderer(component, props, deps);
202
466
  };
203
467
  return this;
204
468
  }
205
469
  /**
206
470
  * Set the content using a Vue component.
207
- * Dynamically imports VueRenderer to avoid hard dependency errors if Vue is not installed.
208
471
  *
209
- * @param component - The Vue component to render.
210
- * @param props - Optional props for the component.
211
- * @param deps - Optional dependencies (Vue, VueServerRenderer) if not globally available.
472
+ * Leverages Vue's component model for email design. The renderer is loaded
473
+ * dynamically to keep the core package lightweight.
474
+ *
475
+ * @param component - The Vue component object.
476
+ * @param props - The properties to pass to the component.
477
+ * @param deps - Optional Vue/VueServerRenderer overrides for custom environments.
478
+ * @returns The current mailable instance for chaining.
479
+ *
480
+ * @example
481
+ * ```typescript
482
+ * mailable.vue(WelcomeEmailComponent, { name: 'Alice' })
483
+ * ```
212
484
  */
213
485
  vue(component, props, deps) {
214
486
  this.rendererResolver = async () => {
215
- const { VueRenderer } = await import("./VueRenderer-IIR5SYTM.mjs");
487
+ const { VueRenderer } = await import("./VueRenderer-3YBRQXME.mjs");
216
488
  return new VueRenderer(component, props, deps);
217
489
  };
218
490
  return this;
219
491
  }
492
+ /**
493
+ * Set the content using an MJML markup string.
494
+ *
495
+ * MJML ensures responsive email compatibility across various clients.
496
+ *
497
+ * @param content - The MJML markup string or inner content.
498
+ * @param options - MJML transformation options.
499
+ * @param options.layout - Optional full MJML layout string. Use '{{content}}' as placeholder.
500
+ * @returns The current mailable instance for chaining.
501
+ *
502
+ * @example
503
+ * ```typescript
504
+ * mailable.mjml('<mj-text>Hello</mj-text>', {
505
+ * layout: '<mjml><mj-body>{{content}}</mj-body></mjml>'
506
+ * })
507
+ * ```
508
+ */
509
+ mjml(content, options) {
510
+ const finalContent = options?.layout?.includes("{{content}}") ? options.layout.replace("{{content}}", content) : content;
511
+ this.rendererResolver = async () => {
512
+ const { MjmlRenderer: MjmlRenderer2 } = await import("./MjmlRenderer-IUH663FT.mjs");
513
+ return new MjmlRenderer2(finalContent, options);
514
+ };
515
+ return this;
516
+ }
517
+ /**
518
+ * Set the content using a React component that outputs MJML.
519
+ *
520
+ * @param component - The React component.
521
+ * @param props - Component properties.
522
+ * @param options - MJML options.
523
+ * @returns The current mailable instance for chaining.
524
+ */
525
+ mjmlReact(component, props, options) {
526
+ this.rendererResolver = async () => {
527
+ const { ReactMjmlRenderer: ReactMjmlRenderer2 } = await import("./ReactMjmlRenderer-C3P5YO5L.mjs");
528
+ return new ReactMjmlRenderer2(component, props, options);
529
+ };
530
+ return this;
531
+ }
532
+ /**
533
+ * Set the content using a Vue component that outputs MJML.
534
+ *
535
+ * @param component - The Vue component.
536
+ * @param props - Component properties.
537
+ * @param options - MJML options.
538
+ * @returns The current mailable instance for chaining.
539
+ */
540
+ mjmlVue(component, props, options) {
541
+ this.rendererResolver = async () => {
542
+ const { VueMjmlRenderer: VueMjmlRenderer2 } = await import("./VueMjmlRenderer-4F4CXHDB.mjs");
543
+ return new VueMjmlRenderer2(component, props, options);
544
+ };
545
+ return this;
546
+ }
220
547
  // ===== Queueable Implementation =====
221
548
  /** The name of the queue to push this mailable to. */
222
549
  queueName;
@@ -227,36 +554,81 @@ var Mailable = class {
227
554
  /** Priority of the message in the queue. */
228
555
  priority;
229
556
  /**
230
- * Set the queue name for this mailable.
557
+ * Set the target queue for background processing.
558
+ *
559
+ * @param queue - The name of the queue.
560
+ * @returns The current mailable instance for chaining.
561
+ *
562
+ * @example
563
+ * ```typescript
564
+ * mailable.onQueue('notifications')
565
+ * ```
231
566
  */
232
567
  onQueue(queue) {
233
568
  this.queueName = queue;
234
569
  return this;
235
570
  }
236
571
  /**
237
- * Set the connection name for this mailable.
572
+ * Set the queue connection to be used.
573
+ *
574
+ * @param connection - The name of the connection (e.g., 'redis', 'sqs').
575
+ * @returns The current mailable instance for chaining.
576
+ *
577
+ * @example
578
+ * ```typescript
579
+ * mailable.onConnection('redis')
580
+ * ```
238
581
  */
239
582
  onConnection(connection) {
240
583
  this.connectionName = connection;
241
584
  return this;
242
585
  }
243
586
  /**
244
- * Set a delay for the queued mailable.
587
+ * Set a delay for the queued message.
588
+ *
589
+ * The message will remain in the queue and only be processed after the delay.
590
+ *
591
+ * @param seconds - The delay in seconds.
592
+ * @returns The current mailable instance for chaining.
593
+ *
594
+ * @example
595
+ * ```typescript
596
+ * mailable.delay(3600) // Delay for 1 hour
597
+ * ```
245
598
  */
246
599
  delay(seconds) {
247
600
  this.delaySeconds = seconds;
248
601
  return this;
249
602
  }
250
603
  /**
251
- * Set the priority for the queued mailable.
604
+ * Set the priority for the queued message.
605
+ *
606
+ * Higher priority messages are typically processed before lower priority ones.
607
+ *
608
+ * @param priority - The priority value (numeric or string).
609
+ * @returns The current mailable instance for chaining.
610
+ *
611
+ * @example
612
+ * ```typescript
613
+ * mailable.withPriority(10)
614
+ * ```
252
615
  */
253
616
  withPriority(priority) {
254
617
  this.priority = priority;
255
618
  return this;
256
619
  }
257
620
  /**
258
- * Queue the mailable for sending.
259
- * Attempts to resolve the mail service from the PlanetCore container.
621
+ * Push the mailable onto the configured queue.
622
+ *
623
+ * Automatically resolves the mail service from the Gravito container and
624
+ * dispatches this mailable for background processing.
625
+ *
626
+ * @returns A promise that resolves when the mailable is queued.
627
+ *
628
+ * @example
629
+ * ```typescript
630
+ * await new WelcomeEmail(user).queue()
631
+ * ```
260
632
  */
261
633
  async queue() {
262
634
  try {
@@ -273,9 +645,17 @@ var Mailable = class {
273
645
  currentLocale;
274
646
  translator;
275
647
  /**
276
- * Set the locale for the message.
648
+ * Set the locale for the email content.
649
+ *
650
+ * Used by the translator to resolve localized strings in templates or components.
651
+ *
652
+ * @param locale - The locale identifier (e.g., 'en-US', 'fr').
653
+ * @returns The current mailable instance for chaining.
277
654
  *
278
- * @param locale - Valid locale string (e.g., 'en', 'zh-TW').
655
+ * @example
656
+ * ```typescript
657
+ * mailable.locale('es')
658
+ * ```
279
659
  */
280
660
  locale(locale) {
281
661
  this.currentLocale = locale;
@@ -289,11 +669,18 @@ var Mailable = class {
289
669
  this.translator = translator;
290
670
  }
291
671
  /**
292
- * Translate a string using the configured translator.
672
+ * Translate a key into a localized string.
293
673
  *
294
- * @param key - Translation key.
295
- * @param replace - Interpolation variables.
296
- * @returns Translated string or the key if translator is missing.
674
+ * Uses the configured translator and current locale to resolve the key.
675
+ *
676
+ * @param key - The translation key.
677
+ * @param replace - Key-value pairs for string interpolation.
678
+ * @returns The translated string, or the key itself if no translator is available.
679
+ *
680
+ * @example
681
+ * ```typescript
682
+ * const text = mailable.t('messages.welcome', { name: 'Alice' })
683
+ * ```
297
684
  */
298
685
  t(key, replace) {
299
686
  if (this.translator) {
@@ -303,8 +690,18 @@ var Mailable = class {
303
690
  }
304
691
  // ===== Internal Systems =====
305
692
  /**
306
- * Compile the envelope based on config defaults and mailable settings.
307
- * Called by OrbitSignal before rendering/sending.
693
+ * Compile the final email envelope.
694
+ *
695
+ * Merges mailable-specific settings with global configuration defaults.
696
+ * This is called internally by the mail service before sending.
697
+ *
698
+ * @param configPromise - The mail configuration or a promise resolving to it.
699
+ * @returns The fully constructed envelope.
700
+ *
701
+ * @example
702
+ * ```typescript
703
+ * const envelope = await mailable.buildEnvelope(config)
704
+ * ```
308
705
  */
309
706
  async buildEnvelope(configPromise) {
310
707
  const config = await Promise.resolve(configPromise);
@@ -337,8 +734,18 @@ var Mailable = class {
337
734
  return envelope;
338
735
  }
339
736
  /**
340
- * Execute the renderer and produce HTML/Text content.
341
- * @returns Object containing rendered html and optional text content.
737
+ * Render the email content to HTML and plain text.
738
+ *
739
+ * Executes the chosen renderer (HTML, Template, React, or Vue) with the
740
+ * provided data and i18n helpers.
741
+ *
742
+ * @returns The rendered HTML and optional plain text content.
743
+ * @throws {Error} If no renderer has been specified.
744
+ *
745
+ * @example
746
+ * ```typescript
747
+ * const { html } = await mailable.renderContent()
748
+ * ```
342
749
  */
343
750
  async renderContent() {
344
751
  if (!this.renderer && this.rendererResolver) {
@@ -579,20 +986,20 @@ var DevServer = class {
579
986
  };
580
987
  router.get(
581
988
  prefix,
582
- wrap((ctx) => {
583
- const entries = this.mailbox.list();
989
+ wrap(async (ctx) => {
990
+ const entries = await this.mailbox.list();
584
991
  ctx.header("Content-Type", "text/html; charset=utf-8");
585
992
  return ctx.html(getMailboxHtml(entries, prefix));
586
993
  })
587
994
  );
588
995
  router.get(
589
996
  `${prefix}/:id`,
590
- wrap((ctx) => {
997
+ wrap(async (ctx) => {
591
998
  const id = ctx.req.param("id");
592
999
  if (!id) {
593
1000
  return ctx.text("Bad Request", 400);
594
1001
  }
595
- const entry = this.mailbox.get(id);
1002
+ const entry = await this.mailbox.get(id);
596
1003
  if (!entry) {
597
1004
  return ctx.text("Email not found", 404);
598
1005
  }
@@ -602,12 +1009,12 @@ var DevServer = class {
602
1009
  );
603
1010
  router.get(
604
1011
  `${prefix}/:id/html`,
605
- wrap((ctx) => {
1012
+ wrap(async (ctx) => {
606
1013
  const id = ctx.req.param("id");
607
1014
  if (!id) {
608
1015
  return ctx.text("Bad Request", 400);
609
1016
  }
610
- const entry = this.mailbox.get(id);
1017
+ const entry = await this.mailbox.get(id);
611
1018
  if (!entry) {
612
1019
  return ctx.text("Not found", 404);
613
1020
  }
@@ -617,12 +1024,12 @@ var DevServer = class {
617
1024
  );
618
1025
  router.get(
619
1026
  `${prefix}/:id/text`,
620
- wrap((ctx) => {
1027
+ wrap(async (ctx) => {
621
1028
  const id = ctx.req.param("id");
622
1029
  if (!id) {
623
1030
  return ctx.text("Bad Request", 400);
624
1031
  }
625
- const entry = this.mailbox.get(id);
1032
+ const entry = await this.mailbox.get(id);
626
1033
  if (!entry) {
627
1034
  return ctx.text("Not found", 404);
628
1035
  }
@@ -632,12 +1039,12 @@ var DevServer = class {
632
1039
  );
633
1040
  router.get(
634
1041
  `${prefix}/:id/raw`,
635
- wrap((ctx) => {
1042
+ wrap(async (ctx) => {
636
1043
  const id = ctx.req.param("id");
637
1044
  if (!id) {
638
1045
  return ctx.json({ error: "Bad Request" }, 400);
639
1046
  }
640
- const entry = this.mailbox.get(id);
1047
+ const entry = await this.mailbox.get(id);
641
1048
  if (!entry) {
642
1049
  return ctx.json({ error: "Not found" }, 404);
643
1050
  }
@@ -652,19 +1059,19 @@ var DevServer = class {
652
1059
  );
653
1060
  router.delete(
654
1061
  `${prefix}/:id`,
655
- wrap((ctx) => {
1062
+ wrap(async (ctx) => {
656
1063
  const id = ctx.req.param("id");
657
1064
  if (!id) {
658
1065
  return ctx.json({ success: false, error: "Bad Request" }, 400);
659
1066
  }
660
- const success = this.mailbox.delete(id);
1067
+ const success = await this.mailbox.delete(id);
661
1068
  return ctx.json({ success });
662
1069
  })
663
1070
  );
664
1071
  router.delete(
665
1072
  prefix,
666
- wrap((ctx) => {
667
- this.mailbox.clear();
1073
+ wrap(async (ctx) => {
1074
+ await this.mailbox.clear();
668
1075
  return ctx.json({ success: true });
669
1076
  })
670
1077
  );
@@ -674,6 +1081,22 @@ var DevServer = class {
674
1081
 
675
1082
  // src/transports/LogTransport.ts
676
1083
  var LogTransport = class {
1084
+ /**
1085
+ * Outputs the message details to the system console.
1086
+ *
1087
+ * Formats the email metadata (From, To, Subject) and content size into a readable
1088
+ * block in the console output.
1089
+ *
1090
+ * @param message - The message to log.
1091
+ * @returns A promise that resolves immediately after logging.
1092
+ *
1093
+ * @example
1094
+ * ```typescript
1095
+ * const transport = new LogTransport();
1096
+ * await transport.send(message);
1097
+ * // Console: 📧 [OrbitSignal] Email Sent (Simulated)...
1098
+ * ```
1099
+ */
677
1100
  async send(message) {
678
1101
  console.log("\n\u{1F4E7} [OrbitSignal] Email Sent (Simulated):");
679
1102
  console.log("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501");
@@ -690,9 +1113,33 @@ var LogTransport = class {
690
1113
 
691
1114
  // src/transports/MemoryTransport.ts
692
1115
  var MemoryTransport = class {
1116
+ /**
1117
+ * Creates a new MemoryTransport instance.
1118
+ *
1119
+ * @param mailbox - The in-memory storage where messages will be collected.
1120
+ */
693
1121
  constructor(mailbox) {
694
1122
  this.mailbox = mailbox;
695
1123
  }
1124
+ /**
1125
+ * Stores the message in the associated mailbox.
1126
+ *
1127
+ * The message is added to the internal list of the `DevMailbox` instance,
1128
+ * making it available for retrieval by the Dev UI or test assertions.
1129
+ *
1130
+ * @param message - The message to store.
1131
+ * @returns A promise that resolves once the message is added to the mailbox.
1132
+ *
1133
+ * @example
1134
+ * ```typescript
1135
+ * await transport.send({
1136
+ * from: { address: 'dev@localhost' },
1137
+ * to: [{ address: 'test@example.com' }],
1138
+ * subject: 'Memory Test',
1139
+ * html: '<p>Stored in memory</p>'
1140
+ * });
1141
+ * ```
1142
+ */
696
1143
  async send(message) {
697
1144
  this.mailbox.add(message);
698
1145
  }
@@ -703,13 +1150,24 @@ var OrbitSignal = class {
703
1150
  config;
704
1151
  devMailbox;
705
1152
  core;
1153
+ eventHandlers = /* @__PURE__ */ new Map();
706
1154
  constructor(config = {}) {
707
1155
  this.config = config;
708
1156
  }
709
1157
  /**
710
- * Install the orbit into PlanetCore
1158
+ * Install the orbit into PlanetCore.
1159
+ *
1160
+ * Registers the mail service in the IoC container and sets up development
1161
+ * tools if enabled. It also injects the service into the GravitoContext
1162
+ * for easy access in route handlers.
711
1163
  *
712
- * @param core - The PlanetCore instance.
1164
+ * @param core - The PlanetCore instance to install into
1165
+ *
1166
+ * @example
1167
+ * ```typescript
1168
+ * const mail = new OrbitSignal(config);
1169
+ * mail.install(core);
1170
+ * ```
713
1171
  */
714
1172
  install(core) {
715
1173
  this.core = core;
@@ -732,37 +1190,123 @@ var OrbitSignal = class {
732
1190
  c.set("mail", this);
733
1191
  return await next();
734
1192
  });
1193
+ if (this.config.webhookPrefix) {
1194
+ core.adapter.post(`${this.config.webhookPrefix}/:driver`, async (c) => {
1195
+ const driverName = c.req.param("driver");
1196
+ const driver = this.config.webhookDrivers?.[driverName];
1197
+ if (!driver) {
1198
+ return c.json({ error: `Webhook driver "${driverName}" not found` }, 404);
1199
+ }
1200
+ try {
1201
+ const results = await driver.handle(c);
1202
+ if (results && Array.isArray(results)) {
1203
+ for (const result of results) {
1204
+ await this.handleWebhook(driverName, result.event, result.payload);
1205
+ }
1206
+ }
1207
+ return c.json({ success: true });
1208
+ } catch (error) {
1209
+ core.logger.error(`[OrbitSignal] Webhook error (${driverName}):`, error);
1210
+ return c.json({ error: "Webhook processing failed" }, 500);
1211
+ }
1212
+ });
1213
+ }
735
1214
  }
736
1215
  /**
737
- * Send a mailable instance
1216
+ * Internal: Handle processed webhook.
1217
+ */
1218
+ async handleWebhook(driver, event, payload) {
1219
+ await this.emit({
1220
+ type: "webhookReceived",
1221
+ // biome-ignore lint/suspicious/noExplicitAny: Dummy mailable for webhook events
1222
+ mailable: {},
1223
+ timestamp: /* @__PURE__ */ new Date(),
1224
+ webhook: { driver, event, payload }
1225
+ });
1226
+ }
1227
+ /**
1228
+ * Send a mailable instance immediately.
1229
+ *
1230
+ * Orchestrates the full email sending lifecycle: building the envelope,
1231
+ * rendering content, emitting events, and delivering via the configured transport.
1232
+ *
1233
+ * @param mailable - The email definition to send
1234
+ * @throws {Error} If mandatory fields (from, to) are missing or transport fails
1235
+ *
1236
+ * @example
1237
+ * ```typescript
1238
+ * await mail.send(new WelcomeEmail(user));
1239
+ * ```
738
1240
  */
739
1241
  async send(mailable) {
740
- const envelope = await mailable.buildEnvelope(this.config);
741
- if (!envelope.from) {
742
- throw new Error('Message is missing "from" address');
743
- }
744
- if (!envelope.to || envelope.to.length === 0) {
745
- throw new Error('Message is missing "to" address');
746
- }
747
- const content = await mailable.renderContent();
748
- const message = {
749
- ...envelope,
750
- from: envelope.from,
751
- to: envelope.to,
752
- subject: envelope.subject || "(No Subject)",
753
- priority: envelope.priority || "normal",
754
- html: content.html
755
- };
756
- if (content.text) {
757
- message.text = content.text;
758
- }
759
- if (!this.config.transport) {
760
- throw new Error("[OrbitSignal] No transport configured. Did you call register the orbit?");
1242
+ try {
1243
+ const envelope = await mailable.buildEnvelope(this.config);
1244
+ if (!envelope.from) {
1245
+ throw new Error('Message is missing "from" address');
1246
+ }
1247
+ if (!envelope.to || envelope.to.length === 0) {
1248
+ throw new Error('Message is missing "to" address');
1249
+ }
1250
+ await this.emit({
1251
+ type: "beforeRender",
1252
+ mailable,
1253
+ timestamp: /* @__PURE__ */ new Date()
1254
+ });
1255
+ const content = await mailable.renderContent();
1256
+ await this.emit({
1257
+ type: "afterRender",
1258
+ mailable,
1259
+ timestamp: /* @__PURE__ */ new Date()
1260
+ });
1261
+ const message = {
1262
+ ...envelope,
1263
+ from: envelope.from,
1264
+ to: envelope.to,
1265
+ subject: envelope.subject || "(No Subject)",
1266
+ priority: envelope.priority || "normal",
1267
+ html: content.html
1268
+ };
1269
+ if (content.text) {
1270
+ message.text = content.text;
1271
+ }
1272
+ await this.emit({
1273
+ type: "beforeSend",
1274
+ mailable,
1275
+ message,
1276
+ timestamp: /* @__PURE__ */ new Date()
1277
+ });
1278
+ if (!this.config.transport) {
1279
+ throw new Error("[OrbitSignal] No transport configured. Did you call register the orbit?");
1280
+ }
1281
+ await this.config.transport.send(message);
1282
+ await this.emit({
1283
+ type: "afterSend",
1284
+ mailable,
1285
+ message,
1286
+ timestamp: /* @__PURE__ */ new Date()
1287
+ });
1288
+ } catch (error) {
1289
+ await this.emit({
1290
+ type: "sendFailed",
1291
+ mailable,
1292
+ error,
1293
+ timestamp: /* @__PURE__ */ new Date()
1294
+ });
1295
+ throw error;
761
1296
  }
762
- await this.config.transport.send(message);
763
1297
  }
764
1298
  /**
765
- * Queue a mailable instance
1299
+ * Queue a mailable instance for background processing.
1300
+ *
1301
+ * Attempts to use the 'queue' service (OrbitStream) if available in the
1302
+ * container. Falls back to immediate sending if no queue service is found.
1303
+ *
1304
+ * @param mailable - The email definition to queue
1305
+ *
1306
+ * @example
1307
+ * ```typescript
1308
+ * await mail.queue(new WelcomeEmail(user));
1309
+ * ```
766
1310
  */
767
1311
  async queue(mailable) {
768
1312
  try {
@@ -775,14 +1319,174 @@ var OrbitSignal = class {
775
1319
  }
776
1320
  await this.send(mailable);
777
1321
  }
1322
+ /**
1323
+ * Register an event handler.
1324
+ *
1325
+ * @description
1326
+ * Subscribe to mail lifecycle events for logging, analytics, or custom processing.
1327
+ *
1328
+ * @param event - The event type to listen for
1329
+ * @param handler - The handler function to execute
1330
+ * @returns This instance for method chaining
1331
+ *
1332
+ * @example
1333
+ * ```typescript
1334
+ * mail.on('afterSend', async (event) => {
1335
+ * await analytics.track('email_sent', {
1336
+ * to: event.message?.to,
1337
+ * subject: event.message?.subject
1338
+ * })
1339
+ * })
1340
+ * ```
1341
+ *
1342
+ * @public
1343
+ * @since 3.1.0
1344
+ */
1345
+ on(event, handler) {
1346
+ const handlers = this.eventHandlers.get(event) || [];
1347
+ handlers.push(handler);
1348
+ this.eventHandlers.set(event, handlers);
1349
+ return this;
1350
+ }
1351
+ async emit(event) {
1352
+ const handlers = this.eventHandlers.get(event.type) || [];
1353
+ for (const handler of handlers) {
1354
+ await handler(event);
1355
+ }
1356
+ }
1357
+ };
1358
+
1359
+ // src/renderers/mjml-templates.ts
1360
+ var baseLayout = `
1361
+ <mjml>
1362
+ <mj-head>
1363
+ <mj-attributes>
1364
+ <mj-all font-family="Arial, Helvetica, sans-serif" />
1365
+ <mj-text font-size="16px" color="#333333" line-height="1.5" />
1366
+ <mj-section padding="20px" />
1367
+ </mj-attributes>
1368
+ <mj-style>
1369
+ .link-white { color: #ffffff !important; text-decoration: none; }
1370
+ .footer-text { font-size: 12px; color: #999999; }
1371
+ </mj-style>
1372
+ </mj-head>
1373
+ <mj-body background-color="#f4f4f4">
1374
+ <mj-section background-color="#ffffff" padding-bottom="0px">
1375
+ <mj-column>
1376
+ <mj-image width="150px" src="https://gravito.dev/logo.png" alt="Gravito" />
1377
+ </mj-column>
1378
+ </mj-section>
1379
+
1380
+ {{content}}
1381
+
1382
+ <mj-section>
1383
+ <mj-column>
1384
+ <mj-divider border-width="1px" border-color="#dddddd" />
1385
+ <mj-text align="center" css-class="footer-text">
1386
+ &copy; ${(/* @__PURE__ */ new Date()).getFullYear()} Gravito Framework. All rights reserved.
1387
+ </mj-text>
1388
+ </mj-column>
1389
+ </mj-section>
1390
+ </mj-body>
1391
+ </mjml>
1392
+ `;
1393
+ var transactionLayout = `
1394
+ <mj-section background-color="#ffffff" padding-top="0px">
1395
+ <mj-column>
1396
+ {{content}}
1397
+ </mj-column>
1398
+ </mj-section>
1399
+ `;
1400
+
1401
+ // src/TypedMailable.ts
1402
+ var TypedMailable = class extends Mailable {
1403
+ };
1404
+
1405
+ // src/transports/BaseTransport.ts
1406
+ var BaseTransport = class {
1407
+ options;
1408
+ /**
1409
+ * Initializes the transport with retry options.
1410
+ *
1411
+ * @param options - Configuration for the retry mechanism.
1412
+ */
1413
+ constructor(options) {
1414
+ this.options = {
1415
+ maxRetries: options?.maxRetries ?? 3,
1416
+ retryDelay: options?.retryDelay ?? 1e3,
1417
+ backoffMultiplier: options?.backoffMultiplier ?? 2
1418
+ };
1419
+ }
1420
+ /**
1421
+ * Orchestrates the message delivery with retry logic.
1422
+ *
1423
+ * This method wraps the concrete `doSend` implementation in a retry loop.
1424
+ * It tracks the last error encountered to provide context if all retries fail.
1425
+ *
1426
+ * @param message - The message to be delivered.
1427
+ * @returns A promise that resolves when the message is successfully sent.
1428
+ * @throws {MailTransportError} If the message cannot be sent after the maximum number of retries.
1429
+ *
1430
+ * @example
1431
+ * ```typescript
1432
+ * const transport = new SmtpTransport(config);
1433
+ * try {
1434
+ * await transport.send(message);
1435
+ * } catch (error) {
1436
+ * console.error('Failed to send email after retries', error);
1437
+ * }
1438
+ * ```
1439
+ */
1440
+ async send(message) {
1441
+ let lastError;
1442
+ let delay = this.options.retryDelay;
1443
+ for (let attempt = 0; attempt <= this.options.maxRetries; attempt++) {
1444
+ try {
1445
+ return await this.doSend(message);
1446
+ } catch (error) {
1447
+ lastError = error;
1448
+ if (attempt < this.options.maxRetries) {
1449
+ await this.sleep(delay);
1450
+ delay *= this.options.backoffMultiplier;
1451
+ }
1452
+ }
1453
+ }
1454
+ throw new MailTransportError(
1455
+ `Mail sending failed after ${this.options.maxRetries} retries`,
1456
+ "UNKNOWN" /* UNKNOWN */,
1457
+ lastError
1458
+ );
1459
+ }
1460
+ /**
1461
+ * Utility method to pause execution for a given duration.
1462
+ *
1463
+ * @param ms - Milliseconds to sleep.
1464
+ * @returns A promise that resolves after the delay.
1465
+ */
1466
+ sleep(ms) {
1467
+ return new Promise((resolve) => setTimeout(resolve, ms));
1468
+ }
778
1469
  };
779
1470
 
780
1471
  // src/transports/SesTransport.ts
781
1472
  import { SESClient, SendRawEmailCommand } from "@aws-sdk/client-ses";
782
1473
  import nodemailer from "nodemailer";
783
- var SesTransport = class {
1474
+ var SesTransport = class extends BaseTransport {
784
1475
  transporter;
1476
+ /**
1477
+ * Initializes the SES transport with the provided configuration.
1478
+ *
1479
+ * Configures the AWS SES client and wraps it in a nodemailer transporter
1480
+ * for consistent message handling.
1481
+ *
1482
+ * @param config - AWS SES connection and retry configuration.
1483
+ */
785
1484
  constructor(config) {
1485
+ super({
1486
+ maxRetries: config.maxRetries,
1487
+ retryDelay: config.retryDelay,
1488
+ backoffMultiplier: config.backoffMultiplier
1489
+ });
786
1490
  const clientConfig = { region: config.region };
787
1491
  if (config.accessKeyId && config.secretAccessKey) {
788
1492
  clientConfig.credentials = {
@@ -795,7 +1499,17 @@ var SesTransport = class {
795
1499
  SES: { ses, aws: { SendRawEmailCommand } }
796
1500
  });
797
1501
  }
798
- async send(message) {
1502
+ /**
1503
+ * Internal method to perform the actual SES delivery.
1504
+ *
1505
+ * Converts the generic `Message` object into a raw email format and sends it
1506
+ * via the SES `SendRawEmail` API.
1507
+ *
1508
+ * @param message - The message to deliver.
1509
+ * @returns A promise that resolves when SES accepts the message for delivery.
1510
+ * @throws {Error} If the SES API returns an error or connection fails.
1511
+ */
1512
+ async doSend(message) {
799
1513
  await this.transporter.sendMail({
800
1514
  from: this.formatAddress(message.from),
801
1515
  to: message.to.map(this.formatAddress),
@@ -816,6 +1530,12 @@ var SesTransport = class {
816
1530
  }))
817
1531
  });
818
1532
  }
1533
+ /**
1534
+ * Formats an Address object into a standard RFC 822 string.
1535
+ *
1536
+ * @param addr - The address object to format.
1537
+ * @returns A string in the format "Name <email@example.com>" or just "email@example.com".
1538
+ */
819
1539
  formatAddress(addr) {
820
1540
  return addr.name ? `"${addr.name}" <${addr.address}>` : addr.address;
821
1541
  }
@@ -823,12 +1543,46 @@ var SesTransport = class {
823
1543
 
824
1544
  // src/transports/SmtpTransport.ts
825
1545
  import nodemailer2 from "nodemailer";
826
- var SmtpTransport = class {
1546
+ var SmtpTransport = class extends BaseTransport {
827
1547
  transporter;
1548
+ /**
1549
+ * Initializes the SMTP transport with the provided configuration.
1550
+ *
1551
+ * Sets up the underlying nodemailer transporter with connection pooling enabled.
1552
+ *
1553
+ * @param config - SMTP connection and retry configuration.
1554
+ */
828
1555
  constructor(config) {
829
- this.transporter = nodemailer2.createTransport(config);
1556
+ super({
1557
+ maxRetries: config.maxRetries,
1558
+ retryDelay: config.retryDelay,
1559
+ backoffMultiplier: config.backoffMultiplier
1560
+ });
1561
+ this.transporter = nodemailer2.createTransport({
1562
+ host: config.host,
1563
+ port: config.port,
1564
+ secure: config.secure,
1565
+ auth: config.auth,
1566
+ tls: config.tls,
1567
+ pool: true,
1568
+ maxConnections: config.poolSize ?? 5,
1569
+ maxMessages: Infinity,
1570
+ rateDelta: 1e3,
1571
+ rateLimit: 10,
1572
+ socketTimeout: config.maxIdleTime ?? 3e4,
1573
+ greetingTimeout: 3e4
1574
+ });
830
1575
  }
831
- async send(message) {
1576
+ /**
1577
+ * Internal method to perform the actual SMTP delivery.
1578
+ *
1579
+ * Maps the generic `Message` object to the format expected by nodemailer.
1580
+ *
1581
+ * @param message - The message to deliver.
1582
+ * @returns A promise that resolves when the SMTP server accepts the message.
1583
+ * @throws {Error} If the SMTP server rejects the message or connection fails.
1584
+ */
1585
+ async doSend(message) {
832
1586
  await this.transporter.sendMail({
833
1587
  from: this.formatAddress(message.from),
834
1588
  to: message.to.map(this.formatAddress),
@@ -849,18 +1603,147 @@ var SmtpTransport = class {
849
1603
  }))
850
1604
  });
851
1605
  }
1606
+ /**
1607
+ * Gracefully shuts down the transport and closes all pooled connections.
1608
+ *
1609
+ * This should be called during application shutdown to ensure no resources are leaked.
1610
+ *
1611
+ * @returns A promise that resolves when all connections are closed.
1612
+ *
1613
+ * @example
1614
+ * ```typescript
1615
+ * await transport.close();
1616
+ * ```
1617
+ */
1618
+ async close() {
1619
+ this.transporter.close();
1620
+ }
1621
+ /**
1622
+ * Verifies the SMTP connection and authentication.
1623
+ *
1624
+ * Useful for health checks or validating configuration during startup.
1625
+ *
1626
+ * @returns A promise that resolves to true if the connection is valid, false otherwise.
1627
+ *
1628
+ * @example
1629
+ * ```typescript
1630
+ * const isValid = await transport.verify();
1631
+ * if (!isValid) throw new Error('SMTP configuration is invalid');
1632
+ * ```
1633
+ */
1634
+ async verify() {
1635
+ try {
1636
+ await this.transporter.verify();
1637
+ return true;
1638
+ } catch {
1639
+ return false;
1640
+ }
1641
+ }
1642
+ /**
1643
+ * Formats an Address object into a standard RFC 822 string.
1644
+ *
1645
+ * @param addr - The address object to format.
1646
+ * @returns A string in the format "Name <email@example.com>" or just "email@example.com".
1647
+ */
852
1648
  formatAddress(addr) {
853
1649
  return addr.name ? `"${addr.name}" <${addr.address}>` : addr.address;
854
1650
  }
855
1651
  };
1652
+
1653
+ // src/webhooks/SendGridWebhookDriver.ts
1654
+ var SendGridWebhookDriver = class {
1655
+ constructor(config = {}) {
1656
+ this.config = config;
1657
+ }
1658
+ /**
1659
+ * Handles the SendGrid webhook request.
1660
+ */
1661
+ async handle(c) {
1662
+ const body = await c.req.json();
1663
+ const events = Array.isArray(body) ? body : [body];
1664
+ if (this.config.publicKey) {
1665
+ const signature = c.req.header("X-Twilio-Email-Event-Webhook-Signature");
1666
+ const timestamp = c.req.header("X-Twilio-Email-Event-Webhook-Timestamp");
1667
+ if (!signature || !timestamp) {
1668
+ throw new Error("Missing SendGrid signature headers");
1669
+ }
1670
+ if (!this.verifySignature(JSON.stringify(body), signature, timestamp)) {
1671
+ throw new Error("Invalid SendGrid signature");
1672
+ }
1673
+ }
1674
+ if (events.length === 0) {
1675
+ return null;
1676
+ }
1677
+ return events.map((e) => ({
1678
+ event: e.event,
1679
+ payload: e
1680
+ }));
1681
+ }
1682
+ /**
1683
+ * Verifies the SendGrid webhook signature.
1684
+ *
1685
+ * @param payload - Raw request body string.
1686
+ * @param signature - Signature from X-Twilio-Email-Event-Webhook-Signature header.
1687
+ * @param timestamp - Timestamp from X-Twilio-Email-Event-Webhook-Timestamp header.
1688
+ * @returns True if signature is valid.
1689
+ *
1690
+ * @remarks
1691
+ * Real SendGrid validation uses Elliptic Curve (ECDSA).
1692
+ * This is a placeholder for the logic structure.
1693
+ */
1694
+ verifySignature(_payload, _signature, _timestamp) {
1695
+ if (!this.config.publicKey) {
1696
+ return true;
1697
+ }
1698
+ return true;
1699
+ }
1700
+ };
1701
+
1702
+ // src/webhooks/SesWebhookDriver.ts
1703
+ var SesWebhookDriver = class {
1704
+ /**
1705
+ * Handles the AWS SES/SNS webhook request.
1706
+ *
1707
+ * @param c - The Gravito request context.
1708
+ * @returns Array of processed events or null if ignored.
1709
+ */
1710
+ async handle(c) {
1711
+ const body = await c.req.json();
1712
+ if (body.Type === "SubscriptionConfirmation") {
1713
+ return [{ event: "sns:subscription", payload: body }];
1714
+ }
1715
+ if (body.Type === "Notification") {
1716
+ const message = typeof body.Message === "string" ? JSON.parse(body.Message) : body.Message;
1717
+ const eventType = message.notificationType?.toLowerCase() || "unknown";
1718
+ return [
1719
+ {
1720
+ event: eventType,
1721
+ payload: message
1722
+ }
1723
+ ];
1724
+ }
1725
+ return null;
1726
+ }
1727
+ };
856
1728
  export {
1729
+ BaseTransport,
857
1730
  DevMailbox,
858
1731
  HtmlRenderer,
859
1732
  LogTransport,
1733
+ MailErrorCode,
1734
+ MailTransportError,
860
1735
  Mailable,
861
1736
  MemoryTransport,
1737
+ MjmlRenderer,
862
1738
  OrbitSignal,
1739
+ ReactMjmlRenderer,
1740
+ SendGridWebhookDriver,
863
1741
  SesTransport,
1742
+ SesWebhookDriver,
864
1743
  SmtpTransport,
865
- TemplateRenderer
1744
+ TemplateRenderer,
1745
+ TypedMailable,
1746
+ VueMjmlRenderer,
1747
+ baseLayout,
1748
+ transactionLayout
866
1749
  };