@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.
- package/README.md +129 -0
- package/dist/OrbitMail-2Z7ZTKYA.mjs +7 -0
- package/dist/OrbitMail-BGV32HWN.mjs +7 -0
- package/dist/OrbitMail-FUYZQSAV.mjs +7 -0
- package/dist/OrbitMail-NAPCRK7B.mjs +7 -0
- package/dist/OrbitMail-REGJ276B.mjs +7 -0
- package/dist/OrbitMail-TCFBJWDT.mjs +7 -0
- package/dist/OrbitMail-XZZW6U4N.mjs +7 -0
- package/dist/OrbitSignal-ZKKMEC27.mjs +7 -0
- package/dist/ReactRenderer-L5INVYKT.mjs +27 -0
- package/dist/VueRenderer-S65ZARRI.mjs +37129 -0
- package/dist/VueRenderer-Z5PRVBNH.mjs +37298 -0
- package/dist/chunk-3U2CYJO5.mjs +367 -0
- package/dist/chunk-3XFC4T6M.mjs +392 -0
- package/dist/chunk-6DZX6EAA.mjs +37 -0
- package/dist/chunk-DT3R2TNV.mjs +367 -0
- package/dist/chunk-GADWIVC4.mjs +400 -0
- package/dist/chunk-HHKFAMSE.mjs +380 -0
- package/dist/chunk-OKRNL6PN.mjs +400 -0
- package/dist/chunk-ULN3GMY2.mjs +367 -0
- package/dist/chunk-XAWO7RSP.mjs +398 -0
- package/dist/index.d.mts +278 -0
- package/dist/index.d.ts +278 -0
- package/dist/index.js +38150 -0
- package/dist/index.mjs +316 -0
- package/package.json +73 -0
- package/src/Mailable.ts +245 -0
- package/src/OrbitSignal.ts +158 -0
- package/src/Queueable.ts +9 -0
- package/src/dev/DevMailbox.ts +64 -0
- package/src/dev/DevServer.ts +89 -0
- package/src/dev/ui/mailbox.ts +68 -0
- package/src/dev/ui/preview.ts +59 -0
- package/src/dev/ui/shared.ts +46 -0
- package/src/index.ts +20 -0
- package/src/renderers/HtmlRenderer.ts +22 -0
- package/src/renderers/ReactRenderer.ts +35 -0
- package/src/renderers/Renderer.ts +11 -0
- package/src/renderers/TemplateRenderer.ts +34 -0
- package/src/renderers/VueRenderer.ts +37 -0
- package/src/transports/LogTransport.ts +17 -0
- package/src/transports/MemoryTransport.ts +11 -0
- package/src/transports/SesTransport.ts +56 -0
- package/src/transports/SmtpTransport.ts +50 -0
- package/src/transports/Transport.ts +8 -0
- package/src/types.ts +71 -0
- package/tests/mailable.test.ts +77 -0
- package/tests/renderers.test.ts +56 -0
- package/tests/transports.test.ts +52 -0
- 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(/ /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(/ /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
|
+
}
|
package/src/Mailable.ts
ADDED
|
@@ -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
|
+
}
|