@gravito/signal 3.0.4 → 3.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -15
- package/dist/Mailable.d.ts +453 -0
- package/dist/OrbitSignal.d.ts +267 -0
- package/{src/Queueable.ts → dist/Queueable.d.ts} +1 -1
- package/{src/TypedMailable.ts → dist/TypedMailable.d.ts} +12 -13
- package/dist/dev/DevMailbox.d.ts +79 -0
- package/dist/dev/DevServer.d.ts +39 -0
- package/dist/dev/storage/FileMailboxStorage.d.ts +16 -0
- package/dist/dev/storage/MailboxStorage.d.ts +14 -0
- package/dist/dev/storage/MemoryMailboxStorage.d.ts +13 -0
- package/dist/dev/ui/mailbox.d.ts +11 -0
- package/dist/dev/ui/preview.d.ts +11 -0
- package/dist/dev/ui/shared.d.ts +15 -0
- package/dist/errors.d.ts +58 -0
- package/{src/events.ts → dist/events.d.ts} +20 -29
- package/dist/{lib-HJTRWKU5.mjs → index.cjs} +4687 -10983
- package/dist/index.d.ts +303 -1874
- package/dist/index.js +317 -45939
- package/dist/index.mjs +59748 -27
- package/dist/renderers/HtmlRenderer.d.ts +34 -0
- package/dist/renderers/MjmlRenderer.d.ts +41 -0
- package/dist/renderers/ReactMjmlRenderer.d.ts +38 -0
- package/dist/renderers/ReactRenderer.d.ts +46 -0
- package/{src/renderers/Renderer.ts → dist/renderers/Renderer.d.ts} +23 -24
- package/dist/renderers/TemplateRenderer.d.ts +55 -0
- package/dist/renderers/VueMjmlRenderer.d.ts +39 -0
- package/dist/renderers/VueRenderer.d.ts +47 -0
- package/dist/renderers/mjml-templates.d.ts +11 -0
- package/dist/transports/BaseTransport.d.ts +111 -0
- package/dist/transports/LogTransport.d.ts +42 -0
- package/dist/transports/MemoryTransport.d.ts +51 -0
- package/dist/transports/SesTransport.d.ts +79 -0
- package/dist/transports/SmtpTransport.d.ts +124 -0
- package/dist/transports/Transport.d.ts +44 -0
- package/dist/types.d.ts +294 -0
- package/{src/utils/html.ts → dist/utils/html.d.ts} +1 -15
- package/dist/webhooks/SendGridWebhookDriver.d.ts +45 -0
- package/dist/webhooks/SesWebhookDriver.d.ts +23 -0
- package/package.json +20 -8
- package/CHANGELOG.md +0 -74
- package/dist/MjmlRenderer-IUH663FT.mjs +0 -8
- package/dist/ReactMjmlRenderer-C3P5YO5L.mjs +0 -8
- package/dist/ReactRenderer-24SQ4KRU.mjs +0 -27
- package/dist/ReactRenderer-2JFLRVST.mjs +0 -45
- package/dist/ReactRenderer-CMCAOEPH.mjs +0 -28
- package/dist/ReactRenderer-KYNA4WKE.mjs +0 -28
- package/dist/ReactRenderer-LYEOSYFS.mjs +0 -28
- package/dist/ReactRenderer-V54CUUEI.mjs +0 -45
- package/dist/VueMjmlRenderer-4F4CXHDB.mjs +0 -8
- package/dist/VueMjmlRenderer-5WZR4CQG.mjs +0 -8
- package/dist/VueMjmlRenderer-U5YMWI44.mjs +0 -8
- package/dist/VueRenderer-3YBRQXME.mjs +0 -48
- package/dist/VueRenderer-46JGXTJ2.mjs +0 -48
- package/dist/VueRenderer-5KWD4R3C.mjs +0 -48
- package/dist/VueRenderer-C23U4O5E.mjs +0 -48
- package/dist/VueRenderer-DWTCD2RF.mjs +0 -31
- package/dist/VueRenderer-IIR5SYTM.mjs +0 -31
- package/dist/VueRenderer-LEVDFLHP.mjs +0 -31
- package/dist/VueRenderer-RNHSCCRI.mjs +0 -48
- package/dist/VueRenderer-SUP66ISX.mjs +0 -29
- package/dist/chunk-3WOR3XSL.mjs +0 -82
- package/dist/chunk-DBFIVHHG.mjs +0 -79
- package/dist/chunk-EBO3CZXG.mjs +0 -15
- package/dist/chunk-HEBXNMVQ.mjs +0 -48
- package/dist/chunk-KB7IDDBT.mjs +0 -82
- package/dist/chunk-LZL5UUPC.mjs +0 -82
- package/dist/chunk-W6LXIJKK.mjs +0 -57
- package/dist/chunk-XBIVBJS2.mjs +0 -8
- package/dist/index.d.mts +0 -2115
- package/dist/server-renderer-4IM3P5XZ.mjs +0 -37183
- package/dist/server-renderer-4W4FI7YG.mjs +0 -37269
- package/dist/server-renderer-7KWFSTPV.mjs +0 -37193
- package/dist/server-renderer-S5FPSTJ2.mjs +0 -37183
- package/dist/server-renderer-X5LUFVWT.mjs +0 -37193
- package/doc/OPTIMIZATION_PLAN.md +0 -496
- package/scripts/check-coverage.ts +0 -64
- package/src/Mailable.ts +0 -674
- package/src/OrbitSignal.ts +0 -451
- package/src/dev/DevMailbox.ts +0 -146
- package/src/dev/DevServer.ts +0 -192
- package/src/dev/storage/FileMailboxStorage.ts +0 -66
- package/src/dev/storage/MailboxStorage.ts +0 -15
- package/src/dev/storage/MemoryMailboxStorage.ts +0 -36
- package/src/dev/ui/mailbox.ts +0 -77
- package/src/dev/ui/preview.ts +0 -103
- package/src/dev/ui/shared.ts +0 -60
- package/src/errors.ts +0 -69
- package/src/index.ts +0 -41
- package/src/renderers/HtmlRenderer.ts +0 -41
- package/src/renderers/MjmlRenderer.ts +0 -73
- package/src/renderers/ReactMjmlRenderer.ts +0 -94
- package/src/renderers/ReactRenderer.ts +0 -66
- package/src/renderers/TemplateRenderer.ts +0 -84
- package/src/renderers/VueMjmlRenderer.ts +0 -99
- package/src/renderers/VueRenderer.ts +0 -71
- package/src/renderers/mjml-templates.ts +0 -50
- package/src/transports/BaseTransport.ts +0 -148
- package/src/transports/LogTransport.ts +0 -55
- package/src/transports/MemoryTransport.ts +0 -55
- package/src/transports/SesTransport.ts +0 -129
- package/src/transports/SmtpTransport.ts +0 -184
- package/src/transports/Transport.ts +0 -45
- package/src/types.ts +0 -309
- package/src/webhooks/SendGridWebhookDriver.ts +0 -80
- package/src/webhooks/SesWebhookDriver.ts +0 -44
- package/tests/DevMailbox.test.ts +0 -54
- package/tests/FileMailboxStorage.test.ts +0 -56
- package/tests/MjmlLayout.test.ts +0 -28
- package/tests/MjmlRenderer.test.ts +0 -53
- package/tests/OrbitSignalWebhook.test.ts +0 -56
- package/tests/ReactMjmlRenderer.test.ts +0 -33
- package/tests/SendGridWebhookDriver.test.ts +0 -69
- package/tests/SesWebhookDriver.test.ts +0 -46
- package/tests/VueMjmlRenderer.test.ts +0 -35
- package/tests/dev-server.test.ts +0 -66
- package/tests/log-transport.test.ts +0 -21
- package/tests/mailable-extra.test.ts +0 -68
- package/tests/mailable.test.ts +0 -77
- package/tests/orbit-signal.test.ts +0 -43
- package/tests/renderers.test.ts +0 -58
- package/tests/template-renderer.test.ts +0 -24
- package/tests/transports.test.ts +0 -52
- package/tests/ui.test.ts +0 -37
- package/tsconfig.json +0 -14
package/src/OrbitSignal.ts
DELETED
|
@@ -1,451 +0,0 @@
|
|
|
1
|
-
import type { GravitoContext, GravitoNext, GravitoOrbit, PlanetCore } from '@gravito/core'
|
|
2
|
-
import { DevMailbox } from './dev/DevMailbox'
|
|
3
|
-
import { DevServer } from './dev/DevServer'
|
|
4
|
-
import type { MailEvent, MailEventHandler, MailEventType } from './events'
|
|
5
|
-
import type { Mailable } from './Mailable'
|
|
6
|
-
import { LogTransport } from './transports/LogTransport'
|
|
7
|
-
import { MemoryTransport } from './transports/MemoryTransport'
|
|
8
|
-
import type { MailConfig, Message } from './types'
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* OrbitSignal - Mail service orbit for Gravito framework.
|
|
12
|
-
*
|
|
13
|
-
* @description
|
|
14
|
-
* A production-ready email service providing multi-transport support, automatic retry,
|
|
15
|
-
* event-driven lifecycle hooks, and development tooling. Integrates seamlessly with
|
|
16
|
-
* Gravito's orbit system and queue infrastructure.
|
|
17
|
-
*
|
|
18
|
-
* @architecture
|
|
19
|
-
* ```
|
|
20
|
-
* OrbitSignal
|
|
21
|
-
* ├── Configuration (MailConfig)
|
|
22
|
-
* │ ├── transport: Transport (SMTP, SES, Log, Memory)
|
|
23
|
-
* │ ├── from: Default sender
|
|
24
|
-
* │ ├── devMode: Development interception
|
|
25
|
-
* │ └── translator: i18n support
|
|
26
|
-
* ├── Lifecycle Events
|
|
27
|
-
* │ ├── beforeRender → afterRender
|
|
28
|
-
* │ ├── beforeSend → afterSend
|
|
29
|
-
* │ └── sendFailed (on error)
|
|
30
|
-
* ├── Transport Layer
|
|
31
|
-
* │ ├── BaseTransport (retry, backoff)
|
|
32
|
-
* │ ├── SmtpTransport (connection pooling)
|
|
33
|
-
* │ ├── SesTransport (AWS SES)
|
|
34
|
-
* │ ├── LogTransport (console output)
|
|
35
|
-
* │ └── MemoryTransport (dev mode)
|
|
36
|
-
* └── Dev Tools
|
|
37
|
-
* ├── DevMailbox (in-memory storage)
|
|
38
|
-
* └── DevServer (preview UI at /__mail)
|
|
39
|
-
* ```
|
|
40
|
-
*
|
|
41
|
-
* @example
|
|
42
|
-
* **Basic SMTP Configuration**
|
|
43
|
-
* ```typescript
|
|
44
|
-
* import { OrbitSignal, SmtpTransport } from '@gravito/signal'
|
|
45
|
-
*
|
|
46
|
-
* const mail = new OrbitSignal({
|
|
47
|
-
* transport: new SmtpTransport({
|
|
48
|
-
* host: 'smtp.mailtrap.io',
|
|
49
|
-
* port: 2525,
|
|
50
|
-
* auth: { user: 'username', pass: 'password' }
|
|
51
|
-
* }),
|
|
52
|
-
* from: { name: 'My App', address: 'noreply@myapp.com' }
|
|
53
|
-
* })
|
|
54
|
-
*
|
|
55
|
-
* mail.install(core)
|
|
56
|
-
* ```
|
|
57
|
-
*
|
|
58
|
-
* @example
|
|
59
|
-
* **AWS SES with Retry Configuration**
|
|
60
|
-
* ```typescript
|
|
61
|
-
* import { OrbitSignal, SesTransport } from '@gravito/signal'
|
|
62
|
-
*
|
|
63
|
-
* const mail = new OrbitSignal({
|
|
64
|
-
* transport: new SesTransport({
|
|
65
|
-
* region: 'us-east-1',
|
|
66
|
-
* retries: 3,
|
|
67
|
-
* retryDelay: 1000,
|
|
68
|
-
* retryMultiplier: 2
|
|
69
|
-
* }),
|
|
70
|
-
* from: { name: 'Production App', address: 'noreply@example.com' }
|
|
71
|
-
* })
|
|
72
|
-
* ```
|
|
73
|
-
*
|
|
74
|
-
* @example
|
|
75
|
-
* **Development Mode with Preview UI**
|
|
76
|
-
* ```typescript
|
|
77
|
-
* const mail = new OrbitSignal({
|
|
78
|
-
* devMode: process.env.NODE_ENV === 'development',
|
|
79
|
-
* devUiPrefix: '/__mail',
|
|
80
|
-
* from: { name: 'Dev App', address: 'dev@localhost' }
|
|
81
|
-
* })
|
|
82
|
-
*
|
|
83
|
-
* // All emails intercepted to memory, view at http://localhost:3000/__mail
|
|
84
|
-
* ```
|
|
85
|
-
*
|
|
86
|
-
* @example
|
|
87
|
-
* **Event-Driven Analytics & Error Handling**
|
|
88
|
-
* ```typescript
|
|
89
|
-
* const mail = new OrbitSignal({ ... })
|
|
90
|
-
*
|
|
91
|
-
* // Track successful sends
|
|
92
|
-
* mail.on('afterSend', async (event) => {
|
|
93
|
-
* await analytics.track('email_sent', {
|
|
94
|
-
* to: event.message?.to,
|
|
95
|
-
* subject: event.message?.subject,
|
|
96
|
-
* timestamp: event.timestamp
|
|
97
|
-
* })
|
|
98
|
-
* })
|
|
99
|
-
*
|
|
100
|
-
* // Log failures for monitoring
|
|
101
|
-
* mail.on('sendFailed', async (event) => {
|
|
102
|
-
* logger.error('Email send failed', {
|
|
103
|
-
* error: event.error?.message,
|
|
104
|
-
* mailable: event.mailable.constructor.name
|
|
105
|
-
* })
|
|
106
|
-
* await sentry.captureException(event.error)
|
|
107
|
-
* })
|
|
108
|
-
* ```
|
|
109
|
-
*
|
|
110
|
-
* @example
|
|
111
|
-
* **SMTP with Connection Pooling**
|
|
112
|
-
* ```typescript
|
|
113
|
-
* const mail = new OrbitSignal({
|
|
114
|
-
* transport: new SmtpTransport({
|
|
115
|
-
* host: 'smtp.gmail.com',
|
|
116
|
-
* port: 465,
|
|
117
|
-
* secure: true,
|
|
118
|
-
* auth: { user: 'user@gmail.com', pass: 'app-password' },
|
|
119
|
-
* poolSize: 5,
|
|
120
|
-
* maxIdleTime: 30000
|
|
121
|
-
* })
|
|
122
|
-
* })
|
|
123
|
-
*
|
|
124
|
-
* // Graceful shutdown
|
|
125
|
-
* process.on('SIGTERM', async () => {
|
|
126
|
-
* await mail.config.transport?.close?.()
|
|
127
|
-
* })
|
|
128
|
-
* ```
|
|
129
|
-
*
|
|
130
|
-
* @example
|
|
131
|
-
* **Usage in Route Handlers**
|
|
132
|
-
* ```typescript
|
|
133
|
-
* // Injected automatically into GravitoContext
|
|
134
|
-
* app.post('/register', async (c) => {
|
|
135
|
-
* const user = await createUser(c.req.json())
|
|
136
|
-
*
|
|
137
|
-
* await c.get('mail').send(new WelcomeEmail(user))
|
|
138
|
-
*
|
|
139
|
-
* return c.json({ success: true })
|
|
140
|
-
* })
|
|
141
|
-
* ```
|
|
142
|
-
*
|
|
143
|
-
* @example
|
|
144
|
-
* **Queue Integration for Background Processing**
|
|
145
|
-
* ```typescript
|
|
146
|
-
* // Requires @gravito/stream
|
|
147
|
-
* const email = new WelcomeEmail(user)
|
|
148
|
-
* .onQueue('emails')
|
|
149
|
-
* .delay(60)
|
|
150
|
-
*
|
|
151
|
-
* await email.queue()
|
|
152
|
-
* ```
|
|
153
|
-
*
|
|
154
|
-
* @example
|
|
155
|
-
* **Error Handling Best Practices**
|
|
156
|
-
* ```typescript
|
|
157
|
-
* try {
|
|
158
|
-
* await mail.send(new InvoiceEmail(order))
|
|
159
|
-
* } catch (error) {
|
|
160
|
-
* if (error instanceof MailTransportError) {
|
|
161
|
-
* switch (error.code) {
|
|
162
|
-
* case MailErrorCode.RATE_LIMIT:
|
|
163
|
-
* await queue.pushDelayed(email, 300)
|
|
164
|
-
* break
|
|
165
|
-
* case MailErrorCode.RECIPIENT_REJECTED:
|
|
166
|
-
* await markUserEmailInvalid(user.id)
|
|
167
|
-
* break
|
|
168
|
-
* default:
|
|
169
|
-
* throw error
|
|
170
|
-
* }
|
|
171
|
-
* }
|
|
172
|
-
* }
|
|
173
|
-
* ```
|
|
174
|
-
*
|
|
175
|
-
* @see {@link Mailable} Base class for email definitions
|
|
176
|
-
* @see {@link TypedMailable} Strongly-typed mailable with generic data
|
|
177
|
-
* @see {@link Transport} Transport interface
|
|
178
|
-
* @see {@link BaseTransport} Retry-enabled base transport
|
|
179
|
-
* @see {@link MailConfig} Configuration interface
|
|
180
|
-
* @see {@link MailEvent} Event types
|
|
181
|
-
* @see {@link MailTransportError} Error handling
|
|
182
|
-
*
|
|
183
|
-
* @since 3.0.0
|
|
184
|
-
* @public
|
|
185
|
-
*/
|
|
186
|
-
export class OrbitSignal implements GravitoOrbit {
|
|
187
|
-
private config: MailConfig
|
|
188
|
-
private devMailbox?: DevMailbox
|
|
189
|
-
private core?: PlanetCore
|
|
190
|
-
private eventHandlers = new Map<MailEventType, MailEventHandler[]>()
|
|
191
|
-
|
|
192
|
-
constructor(config: MailConfig = {}) {
|
|
193
|
-
this.config = config
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* Install the orbit into PlanetCore.
|
|
198
|
-
*
|
|
199
|
-
* Registers the mail service in the IoC container and sets up development
|
|
200
|
-
* tools if enabled. It also injects the service into the GravitoContext
|
|
201
|
-
* for easy access in route handlers.
|
|
202
|
-
*
|
|
203
|
-
* @param core - The PlanetCore instance to install into
|
|
204
|
-
*
|
|
205
|
-
* @example
|
|
206
|
-
* ```typescript
|
|
207
|
-
* const mail = new OrbitSignal(config);
|
|
208
|
-
* mail.install(core);
|
|
209
|
-
* ```
|
|
210
|
-
*/
|
|
211
|
-
install(core: PlanetCore): void {
|
|
212
|
-
this.core = core
|
|
213
|
-
core.logger.info('[OrbitSignal] Initializing Mail Service')
|
|
214
|
-
|
|
215
|
-
// 1. Ensure transport exists (fallback to Log if not set)
|
|
216
|
-
if (!this.config.transport && !this.config.devMode) {
|
|
217
|
-
this.config.transport = new LogTransport()
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// 2. In Dev Mode, override transport and setup Dev Server
|
|
221
|
-
if (this.config.devMode) {
|
|
222
|
-
this.devMailbox = new DevMailbox()
|
|
223
|
-
this.config.transport = new MemoryTransport(this.devMailbox)
|
|
224
|
-
core.logger.info('[OrbitSignal] Dev Mode Enabled: Emails will be intercepted to Dev Mailbox')
|
|
225
|
-
|
|
226
|
-
const devServer = new DevServer(this.devMailbox, this.config.devUiPrefix || '/__mail', {
|
|
227
|
-
allowInProduction: this.config.devUiAllowInProduction,
|
|
228
|
-
gate: this.config.devUiGate,
|
|
229
|
-
})
|
|
230
|
-
devServer.register(core)
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// 3. Register in container
|
|
234
|
-
core.container.instance('mail', this)
|
|
235
|
-
|
|
236
|
-
// 4. Inject mail service into context for easy access in routes
|
|
237
|
-
core.adapter.use('*', async (c: GravitoContext, next: GravitoNext) => {
|
|
238
|
-
c.set('mail', this)
|
|
239
|
-
return await next()
|
|
240
|
-
})
|
|
241
|
-
|
|
242
|
-
// 5. Register Webhook Endpoint
|
|
243
|
-
if (this.config.webhookPrefix) {
|
|
244
|
-
// @ts-expect-error: Accessing internal adapter methods
|
|
245
|
-
core.adapter.post(`${this.config.webhookPrefix}/:driver`, async (c: GravitoContext) => {
|
|
246
|
-
const driverName = c.req.param('driver')
|
|
247
|
-
const driver = this.config.webhookDrivers?.[driverName as string]
|
|
248
|
-
|
|
249
|
-
if (!driver) {
|
|
250
|
-
return c.json({ error: `Webhook driver "${driverName}" not found` }, 404)
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
try {
|
|
254
|
-
const results = await driver.handle(c)
|
|
255
|
-
if (results && Array.isArray(results)) {
|
|
256
|
-
for (const result of results) {
|
|
257
|
-
await this.handleWebhook(driverName as string, result.event, result.payload)
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
return c.json({ success: true })
|
|
261
|
-
} catch (error) {
|
|
262
|
-
core.logger.error(`[OrbitSignal] Webhook error (${driverName}):`, error)
|
|
263
|
-
return c.json({ error: 'Webhook processing failed' }, 500)
|
|
264
|
-
}
|
|
265
|
-
})
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/**
|
|
270
|
-
* Internal: Handle processed webhook.
|
|
271
|
-
*/
|
|
272
|
-
private async handleWebhook(driver: string, event: string, payload: any): Promise<void> {
|
|
273
|
-
// We don't have a specific mailable for generic webhooks, so we create a dummy or pass null
|
|
274
|
-
// But since MailEvent requires a mailable, we might need to adjust the interface or pass a Proxy
|
|
275
|
-
// For now, let's assume it's acceptable to emit with a partial event if types allow,
|
|
276
|
-
// or we might need to add a specialized emitWebhook method.
|
|
277
|
-
|
|
278
|
-
await this.emit({
|
|
279
|
-
type: 'webhookReceived',
|
|
280
|
-
// biome-ignore lint/suspicious/noExplicitAny: Dummy mailable for webhook events
|
|
281
|
-
mailable: {} as any,
|
|
282
|
-
timestamp: new Date(),
|
|
283
|
-
webhook: { driver, event, payload },
|
|
284
|
-
})
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
/**
|
|
288
|
-
* Send a mailable instance immediately.
|
|
289
|
-
*
|
|
290
|
-
* Orchestrates the full email sending lifecycle: building the envelope,
|
|
291
|
-
* rendering content, emitting events, and delivering via the configured transport.
|
|
292
|
-
*
|
|
293
|
-
* @param mailable - The email definition to send
|
|
294
|
-
* @throws {Error} If mandatory fields (from, to) are missing or transport fails
|
|
295
|
-
*
|
|
296
|
-
* @example
|
|
297
|
-
* ```typescript
|
|
298
|
-
* await mail.send(new WelcomeEmail(user));
|
|
299
|
-
* ```
|
|
300
|
-
*/
|
|
301
|
-
async send(mailable: Mailable): Promise<void> {
|
|
302
|
-
try {
|
|
303
|
-
// 1. Build envelope and get configuration
|
|
304
|
-
const envelope = await mailable.buildEnvelope(this.config)
|
|
305
|
-
|
|
306
|
-
// Validate required fields
|
|
307
|
-
if (!envelope.from) {
|
|
308
|
-
throw new Error('Message is missing "from" address')
|
|
309
|
-
}
|
|
310
|
-
if (!envelope.to || envelope.to.length === 0) {
|
|
311
|
-
throw new Error('Message is missing "to" address')
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// 2. Emit beforeRender event
|
|
315
|
-
await this.emit({
|
|
316
|
-
type: 'beforeRender',
|
|
317
|
-
mailable,
|
|
318
|
-
timestamp: new Date(),
|
|
319
|
-
})
|
|
320
|
-
|
|
321
|
-
// 3. Render content
|
|
322
|
-
const content = await mailable.renderContent()
|
|
323
|
-
|
|
324
|
-
// 4. Emit afterRender event
|
|
325
|
-
await this.emit({
|
|
326
|
-
type: 'afterRender',
|
|
327
|
-
mailable,
|
|
328
|
-
timestamp: new Date(),
|
|
329
|
-
})
|
|
330
|
-
|
|
331
|
-
// 5. Construct full message
|
|
332
|
-
const message: Message = {
|
|
333
|
-
...envelope,
|
|
334
|
-
from: envelope.from!,
|
|
335
|
-
to: envelope.to!,
|
|
336
|
-
subject: envelope.subject || '(No Subject)',
|
|
337
|
-
priority: envelope.priority || 'normal',
|
|
338
|
-
html: content.html,
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
if (content.text) {
|
|
342
|
-
message.text = content.text
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// 6. Emit beforeSend event
|
|
346
|
-
await this.emit({
|
|
347
|
-
type: 'beforeSend',
|
|
348
|
-
mailable,
|
|
349
|
-
message,
|
|
350
|
-
timestamp: new Date(),
|
|
351
|
-
})
|
|
352
|
-
|
|
353
|
-
// 7. Send via transport
|
|
354
|
-
if (!this.config.transport) {
|
|
355
|
-
throw new Error('[OrbitSignal] No transport configured. Did you call register the orbit?')
|
|
356
|
-
}
|
|
357
|
-
await this.config.transport.send(message)
|
|
358
|
-
|
|
359
|
-
// 8. Emit afterSend event
|
|
360
|
-
await this.emit({
|
|
361
|
-
type: 'afterSend',
|
|
362
|
-
mailable,
|
|
363
|
-
message,
|
|
364
|
-
timestamp: new Date(),
|
|
365
|
-
})
|
|
366
|
-
} catch (error) {
|
|
367
|
-
// Emit sendFailed event
|
|
368
|
-
await this.emit({
|
|
369
|
-
type: 'sendFailed',
|
|
370
|
-
mailable,
|
|
371
|
-
error: error as Error,
|
|
372
|
-
timestamp: new Date(),
|
|
373
|
-
})
|
|
374
|
-
throw error
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
/**
|
|
379
|
-
* Queue a mailable instance for background processing.
|
|
380
|
-
*
|
|
381
|
-
* Attempts to use the 'queue' service (OrbitStream) if available in the
|
|
382
|
-
* container. Falls back to immediate sending if no queue service is found.
|
|
383
|
-
*
|
|
384
|
-
* @param mailable - The email definition to queue
|
|
385
|
-
*
|
|
386
|
-
* @example
|
|
387
|
-
* ```typescript
|
|
388
|
-
* await mail.queue(new WelcomeEmail(user));
|
|
389
|
-
* ```
|
|
390
|
-
*/
|
|
391
|
-
async queue(mailable: Mailable): Promise<void> {
|
|
392
|
-
try {
|
|
393
|
-
// 嘗試從容器獲取隊列服務 (OrbitStream)
|
|
394
|
-
const queue = this.core?.container.make<any>('queue')
|
|
395
|
-
if (queue) {
|
|
396
|
-
await queue.push(mailable)
|
|
397
|
-
return
|
|
398
|
-
}
|
|
399
|
-
} catch (_e) {
|
|
400
|
-
// 找不到隊列服務時,會拋出錯誤,我們捕捉並降級
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
// Fallback: 直接發送
|
|
404
|
-
await this.send(mailable)
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
/**
|
|
408
|
-
* Register an event handler.
|
|
409
|
-
*
|
|
410
|
-
* @description
|
|
411
|
-
* Subscribe to mail lifecycle events for logging, analytics, or custom processing.
|
|
412
|
-
*
|
|
413
|
-
* @param event - The event type to listen for
|
|
414
|
-
* @param handler - The handler function to execute
|
|
415
|
-
* @returns This instance for method chaining
|
|
416
|
-
*
|
|
417
|
-
* @example
|
|
418
|
-
* ```typescript
|
|
419
|
-
* mail.on('afterSend', async (event) => {
|
|
420
|
-
* await analytics.track('email_sent', {
|
|
421
|
-
* to: event.message?.to,
|
|
422
|
-
* subject: event.message?.subject
|
|
423
|
-
* })
|
|
424
|
-
* })
|
|
425
|
-
* ```
|
|
426
|
-
*
|
|
427
|
-
* @public
|
|
428
|
-
* @since 3.1.0
|
|
429
|
-
*/
|
|
430
|
-
on(event: MailEventType, handler: MailEventHandler): this {
|
|
431
|
-
const handlers = this.eventHandlers.get(event) || []
|
|
432
|
-
handlers.push(handler)
|
|
433
|
-
this.eventHandlers.set(event, handlers)
|
|
434
|
-
return this
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
private async emit(event: MailEvent): Promise<void> {
|
|
438
|
-
const handlers = this.eventHandlers.get(event.type) || []
|
|
439
|
-
for (const handler of handlers) {
|
|
440
|
-
await handler(event)
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
// Module augmentation for GravitoVariables
|
|
446
|
-
declare module '@gravito/core' {
|
|
447
|
-
interface GravitoVariables {
|
|
448
|
-
/** Mail service for sending emails */
|
|
449
|
-
mail?: OrbitSignal
|
|
450
|
-
}
|
|
451
|
-
}
|
package/src/dev/DevMailbox.ts
DELETED
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto'
|
|
2
|
-
import type { Message } from '../types'
|
|
3
|
-
import type { MailboxStorage } from './storage/MailboxStorage'
|
|
4
|
-
import { MemoryMailboxStorage } from './storage/MemoryMailboxStorage'
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Entry structure for messages stored in DevMailbox.
|
|
8
|
-
*/
|
|
9
|
-
export interface MailboxEntry {
|
|
10
|
-
/** Unique identifier for the entry. */
|
|
11
|
-
id: string
|
|
12
|
-
/** The email envelope metadata. */
|
|
13
|
-
envelope: any
|
|
14
|
-
/** The rendered HTML content. */
|
|
15
|
-
html: string
|
|
16
|
-
/** Optional plain text content. */
|
|
17
|
-
text?: string
|
|
18
|
-
/** Timestamp when the message was captured. */
|
|
19
|
-
sentAt: Date
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Capture and store emails during development for preview and testing.
|
|
24
|
-
*
|
|
25
|
-
* Supports different storage engines (Memory, FileSystem) and implements
|
|
26
|
-
* capacity limits via a Ring Buffer strategy.
|
|
27
|
-
*
|
|
28
|
-
* @example
|
|
29
|
-
* ```typescript
|
|
30
|
-
* // Default memory mailbox
|
|
31
|
-
* const mailbox = new DevMailbox()
|
|
32
|
-
*
|
|
33
|
-
* // Persistent file mailbox
|
|
34
|
-
* const storage = new FileMailboxStorage('./storage/mail')
|
|
35
|
-
* const persistentMailbox = new DevMailbox(100, storage)
|
|
36
|
-
* ```
|
|
37
|
-
*
|
|
38
|
-
* @since 3.0.0
|
|
39
|
-
* @public
|
|
40
|
-
*/
|
|
41
|
-
export class DevMailbox {
|
|
42
|
-
private storage: MailboxStorage
|
|
43
|
-
private _maxEntries = 50
|
|
44
|
-
|
|
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> {
|
|
64
|
-
const entry: MailboxEntry = {
|
|
65
|
-
id: randomUUID(),
|
|
66
|
-
envelope: {
|
|
67
|
-
from: message.from,
|
|
68
|
-
to: message.to,
|
|
69
|
-
...(message.cc ? { cc: message.cc } : {}),
|
|
70
|
-
...(message.bcc ? { bcc: message.bcc } : {}),
|
|
71
|
-
...(message.replyTo ? { replyTo: message.replyTo } : {}),
|
|
72
|
-
subject: message.subject,
|
|
73
|
-
...(message.priority ? { priority: message.priority } : {}),
|
|
74
|
-
...(message.attachments ? { attachments: message.attachments } : {}),
|
|
75
|
-
},
|
|
76
|
-
html: message.html,
|
|
77
|
-
...(message.text ? { text: message.text } : {}),
|
|
78
|
-
sentAt: new Date(),
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
await this.storage.push(entry)
|
|
82
|
-
await this.storage.trim(this._maxEntries)
|
|
83
|
-
|
|
84
|
-
return entry
|
|
85
|
-
}
|
|
86
|
-
|
|
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
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Lists all messages in the mailbox.
|
|
106
|
-
*/
|
|
107
|
-
async list(): Promise<MailboxEntry[]> {
|
|
108
|
-
return await this.storage.all()
|
|
109
|
-
}
|
|
110
|
-
|
|
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)
|
|
129
|
-
if (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
|
-
}
|
|
135
|
-
return true
|
|
136
|
-
}
|
|
137
|
-
return false
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Clears all messages from the mailbox.
|
|
142
|
-
*/
|
|
143
|
-
async clear(): Promise<void> {
|
|
144
|
-
await this.storage.clear()
|
|
145
|
-
}
|
|
146
|
-
}
|