@gravito/signal 3.1.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/dist/OrbitSignal.d.ts +6 -0
  2. package/dist/errors/codes.d.ts +19 -0
  3. package/dist/errors.d.ts +18 -11
  4. package/dist/index.cjs +55148 -81886
  5. package/dist/index.d.ts +514 -1
  6. package/dist/index.js +61553 -0
  7. package/dist/index.mjs +55158 -81907
  8. package/dist/transports/BaseTransport.d.ts +15 -23
  9. package/package.json +16 -4
  10. package/CHANGELOG.md +0 -74
  11. package/build.ts +0 -133
  12. package/dist/index.cjs.map +0 -712
  13. package/dist/index.mjs.map +0 -710
  14. package/doc/ADVANCED_RENDERING.md +0 -71
  15. package/doc/DISTRIBUTED_MESSAGING.md +0 -79
  16. package/doc/OPTIMIZATION_PLAN.md +0 -496
  17. package/package.json.bak +0 -75
  18. package/scripts/check-coverage.ts +0 -64
  19. package/src/Mailable.ts +0 -674
  20. package/src/OrbitSignal.ts +0 -451
  21. package/src/Queueable.ts +0 -9
  22. package/src/TypedMailable.ts +0 -96
  23. package/src/dev/DevMailbox.ts +0 -146
  24. package/src/dev/DevServer.ts +0 -192
  25. package/src/dev/storage/FileMailboxStorage.ts +0 -66
  26. package/src/dev/storage/MailboxStorage.ts +0 -15
  27. package/src/dev/storage/MemoryMailboxStorage.ts +0 -36
  28. package/src/dev/ui/mailbox.ts +0 -77
  29. package/src/dev/ui/preview.ts +0 -103
  30. package/src/dev/ui/shared.ts +0 -60
  31. package/src/errors.ts +0 -69
  32. package/src/events.ts +0 -72
  33. package/src/index.ts +0 -41
  34. package/src/renderers/HtmlRenderer.ts +0 -41
  35. package/src/renderers/MjmlRenderer.ts +0 -73
  36. package/src/renderers/ReactMjmlRenderer.ts +0 -94
  37. package/src/renderers/ReactRenderer.ts +0 -66
  38. package/src/renderers/Renderer.ts +0 -67
  39. package/src/renderers/TemplateRenderer.ts +0 -84
  40. package/src/renderers/VueMjmlRenderer.ts +0 -99
  41. package/src/renderers/VueRenderer.ts +0 -71
  42. package/src/renderers/mjml-templates.ts +0 -50
  43. package/src/transports/BaseTransport.ts +0 -148
  44. package/src/transports/LogTransport.ts +0 -55
  45. package/src/transports/MemoryTransport.ts +0 -55
  46. package/src/transports/SesTransport.ts +0 -129
  47. package/src/transports/SmtpTransport.ts +0 -184
  48. package/src/transports/Transport.ts +0 -45
  49. package/src/types.ts +0 -309
  50. package/src/utils/html.ts +0 -43
  51. package/src/webhooks/SendGridWebhookDriver.ts +0 -80
  52. package/src/webhooks/SesWebhookDriver.ts +0 -44
  53. package/tests/DevMailbox.test.ts +0 -54
  54. package/tests/FileMailboxStorage.test.ts +0 -56
  55. package/tests/MjmlLayout.test.ts +0 -28
  56. package/tests/MjmlRenderer.test.ts +0 -53
  57. package/tests/OrbitSignalWebhook.test.ts +0 -56
  58. package/tests/ReactMjmlRenderer.test.ts +0 -33
  59. package/tests/SendGridWebhookDriver.test.ts +0 -69
  60. package/tests/SesWebhookDriver.test.ts +0 -46
  61. package/tests/VueMjmlRenderer.test.ts +0 -35
  62. package/tests/dev-server.test.ts +0 -66
  63. package/tests/log-transport.test.ts +0 -21
  64. package/tests/mailable-extra.test.ts +0 -68
  65. package/tests/mailable.test.ts +0 -77
  66. package/tests/orbit-signal.test.ts +0 -43
  67. package/tests/renderers.test.ts +0 -58
  68. package/tests/template-renderer.test.ts +0 -24
  69. package/tests/transports.test.ts +0 -52
  70. package/tests/ui.test.ts +0 -37
  71. package/tsconfig.build.json +0 -24
  72. package/tsconfig.json +0 -9
@@ -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/Queueable.ts DELETED
@@ -1,9 +0,0 @@
1
- /**
2
- * The Queueable interface has been moved to `@gravito/stream`.
3
- *
4
- * This file is kept only as a backward-compatible re-export.
5
- * New code should import from `@gravito/stream` directly.
6
- *
7
- * @deprecated Use `import type { Queueable } from '@gravito/stream'`
8
- */
9
- export type { Queueable } from '@gravito/stream'
@@ -1,96 +0,0 @@
1
- import { Mailable } from './Mailable'
2
-
3
- /**
4
- * Abstract base class for strongly-typed Mailable messages.
5
- *
6
- * @description
7
- * TypedMailable extends the base Mailable class to provide compile-time type safety
8
- * for email data props. This ensures that the data passed to templates, React, or Vue
9
- * components is correctly typed and validated at build time.
10
- *
11
- * @typeParam TData - The shape of data required by this mailable's template/component.
12
- * Must extend Record<string, unknown>.
13
- *
14
- * @example
15
- * ```typescript
16
- * import { TypedMailable } from '@gravito/signal'
17
- *
18
- * // Define the data interface
19
- * interface WelcomeData {
20
- * name: string
21
- * email: string
22
- * activationUrl: string
23
- * }
24
- *
25
- * // Create strongly-typed mailable
26
- * class WelcomeEmail extends TypedMailable<WelcomeData> {
27
- * protected data: WelcomeData
28
- *
29
- * constructor(data: WelcomeData) {
30
- * super()
31
- * this.data = data
32
- * }
33
- *
34
- * build() {
35
- * return this
36
- * .to(this.data.email)
37
- * .subject('Welcome to Gravito!')
38
- * .view('emails/welcome', this.data) // Type-safe: compiler ensures WelcomeData matches template
39
- * }
40
- * }
41
- *
42
- * // Usage - compiler enforces correct data shape
43
- * const email = new WelcomeEmail({
44
- * name: 'Alice',
45
- * email: 'alice@example.com',
46
- * activationUrl: 'https://app.com/activate?token=abc123'
47
- * })
48
- *
49
- * await mail.send(email)
50
- * ```
51
- *
52
- * @example
53
- * ```typescript
54
- * // With React components
55
- * interface InvoiceData {
56
- * invoiceNumber: string
57
- * amount: number
58
- * dueDate: Date
59
- * items: Array<{ name: string; price: number }>
60
- * }
61
- *
62
- * class InvoiceEmail extends TypedMailable<InvoiceData> {
63
- * protected data: InvoiceData
64
- *
65
- * constructor(data: InvoiceData) {
66
- * super()
67
- * this.data = data
68
- * }
69
- *
70
- * build() {
71
- * return this
72
- * .to('billing@example.com')
73
- * .subject(`Invoice ${this.data.invoiceNumber}`)
74
- * .react(InvoiceComponent, this.data) // Type-safe props
75
- * }
76
- * }
77
- * ```
78
- *
79
- * @see {@link Mailable} Base mailable class
80
- * @see {@link OrbitSignal} Mail service orchestrator
81
- *
82
- * @public
83
- * @since 3.0.0
84
- */
85
- export abstract class TypedMailable<TData extends Record<string, unknown>> extends Mailable {
86
- /**
87
- * The strongly-typed data for this mailable.
88
- *
89
- * This property holds the data that will be passed to the template or component
90
- * during rendering. By defining it as an abstract property with the generic
91
- * type TData, we force subclasses to provide a concrete, type-safe implementation.
92
- *
93
- * @protected
94
- */
95
- protected abstract data: TData
96
- }
@@ -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
- }