@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
@@ -1,45 +1,66 @@
1
1
  import { randomUUID } from 'node:crypto'
2
- import type { Envelope, Message } from '../types'
2
+ import type { Message } from '../types'
3
+ import type { MailboxStorage } from './storage/MailboxStorage'
4
+ import { MemoryMailboxStorage } from './storage/MemoryMailboxStorage'
3
5
 
4
6
  /**
5
- * Represents a captured email in the development mailbox.
6
- *
7
- * @public
7
+ * Entry structure for messages stored in DevMailbox.
8
8
  */
9
9
  export interface MailboxEntry {
10
- /** Unique identifier for the email */
10
+ /** Unique identifier for the entry. */
11
11
  id: string
12
- /** Email envelope information (from, to, subject, etc.) */
13
- envelope: Envelope
14
- /** HTML content of the email */
12
+ /** The email envelope metadata. */
13
+ envelope: any
14
+ /** The rendered HTML content. */
15
15
  html: string
16
- /** Plain text content of the email (optional) */
16
+ /** Optional plain text content. */
17
17
  text?: string
18
- /** Timestamp when the email was captured */
18
+ /** Timestamp when the message was captured. */
19
19
  sentAt: Date
20
20
  }
21
21
 
22
22
  /**
23
- * In-memory mailbox for capturing emails during development.
23
+ * Capture and store emails during development for preview and testing.
24
24
  *
25
- * Stores up to 50 recent emails and provides methods to list,
26
- * retrieve, and delete captured messages.
25
+ * Supports different storage engines (Memory, FileSystem) and implements
26
+ * capacity limits via a Ring Buffer strategy.
27
27
  *
28
28
  * @example
29
29
  * ```typescript
30
+ * // Default memory mailbox
30
31
  * const mailbox = new DevMailbox()
31
- * mailbox.add(message)
32
- * const emails = mailbox.list()
32
+ *
33
+ * // Persistent file mailbox
34
+ * const storage = new FileMailboxStorage('./storage/mail')
35
+ * const persistentMailbox = new DevMailbox(100, storage)
33
36
  * ```
34
37
  *
35
38
  * @since 3.0.0
36
39
  * @public
37
40
  */
38
41
  export class DevMailbox {
39
- private entries: MailboxEntry[] = []
40
- private maxEntries = 50
42
+ private storage: MailboxStorage
43
+ private _maxEntries = 50
41
44
 
42
- add(message: Message): MailboxEntry {
45
+ /**
46
+ * Creates an instance of DevMailbox.
47
+ *
48
+ * @param maxEntries - Maximum number of emails to store (default: 50)
49
+ * @param storage - Optional custom storage engine (defaults to Memory)
50
+ */
51
+ constructor(maxEntries?: number, storage?: MailboxStorage) {
52
+ if (maxEntries !== undefined) {
53
+ this._maxEntries = maxEntries
54
+ }
55
+ this.storage = storage ?? new MemoryMailboxStorage()
56
+ }
57
+
58
+ /**
59
+ * Adds a new message to the mailbox.
60
+ *
61
+ * If the mailbox exceeds the maximum capacity, the oldest messages are removed.
62
+ */
63
+ async add(message: Message): Promise<MailboxEntry> {
43
64
  const entry: MailboxEntry = {
44
65
  id: randomUUID(),
45
66
  envelope: {
@@ -57,34 +78,69 @@ export class DevMailbox {
57
78
  sentAt: new Date(),
58
79
  }
59
80
 
60
- this.entries.unshift(entry)
61
-
62
- // Limit size
63
- if (this.entries.length > this.maxEntries) {
64
- this.entries = this.entries.slice(0, this.maxEntries)
65
- }
81
+ await this.storage.push(entry)
82
+ await this.storage.trim(this._maxEntries)
66
83
 
67
84
  return entry
68
85
  }
69
86
 
70
- list(): MailboxEntry[] {
71
- 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: number): Promise<void> {
93
+ this._maxEntries = count
94
+ await this.storage.trim(this._maxEntries)
95
+ }
96
+
97
+ /**
98
+ * Returns the maximum capacity of the mailbox.
99
+ */
100
+ get maxEntries(): number {
101
+ return this._maxEntries
72
102
  }
73
103
 
74
- get(id: string): MailboxEntry | undefined {
75
- return this.entries.find((e) => e.id === id)
104
+ /**
105
+ * Lists all messages in the mailbox.
106
+ */
107
+ async list(): Promise<MailboxEntry[]> {
108
+ return await this.storage.all()
76
109
  }
77
110
 
78
- delete(id: string): boolean {
79
- const index = this.entries.findIndex((e) => e.id === id)
111
+ /**
112
+ * Retrieves a specific message by ID.
113
+ */
114
+ async get(id: string): Promise<MailboxEntry | undefined> {
115
+ const entries = await this.storage.all()
116
+ return entries.find((e) => e.id === id)
117
+ }
118
+
119
+ /**
120
+ * Deletes a specific message by ID.
121
+ */
122
+ async delete(id: string): Promise<boolean> {
123
+ if (this.storage.delete) {
124
+ return await this.storage.delete(id)
125
+ }
126
+
127
+ const entries = await this.storage.all()
128
+ const index = entries.findIndex((e) => e.id === id)
80
129
  if (index !== -1) {
81
- this.entries.splice(index, 1)
130
+ await this.clear()
131
+ entries.splice(index, 1)
132
+ for (const entry of entries.reverse()) {
133
+ await this.storage.push(entry)
134
+ }
82
135
  return true
83
136
  }
84
137
  return false
85
138
  }
86
139
 
87
- clear(): void {
88
- this.entries = []
140
+ /**
141
+ * Clears all messages from the mailbox.
142
+ */
143
+ async clear(): Promise<void> {
144
+ await this.storage.clear()
89
145
  }
90
146
  }
@@ -78,8 +78,8 @@ export class DevServer {
78
78
  // 1. Mailbox List
79
79
  router.get(
80
80
  prefix,
81
- wrap((ctx) => {
82
- const entries = this.mailbox.list()
81
+ wrap(async (ctx) => {
82
+ const entries = await this.mailbox.list()
83
83
  ctx.header('Content-Type', 'text/html; charset=utf-8')
84
84
  return ctx.html(getMailboxHtml(entries, prefix))
85
85
  })
@@ -88,13 +88,13 @@ export class DevServer {
88
88
  // 2. Single Email Preview
89
89
  router.get(
90
90
  `${prefix}/:id`,
91
- wrap((ctx) => {
91
+ wrap(async (ctx) => {
92
92
  const id = ctx.req.param('id')
93
93
  if (!id) {
94
94
  return ctx.text('Bad Request', 400)
95
95
  }
96
96
 
97
- const entry = this.mailbox.get(id)
97
+ const entry = await this.mailbox.get(id)
98
98
  if (!entry) {
99
99
  return ctx.text('Email not found', 404)
100
100
  }
@@ -106,13 +106,13 @@ export class DevServer {
106
106
  // 3. Iframe Content: HTML
107
107
  router.get(
108
108
  `${prefix}/:id/html`,
109
- wrap((ctx) => {
109
+ wrap(async (ctx) => {
110
110
  const id = ctx.req.param('id')
111
111
  if (!id) {
112
112
  return ctx.text('Bad Request', 400)
113
113
  }
114
114
 
115
- const entry = this.mailbox.get(id)
115
+ const entry = await this.mailbox.get(id)
116
116
  if (!entry) {
117
117
  return ctx.text('Not found', 404)
118
118
  }
@@ -124,13 +124,13 @@ export class DevServer {
124
124
  // 4. Iframe Content: Text
125
125
  router.get(
126
126
  `${prefix}/:id/text`,
127
- wrap((ctx) => {
127
+ wrap(async (ctx) => {
128
128
  const id = ctx.req.param('id')
129
129
  if (!id) {
130
130
  return ctx.text('Bad Request', 400)
131
131
  }
132
132
 
133
- const entry = this.mailbox.get(id)
133
+ const entry = await this.mailbox.get(id)
134
134
  if (!entry) {
135
135
  return ctx.text('Not found', 404)
136
136
  }
@@ -143,13 +143,13 @@ export class DevServer {
143
143
  // 5. Raw JSON
144
144
  router.get(
145
145
  `${prefix}/:id/raw`,
146
- wrap((ctx) => {
146
+ wrap(async (ctx) => {
147
147
  const id = ctx.req.param('id')
148
148
  if (!id) {
149
149
  return ctx.json({ error: 'Bad Request' }, 400)
150
150
  }
151
151
 
152
- const entry = this.mailbox.get(id)
152
+ const entry = await this.mailbox.get(id)
153
153
  if (!entry) {
154
154
  return ctx.json({ error: 'Not found' }, 404)
155
155
  }
@@ -167,13 +167,13 @@ export class DevServer {
167
167
 
168
168
  router.delete(
169
169
  `${prefix}/:id`,
170
- wrap((ctx) => {
170
+ wrap(async (ctx) => {
171
171
  const id = ctx.req.param('id')
172
172
  if (!id) {
173
173
  return ctx.json({ success: false, error: 'Bad Request' }, 400)
174
174
  }
175
175
 
176
- const success = this.mailbox.delete(id)
176
+ const success = await this.mailbox.delete(id)
177
177
  return ctx.json({ success })
178
178
  })
179
179
  )
@@ -181,8 +181,8 @@ export class DevServer {
181
181
  // 7. API: Clear All
182
182
  router.delete(
183
183
  prefix,
184
- wrap((ctx) => {
185
- this.mailbox.clear()
184
+ wrap(async (ctx) => {
185
+ await this.mailbox.clear()
186
186
  return ctx.json({ success: true })
187
187
  })
188
188
  )
@@ -0,0 +1,66 @@
1
+ import { mkdir, readFile, unlink, writeFile } from 'node:fs/promises'
2
+ import { join } from 'node:path'
3
+ import type { MailboxEntry } from '../DevMailbox'
4
+ import type { MailboxStorage } from './MailboxStorage'
5
+
6
+ /**
7
+ * File-based storage for DevMailbox (Persistence).
8
+ * Stores emails as JSON in a specified directory.
9
+ */
10
+ export class FileMailboxStorage implements MailboxStorage {
11
+ private filePath: string
12
+
13
+ constructor(storageDir: string) {
14
+ this.filePath = join(storageDir, 'mailbox.json')
15
+ }
16
+
17
+ async all(): Promise<MailboxEntry[]> {
18
+ try {
19
+ const content = await readFile(this.filePath, 'utf-8')
20
+ return JSON.parse(content)
21
+ } catch (_e) {
22
+ return []
23
+ }
24
+ }
25
+
26
+ async push(entry: MailboxEntry): Promise<void> {
27
+ const entries = await this.all()
28
+ entries.unshift(entry)
29
+ await this.save(entries)
30
+ }
31
+
32
+ async trim(max: number): Promise<void> {
33
+ const entries = await this.all()
34
+ if (entries.length > max) {
35
+ await this.save(entries.slice(0, max))
36
+ }
37
+ }
38
+
39
+ async clear(): Promise<void> {
40
+ try {
41
+ await unlink(this.filePath)
42
+ } catch (_e) {
43
+ // Ignore if file doesn't exist
44
+ }
45
+ }
46
+
47
+ async delete(id: string): Promise<boolean> {
48
+ const entries = await this.all()
49
+ const index = entries.findIndex((e) => e.id === id)
50
+ if (index !== -1) {
51
+ entries.splice(index, 1)
52
+ await this.save(entries)
53
+ return true
54
+ }
55
+ return false
56
+ }
57
+
58
+ private async save(entries: MailboxEntry[]): Promise<void> {
59
+ try {
60
+ await mkdir(join(this.filePath, '..'), { recursive: true })
61
+ await writeFile(this.filePath, JSON.stringify(entries, null, 2), 'utf-8')
62
+ } catch (error) {
63
+ console.error('[FileMailboxStorage] Failed to save mailbox:', error)
64
+ }
65
+ }
66
+ }
@@ -0,0 +1,15 @@
1
+ import type { MailboxEntry } from '../DevMailbox'
2
+
3
+ /**
4
+ * Interface for DevMailbox storage engines.
5
+ */
6
+ export interface MailboxStorage {
7
+ /** Retrieve all entries. */
8
+ all(): Promise<MailboxEntry[]>
9
+ /** Add a single entry. */
10
+ push(entry: MailboxEntry): Promise<void>
11
+ /** Trim entries to a specific count. */
12
+ trim(max: number): Promise<void>
13
+ clear(): Promise<void>
14
+ delete?(id: string): Promise<boolean>
15
+ }
@@ -0,0 +1,36 @@
1
+ import type { MailboxEntry } from '../DevMailbox'
2
+ import type { MailboxStorage } from './MailboxStorage'
3
+
4
+ /**
5
+ * Memory-based storage for DevMailbox (Default).
6
+ */
7
+ export class MemoryMailboxStorage implements MailboxStorage {
8
+ private entries: MailboxEntry[] = []
9
+
10
+ async all(): Promise<MailboxEntry[]> {
11
+ return [...this.entries]
12
+ }
13
+
14
+ async push(entry: MailboxEntry): Promise<void> {
15
+ this.entries.unshift(entry)
16
+ }
17
+
18
+ async trim(max: number): Promise<void> {
19
+ if (this.entries.length > max) {
20
+ this.entries = this.entries.slice(0, max)
21
+ }
22
+ }
23
+
24
+ async clear(): Promise<void> {
25
+ this.entries = []
26
+ }
27
+
28
+ async delete(id: string): Promise<boolean> {
29
+ const index = this.entries.findIndex((e) => e.id === id)
30
+ if (index !== -1) {
31
+ this.entries.splice(index, 1)
32
+ return true
33
+ }
34
+ return false
35
+ }
36
+ }
@@ -47,7 +47,7 @@ export function getMailboxHtml(entries: MailboxEntry[], prefix: string): string
47
47
  ${entry.envelope.subject || '(No Subject)'}
48
48
  </div>
49
49
  <div class="meta" style="margin-top: 4px;">
50
- To: ${entry.envelope.to?.map((t) => t.address).join(', ')}
50
+ To: ${entry.envelope.to?.map((t: any) => t.address).join(', ')}
51
51
  </div>
52
52
  </a>
53
53
  `
@@ -14,7 +14,7 @@ export function getPreviewHtml(entry: MailboxEntry, prefix: string): string {
14
14
  const from = entry.envelope.from
15
15
  ? `${entry.envelope.from.name || ''} &lt;${entry.envelope.from.address}&gt;`
16
16
  : 'Unknown'
17
- const to = entry.envelope.to?.map((t) => t.address).join(', ') || 'Unknown'
17
+ const to = entry.envelope.to?.map((t: any) => t.address).join(', ') || 'Unknown'
18
18
 
19
19
  const content = `
20
20
  <div class="header">
@@ -31,12 +31,12 @@ export function getPreviewHtml(entry: MailboxEntry, prefix: string): string {
31
31
  <div class="meta" style="margin-bottom: 5px;">To: ${to}</div>
32
32
  ${
33
33
  entry.envelope.cc
34
- ? `<div class="meta" style="margin-bottom: 5px;">CC: ${entry.envelope.cc.map((t) => t.address).join(', ')}</div>`
34
+ ? `<div class="meta" style="margin-bottom: 5px;">CC: ${entry.envelope.cc.map((t: any) => t.address).join(', ')}</div>`
35
35
  : ''
36
36
  }
37
37
  ${
38
38
  entry.envelope.bcc
39
- ? `<div class="meta" style="margin-bottom: 5px;">BCC: ${entry.envelope.bcc.map((t) => t.address).join(', ')}</div>`
39
+ ? `<div class="meta" style="margin-bottom: 5px;">BCC: ${entry.envelope.bcc.map((t: any) => t.address).join(', ')}</div>`
40
40
  : ''
41
41
  }
42
42
  ${entry.envelope.priority ? `<div class="meta" style="margin-bottom: 5px;">Priority: ${entry.envelope.priority}</div>` : ''}
@@ -49,7 +49,7 @@ export function getPreviewHtml(entry: MailboxEntry, prefix: string): string {
49
49
  <div style="display: flex; gap: 10px; flex-wrap: wrap;">
50
50
  ${entry.envelope.attachments
51
51
  .map(
52
- (att) => `
52
+ (att: any) => `
53
53
  <div style="background: var(--bg-dark); padding: 8px 12px; border-radius: 6px; border: 1px solid var(--border); display: flex; align-items: center; gap: 8px;">
54
54
  <span style="font-size: 20px;">📎</span>
55
55
  <div>
package/src/errors.ts ADDED
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Mail transport error codes.
3
+ *
4
+ * Categorizes common failure modes in the mail delivery process to allow
5
+ * for programmatic handling (e.g., retries on rate limits).
6
+ *
7
+ * @public
8
+ * @since 3.1.0
9
+ */
10
+ export enum MailErrorCode {
11
+ /** Connection to mail server failed */
12
+ CONNECTION_FAILED = 'CONNECTION_FAILED',
13
+ /** Authentication with mail server failed */
14
+ AUTH_FAILED = 'AUTH_FAILED',
15
+ /** One or more recipients were rejected by the server */
16
+ RECIPIENT_REJECTED = 'RECIPIENT_REJECTED',
17
+ /** The message was rejected by the server */
18
+ MESSAGE_REJECTED = 'MESSAGE_REJECTED',
19
+ /** Rate limit exceeded */
20
+ RATE_LIMIT = 'RATE_LIMIT',
21
+ /** Unknown or unclassified error */
22
+ UNKNOWN = 'UNKNOWN',
23
+ }
24
+
25
+ /**
26
+ * Error class for mail transport failures.
27
+ *
28
+ * Provides structured error information for mail sending failures,
29
+ * including error codes and original cause tracking for debugging.
30
+ *
31
+ * @example
32
+ * ```typescript
33
+ * throw new MailTransportError(
34
+ * 'Failed to connect to SMTP server',
35
+ * MailErrorCode.CONNECTION_FAILED,
36
+ * originalError
37
+ * )
38
+ * ```
39
+ *
40
+ * @public
41
+ * @since 3.1.0
42
+ */
43
+ export class MailTransportError extends Error {
44
+ /**
45
+ * Create a new mail transport error.
46
+ *
47
+ * @param message - Human-readable error message
48
+ * @param code - Categorized error code
49
+ * @param cause - Original error that caused this failure
50
+ *
51
+ * @example
52
+ * ```typescript
53
+ * const error = new MailTransportError('Auth failed', MailErrorCode.AUTH_FAILED);
54
+ * ```
55
+ */
56
+ constructor(
57
+ message: string,
58
+ public readonly code: MailErrorCode,
59
+ public readonly cause?: Error
60
+ ) {
61
+ super(message)
62
+ this.name = 'MailTransportError'
63
+
64
+ // Maintain proper stack trace in V8 environments
65
+ if (Error.captureStackTrace) {
66
+ Error.captureStackTrace(this, MailTransportError)
67
+ }
68
+ }
69
+ }
package/src/events.ts ADDED
@@ -0,0 +1,72 @@
1
+ import type { Mailable } from './Mailable'
2
+ import type { Message } from './types'
3
+
4
+ /**
5
+ * Mail event types.
6
+ *
7
+ * Defines the available lifecycle hooks for the mail service.
8
+ *
9
+ * @public
10
+ * @since 3.1.0
11
+ */
12
+ export type MailEventType =
13
+ | 'beforeSend'
14
+ | 'afterSend'
15
+ | 'sendFailed'
16
+ | 'beforeRender'
17
+ | 'afterRender'
18
+ | 'webhookReceived'
19
+
20
+ /**
21
+ * Mail lifecycle event.
22
+ *
23
+ * Emitted at key points during email sending lifecycle.
24
+ * Used for logging, analytics, or custom processing.
25
+ *
26
+ * @example
27
+ * ```typescript
28
+ * mail.on('afterSend', async (event) => {
29
+ * console.log('Email sent to:', event.message?.to)
30
+ * })
31
+ * ```
32
+ *
33
+ * @public
34
+ * @since 3.1.0
35
+ */
36
+ export interface MailEvent {
37
+ /** Type of event */
38
+ type: MailEventType
39
+ /** Mailable instance that triggered the event */
40
+ mailable: Mailable
41
+ /** Finalized message (available for send events) */
42
+ message?: Message
43
+ /** Error that occurred (available for sendFailed event) */
44
+ error?: Error
45
+ /** Timestamp when event occurred */
46
+ timestamp: Date
47
+ /** Webhook payload (available for webhookReceived event) */
48
+ webhook?: {
49
+ driver: string
50
+ event: string
51
+ payload: any
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Mail event handler function.
57
+ *
58
+ * Defines the signature for functions that subscribe to mail events.
59
+ *
60
+ * @param event - The mail event object
61
+ *
62
+ * @example
63
+ * ```typescript
64
+ * const handler: MailEventHandler = (event) => {
65
+ * console.log(`Event ${event.type} triggered`);
66
+ * };
67
+ * ```
68
+ *
69
+ * @public
70
+ * @since 3.1.0
71
+ */
72
+ export type MailEventHandler = (event: MailEvent) => void | Promise<void>
package/src/index.ts CHANGED
@@ -1,17 +1,34 @@
1
+ /**
2
+ * OrbitSignal - The central Event Bus and Mail Service for Gravito.
3
+ *
4
+ * This module provides the core infrastructure for sending emails and managing
5
+ * cross-module signals within the Gravito ecosystem. It supports multiple
6
+ * transport drivers, template rendering, and a robust development environment.
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+
1
11
  // import './augmentation'
2
12
 
3
13
  export type { Queueable } from '@gravito/stream'
4
14
  export { DevMailbox, type MailboxEntry } from './dev/DevMailbox'
15
+ export { MailErrorCode, MailTransportError } from './errors'
16
+ export type { MailEvent, MailEventHandler, MailEventType } from './events'
5
17
  export { Mailable } from './Mailable'
6
18
  export { OrbitSignal } from './OrbitSignal'
7
19
  export { HtmlRenderer } from './renderers/HtmlRenderer'
20
+ export { MjmlRenderer } from './renderers/MjmlRenderer'
21
+ export * from './renderers/mjml-templates'
22
+ export { ReactMjmlRenderer } from './renderers/ReactMjmlRenderer'
8
23
  export type { Renderer, RenderResult } from './renderers/Renderer'
9
24
  export { TemplateRenderer } from './renderers/TemplateRenderer'
25
+ export { VueMjmlRenderer } from './renderers/VueMjmlRenderer'
26
+ export { TypedMailable } from './TypedMailable'
27
+ export { BaseTransport, type TransportOptions } from './transports/BaseTransport'
10
28
  export { LogTransport } from './transports/LogTransport'
11
29
  export { MemoryTransport } from './transports/MemoryTransport'
12
30
  export { SesTransport } from './transports/SesTransport'
13
31
  export { SmtpTransport } from './transports/SmtpTransport'
14
-
15
32
  export type { Transport } from './transports/Transport'
16
33
  export type {
17
34
  Address,
@@ -20,3 +37,5 @@ export type {
20
37
  MailConfig,
21
38
  Message,
22
39
  } from './types'
40
+ export { type SendGridWebhookConfig, SendGridWebhookDriver } from './webhooks/SendGridWebhookDriver'
41
+ export { SesWebhookDriver } from './webhooks/SesWebhookDriver'
@@ -1,39 +1,41 @@
1
+ import { stripHtml } from '../utils/html'
1
2
  import type { Renderer, RenderResult } from './Renderer'
2
3
 
3
4
  /**
4
5
  * Renderer for plain HTML content.
5
6
  *
6
- * Renders static HTML strings and automatically generates
7
- * a plain text version by stripping HTML tags.
7
+ * The simplest renderer - accepts raw HTML strings and automatically generates
8
+ * a plain text version by stripping HTML tags. Ideal for pre-rendered HTML or
9
+ * when using external HTML generation libraries.
8
10
  *
9
11
  * @example
10
12
  * ```typescript
11
- * const renderer = new HtmlRenderer('<h1>Hello</h1><p>World</p>')
12
- * const { html, text } = await renderer.render()
13
- * // html: '<h1>Hello</h1><p>World</p>'
14
- * // text: 'Hello World'
13
+ * const renderer = new HtmlRenderer('<h1>Hello</h1>');
14
+ * const result = await renderer.render();
15
+ * // result.html: '<h1>Hello</h1>'
16
+ * // result.text: 'Hello'
15
17
  * ```
16
18
  *
17
- * @since 3.0.0
18
19
  * @public
20
+ * @since 3.0.0
19
21
  */
20
22
  export class HtmlRenderer implements Renderer {
23
+ /**
24
+ * Creates an instance of HtmlRenderer.
25
+ *
26
+ * @param content - The raw HTML string to be rendered.
27
+ */
21
28
  constructor(private content: string) {}
22
29
 
30
+ /**
31
+ * Returns the original HTML and a stripped plain text version.
32
+ *
33
+ * @returns A promise resolving to the rendered content.
34
+ */
23
35
  async render(): Promise<RenderResult> {
24
36
  return {
25
37
  html: this.content,
26
- text: this.stripHtml(this.content),
38
+ text: stripHtml(this.content),
27
39
  }
28
40
  }
29
-
30
- private stripHtml(html: string): string {
31
- return html
32
- .replace(/<style(?:\s[^>]*)?>[\s\S]*?<\/style>/gi, '') // Remove styles
33
- .replace(/<script(?:\s[^>]*)?>[\s\S]*?<\/script>/gi, '') // Remove scripts
34
- .replace(/<[^>]+>/g, '') // Remove tags
35
- .replace(/&nbsp;/g, ' ') // Replace non-breaking space
36
- .replace(/\s+/g, ' ') // Collapse whitespace
37
- .trim()
38
- }
39
41
  }