@gravito/signal 1.0.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +129 -0
  2. package/dist/OrbitMail-2Z7ZTKYA.mjs +7 -0
  3. package/dist/OrbitMail-BGV32HWN.mjs +7 -0
  4. package/dist/OrbitMail-FUYZQSAV.mjs +7 -0
  5. package/dist/OrbitMail-NAPCRK7B.mjs +7 -0
  6. package/dist/OrbitMail-REGJ276B.mjs +7 -0
  7. package/dist/OrbitMail-TCFBJWDT.mjs +7 -0
  8. package/dist/OrbitMail-XZZW6U4N.mjs +7 -0
  9. package/dist/OrbitSignal-ZKKMEC27.mjs +7 -0
  10. package/dist/ReactRenderer-L5INVYKT.mjs +27 -0
  11. package/dist/VueRenderer-S65ZARRI.mjs +37129 -0
  12. package/dist/VueRenderer-Z5PRVBNH.mjs +37298 -0
  13. package/dist/chunk-3U2CYJO5.mjs +367 -0
  14. package/dist/chunk-3XFC4T6M.mjs +392 -0
  15. package/dist/chunk-6DZX6EAA.mjs +37 -0
  16. package/dist/chunk-DT3R2TNV.mjs +367 -0
  17. package/dist/chunk-GADWIVC4.mjs +400 -0
  18. package/dist/chunk-HHKFAMSE.mjs +380 -0
  19. package/dist/chunk-OKRNL6PN.mjs +400 -0
  20. package/dist/chunk-ULN3GMY2.mjs +367 -0
  21. package/dist/chunk-XAWO7RSP.mjs +398 -0
  22. package/dist/index.d.mts +278 -0
  23. package/dist/index.d.ts +278 -0
  24. package/dist/index.js +38150 -0
  25. package/dist/index.mjs +316 -0
  26. package/package.json +73 -0
  27. package/src/Mailable.ts +245 -0
  28. package/src/OrbitSignal.ts +158 -0
  29. package/src/Queueable.ts +9 -0
  30. package/src/dev/DevMailbox.ts +64 -0
  31. package/src/dev/DevServer.ts +89 -0
  32. package/src/dev/ui/mailbox.ts +68 -0
  33. package/src/dev/ui/preview.ts +59 -0
  34. package/src/dev/ui/shared.ts +46 -0
  35. package/src/index.ts +20 -0
  36. package/src/renderers/HtmlRenderer.ts +22 -0
  37. package/src/renderers/ReactRenderer.ts +35 -0
  38. package/src/renderers/Renderer.ts +11 -0
  39. package/src/renderers/TemplateRenderer.ts +34 -0
  40. package/src/renderers/VueRenderer.ts +37 -0
  41. package/src/transports/LogTransport.ts +17 -0
  42. package/src/transports/MemoryTransport.ts +11 -0
  43. package/src/transports/SesTransport.ts +56 -0
  44. package/src/transports/SmtpTransport.ts +50 -0
  45. package/src/transports/Transport.ts +8 -0
  46. package/src/types.ts +71 -0
  47. package/tests/mailable.test.ts +77 -0
  48. package/tests/renderers.test.ts +56 -0
  49. package/tests/transports.test.ts +52 -0
  50. package/tsconfig.json +19 -0
package/dist/index.mjs ADDED
@@ -0,0 +1,316 @@
1
+ import {
2
+ DevMailbox,
3
+ LogTransport,
4
+ MemoryTransport,
5
+ OrbitSignal
6
+ } from "./chunk-GADWIVC4.mjs";
7
+ import "./chunk-6DZX6EAA.mjs";
8
+
9
+ // src/renderers/HtmlRenderer.ts
10
+ var HtmlRenderer = class {
11
+ constructor(content) {
12
+ this.content = content;
13
+ }
14
+ async render() {
15
+ return {
16
+ html: this.content,
17
+ text: this.stripHtml(this.content)
18
+ };
19
+ }
20
+ stripHtml(html) {
21
+ return html.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "").replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "").replace(/<[^>]+>/g, "").replace(/&nbsp;/g, " ").replace(/\s+/g, " ").trim();
22
+ }
23
+ };
24
+
25
+ // src/renderers/TemplateRenderer.ts
26
+ import { TemplateEngine } from "@gravito/prism";
27
+ var TemplateRenderer = class {
28
+ engine;
29
+ template;
30
+ constructor(templateName, viewsDir) {
31
+ this.template = templateName;
32
+ const defaultDir = viewsDir || `${process.cwd()}/src/emails`;
33
+ this.engine = new TemplateEngine(defaultDir);
34
+ }
35
+ async render(data) {
36
+ const html = this.engine.render(this.template, data, {});
37
+ return {
38
+ html,
39
+ text: this.stripHtml(html)
40
+ };
41
+ }
42
+ stripHtml(html) {
43
+ return html.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "").replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "").replace(/<[^>]+>/g, "").replace(/&nbsp;/g, " ").replace(/\s+/g, " ").trim();
44
+ }
45
+ };
46
+
47
+ // src/Mailable.ts
48
+ var Mailable = class {
49
+ envelope = {};
50
+ renderer;
51
+ rendererResolver;
52
+ renderData = {};
53
+ // ===== Fluent API (Envelope Construction) =====
54
+ from(address) {
55
+ this.envelope.from = typeof address === "string" ? { address } : address;
56
+ return this;
57
+ }
58
+ to(address) {
59
+ this.envelope.to = this.normalizeAddressArray(address);
60
+ return this;
61
+ }
62
+ cc(address) {
63
+ this.envelope.cc = this.normalizeAddressArray(address);
64
+ return this;
65
+ }
66
+ bcc(address) {
67
+ this.envelope.bcc = this.normalizeAddressArray(address);
68
+ return this;
69
+ }
70
+ replyTo(address) {
71
+ this.envelope.replyTo = typeof address === "string" ? { address } : address;
72
+ return this;
73
+ }
74
+ subject(subject) {
75
+ this.envelope.subject = subject;
76
+ return this;
77
+ }
78
+ priority(level) {
79
+ this.envelope.priority = level;
80
+ return this;
81
+ }
82
+ attach(attachment) {
83
+ this.envelope.attachments = this.envelope.attachments || [];
84
+ this.envelope.attachments.push(attachment);
85
+ return this;
86
+ }
87
+ // ===== Content Methods (Renderer Selection) =====
88
+ /**
89
+ * Set the content using raw HTML string.
90
+ */
91
+ html(content) {
92
+ this.renderer = new HtmlRenderer(content);
93
+ return this;
94
+ }
95
+ /**
96
+ * Set the content using an OrbitPrism template.
97
+ * @param template Template name (relative to viewsDir/emails)
98
+ * @param data Data to pass to the template
99
+ */
100
+ view(template, data) {
101
+ this.renderer = new TemplateRenderer(template, void 0);
102
+ this.renderData = data || {};
103
+ return this;
104
+ }
105
+ /**
106
+ * Set the content using a React component.
107
+ * Dynamically imports ReactRenderer to avoid hard dependency errors if React is not installed.
108
+ */
109
+ react(component, props) {
110
+ this.rendererResolver = async () => {
111
+ const { ReactRenderer } = await import("./ReactRenderer-L5INVYKT.mjs");
112
+ return new ReactRenderer(component, props);
113
+ };
114
+ return this;
115
+ }
116
+ /**
117
+ * Set the content using a Vue component.
118
+ * Dynamically imports VueRenderer to avoid hard dependency errors if Vue is not installed.
119
+ */
120
+ vue(component, props) {
121
+ this.rendererResolver = async () => {
122
+ const { VueRenderer } = await import("./VueRenderer-Z5PRVBNH.mjs");
123
+ return new VueRenderer(component, props);
124
+ };
125
+ return this;
126
+ }
127
+ // ===== Queueable Implementation =====
128
+ queueName;
129
+ connectionName;
130
+ delaySeconds;
131
+ onQueue(queue) {
132
+ this.queueName = queue;
133
+ return this;
134
+ }
135
+ onConnection(connection) {
136
+ this.connectionName = connection;
137
+ return this;
138
+ }
139
+ delay(seconds) {
140
+ this.delaySeconds = seconds;
141
+ return this;
142
+ }
143
+ /**
144
+ * Queue the mailable for sending.
145
+ */
146
+ async queue() {
147
+ const { OrbitSignal: OrbitSignal2 } = await import("./OrbitSignal-ZKKMEC27.mjs");
148
+ return OrbitSignal2.getInstance().queue(this);
149
+ }
150
+ // ===== I18n Support =====
151
+ currentLocale;
152
+ translator;
153
+ /**
154
+ * Set the locale for the message.
155
+ */
156
+ locale(locale) {
157
+ this.currentLocale = locale;
158
+ return this;
159
+ }
160
+ /**
161
+ * Internal: Set the translator function (called by OrbitSignal)
162
+ */
163
+ setTranslator(translator) {
164
+ this.translator = translator;
165
+ }
166
+ /**
167
+ * Translate a string using the configured translator.
168
+ */
169
+ t(key, replace) {
170
+ if (this.translator) {
171
+ return this.translator(key, replace, this.currentLocale);
172
+ }
173
+ return key;
174
+ }
175
+ // ===== Internal Systems =====
176
+ /**
177
+ * Compile the envelope based on config defaults and mailable settings.
178
+ */
179
+ async buildEnvelope(configPromise) {
180
+ const config = await Promise.resolve(configPromise);
181
+ if (config.translator) {
182
+ this.setTranslator(config.translator);
183
+ }
184
+ this.build();
185
+ if (this.renderer instanceof TemplateRenderer && config.viewsDir) {
186
+ }
187
+ const envelope = {
188
+ from: this.envelope.from || config.from,
189
+ to: this.envelope.to || [],
190
+ subject: this.envelope.subject || "(No Subject)",
191
+ priority: this.envelope.priority || "normal"
192
+ };
193
+ if (this.envelope.cc) {
194
+ envelope.cc = this.envelope.cc;
195
+ }
196
+ if (this.envelope.bcc) {
197
+ envelope.bcc = this.envelope.bcc;
198
+ }
199
+ if (this.envelope.replyTo) {
200
+ envelope.replyTo = this.envelope.replyTo;
201
+ }
202
+ if (this.envelope.attachments) {
203
+ envelope.attachments = this.envelope.attachments;
204
+ }
205
+ return envelope;
206
+ }
207
+ /**
208
+ * execute the renderer
209
+ */
210
+ async renderContent() {
211
+ if (!this.renderer && this.rendererResolver) {
212
+ this.renderer = await this.rendererResolver();
213
+ }
214
+ if (!this.renderer) {
215
+ throw new Error("No content renderer specified. Use html(), view(), react(), or vue().");
216
+ }
217
+ this.renderData = {
218
+ ...this.renderData,
219
+ locale: this.currentLocale,
220
+ t: (key, replace) => this.t(key, replace)
221
+ };
222
+ return this.renderer.render(this.renderData);
223
+ }
224
+ normalizeAddressArray(input) {
225
+ const arr = Array.isArray(input) ? input : [input];
226
+ return arr.map((item) => typeof item === "string" ? { address: item } : item);
227
+ }
228
+ };
229
+
230
+ // src/transports/SesTransport.ts
231
+ import { SESClient, SendRawEmailCommand } from "@aws-sdk/client-ses";
232
+ import nodemailer from "nodemailer";
233
+ var SesTransport = class {
234
+ transporter;
235
+ constructor(config) {
236
+ const clientConfig = { region: config.region };
237
+ if (config.accessKeyId && config.secretAccessKey) {
238
+ clientConfig.credentials = {
239
+ accessKeyId: config.accessKeyId,
240
+ secretAccessKey: config.secretAccessKey
241
+ };
242
+ }
243
+ const ses = new SESClient(clientConfig);
244
+ this.transporter = nodemailer.createTransport({
245
+ SES: { ses, aws: { SendRawEmailCommand } }
246
+ });
247
+ }
248
+ async send(message) {
249
+ await this.transporter.sendMail({
250
+ from: this.formatAddress(message.from),
251
+ to: message.to.map(this.formatAddress),
252
+ cc: message.cc?.map(this.formatAddress),
253
+ bcc: message.bcc?.map(this.formatAddress),
254
+ replyTo: message.replyTo ? this.formatAddress(message.replyTo) : void 0,
255
+ subject: message.subject,
256
+ html: message.html,
257
+ text: message.text,
258
+ headers: message.headers,
259
+ priority: message.priority,
260
+ attachments: message.attachments?.map((a) => ({
261
+ filename: a.filename,
262
+ content: a.content,
263
+ contentType: a.contentType,
264
+ cid: a.cid,
265
+ encoding: a.encoding
266
+ }))
267
+ });
268
+ }
269
+ formatAddress(addr) {
270
+ return addr.name ? `"${addr.name}" <${addr.address}>` : addr.address;
271
+ }
272
+ };
273
+
274
+ // src/transports/SmtpTransport.ts
275
+ import nodemailer2 from "nodemailer";
276
+ var SmtpTransport = class {
277
+ transporter;
278
+ constructor(config) {
279
+ this.transporter = nodemailer2.createTransport(config);
280
+ }
281
+ async send(message) {
282
+ await this.transporter.sendMail({
283
+ from: this.formatAddress(message.from),
284
+ to: message.to.map(this.formatAddress),
285
+ cc: message.cc?.map(this.formatAddress),
286
+ bcc: message.bcc?.map(this.formatAddress),
287
+ replyTo: message.replyTo ? this.formatAddress(message.replyTo) : void 0,
288
+ subject: message.subject,
289
+ html: message.html,
290
+ text: message.text,
291
+ headers: message.headers,
292
+ priority: message.priority,
293
+ attachments: message.attachments?.map((a) => ({
294
+ filename: a.filename,
295
+ content: a.content,
296
+ contentType: a.contentType,
297
+ cid: a.cid,
298
+ encoding: a.encoding
299
+ }))
300
+ });
301
+ }
302
+ formatAddress(addr) {
303
+ return addr.name ? `"${addr.name}" <${addr.address}>` : addr.address;
304
+ }
305
+ };
306
+ export {
307
+ DevMailbox,
308
+ HtmlRenderer,
309
+ LogTransport,
310
+ Mailable,
311
+ MemoryTransport,
312
+ OrbitSignal,
313
+ SesTransport,
314
+ SmtpTransport,
315
+ TemplateRenderer
316
+ };
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "@gravito/signal",
3
+ "version": "1.0.0-alpha.2",
4
+ "description": "Powerful email framework for Gravito applications with Dev UI and multi-renderer support.",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "scripts": {
9
+ "build": "tsup src/index.ts --format cjs,esm --dts",
10
+ "test": "bun test"
11
+ },
12
+ "keywords": [
13
+ "gravito",
14
+ "mail",
15
+ "smtp",
16
+ "mjml",
17
+ "email",
18
+ "react-email",
19
+ "vue-email"
20
+ ],
21
+ "author": "Carl Lee <carllee0520@gmail.com>",
22
+ "license": "MIT",
23
+ "dependencies": {
24
+ "@aws-sdk/client-ses": "^3.953.0",
25
+ "mjml": "^4.14.1",
26
+ "nodemailer": "^6.9.1"
27
+ },
28
+ "peerDependencies": {
29
+ "gravito-core": "1.0.0-beta.2",
30
+ "@gravito/stream": "1.0.0-alpha.2",
31
+ "@gravito/prism": "1.0.0-beta.2",
32
+ "react": "^18.0.0",
33
+ "react-dom": "^18.0.0",
34
+ "vue": "^3.0.0"
35
+ },
36
+ "peerDependenciesMeta": {
37
+ "react": {
38
+ "optional": true
39
+ },
40
+ "react-dom": {
41
+ "optional": true
42
+ },
43
+ "vue": {
44
+ "optional": true
45
+ },
46
+ "@gravito/prism": {
47
+ "optional": true
48
+ }
49
+ },
50
+ "devDependencies": {
51
+ "@gravito/stream": "1.0.0-alpha.2",
52
+ "@gravito/prism": "1.0.0-beta.2",
53
+ "@types/mjml": "^4.7.4",
54
+ "@types/nodemailer": "^6.4.14",
55
+ "@types/react": "^18.2.0",
56
+ "@types/react-dom": "^18.2.0",
57
+ "@vue/server-renderer": "^3.0.0",
58
+ "gravito-core": "1.0.0-beta.2",
59
+ "hono": "^4.11.1",
60
+ "tsup": "^8.0.2",
61
+ "typescript": "^5.0.0",
62
+ "vue": "^3.0.0"
63
+ },
64
+ "publishConfig": {
65
+ "access": "public"
66
+ },
67
+ "homepage": "https://github.com/gravito-framework/gravito#readme",
68
+ "repository": {
69
+ "type": "git",
70
+ "url": "git+https://github.com/gravito-framework/gravito.git",
71
+ "directory": "packages/signal"
72
+ }
73
+ }
@@ -0,0 +1,245 @@
1
+ import type { Queueable } from '@gravito/stream' // Import Queueable from orbit-queue
2
+ import { HtmlRenderer } from './renderers/HtmlRenderer'
3
+ import type { Renderer } from './renderers/Renderer'
4
+ import { TemplateRenderer } from './renderers/TemplateRenderer'
5
+ import type { Address, Attachment, Envelope, MailConfig } from './types'
6
+
7
+ // Type placeholders for React/Vue components to avoid hard dependencies in core
8
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9
+ type ComponentType = any
10
+
11
+ export abstract class Mailable implements Queueable {
12
+ protected envelope: Partial<Envelope> = {}
13
+ protected renderer?: Renderer
14
+ private rendererResolver?: () => Promise<Renderer>
15
+ protected renderData: Record<string, unknown> = {}
16
+
17
+ // ===== Fluent API (Envelope Construction) =====
18
+
19
+ from(address: string | Address): this {
20
+ this.envelope.from = typeof address === 'string' ? { address } : address
21
+ return this
22
+ }
23
+
24
+ to(address: string | Address | (string | Address)[]): this {
25
+ this.envelope.to = this.normalizeAddressArray(address)
26
+ return this
27
+ }
28
+
29
+ cc(address: string | Address | (string | Address)[]): this {
30
+ this.envelope.cc = this.normalizeAddressArray(address)
31
+ return this
32
+ }
33
+
34
+ bcc(address: string | Address | (string | Address)[]): this {
35
+ this.envelope.bcc = this.normalizeAddressArray(address)
36
+ return this
37
+ }
38
+
39
+ replyTo(address: string | Address): this {
40
+ this.envelope.replyTo = typeof address === 'string' ? { address } : address
41
+ return this
42
+ }
43
+
44
+ subject(subject: string): this {
45
+ this.envelope.subject = subject
46
+ return this
47
+ }
48
+
49
+ priority(level: 'high' | 'normal' | 'low'): this {
50
+ this.envelope.priority = level
51
+ return this
52
+ }
53
+
54
+ attach(attachment: Attachment): this {
55
+ this.envelope.attachments = this.envelope.attachments || []
56
+ this.envelope.attachments.push(attachment)
57
+ return this
58
+ }
59
+
60
+ // ===== Content Methods (Renderer Selection) =====
61
+
62
+ /**
63
+ * Set the content using raw HTML string.
64
+ */
65
+ html(content: string): this {
66
+ this.renderer = new HtmlRenderer(content)
67
+ return this
68
+ }
69
+
70
+ /**
71
+ * Set the content using an OrbitPrism template.
72
+ * @param template Template name (relative to viewsDir/emails)
73
+ * @param data Data to pass to the template
74
+ */
75
+ view(template: string, data?: Record<string, unknown>): this {
76
+ this.renderer = new TemplateRenderer(template, undefined) // Dir will be injected later if possible, or use default
77
+ this.renderData = data || {}
78
+ return this
79
+ }
80
+
81
+ /**
82
+ * Set the content using a React component.
83
+ * Dynamically imports ReactRenderer to avoid hard dependency errors if React is not installed.
84
+ */
85
+ react<P extends object>(component: ComponentType, props?: P): this {
86
+ this.rendererResolver = async () => {
87
+ const { ReactRenderer } = await import('./renderers/ReactRenderer')
88
+ return new ReactRenderer(component, props)
89
+ }
90
+ return this
91
+ }
92
+
93
+ /**
94
+ * Set the content using a Vue component.
95
+ * Dynamically imports VueRenderer to avoid hard dependency errors if Vue is not installed.
96
+ */
97
+ vue<P extends object>(component: ComponentType, props?: P): this {
98
+ this.rendererResolver = async () => {
99
+ const { VueRenderer } = await import('./renderers/VueRenderer')
100
+ return new VueRenderer(component, props as any)
101
+ }
102
+ return this
103
+ }
104
+
105
+ // ===== Life Cycle =====
106
+
107
+ /**
108
+ * Setup the mailable. This is where you call from(), to(), view(), etc.
109
+ */
110
+ abstract build(): this
111
+
112
+ // ===== Queueable Implementation =====
113
+
114
+ queueName?: string
115
+ connectionName?: string
116
+ delaySeconds?: number
117
+
118
+ onQueue(queue: string): this {
119
+ this.queueName = queue
120
+ return this
121
+ }
122
+
123
+ onConnection(connection: string): this {
124
+ this.connectionName = connection
125
+ return this
126
+ }
127
+
128
+ delay(seconds: number): this {
129
+ this.delaySeconds = seconds
130
+ return this
131
+ }
132
+
133
+ /**
134
+ * Queue the mailable for sending.
135
+ */
136
+ async queue(): Promise<void> {
137
+ // Avoid circular dependency by dynamically importing OrbitSignal
138
+ const { OrbitSignal } = await import('./OrbitSignal')
139
+ return OrbitSignal.getInstance().queue(this)
140
+ }
141
+
142
+ // ===== I18n Support =====
143
+
144
+ protected currentLocale?: string
145
+ protected translator?: (key: string, replace?: Record<string, unknown>, locale?: string) => string
146
+
147
+ /**
148
+ * Set the locale for the message.
149
+ */
150
+ locale(locale: string): this {
151
+ this.currentLocale = locale
152
+ return this
153
+ }
154
+
155
+ /**
156
+ * Internal: Set the translator function (called by OrbitSignal)
157
+ */
158
+ setTranslator(
159
+ translator: (key: string, replace?: Record<string, unknown>, locale?: string) => string
160
+ ): void {
161
+ this.translator = translator
162
+ }
163
+
164
+ /**
165
+ * Translate a string using the configured translator.
166
+ */
167
+ t(key: string, replace?: Record<string, unknown>): string {
168
+ if (this.translator) {
169
+ return this.translator(key, replace, this.currentLocale)
170
+ }
171
+ return key // Fallback: just return the key if no translator
172
+ }
173
+
174
+ // ===== Internal Systems =====
175
+
176
+ /**
177
+ * Compile the envelope based on config defaults and mailable settings.
178
+ */
179
+ async buildEnvelope(configPromise: MailConfig | Promise<MailConfig>): Promise<Envelope> {
180
+ const config = await Promise.resolve(configPromise)
181
+
182
+ // Inject translator from config if available
183
+ if (config.translator) {
184
+ this.setTranslator(config.translator)
185
+ }
186
+
187
+ this.build() // User logic executes here
188
+
189
+ // Ensure Renderer is initialized if using TemplateRenderer with config path
190
+ if (this.renderer instanceof TemplateRenderer && config.viewsDir) {
191
+ // Here we could re-initialize TemplateRenderer if we had a setter for viewsDir
192
+ // For now, it defaults to process.cwd()/src/emails which is standard
193
+ }
194
+
195
+ const envelope: Envelope = {
196
+ from: this.envelope.from || config.from,
197
+ to: this.envelope.to || [],
198
+ subject: this.envelope.subject || '(No Subject)',
199
+ priority: this.envelope.priority || 'normal',
200
+ }
201
+
202
+ if (this.envelope.cc) {
203
+ envelope.cc = this.envelope.cc
204
+ }
205
+ if (this.envelope.bcc) {
206
+ envelope.bcc = this.envelope.bcc
207
+ }
208
+ if (this.envelope.replyTo) {
209
+ envelope.replyTo = this.envelope.replyTo
210
+ }
211
+ if (this.envelope.attachments) {
212
+ envelope.attachments = this.envelope.attachments
213
+ }
214
+
215
+ return envelope
216
+ }
217
+
218
+ /**
219
+ * execute the renderer
220
+ */
221
+ async renderContent(): Promise<{ html: string; text?: string }> {
222
+ // Resolve lazy renderer if needed
223
+ if (!this.renderer && this.rendererResolver) {
224
+ this.renderer = await this.rendererResolver()
225
+ }
226
+
227
+ if (!this.renderer) {
228
+ throw new Error('No content renderer specified. Use html(), view(), react(), or vue().')
229
+ }
230
+
231
+ // Inject i18n helpers into renderData
232
+ this.renderData = {
233
+ ...this.renderData,
234
+ locale: this.currentLocale,
235
+ t: (key: string, replace?: Record<string, unknown>) => this.t(key, replace),
236
+ }
237
+
238
+ return this.renderer.render(this.renderData)
239
+ }
240
+
241
+ private normalizeAddressArray(input: string | Address | (string | Address)[]): Address[] {
242
+ const arr = Array.isArray(input) ? input : [input]
243
+ return arr.map((item) => (typeof item === 'string' ? { address: item } : item))
244
+ }
245
+ }