@zyther/tzmail 0.1.4 → 0.5.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 (132) hide show
  1. package/README.md +411 -78
  2. package/dist/core/enums/theme.enum.d.ts +11 -1
  3. package/dist/core/enums/theme.enum.d.ts.map +1 -1
  4. package/dist/core/enums/theme.enum.js +10 -0
  5. package/dist/core/interfaces/email.interface.d.ts +95 -5
  6. package/dist/core/interfaces/email.interface.d.ts.map +1 -1
  7. package/dist/core/interfaces/template.interface.d.ts +12 -1
  8. package/dist/core/interfaces/template.interface.d.ts.map +1 -1
  9. package/dist/factories/email-factory.d.ts +32 -6
  10. package/dist/factories/email-factory.d.ts.map +1 -1
  11. package/dist/factories/email-factory.js +110 -6
  12. package/dist/factories/template-factory.d.ts.map +1 -1
  13. package/dist/factories/template-factory.js +170 -60
  14. package/dist/index.d.ts +16 -3
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +23 -1
  17. package/dist/providers/mailgun.transport.d.ts +10 -0
  18. package/dist/providers/mailgun.transport.d.ts.map +1 -0
  19. package/dist/providers/mailgun.transport.js +85 -0
  20. package/dist/providers/nodemailer.transport.d.ts +11 -0
  21. package/dist/providers/nodemailer.transport.d.ts.map +1 -0
  22. package/dist/providers/nodemailer.transport.js +23 -0
  23. package/dist/providers/provider-utils.d.ts +17 -0
  24. package/dist/providers/provider-utils.d.ts.map +1 -0
  25. package/dist/providers/provider-utils.js +89 -0
  26. package/dist/providers/provider.interface.d.ts +30 -0
  27. package/dist/providers/provider.interface.d.ts.map +1 -0
  28. package/dist/providers/provider.interface.js +2 -0
  29. package/dist/providers/sendgrid.transport.d.ts +10 -0
  30. package/dist/providers/sendgrid.transport.d.ts.map +1 -0
  31. package/dist/providers/sendgrid.transport.js +76 -0
  32. package/dist/queues/bullmq-email-queue.d.ts +153 -0
  33. package/dist/queues/bullmq-email-queue.d.ts.map +1 -0
  34. package/dist/queues/bullmq-email-queue.js +292 -0
  35. package/dist/services/attachment.service.d.ts +15 -37
  36. package/dist/services/attachment.service.d.ts.map +1 -1
  37. package/dist/services/attachment.service.js +58 -46
  38. package/dist/services/email.service.d.ts +22 -22
  39. package/dist/services/email.service.d.ts.map +1 -1
  40. package/dist/services/email.service.js +117 -33
  41. package/dist/services/template.service.d.ts +24 -111
  42. package/dist/services/template.service.d.ts.map +1 -1
  43. package/dist/services/template.service.js +273 -362
  44. package/dist/templates/base/color-accessibility.d.ts +4 -0
  45. package/dist/templates/base/color-accessibility.d.ts.map +1 -0
  46. package/dist/templates/base/color-accessibility.js +38 -0
  47. package/dist/templates/base/template-builder.d.ts +4 -1
  48. package/dist/templates/base/template-builder.d.ts.map +1 -1
  49. package/dist/templates/base/template-builder.js +276 -527
  50. package/dist/templates/themes/aurora.theme.d.ts +12 -0
  51. package/dist/templates/themes/aurora.theme.d.ts.map +1 -0
  52. package/dist/templates/themes/aurora.theme.js +88 -0
  53. package/dist/templates/themes/corporate.theme.d.ts +2 -1
  54. package/dist/templates/themes/corporate.theme.d.ts.map +1 -1
  55. package/dist/templates/themes/corporate.theme.js +139 -102
  56. package/dist/templates/themes/editorial.theme.d.ts +12 -0
  57. package/dist/templates/themes/editorial.theme.d.ts.map +1 -0
  58. package/dist/templates/themes/editorial.theme.js +90 -0
  59. package/dist/templates/themes/forest.theme.d.ts +12 -0
  60. package/dist/templates/themes/forest.theme.d.ts.map +1 -0
  61. package/dist/templates/themes/forest.theme.js +89 -0
  62. package/dist/templates/themes/glass.theme.d.ts +12 -0
  63. package/dist/templates/themes/glass.theme.d.ts.map +1 -0
  64. package/dist/templates/themes/glass.theme.js +88 -0
  65. package/dist/templates/themes/lavender.theme.d.ts +12 -0
  66. package/dist/templates/themes/lavender.theme.d.ts.map +1 -0
  67. package/dist/templates/themes/lavender.theme.js +91 -0
  68. package/dist/templates/themes/midnight.theme.d.ts +12 -0
  69. package/dist/templates/themes/midnight.theme.d.ts.map +1 -0
  70. package/dist/templates/themes/midnight.theme.js +91 -0
  71. package/dist/templates/themes/minimal.theme.d.ts +2 -1
  72. package/dist/templates/themes/minimal.theme.d.ts.map +1 -1
  73. package/dist/templates/themes/minimal.theme.js +142 -107
  74. package/dist/templates/themes/modern.theme.d.ts +2 -1
  75. package/dist/templates/themes/modern.theme.d.ts.map +1 -1
  76. package/dist/templates/themes/modern.theme.js +109 -74
  77. package/dist/templates/themes/monokai.theme.d.ts +2 -1
  78. package/dist/templates/themes/monokai.theme.d.ts.map +1 -1
  79. package/dist/templates/themes/monokai.theme.js +102 -66
  80. package/dist/templates/themes/neobrutal.theme.d.ts +12 -0
  81. package/dist/templates/themes/neobrutal.theme.d.ts.map +1 -0
  82. package/dist/templates/themes/neobrutal.theme.js +91 -0
  83. package/dist/templates/themes/ocean.theme.d.ts +12 -0
  84. package/dist/templates/themes/ocean.theme.d.ts.map +1 -0
  85. package/dist/templates/themes/ocean.theme.js +90 -0
  86. package/dist/templates/themes/pastel.theme.d.ts +12 -0
  87. package/dist/templates/themes/pastel.theme.d.ts.map +1 -0
  88. package/dist/templates/themes/pastel.theme.js +89 -0
  89. package/dist/templates/themes/sunset.theme.d.ts +12 -0
  90. package/dist/templates/themes/sunset.theme.d.ts.map +1 -0
  91. package/dist/templates/themes/sunset.theme.js +90 -0
  92. package/dist/templates/themes/system.theme.d.ts +2 -1
  93. package/dist/templates/themes/system.theme.d.ts.map +1 -1
  94. package/dist/templates/themes/system.theme.js +81 -47
  95. package/dist/templates/themes/theme.interface.d.ts +48 -0
  96. package/dist/templates/themes/theme.interface.d.ts.map +1 -1
  97. package/package.json +36 -14
  98. package/src/core/enums/template-part.enum.ts +7 -0
  99. package/src/core/enums/theme.enum.ts +18 -0
  100. package/src/core/interfaces/email.interface.ts +116 -0
  101. package/src/core/interfaces/template.interface.ts +71 -0
  102. package/src/factories/email-factory.ts +208 -0
  103. package/src/factories/template-factory.ts +201 -0
  104. package/src/index.ts +53 -0
  105. package/src/providers/mailgun.transport.ts +80 -0
  106. package/src/providers/nodemailer.transport.ts +20 -0
  107. package/src/providers/provider-utils.ts +91 -0
  108. package/src/providers/provider.interface.ts +26 -0
  109. package/src/providers/sendgrid.transport.ts +80 -0
  110. package/src/queues/bullmq-email-queue.ts +418 -0
  111. package/src/services/attachment.service.ts +98 -0
  112. package/src/services/email.service.ts +151 -0
  113. package/src/services/template.service.ts +431 -0
  114. package/src/templates/base/color-accessibility.ts +31 -0
  115. package/src/templates/base/template-builder.ts +438 -0
  116. package/src/templates/themes/aurora.theme.ts +91 -0
  117. package/src/templates/themes/corporate.theme.ts +156 -0
  118. package/src/templates/themes/editorial.theme.ts +93 -0
  119. package/src/templates/themes/forest.theme.ts +92 -0
  120. package/src/templates/themes/glass.theme.ts +91 -0
  121. package/src/templates/themes/lavender.theme.ts +94 -0
  122. package/src/templates/themes/midnight.theme.ts +94 -0
  123. package/src/templates/themes/minimal.theme.ts +158 -0
  124. package/src/templates/themes/modern.theme.ts +124 -0
  125. package/src/templates/themes/monokai.theme.ts +117 -0
  126. package/src/templates/themes/neobrutal.theme.ts +94 -0
  127. package/src/templates/themes/ocean.theme.ts +93 -0
  128. package/src/templates/themes/pastel.theme.ts +92 -0
  129. package/src/templates/themes/sunset.theme.ts +93 -0
  130. package/src/templates/themes/system.theme.ts +92 -0
  131. package/src/templates/themes/theme.interface.ts +95 -0
  132. package/src/tests/template.service.test.ts +1 -0
package/README.md CHANGED
@@ -1,133 +1,466 @@
1
1
  # tzmail
2
2
 
3
- A powerful and flexible TypeScript/Node.js email package built on top of Nodemailer, featuring a robust templating system with built-in themes, caching, and easy attachment management.
3
+ `@zyther/tzmail` is a TypeScript/Node.js library for generating templates and sending messages via SMTP, SendGrid, Amazon SES, or Mailgun. It includes ready-made themes, basic validation, bounded caching, attachments, and independent provider factories.
4
4
 
5
- ## Features
5
+ > **Scope:** tzmail is a templates, transports, and sending queue layer, not a complete campaign platform. For high volume, use a durable queue, define idempotency and retries, and coordinate limits with the provider. SMTP or an API accepting the message does not guarantee inbox delivery.
6
6
 
7
- - 🚀 **Singleton Factory**: Easy initialization and global access.
7
+ ## Requirements and installation
8
8
 
9
- - 🎨 **Themed Templates**: 5 built-in professional themes (Modern, Corporate, Minimal, Monokai, System).
9
+ - Node.js 20 or later.
10
+ - `nodemailer` 6.9, 7, or 8, installed as a peer dependency.
11
+ - To use Amazon SES, also install the optional peer `@aws-sdk/client-sesv2`; SendGrid and Mailgun use Node's native `fetch` and do not need the vendor SDKs.
12
+ - To use the asynchronous queue, install the optional peer `bullmq` and provide a compatible Redis instance.
10
13
 
11
- - 🌓 **Dark Mode Support**: All templates support light and dark variants.
14
+ ```bash
15
+ npm install @zyther/tzmail nodemailer
16
+ # Optional, only if using the queue:
17
+ npm install bullmq
18
+ ```
12
19
 
13
- - ⚡ **Performance**: In-memory template caching with TTL and version history.
20
+ ## Quick start
14
21
 
15
- - 📎 **Attachment Service**: Simple helpers for local files and buffers.
22
+ Use environment variables or a secret manager for credentials; do not store SMTP passwords in source code.
16
23
 
17
- - ✅ **Validation**: Built-in configuration validation for templates.
24
+ ```typescript
25
+ import {
26
+ EmailFactory,
27
+ ThemeType,
28
+ type IEmailConfig,
29
+ type ITemplateConfig
30
+ } from '@zyther/tzmail';
31
+
32
+ const smtp: IEmailConfig = {
33
+ host: process.env.SMTP_HOST!,
34
+ port: Number(process.env.SMTP_PORT ?? 587),
35
+ secure: false, // Implicit TLS usually uses 465; 587 usually negotiates STARTTLS
36
+ auth: {
37
+ user: process.env.SMTP_USER!,
38
+ pass: process.env.SMTP_PASSWORD!
39
+ },
40
+ defaultFrom: 'Acme <noreply@example.com>'
41
+ };
18
42
 
19
- ## Installation
43
+ // create() is suitable for services/tenants with isolated configurations.
44
+ const mailer = EmailFactory.create(smtp);
45
+ const templateConfig: ITemplateConfig = {
46
+ header: { show: true, logo: { type: 'text', text: 'Acme' } },
47
+ body: {
48
+ title: 'Welcome!',
49
+ message: 'Your account is ready.',
50
+ buttonText: 'Open account',
51
+ buttonUrl: 'https://example.com/account'
52
+ },
53
+ footer: { show: true, unsubscribeText: 'Unsubscribe' }
54
+ };
20
55
 
21
- ```bash
22
- npm install @zyther/tzmail
23
- # or
24
- yarn add @zyther/tzmail
56
+ const template = mailer.getTemplateService().createTemplate(
57
+ ThemeType.MODERN,
58
+ 'light',
59
+ templateConfig
60
+ );
61
+
62
+ const result = await mailer.sendEmail({
63
+ to: 'pessoa@example.com',
64
+ subject: 'Your account is ready',
65
+ template
66
+ });
67
+
68
+ if (result.status !== 'success') {
69
+ // Log operational metadata according to the application's policy.
70
+ // Avoid sending message bodies, credentials, or personal data to logs.
71
+ console.error('Email send failed', result.status, result.errorStage);
72
+ }
73
+
74
+ // During graceful process/worker shutdown:
75
+ mailer.close();
25
76
  ```
26
77
 
27
- ## Quick Start
78
+ ## SMTP and throughput
28
79
 
29
- ### 1. Initialize the Factory
80
+ `IEmailConfig` exposes the main SMTP and Nodemailer pool options. `pool`, `maxConnections`, `maxMessages`, `rateDelta`, and `rateLimit` control the pool and sending rate **for a single instance/process**:
30
81
 
31
82
  ```typescript
32
- import { EmailFactory } from '@zyther/tzmail';
33
-
34
- const smtpConfig = {
35
- host: 'smtp.example.com',
83
+ const smtp: IEmailConfig = {
84
+ host: process.env.SMTP_HOST!,
36
85
  port: 587,
37
- auth: {
38
- user: 'user@example.com',
39
- pass: 'password'
40
- },
41
- defaultFrom: 'My App <noreply@myapp.com>'
86
+ secure: false,
87
+ auth: { user: process.env.SMTP_USER!, pass: process.env.SMTP_PASSWORD! },
88
+ defaultFrom: 'Acme <noreply@example.com>',
89
+ pool: true,
90
+ maxConnections: 5,
91
+ maxMessages: 100,
92
+ rateDelta: 1_000,
93
+ rateLimit: 50,
94
+ connectionTimeout: 30_000,
95
+ greetingTimeout: 30_000,
96
+ socketTimeout: 60_000
42
97
  };
43
98
 
44
- const emailFactory = EmailFactory.initialize(smtpConfig);
99
+ const mailer = EmailFactory.create(smtp);
100
+ await mailer.verifyTransport(); // validates the connection/credentials; does not send email
45
101
  ```
46
102
 
47
- ### 2. Create and Send an Email
103
+ `auth` supports `login` credentials (username/password), `oauth2` credentials (for example, `user`, `clientId`, `clientSecret`, `refreshToken`, or `accessToken`), and Nodemailer's `custom` mechanism. Store tokens, callbacks, and secrets in a secret manager; the password example above is not required for OAuth2.
104
+
105
+ Choose limits according to your SMTP provider. Adding replicas multiplies connections and local limits; apply distributed control/a queue and quotas in the system that coordinates the workers.
106
+
107
+ `EmailFactory.create(config, templateOptions?, attachmentOptions?)` creates an isolated instance. `EmailFactory.initialize(...)`/`getInstance()` remain available for a process-wide singleton; a second initialization with different settings throws an error instead of silently reusing the first SMTP configuration. Close each created instance with `close()` during shutdown.
108
+
109
+ ## Asynchronous queue with BullMQ
110
+
111
+ BullMQ is an optional peer. Install it and configure Redis before creating the queue. Producer processes only need to add jobs; run `startWorker()` in dedicated workers and use the same `queueName`, `prefix`, and Redis connection. Workers can be scaled horizontally. `concurrency` controls simultaneous jobs per worker, while `limiter` applies a shared per-queue rate across BullMQ workers.
48
112
 
49
113
  ```typescript
50
- const templateService = emailFactory.getTemplateService();
114
+ import { EmailFactory, type IEmailConfig } from '@zyther/tzmail';
115
+
116
+ const queue = EmailFactory.createBullMQQueue({
117
+ connection: {
118
+ host: process.env.REDIS_HOST!,
119
+ port: Number(process.env.REDIS_PORT ?? 6379),
120
+ password: process.env.REDIS_PASSWORD,
121
+ tls: process.env.REDIS_TLS === 'true' ? {} : undefined
122
+ },
123
+ queueName: 'transactional-email',
124
+ concurrency: 10,
125
+ limiter: { max: 100, duration: 1_000 },
126
+ // BullMQ data points per minute: 14 days = 14 * 24 * 60 points.
127
+ workerMetrics: { maxDataPoints: 20_160 },
128
+ defaultJobOptions: {
129
+ attempts: 5,
130
+ backoff: { type: 'exponential', delay: 1_000, jitter: 0.2 }
131
+ }
132
+ });
51
133
 
52
- // Create a template
53
- const welcomeTemplate = templateService.createTemplate(
54
- 'modern', // ThemeType
55
- 'light', // Variant
56
- {
57
- body: {
58
- title: 'Welcome!',
59
- message: 'Thanks for joining us.',
60
- buttonText: 'Get Started',
61
- buttonUrl: 'https://myapp.com'
62
- }
134
+ // Producer/API process:
135
+ const job = await queue.enqueueEmail({
136
+ to: 'ana@example.com',
137
+ subject: 'Receipt',
138
+ text: 'Payment confirmed.'
139
+ }, { idempotencyKey: 'receipt-order-123' });
140
+ console.log('Job ID:', job.id);
141
+
142
+ // Or enqueue a batch, sent to Redis via addBulk:
143
+ await queue.enqueueBulk([
144
+ { email: { to: 'a@example.com', subject: 'Hello', text: 'A' }, job: { idempotencyKey: 'batch-a' } },
145
+ { email: { to: 'b@example.com', subject: 'Hello', text: 'B' }, job: { idempotencyKey: 'batch-b' } }
146
+ ]);
147
+ ```
148
+
149
+ Expose `exportPrometheusMetrics()` on an internal `/metrics` endpoint; it returns the text format expected by Prometheus and can accept low-cardinality global labels:
150
+
151
+ ```typescript
152
+ import { createServer } from 'node:http';
153
+
154
+ const metricsServer = createServer(async (req, res) => {
155
+ if (req.method !== 'GET' || req.url !== '/metrics') {
156
+ res.writeHead(404).end();
157
+ return;
63
158
  }
64
- );
159
+ try {
160
+ const body = await queue.exportPrometheusMetrics({
161
+ env: process.env.NODE_ENV ?? 'production',
162
+ service: 'tzmail'
163
+ });
164
+ res.writeHead(200, {
165
+ 'content-type': 'text/plain; version=0.0.4; charset=utf-8',
166
+ 'content-length': String(Buffer.byteLength(body))
167
+ });
168
+ res.end(body);
169
+ } catch {
170
+ res.writeHead(500).end('Metrics unavailable');
171
+ }
172
+ });
173
+ metricsServer.listen(9464, '0.0.0.0');
174
+ ```
175
+
176
+ Configure Prometheus to scrape `mail-metrics:9464` every 15 seconds. A dedicated exporter per queue avoids duplicate count series when multiple replicas query the same Redis instance. In Grafana, create dashboards for waiting, active, and failed jobs, and alerts for growing backlogs/failures. Do not use email addresses, job IDs, or other unbounded values as labels.
177
+
178
+ In a worker process, create the transport as usual and start the consumer with the same queue configuration:
179
+
180
+ ```typescript
181
+ const mailer = EmailFactory.createSendGrid({
182
+ apiKey: process.env.SENDGRID_API_KEY!,
183
+ defaultFrom: 'Acme <noreply@example.com>'
184
+ });
185
+ queue.startWorker(mailer, { concurrency: 20 }); // inherits workerMetrics from the configuration
186
+
187
+ // Global events from all workers; use them for real-time logging/alerts.
188
+ const events = queue.createQueueEvents();
189
+ events.on('completed', ({ jobId }) => logger.info({ jobId }, 'email job completed'));
190
+ events.on('failed', ({ jobId, failedReason }) => logger.error({ jobId, failedReason }, 'email job failed'));
191
+
192
+ // During graceful shutdown, wait for active jobs before closing the transport:
193
+ await queue.close();
194
+ mailer.close();
195
+ ```
196
+
197
+ `enqueueEmail()` accepts `idempotencyKey` to deduplicate jobs while the previous job is still retained in the queue; the key is transformed into a Redis ID without exposing the original value. Jobs removed after completion/failure are no longer deduplicated. `enqueueBulk()` accepts up to 1,000 messages by default; a partial failure during bulk insertion may leave some jobs enqueued, so use idempotency keys and check the queue before retrying the batch. `getJobCounts()` returns counts of waiting, active, completed, failed, delayed, and paused jobs. `getMetrics('completed' | 'failed')` reads throughput series in one-minute intervals stored in Redis; all workers in the queue must use the same `workerMetrics.maxDataPoints`.
198
+
199
+ The default options are 5 attempts, exponential backoff with jitter, retention of completed jobs for up to 24 hours/10,000 items, and failed jobs for up to 7 days/50,000 items. These can be changed in `defaultJobOptions` or per job. BullMQ retries worker failures; a transport failure after remote acceptance but before the response can therefore duplicate a send. Job idempotency prevents duplicate enqueueing while a job is retained, but does not provide exactly-once delivery by the provider. Validation/rendering errors and partial deliveries are marked as unrecoverable. Partial deliveries are not retried automatically, to avoid resending to recipients who have already been accepted; review these failed jobs and handle rejected recipients through a dedicated flow.
200
+
201
+ The template is rendered before entering the queue, to avoid serializing closures/functions. `Buffer` content is converted to Base64 and stored in Redis. Attachment `path` remains a local path that must be accessible to all workers. The default maximum serialized size is 10 MiB per job (`maxPayloadBytes`); increase it cautiously, since large attachments consume Redis and memory. The payload contains email addresses and content: protect Redis with TLS, authentication, network controls, and appropriate retention. `close()` waits for active workers and closes the queue; call it during graceful shutdown.
202
+
203
+ See also the official BullMQ documentation on [retries and backoff](https://docs.bullmq.io/guide/retrying-failing-jobs), [job IDs](https://docs.bullmq.io/guide/jobs/job-ids), [bulk insertion](https://docs.bullmq.io/guide/queues/adding-bulks), [rate limiting](https://docs.bullmq.io/guide/rate-limiting), and [graceful worker shutdown](https://docs.bullmq.io/guide/workers/graceful-shutdown).
204
+
205
+ ## Email providers
206
+
207
+ Provider factories create independent instances, so you can, for example, maintain a transactional sender through SES and a marketing sender through SendGrid in the same process. There is no shared singleton state across keys or tenants.
208
+
209
+ ### SendGrid Web API
210
+
211
+ The adapter uses the official v3 API via `fetch` (it does not require `@sendgrid/mail`). The sender must be verified in SendGrid. API attachments are Base64-encoded and held in memory during the call; configure `maxAttachmentBytes` to limit the size of each file.
212
+
213
+ ```typescript
214
+ const sendgrid = EmailFactory.createSendGrid({
215
+ apiKey: process.env.SENDGRID_API_KEY!,
216
+ defaultFrom: 'Acme <noreply@example.com>',
217
+ requestTimeoutMs: 30_000,
218
+ maxAttachmentBytes: 10 * 1024 * 1024
219
+ });
220
+
221
+ const result = await sendgrid.sendEmail({
222
+ to: ['ana@example.com', 'bruno@example.com'],
223
+ subject: 'Account update',
224
+ text: 'Your account has been updated.'
225
+ });
226
+ sendgrid.close();
227
+ ```
228
+
229
+ Use `apiUrl` if the account is configured for a regional endpoint. The default is `https://api.sendgrid.com`. References: [Mail Send API](https://www.twilio.com/docs/sendgrid/api-reference/mail-send/mail-send) and [attachments in the official SDK](https://github.com/sendgrid/sendgrid-nodejs/blob/main/docs/use-cases/attachments.md).
230
+
231
+ ### Mailgun Messages API
65
232
 
66
- // Send the email
67
- const result = await emailFactory.sendEmail({
68
- to: 'user@example.com',
69
- subject: 'Welcome aboard!',
70
- template: welcomeTemplate
233
+ The adapter uses the Messages API via `fetch` and multipart/form-data; it does not require `mailgun.js`. Set the sending domain and account region. `apiUrl` should only be customized for a custom or controlled endpoint.
234
+
235
+ ```typescript
236
+ const mailgun = EmailFactory.createMailgun({
237
+ apiKey: process.env.MAILGUN_API_KEY!,
238
+ domain: process.env.MAILGUN_DOMAIN!,
239
+ region: 'eu', // omit or use 'us' for the US API
240
+ defaultFrom: 'Acme <noreply@example.com>',
241
+ requestTimeoutMs: 30_000,
242
+ maxAttachmentBytes: 10 * 1024 * 1024
243
+ });
244
+
245
+ await mailgun.sendEmail({
246
+ to: 'ana@example.com',
247
+ subject: 'Hello',
248
+ html: '<p>Message sent via Mailgun.</p>'
71
249
  });
250
+ mailgun.close();
251
+ ```
252
+
253
+ The adapter uses `https://api.mailgun.net` in the US and `https://api.eu.mailgun.net` for `region: 'eu'`. Attachments are prepared in memory before the POST; set an appropriate limit. For inline images, Mailgun derives the Content-ID from the file name; when `cid` is provided, the adapter converts `cid:<value>` references in the HTML to `cid:<filename>`. See the [official Messages API](https://documentation.mailgun.com/docs/mailgun/api-reference/send/mailgun/messages).
254
+
255
+ ### Amazon SES v2
256
+
257
+ Install the official SDK only if you are going to use SES:
72
258
 
73
- console.log(result.success ? 'Sent!' : 'Failed');
259
+ ```bash
260
+ npm install @aws-sdk/client-sesv2
261
+ ```
262
+
263
+ ```typescript
264
+ const ses = EmailFactory.createSES({
265
+ region: process.env.AWS_REGION ?? 'us-east-1',
266
+ defaultFrom: 'Acme <noreply@example.com>'
267
+ // Without explicit credentials, the AWS SDK uses the default credential chain,
268
+ // such as an IAM role, environment variables, or local credentials.
269
+ });
270
+
271
+ await ses.sendEmail({
272
+ to: 'ana@example.com',
273
+ subject: 'Receipt',
274
+ text: 'Payment confirmed.'
275
+ });
276
+ ses.close();
74
277
  ```
75
278
 
76
- ## Core Components
279
+ The adapter uses Nodemailer's SESv2 transport. The sender identity must be verified and the role/credential must have the `ses:SendEmail` permission. The SES transport does not enforce rate limits on its own; use the BullMQ queue or your own coordination to respect the account quota. Nodemailer documents that the SES transport builds the full message in memory, including attachments. See [SES transport](https://nodemailer.com/transports/ses) and [AWS SDK credentials](https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/ses-examples-sending-email.html).
280
+
281
+ ### Custom transports and verification
282
+
283
+ `EmailFactory.createWithTransport(transport, defaultFrom?, templateOptions?, attachmentOptions?)` lets you connect a custom adapter that implements `IEmailTransport`. `verifyTransport()` works when the transport offers a non-destructive check; SendGrid and Mailgun do not send test requests because the sending API does not provide a safe health check with minimal permissions. For these providers, validate the key and permissions in the deployment environment and monitor actual send responses.
284
+
285
+ All factories accept the same optional template and attachment options. The default `requestTimeoutMs` for the SendGrid and Mailgun APIs is 30 seconds. `maxAttachmentBytes` is optional and limits each individual attachment; SendGrid/Mailgun files and SES messages may be buffered in memory. For large files, use conservative limits and SMTP transport when appropriate.
286
+
287
+ ## Templates and themes
288
+
289
+ The library includes 15 themes, each with `light` and `dark` variants. The original options are `ThemeType.MODERN`, `CORPORATE`, `MINIMAL`, `MONOKAI`, and `SYSTEM`; ten new palettes are also available:
290
+
291
+ | Theme | Style |
292
+ | --- | --- |
293
+ | `AURORA` | Cool teal and violet, inspired by the northern lights. |
294
+ | `OCEAN` | Calm navy, marine blue, and cyan. |
295
+ | `SUNSET` | Warm coral, terracotta, amber, and plum. |
296
+ | `LAVENDER` | Elegant violet and rose accents on soft surfaces. |
297
+ | `FOREST` | Evergreen, moss, sage, and warm cream. |
298
+ | `EDITORIAL` | Serif-led magazine styling with warm paper and ink tones. |
299
+ | `PASTEL` | Friendly peach, mint, and lavender accents with readable contrast. |
300
+ | `NEOBRUTAL` | Bold black and white with electric yellow and cobalt accents. |
301
+ | `GLASS` | Cool blue-gray colors on solid, email-client-friendly surfaces. |
302
+ | `MIDNIGHT` | Premium midnight navy with champagne-gold accents. |
303
+
304
+ All themes use fluid, table-based email markup. On screens up to 600px wide, calls to action expand to the available width and footer links stack vertically. Use `TemplateFactory.listThemes()` to enumerate available themes and `TemplateFactory.getThemeInfo(theme)` to read each theme's description and features.
305
+
306
+ Open the standalone [interactive theme gallery](examples/theme-gallery.html) in a browser to compare all 15 themes, switch between light and dark variants, search by theme name, and inspect expanded previews. It embeds sample emails rendered by the current `TemplateFactory` and requires no server or external assets.
307
+
308
+ Each built-in `ITheme` now includes an `emailStyles` profile for desktop width, container surfaces, header/body/footer accents, spacing, and solid or outline CTAs. Color accents are palette references, so the same profile adapts to light and dark variants. Custom `ITheme` implementations can omit `emailStyles` and receive conservative email-safe defaults. The renderer avoids CSS gradients, hover-only behavior, animation, and glass effects that are unreliable in email clients.
309
+
310
+ Theme-selected or caller-supplied logo, header, body, and footer text colors are checked against their effective backgrounds. If a requested foreground would be unreadable, the renderer substitutes a WCAG AA-safe fallback while preserving the theme accent on borders and fills.
311
+
312
+ Body copy and its CTA share the configured `body.alignment` (`left`, `center`, or `right`); `body.backgroundColor` and `body.textColor` can override the theme's body palette. Footer navigation and unsubscribe links have no underline by default. Set `linkDecoration: 'underline'` when underlined links are preferred; the setting applies consistently to both.
313
+
314
+ Templates also support `language` (default `en`), `direction` (`ltr`, `rtl`, or `auto`; default `ltr`), and a hidden `preheader` (up to 500 printable characters) for inbox previews. Preheaders support the same escaped per-recipient placeholders as other text fields. After changing a theme or the renderer, refresh the gallery's embedded samples with `npm run gallery:build`.
315
+
316
+ ```typescript
317
+ const html = await mailer.getTemplateService().previewTemplate(
318
+ ThemeType.MIDNIGHT,
319
+ 'dark',
320
+ {
321
+ language: 'ar',
322
+ direction: 'rtl',
323
+ preheader: 'مرحبًا، {{user.firstName}} — your account update is ready.',
324
+ body: { title: 'Welcome, {{user.firstName}}', message: 'Your update is available.' }
325
+ },
326
+ { templateData: { user: { firstName: 'Samira' } } }
327
+ );
328
+ ```
77
329
 
78
- ### Themes
330
+ ```typescript
331
+ const service = mailer.getTemplateService();
79
332
 
80
- | Theme | Best For | Features |
81
- | --- | --- | --- |
82
- | `MODERN` | Modern UI apps | Gradients, glassmorphism, rounded corners. |
83
- | `CORPORATE` | Business/Enterprise | Serif typography, gold accents, structured layout. |
84
- | `MINIMAL` | Clean newsletters | High whitespace, focused content, neutral colors. |
85
- | `MONOKAI` | Technical content | Syntax highlighting for code blocks, vibrant colors. |
86
- | `SYSTEM` | General purpose | Clean, accessible, system-native look. |
333
+ const html = await service.previewTemplate(
334
+ ThemeType.CORPORATE,
335
+ 'light',
336
+ {
337
+ header: { show: true, logo: { type: 'text', text: 'Acme' } },
338
+ body: { title: 'Hello, Ana', message: 'See what is new.' },
339
+ footer: {
340
+ show: true,
341
+ links: [{ text: 'Privacy', url: 'https://example.com/privacy' }],
342
+ unsubscribeText: 'Unsubscribe',
343
+ unsubscribeUrl: 'https://example.com/unsubscribe?token=...'
344
+ }
345
+ },
346
+ {}
347
+ );
348
+ ```
87
349
 
88
- ### Attachment Service
350
+ `ITemplateServiceOptions` options include:
89
351
 
90
- Easily add attachments from paths or buffers:
352
+ | Option | Default | Description |
353
+ | --- | ---: | --- |
354
+ | `cache` | `true` | In-memory cache by configuration. Can be disabled per call with `{ cache: false }`. |
355
+ | `cacheTTL` | `3600` | TTL in seconds; `0` disables storage. |
356
+ | `maxCacheEntries` | `100` | Positive limit on cached entries; approximate LRU eviction. |
357
+ | `maxHistoryEntries` | `100` | Limit on IDs in the history. |
358
+ | `maxVersionsPerTemplate` | `10` | Versions retained per template. |
359
+ | `validateConfig` | `true` | Validates template options before rendering. |
91
360
 
92
361
  ```typescript
93
- const attachmentService = emailFactory.getAttachmentService();
94
- const file = await attachmentService.addFromPath('path/to/report.pdf');
95
-
96
- await emailFactory.sendEmail({
97
- to: 'user@example.com',
98
- subject: 'Monthly Report',
99
- attachments: [file],
100
- html: '<p>Please find the report attached.</p>'
362
+ import { TemplateService } from '@zyther/tzmail';
363
+
364
+ const service = new TemplateService({
365
+ cache: true,
366
+ cacheTTL: 900,
367
+ maxCacheEntries: 500,
368
+ maxHistoryEntries: 200
101
369
  });
102
370
  ```
103
371
 
104
- ## Configuration Reference
372
+ The cache and history are local to the process, not distributed. Call `service.dispose()` when you create `TemplateService` directly and no longer need it. `EmailFactory.close()` stops the associated service timer.
373
+ `getTemplateStats()` returns `totalTemplates`, `cachedTemplates`, `totalHits`, `cacheRequests`, `cacheHitRate`, and counts by theme; requests that bypass/disable the cache are not included in the hit rate.
105
374
 
106
- ### `ITemplateConfig`
375
+ ### HTML and URL policy
107
376
 
108
- Customize your email structure:
377
+ - `title`, `message`, logo name, button/link/footer text, and textual metadata are escaped by the renderer.
378
+ - `body.content` is the only configuration field treated as **trusted HTML** to allow custom markup. Do not pass user-provided HTML without sanitizing it with a policy appropriate to the application.
379
+ - `TemplateBuilder.buildBody()` is a low-level API and also expects trusted HTML; prefer `TemplateService` for validation and interpolation.
380
+ - `IEmailOptions.html` is also passed through as HTML supplied by the caller; treat it as trusted content or sanitize it before sending.
381
+ - Action and link URLs accept absolute `https:`, `http:`, or `mailto:` URLs; images/logos and social links accept `https:`/`http:`. Schemes such as `javascript:` and `data:` and attributes containing injection characters are rejected.
382
+ - Colors and some style values are validated. The library's validation does not replace business validation or sanitization of custom HTML.
109
383
 
110
- - **header**: `{ show: boolean, logo: { type: 'text' | 'image', ... } }`
384
+ For per-recipient customization, use `{{placeholder}}` in template text and pass `templateData` in the message. Nested keys are supported (`{{user.firstName}}`); leaf values must be primitives (`string`, `number`, `boolean`, or `null`), and a placeholder without a value causes rendering to fail instead of sending incomplete text. Values inserted into text are escaped; insertions into `body.content` are escaped before being added to the trusted HTML; URLs are validated after interpolation.
111
385
 
112
- - **body**: `{ title, message, buttonText, buttonUrl, alignment, ... }`
386
+ ```typescript
387
+ await mailer.sendEmail({
388
+ to: 'ana@example.com',
389
+ subject: 'Welcome',
390
+ template,
391
+ templateData: {
392
+ user: { firstName: 'Ana' },
393
+ accountId: 'acct-123',
394
+ actionUrl: 'https://example.com/account/acct-123'
395
+ }
396
+ });
397
+ ```
113
398
 
114
- - **footer**: `{ show: boolean, links: [], socialLinks: [], copyrightText }`
399
+ For example, use `title: 'Hello, {{user.firstName}}'` and `buttonUrl: '{{actionUrl}}'` in the template configuration. The dynamic URL must still comply with the allowed schemes.
115
400
 
116
- - **layout**: `'full'` | `'minimal'`
401
+ ## Attachments
117
402
 
118
- - **spacing**: `'compact'` | `'normal'` | `'relaxed'`
403
+ Attachments specified by path remain as paths so Nodemailer can stream them without loading entire large files into memory. In production, prefer restricting the allowed root and enforcing a size limit:
119
404
 
120
- ## Error Handling
405
+ ```typescript
406
+ const mailer = EmailFactory.create(
407
+ smtp,
408
+ { cache: true, cacheTTL: 900 },
409
+ { allowedRoot: '/srv/app/mail-attachments', maxSizeBytes: 10 * 1024 * 1024 }
410
+ );
411
+
412
+ const attachment = await mailer.getAttachmentService().addFromPath(
413
+ '/srv/app/mail-attachments/report.pdf'
414
+ );
415
+ await mailer.sendEmail({
416
+ to: 'finance@example.com',
417
+ subject: 'Report',
418
+ text: 'Please find the report attached.',
419
+ attachments: [attachment]
420
+ });
421
+ ```
422
+
423
+ `allowedRoot` resolves real paths and rejects paths outside the root, including symlink escapes at validation time. Keep the root and its directories unwritable by untrusted users to reduce the risk of the file being swapped between validation and reading. If it is not set, the application is responsible for ensuring that paths are trusted. `maxSizeBytes` is optional; buffer attachments are also checked. `addFromUrl()` does not download URLs and remains unimplemented.
121
424
 
122
- The `sendEmail` method returns a result object:
425
+ ## Results and error handling
426
+
427
+ `sendEmail()` always returns an `IEmailSendResult`; validation, rendering, and transport are identified by `errorStage`:
123
428
 
124
429
  ```typescript
125
- {
430
+ type IEmailSendResult = {
126
431
  success: boolean;
432
+ status: 'success' | 'partial' | 'failure';
127
433
  messageId?: string;
128
434
  response?: string;
129
- error?: any;
130
- }
435
+ accepted?: Array<string | { address: string; name: string }>;
436
+ rejected?: Array<string | { address: string; name: string }>;
437
+ error?: unknown;
438
+ errorStage?: 'validation' | 'render' | 'smtp' | 'provider';
439
+ };
440
+ ```
441
+
442
+ `success` is `true` only when the transport reports no rejected recipients. `partial` means partial acceptance and rejection; route these cases to explicit handling/retry in the application. An API or server accepting the message does not prove final delivery, that it was read, or that it avoided spam.
443
+
444
+ ## Development and validation
445
+
446
+ ```bash
447
+ npm ci
448
+ npm run lint
449
+ npm run typecheck
450
+ npm test
451
+ npm pack --dry-run
131
452
  ```
132
453
 
133
- ---
454
+ Tests use the native `node:test` runner and fake transports; they do not send real emails. The GitHub Actions pipeline runs lint, typecheck, build/tests, and package inspection on pushes, pull requests, and before publishing an npm release.
455
+
456
+ ## Production considerations
457
+
458
+ 1. Use `BullMQEmailQueue` or another durable queue/outbox with workers, idempotency, and retries with backoff; do not automatically retry sends without a duplicate-prevention strategy.
459
+ 2. Configure SPF, DKIM, DMARC, sending domain/reputation, quotas, and bounce/unsubscribe handling with the provider and in the application.
460
+ 3. Keep secrets out of the repository and rotate them; do not log message content or link tokens.
461
+ 4. Size `maxConnections`/`rateLimit` according to the provider and the number of replicas. Nodemailer pool limits are local; the BullMQ limiter is shared by the queue.
462
+ 5. Monitor `status`, `errorStage`, `accepted`/`rejected`, latency, and queues with redacted logs and custom metrics.
463
+
464
+ ## License
465
+
466
+ ISC. See `LICENSE`/repository metadata for the applicable terms.
@@ -3,6 +3,16 @@ export declare enum ThemeType {
3
3
  MONOKAI = "monokai",
4
4
  MODERN = "modern",
5
5
  CORPORATE = "corporate",
6
- MINIMAL = "minimal"
6
+ MINIMAL = "minimal",
7
+ AURORA = "aurora",
8
+ OCEAN = "ocean",
9
+ SUNSET = "sunset",
10
+ LAVENDER = "lavender",
11
+ FOREST = "forest",
12
+ EDITORIAL = "editorial",
13
+ PASTEL = "pastel",
14
+ NEOBRUTAL = "neobrutal",
15
+ GLASS = "glass",
16
+ MIDNIGHT = "midnight"
7
17
  }
8
18
  //# sourceMappingURL=theme.enum.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"theme.enum.d.ts","sourceRoot":"","sources":["../../../src/core/enums/theme.enum.ts"],"names":[],"mappings":"AACA,oBAAY,SAAS;IACnB,MAAM,WAAW;IACjB,OAAO,YAAY;IACnB,MAAM,WAAW;IACjB,SAAS,cAAc;IACvB,OAAO,YAAY;CACpB"}
1
+ {"version":3,"file":"theme.enum.d.ts","sourceRoot":"","sources":["../../../src/core/enums/theme.enum.ts"],"names":[],"mappings":"AACA,oBAAY,SAAS;IACnB,MAAM,WAAW;IACjB,OAAO,YAAY;IACnB,MAAM,WAAW;IACjB,SAAS,cAAc;IACvB,OAAO,YAAY;IACnB,MAAM,WAAW;IACjB,KAAK,UAAU;IACf,MAAM,WAAW;IACjB,QAAQ,aAAa;IACrB,MAAM,WAAW;IACjB,SAAS,cAAc;IACvB,MAAM,WAAW;IACjB,SAAS,cAAc;IACvB,KAAK,UAAU;IACf,QAAQ,aAAa;CACtB"}
@@ -8,4 +8,14 @@ var ThemeType;
8
8
  ThemeType["MODERN"] = "modern";
9
9
  ThemeType["CORPORATE"] = "corporate";
10
10
  ThemeType["MINIMAL"] = "minimal";
11
+ ThemeType["AURORA"] = "aurora";
12
+ ThemeType["OCEAN"] = "ocean";
13
+ ThemeType["SUNSET"] = "sunset";
14
+ ThemeType["LAVENDER"] = "lavender";
15
+ ThemeType["FOREST"] = "forest";
16
+ ThemeType["EDITORIAL"] = "editorial";
17
+ ThemeType["PASTEL"] = "pastel";
18
+ ThemeType["NEOBRUTAL"] = "neobrutal";
19
+ ThemeType["GLASS"] = "glass";
20
+ ThemeType["MIDNIGHT"] = "midnight";
11
21
  })(ThemeType || (exports.ThemeType = ThemeType = {}));