@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
@@ -0,0 +1,148 @@
1
+ import { MailErrorCode, MailTransportError } from '../errors'
2
+ import type { Message, Transport } from '../types'
3
+
4
+ /**
5
+ * Transport retry configuration options.
6
+ *
7
+ * Defines the behavior of the automatic retry mechanism, including the number of attempts
8
+ * and the timing between them using exponential backoff.
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * const options: TransportOptions = {
13
+ * maxRetries: 5,
14
+ * retryDelay: 500,
15
+ * backoffMultiplier: 3
16
+ * };
17
+ * ```
18
+ *
19
+ * @public
20
+ */
21
+ export interface TransportOptions {
22
+ /**
23
+ * Maximum number of retry attempts before giving up.
24
+ * Set to 0 to disable retries.
25
+ */
26
+ maxRetries?: number
27
+ /**
28
+ * Initial delay in milliseconds before the first retry attempt.
29
+ */
30
+ retryDelay?: number
31
+ /**
32
+ * Multiplier applied to the delay after each failed attempt.
33
+ * Used to implement exponential backoff to avoid overwhelming the service.
34
+ */
35
+ backoffMultiplier?: number
36
+ }
37
+
38
+ /**
39
+ * Base transport class with automatic retry mechanism.
40
+ *
41
+ * This abstract class provides a robust foundation for all transport implementations by
42
+ * handling transient failures through an exponential backoff retry strategy. It ensures
43
+ * that temporary network issues or service rate limits do not immediately fail the email delivery.
44
+ *
45
+ * The retry mechanism works as follows:
46
+ * 1. Attempt to send the message via `doSend()`.
47
+ * 2. If it fails, wait for `retryDelay` milliseconds.
48
+ * 3. Increment the delay by `backoffMultiplier` for the next attempt.
49
+ * 4. Repeat until success or `maxRetries` is reached.
50
+ *
51
+ * @example
52
+ * ```typescript
53
+ * class MyTransport extends BaseTransport {
54
+ * constructor() {
55
+ * super({ maxRetries: 3, retryDelay: 1000 })
56
+ * }
57
+ *
58
+ * protected async doSend(message: Message): Promise<void> {
59
+ * // Actual implementation of the sending logic
60
+ * // If this throws, BaseTransport will catch and retry
61
+ * }
62
+ * }
63
+ * ```
64
+ *
65
+ * @public
66
+ */
67
+ export abstract class BaseTransport implements Transport {
68
+ protected options: Required<TransportOptions>
69
+
70
+ /**
71
+ * Initializes the transport with retry options.
72
+ *
73
+ * @param options - Configuration for the retry mechanism.
74
+ */
75
+ constructor(options?: TransportOptions) {
76
+ this.options = {
77
+ maxRetries: options?.maxRetries ?? 3,
78
+ retryDelay: options?.retryDelay ?? 1000,
79
+ backoffMultiplier: options?.backoffMultiplier ?? 2,
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Orchestrates the message delivery with retry logic.
85
+ *
86
+ * This method wraps the concrete `doSend` implementation in a retry loop.
87
+ * It tracks the last error encountered to provide context if all retries fail.
88
+ *
89
+ * @param message - The message to be delivered.
90
+ * @returns A promise that resolves when the message is successfully sent.
91
+ * @throws {MailTransportError} If the message cannot be sent after the maximum number of retries.
92
+ *
93
+ * @example
94
+ * ```typescript
95
+ * const transport = new SmtpTransport(config);
96
+ * try {
97
+ * await transport.send(message);
98
+ * } catch (error) {
99
+ * console.error('Failed to send email after retries', error);
100
+ * }
101
+ * ```
102
+ */
103
+ async send(message: Message): Promise<void> {
104
+ let lastError: Error | undefined
105
+ let delay = this.options.retryDelay
106
+
107
+ for (let attempt = 0; attempt <= this.options.maxRetries; attempt++) {
108
+ try {
109
+ return await this.doSend(message)
110
+ } catch (error) {
111
+ lastError = error as Error
112
+
113
+ if (attempt < this.options.maxRetries) {
114
+ await this.sleep(delay)
115
+ delay *= this.options.backoffMultiplier
116
+ }
117
+ }
118
+ }
119
+
120
+ throw new MailTransportError(
121
+ `Mail sending failed after ${this.options.maxRetries} retries`,
122
+ MailErrorCode.UNKNOWN,
123
+ lastError
124
+ )
125
+ }
126
+
127
+ /**
128
+ * Actual transport implementation to be provided by subclasses.
129
+ *
130
+ * This method should contain the protocol-specific logic for delivering the message.
131
+ * It will be automatically retried by the `send` method if it throws an error.
132
+ *
133
+ * @param message - The message to send.
134
+ * @returns A promise that resolves when the delivery is successful.
135
+ * @throws {Error} Any error encountered during delivery, which will trigger a retry.
136
+ */
137
+ protected abstract doSend(message: Message): Promise<void>
138
+
139
+ /**
140
+ * Utility method to pause execution for a given duration.
141
+ *
142
+ * @param ms - Milliseconds to sleep.
143
+ * @returns A promise that resolves after the delay.
144
+ */
145
+ private sleep(ms: number): Promise<void> {
146
+ return new Promise((resolve) => setTimeout(resolve, ms))
147
+ }
148
+ }
@@ -3,20 +3,42 @@ import type { Message, Transport } from '../types'
3
3
  /**
4
4
  * Log transport for development and testing.
5
5
  *
6
- * Logs email details to the console instead of sending them.
7
- * Useful for debugging and local development.
6
+ * This transport outputs email details directly to the console instead of performing
7
+ * actual delivery. It is essential for local development to avoid sending real emails
8
+ * while still being able to verify the content, recipients, and subject of outgoing mail.
8
9
  *
9
10
  * @example
10
11
  * ```typescript
11
- * const transport = new LogTransport()
12
- * await transport.send(message)
13
- * // Outputs email details to console
12
+ * import { LogTransport } from '@gravito/signal';
13
+ *
14
+ * const transport = new LogTransport();
15
+ * await transport.send({
16
+ * from: { address: 'dev@localhost' },
17
+ * to: [{ address: 'user@example.com' }],
18
+ * subject: 'Test Email',
19
+ * html: '<h1>Hello</h1>'
20
+ * });
14
21
  * ```
15
22
  *
16
- * @since 3.0.0
17
23
  * @public
18
24
  */
19
25
  export class LogTransport implements Transport {
26
+ /**
27
+ * Outputs the message details to the system console.
28
+ *
29
+ * Formats the email metadata (From, To, Subject) and content size into a readable
30
+ * block in the console output.
31
+ *
32
+ * @param message - The message to log.
33
+ * @returns A promise that resolves immediately after logging.
34
+ *
35
+ * @example
36
+ * ```typescript
37
+ * const transport = new LogTransport();
38
+ * await transport.send(message);
39
+ * // Console: 📧 [OrbitSignal] Email Sent (Simulated)...
40
+ * ```
41
+ */
20
42
  async send(message: Message): Promise<void> {
21
43
  console.log('\n📧 [OrbitSignal] Email Sent (Simulated):')
22
44
  console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
@@ -4,22 +4,50 @@ import type { Message, Transport } from '../types'
4
4
  /**
5
5
  * Memory transport for development mode.
6
6
  *
7
- * Captures emails to an in-memory mailbox instead of sending them.
8
- * Used automatically when `devMode` is enabled in OrbitSignal.
7
+ * This transport captures outgoing emails and stores them in an in-memory mailbox.
8
+ * It is primarily used by the Gravito Dev UI to provide a live preview of emails
9
+ * during development without requiring an external mail server.
9
10
  *
10
11
  * @example
11
12
  * ```typescript
12
- * const mailbox = new DevMailbox()
13
- * const transport = new MemoryTransport(mailbox)
14
- * await transport.send(message)
13
+ * import { DevMailbox, MemoryTransport } from '@gravito/signal';
14
+ *
15
+ * const mailbox = new DevMailbox();
16
+ * const transport = new MemoryTransport(mailbox);
17
+ * await transport.send(message);
18
+ *
19
+ * console.log(mailbox.getAll().length); // 1
15
20
  * ```
16
21
  *
17
- * @since 3.0.0
18
22
  * @public
19
23
  */
20
24
  export class MemoryTransport implements Transport {
25
+ /**
26
+ * Creates a new MemoryTransport instance.
27
+ *
28
+ * @param mailbox - The in-memory storage where messages will be collected.
29
+ */
21
30
  constructor(private mailbox: DevMailbox) {}
22
31
 
32
+ /**
33
+ * Stores the message in the associated mailbox.
34
+ *
35
+ * The message is added to the internal list of the `DevMailbox` instance,
36
+ * making it available for retrieval by the Dev UI or test assertions.
37
+ *
38
+ * @param message - The message to store.
39
+ * @returns A promise that resolves once the message is added to the mailbox.
40
+ *
41
+ * @example
42
+ * ```typescript
43
+ * await transport.send({
44
+ * from: { address: 'dev@localhost' },
45
+ * to: [{ address: 'test@example.com' }],
46
+ * subject: 'Memory Test',
47
+ * html: '<p>Stored in memory</p>'
48
+ * });
49
+ * ```
50
+ */
23
51
  async send(message: Message): Promise<void> {
24
52
  this.mailbox.add(message)
25
53
  // console.log(`[MemoryTransport] Email stored in DevMailbox for: ${message.to.map(t => t.address).join(', ')}`);
@@ -1,45 +1,74 @@
1
1
  import { SESClient, SendRawEmailCommand } from '@aws-sdk/client-ses'
2
2
  import nodemailer from 'nodemailer'
3
- import type { Address, Message, Transport } from '../types'
3
+ import type { Address, Message } from '../types'
4
+ import { BaseTransport, type TransportOptions } from './BaseTransport'
4
5
 
5
6
  /**
6
7
  * Configuration for AWS SES email transport.
7
8
  *
9
+ * Defines the AWS region and credentials required to communicate with the
10
+ * Amazon Simple Email Service API.
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * const config: SesConfig = {
15
+ * region: 'us-east-1',
16
+ * accessKeyId: 'AKIA...',
17
+ * secretAccessKey: 'wJalr...',
18
+ * maxRetries: 5
19
+ * };
20
+ * ```
21
+ *
8
22
  * @public
9
- * @since 3.0.0
10
23
  */
11
- export interface SesConfig {
12
- /** AWS region (e.g., 'us-east-1') */
24
+ export interface SesConfig extends TransportOptions {
25
+ /** AWS region where the SES service is hosted (e.g., 'us-east-1'). */
13
26
  region: string
14
- /** AWS access key ID (optional, uses default credentials if not provided) */
27
+ /** AWS access key ID. If omitted, the SDK will attempt to use default credential providers. */
15
28
  accessKeyId?: string
16
- /** AWS secret access key (optional, uses default credentials if not provided) */
29
+ /** AWS secret access key. Required if accessKeyId is provided. */
17
30
  secretAccessKey?: string
18
31
  }
19
32
 
20
33
  /**
21
- * AWS SES (Simple Email Service) transport.
34
+ * AWS SES (Simple Email Service) transport with automatic retry.
22
35
  *
23
- * Sends emails using Amazon SES with support for attachments,
24
- * HTML/text content, and all standard email features.
36
+ * This transport delivers emails via the Amazon SES API. It requires the
37
+ * `@aws-sdk/client-ses` package to be installed as a dependency. It provides
38
+ * a reliable way to send high volumes of email using AWS infrastructure and
39
+ * includes automatic retry logic for transient API errors.
25
40
  *
26
41
  * @example
27
42
  * ```typescript
43
+ * import { SesTransport } from '@gravito/signal';
44
+ *
28
45
  * const transport = new SesTransport({
29
- * region: 'us-east-1',
30
- * accessKeyId: process.env.AWS_ACCESS_KEY_ID,
31
- * secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
32
- * })
33
- * await transport.send(message)
46
+ * region: 'us-west-2'
47
+ * });
48
+ *
49
+ * await transport.send(message);
34
50
  * ```
35
51
  *
36
- * @since 3.0.0
37
52
  * @public
38
53
  */
39
- export class SesTransport implements Transport {
54
+ export class SesTransport extends BaseTransport {
40
55
  private transporter: nodemailer.Transporter
41
56
 
57
+ /**
58
+ * Initializes the SES transport with the provided configuration.
59
+ *
60
+ * Configures the AWS SES client and wraps it in a nodemailer transporter
61
+ * for consistent message handling.
62
+ *
63
+ * @param config - AWS SES connection and retry configuration.
64
+ */
42
65
  constructor(config: SesConfig) {
66
+ super({
67
+ maxRetries: config.maxRetries,
68
+ retryDelay: config.retryDelay,
69
+ backoffMultiplier: config.backoffMultiplier,
70
+ })
71
+
43
72
  const clientConfig: any = { region: config.region }
44
73
 
45
74
  if (config.accessKeyId && config.secretAccessKey) {
@@ -56,7 +85,17 @@ export class SesTransport implements Transport {
56
85
  } as any)
57
86
  }
58
87
 
59
- async send(message: Message): Promise<void> {
88
+ /**
89
+ * Internal method to perform the actual SES delivery.
90
+ *
91
+ * Converts the generic `Message` object into a raw email format and sends it
92
+ * via the SES `SendRawEmail` API.
93
+ *
94
+ * @param message - The message to deliver.
95
+ * @returns A promise that resolves when SES accepts the message for delivery.
96
+ * @throws {Error} If the SES API returns an error or connection fails.
97
+ */
98
+ protected async doSend(message: Message): Promise<void> {
60
99
  await this.transporter.sendMail({
61
100
  from: this.formatAddress(message.from),
62
101
  to: message.to.map(this.formatAddress),
@@ -78,6 +117,12 @@ export class SesTransport implements Transport {
78
117
  })
79
118
  }
80
119
 
120
+ /**
121
+ * Formats an Address object into a standard RFC 822 string.
122
+ *
123
+ * @param addr - The address object to format.
124
+ * @returns A string in the format "Name <email@example.com>" or just "email@example.com".
125
+ */
81
126
  private formatAddress(addr: Address): string {
82
127
  return addr.name ? `"${addr.name}" <${addr.address}>` : addr.address
83
128
  }
@@ -1,66 +1,118 @@
1
1
  import nodemailer from 'nodemailer'
2
- import type { Address, Message, Transport } from '../types'
2
+ import type { Address, Message } from '../types'
3
+ import { BaseTransport, type TransportOptions } from './BaseTransport'
3
4
 
4
5
  /**
5
6
  * Configuration for SMTP email transport.
6
7
  *
8
+ * Defines the connection parameters, authentication, and pooling settings for
9
+ * communicating with an SMTP server.
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * const config: SmtpConfig = {
14
+ * host: 'smtp.mailtrap.io',
15
+ * port: 2525,
16
+ * auth: { user: 'username', pass: 'password' },
17
+ * poolSize: 10
18
+ * };
19
+ * ```
20
+ *
7
21
  * @public
8
- * @since 3.0.0
9
22
  */
10
- export interface SmtpConfig {
11
- /** SMTP server hostname */
23
+ export interface SmtpConfig extends TransportOptions {
24
+ /** SMTP server hostname or IP address. */
12
25
  host: string
13
- /** SMTP server port (typically 25, 465, or 587) */
26
+ /** SMTP server port (typically 25, 465, or 587). */
14
27
  port: number
15
- /** Use TLS/SSL connection (true for port 465) */
28
+ /** Whether to use a secure TLS/SSL connection. Should be true for port 465. */
16
29
  secure?: boolean
17
- /** Authentication credentials */
30
+ /** Authentication credentials for the SMTP server. */
18
31
  auth?: {
19
- /** SMTP username */
32
+ /** SMTP username. */
20
33
  user: string
21
- /** SMTP password */
34
+ /** SMTP password. */
22
35
  pass: string
23
36
  }
24
- /** TLS options */
37
+ /** TLS specific options for the connection. */
25
38
  tls?: {
26
- /** Reject unauthorized certificates */
39
+ /** Whether to reject unauthorized certificates (useful for self-signed certs). */
27
40
  rejectUnauthorized?: boolean
28
- /** Cipher suite */
41
+ /** Specific cipher suite to use for the connection. */
29
42
  ciphers?: string
30
43
  }
44
+ /** Number of concurrent connections to maintain in the pool. */
45
+ poolSize?: number
46
+ /** Maximum time in milliseconds a connection can remain idle before being closed. */
47
+ maxIdleTime?: number
31
48
  }
32
49
 
33
50
  /**
34
- * SMTP email transport.
51
+ * SMTP email transport with connection pooling and automatic retry.
35
52
  *
36
- * Sends emails using standard SMTP protocol with support for
37
- * authentication, TLS/SSL, attachments, and all email features.
53
+ * This transport uses the standard SMTP protocol to deliver emails. It leverages
54
+ * `nodemailer` for robust protocol implementation and includes built-in support
55
+ * for connection pooling to improve performance when sending multiple emails.
56
+ * It inherits automatic retry logic from `BaseTransport`.
38
57
  *
39
58
  * @example
40
59
  * ```typescript
60
+ * import { SmtpTransport } from '@gravito/signal';
61
+ *
41
62
  * const transport = new SmtpTransport({
42
- * host: 'smtp.gmail.com',
63
+ * host: 'smtp.example.com',
43
64
  * port: 587,
44
- * secure: false,
45
- * auth: {
46
- * user: 'user@gmail.com',
47
- * pass: 'app-password'
48
- * }
49
- * })
50
- * await transport.send(message)
65
+ * auth: { user: 'user', pass: 'pass' }
66
+ * });
67
+ *
68
+ * await transport.send(message);
51
69
  * ```
52
70
  *
53
- * @since 3.0.0
54
71
  * @public
55
72
  */
56
- export class SmtpTransport implements Transport {
73
+ export class SmtpTransport extends BaseTransport {
57
74
  private transporter: nodemailer.Transporter
58
75
 
76
+ /**
77
+ * Initializes the SMTP transport with the provided configuration.
78
+ *
79
+ * Sets up the underlying nodemailer transporter with connection pooling enabled.
80
+ *
81
+ * @param config - SMTP connection and retry configuration.
82
+ */
59
83
  constructor(config: SmtpConfig) {
60
- this.transporter = nodemailer.createTransport(config)
84
+ super({
85
+ maxRetries: config.maxRetries,
86
+ retryDelay: config.retryDelay,
87
+ backoffMultiplier: config.backoffMultiplier,
88
+ })
89
+
90
+ this.transporter = nodemailer.createTransport({
91
+ host: config.host,
92
+ port: config.port,
93
+ secure: config.secure,
94
+ auth: config.auth,
95
+ tls: config.tls,
96
+ pool: true,
97
+ maxConnections: config.poolSize ?? 5,
98
+ maxMessages: Infinity,
99
+ rateDelta: 1000,
100
+ rateLimit: 10,
101
+ socketTimeout: config.maxIdleTime ?? 30000,
102
+ greetingTimeout: 30000,
103
+ })
61
104
  }
62
105
 
63
- async send(message: Message): Promise<void> {
106
+ /**
107
+ * Internal method to perform the actual SMTP delivery.
108
+ *
109
+ * Maps the generic `Message` object to the format expected by nodemailer.
110
+ *
111
+ * @param message - The message to deliver.
112
+ * @returns A promise that resolves when the SMTP server accepts the message.
113
+ * @throws {Error} If the SMTP server rejects the message or connection fails.
114
+ */
115
+ protected async doSend(message: Message): Promise<void> {
64
116
  await this.transporter.sendMail({
65
117
  from: this.formatAddress(message.from),
66
118
  to: message.to.map(this.formatAddress),
@@ -82,6 +134,50 @@ export class SmtpTransport implements Transport {
82
134
  })
83
135
  }
84
136
 
137
+ /**
138
+ * Gracefully shuts down the transport and closes all pooled connections.
139
+ *
140
+ * This should be called during application shutdown to ensure no resources are leaked.
141
+ *
142
+ * @returns A promise that resolves when all connections are closed.
143
+ *
144
+ * @example
145
+ * ```typescript
146
+ * await transport.close();
147
+ * ```
148
+ */
149
+ async close(): Promise<void> {
150
+ this.transporter.close()
151
+ }
152
+
153
+ /**
154
+ * Verifies the SMTP connection and authentication.
155
+ *
156
+ * Useful for health checks or validating configuration during startup.
157
+ *
158
+ * @returns A promise that resolves to true if the connection is valid, false otherwise.
159
+ *
160
+ * @example
161
+ * ```typescript
162
+ * const isValid = await transport.verify();
163
+ * if (!isValid) throw new Error('SMTP configuration is invalid');
164
+ * ```
165
+ */
166
+ async verify(): Promise<boolean> {
167
+ try {
168
+ await this.transporter.verify()
169
+ return true
170
+ } catch {
171
+ return false
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Formats an Address object into a standard RFC 822 string.
177
+ *
178
+ * @param addr - The address object to format.
179
+ * @returns A string in the format "Name <email@example.com>" or just "email@example.com".
180
+ */
85
181
  private formatAddress(addr: Address): string {
86
182
  return addr.name ? `"${addr.name}" <${addr.address}>` : addr.address
87
183
  }
@@ -1,16 +1,45 @@
1
1
  import type { Message } from '../types'
2
2
 
3
3
  /**
4
- * Interface for email transport mechanisms (SMTP, SES, etc.).
4
+ * Interface for email transport mechanisms.
5
+ *
6
+ * Transports are responsible for the final delivery of an email message to its destination,
7
+ * whether it's a real SMTP server, a cloud service like AWS SES, or a local log for development.
8
+ * This abstraction allows the core mail service to remain agnostic of the delivery method.
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * class CustomTransport implements Transport {
13
+ * async send(message: Message): Promise<void> {
14
+ * // Implementation logic to deliver the message
15
+ * console.log(`Sending email to ${message.to[0].address}`);
16
+ * }
17
+ * }
18
+ * ```
5
19
  *
6
20
  * @public
7
- * @since 3.0.0
8
21
  */
9
22
  export interface Transport {
10
23
  /**
11
- * Send the given message using the underlying transport.
24
+ * Send the given message using the underlying transport mechanism.
25
+ *
26
+ * This method handles the actual communication with the delivery service.
27
+ * Implementations should handle connection management and protocol-specific logic.
28
+ *
29
+ * @param message - The finalized message object containing recipients, subject, and content.
30
+ * @returns A promise that resolves when the message has been successfully handed off to the transport.
31
+ * @throws {MailTransportError} If the delivery fails after all internal retry attempts.
12
32
  *
13
- * @param message - The finalized message to send.
33
+ * @example
34
+ * ```typescript
35
+ * const transport: Transport = new LogTransport();
36
+ * await transport.send({
37
+ * from: { address: 'sender@example.com' },
38
+ * to: [{ address: 'receiver@example.com' }],
39
+ * subject: 'Hello',
40
+ * html: '<p>World</p>'
41
+ * });
42
+ * ```
14
43
  */
15
44
  send(message: Message): Promise<void>
16
45
  }