@nebutra/email 0.1.0 → 0.1.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/.turbo/turbo-build.log +17 -0
- package/.turbo/turbo-test.log +16 -0
- package/.turbo/turbo-typecheck.log +4 -0
- package/CHANGELOG.md +30 -0
- package/LICENSE +21 -676
- package/README.md +21 -4
- package/dist/index.d.ts +415 -0
- package/dist/index.js +740 -0
- package/dist/index.js.map +1 -0
- package/package.json +5 -4
- package/src/__tests__/provider.test.ts +5 -5
- package/src/index.ts +85 -30
- package/src/templates/_layout.ts +15 -6
- package/src/templates/invitation.tsx +2 -1
package/dist/index.js
ADDED
|
@@ -0,0 +1,740 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { brand, colors as colors3 } from "@nebutra/brand/metadata";
|
|
3
|
+
import { getBrandMailFrom, getBrandOrigin as getBrandOrigin2 } from "@nebutra/brand/metadata-helpers";
|
|
4
|
+
|
|
5
|
+
// src/providers/console.ts
|
|
6
|
+
var counter = 0;
|
|
7
|
+
var ConsoleEmailProvider = class {
|
|
8
|
+
name = "console";
|
|
9
|
+
async send(opts) {
|
|
10
|
+
counter += 1;
|
|
11
|
+
const id = `console_${Date.now()}_${counter}`;
|
|
12
|
+
const recipients = Array.isArray(opts.to) ? opts.to.join(", ") : opts.to;
|
|
13
|
+
const separator = "\u2500".repeat(60);
|
|
14
|
+
const lines = [
|
|
15
|
+
`
|
|
16
|
+
${separator}`,
|
|
17
|
+
`\u{1F4E7} Email (console provider)`,
|
|
18
|
+
`${separator}`,
|
|
19
|
+
`From: ${opts.from}`,
|
|
20
|
+
`To: ${recipients}`,
|
|
21
|
+
`Subject: ${opts.subject}`
|
|
22
|
+
];
|
|
23
|
+
if (opts.replyTo) {
|
|
24
|
+
lines.push(`Reply-To: ${opts.replyTo}`);
|
|
25
|
+
}
|
|
26
|
+
if (opts.tags?.length) {
|
|
27
|
+
const tagStr = opts.tags.map((t) => `${t.name}=${t.value}`).join(", ");
|
|
28
|
+
lines.push(`Tags: ${tagStr}`);
|
|
29
|
+
}
|
|
30
|
+
lines.push(`ID: ${id}`);
|
|
31
|
+
lines.push(`${separator}
|
|
32
|
+
`);
|
|
33
|
+
process.stdout.write(lines.join("\n"));
|
|
34
|
+
return { id };
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// src/providers/nodemailer.ts
|
|
39
|
+
var NodemailerEmailProvider = class {
|
|
40
|
+
name = "nodemailer";
|
|
41
|
+
config;
|
|
42
|
+
transporter;
|
|
43
|
+
constructor(config) {
|
|
44
|
+
if (!config.host) {
|
|
45
|
+
throw new Error("SMTP_HOST is required for Nodemailer provider");
|
|
46
|
+
}
|
|
47
|
+
this.config = config;
|
|
48
|
+
}
|
|
49
|
+
async getTransporter() {
|
|
50
|
+
if (this.transporter) return this.transporter;
|
|
51
|
+
const nodemailer = await import("nodemailer");
|
|
52
|
+
this.transporter = nodemailer.createTransport({
|
|
53
|
+
host: this.config.host,
|
|
54
|
+
port: this.config.port,
|
|
55
|
+
secure: this.config.secure,
|
|
56
|
+
...this.config.auth ? { auth: this.config.auth } : {}
|
|
57
|
+
});
|
|
58
|
+
return this.transporter;
|
|
59
|
+
}
|
|
60
|
+
async send(opts) {
|
|
61
|
+
const transporter = await this.getTransporter();
|
|
62
|
+
const info = await transporter.sendMail({
|
|
63
|
+
from: opts.from,
|
|
64
|
+
to: Array.isArray(opts.to) ? opts.to.join(", ") : opts.to,
|
|
65
|
+
subject: opts.subject,
|
|
66
|
+
html: opts.html,
|
|
67
|
+
...opts.replyTo ? { replyTo: opts.replyTo } : {},
|
|
68
|
+
...opts.tags?.length ? {
|
|
69
|
+
headers: {
|
|
70
|
+
"X-Email-Tags": opts.tags.map((t) => `${t.name}=${t.value}`).join(", ")
|
|
71
|
+
}
|
|
72
|
+
} : {}
|
|
73
|
+
});
|
|
74
|
+
return { id: info.messageId };
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// src/providers/resend.ts
|
|
79
|
+
import { Resend } from "resend";
|
|
80
|
+
var ResendEmailProvider = class {
|
|
81
|
+
name = "resend";
|
|
82
|
+
client;
|
|
83
|
+
constructor(apiKey) {
|
|
84
|
+
if (!apiKey) {
|
|
85
|
+
throw new Error("RESEND_API_KEY is required for Resend provider");
|
|
86
|
+
}
|
|
87
|
+
this.client = new Resend(apiKey);
|
|
88
|
+
}
|
|
89
|
+
async send(opts) {
|
|
90
|
+
const { data, error } = await this.client.emails.send({
|
|
91
|
+
from: opts.from,
|
|
92
|
+
to: Array.isArray(opts.to) ? opts.to : [opts.to],
|
|
93
|
+
subject: opts.subject,
|
|
94
|
+
html: opts.html,
|
|
95
|
+
...opts.replyTo ? { replyTo: opts.replyTo } : {},
|
|
96
|
+
...opts.tags ? { tags: opts.tags } : {}
|
|
97
|
+
});
|
|
98
|
+
if (error) {
|
|
99
|
+
throw new Error(`Resend send failed: ${error.message}`);
|
|
100
|
+
}
|
|
101
|
+
return { id: data?.id ?? "unknown" };
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// src/provider.ts
|
|
106
|
+
var cachedProvider;
|
|
107
|
+
function detectProviderType() {
|
|
108
|
+
const explicit = process.env.EMAIL_PROVIDER;
|
|
109
|
+
if (explicit === "resend" || explicit === "nodemailer" || explicit === "console") {
|
|
110
|
+
return explicit;
|
|
111
|
+
}
|
|
112
|
+
if (process.env.RESEND_API_KEY) return "resend";
|
|
113
|
+
if (process.env.SMTP_HOST) return "nodemailer";
|
|
114
|
+
return "console";
|
|
115
|
+
}
|
|
116
|
+
function createProvider(type) {
|
|
117
|
+
switch (type) {
|
|
118
|
+
case "resend":
|
|
119
|
+
return new ResendEmailProvider(process.env.RESEND_API_KEY);
|
|
120
|
+
case "nodemailer":
|
|
121
|
+
return new NodemailerEmailProvider({
|
|
122
|
+
host: process.env.SMTP_HOST,
|
|
123
|
+
port: Number(process.env.SMTP_PORT ?? "587"),
|
|
124
|
+
secure: process.env.SMTP_SECURE === "true",
|
|
125
|
+
auth: process.env.SMTP_USER ? { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS ?? "" } : void 0
|
|
126
|
+
});
|
|
127
|
+
case "console":
|
|
128
|
+
return new ConsoleEmailProvider();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function getEmailProvider() {
|
|
132
|
+
if (cachedProvider) return cachedProvider;
|
|
133
|
+
const type = detectProviderType();
|
|
134
|
+
cachedProvider = createProvider(type);
|
|
135
|
+
return cachedProvider;
|
|
136
|
+
}
|
|
137
|
+
function resetEmailProvider() {
|
|
138
|
+
cachedProvider = void 0;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// src/templates/invitation.tsx
|
|
142
|
+
import { colors as colors2 } from "@nebutra/brand/metadata";
|
|
143
|
+
|
|
144
|
+
// src/templates/_layout.ts
|
|
145
|
+
import { colors } from "@nebutra/brand/metadata";
|
|
146
|
+
import { getBrandOrigin } from "@nebutra/brand/metadata-helpers";
|
|
147
|
+
var BRAND_HEADER = colors.primary["500"];
|
|
148
|
+
var COLOR_BG = "#f8fafc";
|
|
149
|
+
var COLOR_BORDER = "#e2e8f0";
|
|
150
|
+
var COLOR_HEADING = "#0f172a";
|
|
151
|
+
var COLOR_BODY = "#475569";
|
|
152
|
+
var COLOR_MUTED = "#94a3b8";
|
|
153
|
+
function baseLayout({ brandName, preview: preview5, body }) {
|
|
154
|
+
return `<!DOCTYPE html>
|
|
155
|
+
<html lang="en">
|
|
156
|
+
<head>
|
|
157
|
+
<meta charset="UTF-8" />
|
|
158
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
159
|
+
<meta name="x-apple-disable-message-reformatting" />
|
|
160
|
+
<title>${escapeHtml(brandName)}</title>
|
|
161
|
+
</head>
|
|
162
|
+
<body style="margin:0;padding:0;background:${COLOR_BG};font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;">
|
|
163
|
+
<div style="display:none;max-height:0;overflow:hidden;">${escapeHtml(preview5)} ‌ </div>
|
|
164
|
+
<table width="100%" cellpadding="0" cellspacing="0" role="presentation" style="background:${COLOR_BG};">
|
|
165
|
+
<tr><td align="center" style="padding:40px 16px;">
|
|
166
|
+
<table width="600" cellpadding="0" cellspacing="0" role="presentation" style="background:#ffffff;border-radius:12px;border:1px solid ${COLOR_BORDER};overflow:hidden;max-width:600px;width:100%;">
|
|
167
|
+
<tr><td style="background:${BRAND_HEADER};padding:32px 40px;">
|
|
168
|
+
<h1 style="margin:0;color:#ffffff;font-size:24px;font-weight:700;letter-spacing:-0.5px;">${escapeHtml(brandName)}</h1>
|
|
169
|
+
</td></tr>
|
|
170
|
+
<tr><td style="padding:40px;">
|
|
171
|
+
${body}
|
|
172
|
+
</td></tr>
|
|
173
|
+
<tr><td style="padding:24px 40px;border-top:1px solid ${COLOR_BORDER};background:${COLOR_BG};">
|
|
174
|
+
<p style="margin:0;font-size:12px;color:${COLOR_MUTED};text-align:center;">
|
|
175
|
+
© ${(/* @__PURE__ */ new Date()).getFullYear()} ${escapeHtml(brandName)} ·
|
|
176
|
+
<a href="${getBrandOrigin("landing")}/privacy" style="color:${COLOR_MUTED};">Privacy</a> ·
|
|
177
|
+
<a href="${getBrandOrigin("landing")}/unsubscribe" style="color:${COLOR_MUTED};">Unsubscribe</a>
|
|
178
|
+
</p>
|
|
179
|
+
</td></tr>
|
|
180
|
+
</table>
|
|
181
|
+
</td></tr>
|
|
182
|
+
</table>
|
|
183
|
+
</body>
|
|
184
|
+
</html>`;
|
|
185
|
+
}
|
|
186
|
+
var palette = {
|
|
187
|
+
/** @deprecated alias of solid identity header — not a product skin */
|
|
188
|
+
gradient: BRAND_HEADER,
|
|
189
|
+
header: BRAND_HEADER,
|
|
190
|
+
background: COLOR_BG,
|
|
191
|
+
border: COLOR_BORDER,
|
|
192
|
+
heading: COLOR_HEADING,
|
|
193
|
+
body: COLOR_BODY,
|
|
194
|
+
muted: COLOR_MUTED
|
|
195
|
+
};
|
|
196
|
+
function escapeHtml(value) {
|
|
197
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// src/templates/invitation.tsx
|
|
201
|
+
function subject(props) {
|
|
202
|
+
return `${props.inviterName} invited you to join ${props.organizationName}`;
|
|
203
|
+
}
|
|
204
|
+
function preview(props) {
|
|
205
|
+
return `You've been invited to ${props.organizationName} as ${props.role}`;
|
|
206
|
+
}
|
|
207
|
+
function render(props) {
|
|
208
|
+
const inviterName = escapeHtml(props.inviterName);
|
|
209
|
+
const organizationName = escapeHtml(props.organizationName);
|
|
210
|
+
const role = escapeHtml(props.role);
|
|
211
|
+
const acceptUrl = escapeHtml(props.acceptUrl);
|
|
212
|
+
const expiresAt = escapeHtml(props.expiresAt);
|
|
213
|
+
const brandName = escapeHtml(props.brandName);
|
|
214
|
+
const body = `
|
|
215
|
+
<h2 style="margin:0 0 16px;font-size:22px;color:${palette.heading};">You're invited to ${organizationName}</h2>
|
|
216
|
+
<p style="margin:0 0 16px;font-size:15px;color:${palette.body};line-height:1.6;">
|
|
217
|
+
<strong>${inviterName}</strong> has invited you to collaborate on
|
|
218
|
+
<strong>${organizationName}</strong>.
|
|
219
|
+
</p>
|
|
220
|
+
<p style="margin:0 0 24px;font-size:14px;color:${palette.body};">
|
|
221
|
+
Role:
|
|
222
|
+
<span style="display:inline-block;background:#e0f2fe;color:${colors2.primary["500"]};padding:4px 10px;border-radius:999px;font-size:13px;font-weight:600;margin-left:6px;">
|
|
223
|
+
${role}
|
|
224
|
+
</span>
|
|
225
|
+
</p>
|
|
226
|
+
<a href="${acceptUrl}" style="display:inline-block;background:${palette.gradient};color:#ffffff;text-decoration:none;border-radius:8px;padding:12px 24px;font-size:15px;font-weight:600;margin:0 0 24px;">
|
|
227
|
+
Accept invitation →
|
|
228
|
+
</a>
|
|
229
|
+
<p style="margin:0 0 12px;font-size:13px;color:${palette.muted};">
|
|
230
|
+
This invitation expires on <strong>${expiresAt}</strong>.
|
|
231
|
+
</p>
|
|
232
|
+
<p style="margin:0;font-size:13px;color:${palette.muted};">
|
|
233
|
+
Sent by ${brandName} on behalf of ${organizationName}. If you weren't expecting this, you can
|
|
234
|
+
safely ignore the message.
|
|
235
|
+
</p>
|
|
236
|
+
`;
|
|
237
|
+
return baseLayout({
|
|
238
|
+
brandName: props.brandName,
|
|
239
|
+
preview: preview(props),
|
|
240
|
+
body
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
var invitationEmail = { subject, preview, render };
|
|
244
|
+
|
|
245
|
+
// src/templates/password-reset.tsx
|
|
246
|
+
function subject2(props) {
|
|
247
|
+
return `Reset your ${props.brandName} password`;
|
|
248
|
+
}
|
|
249
|
+
function preview2(props) {
|
|
250
|
+
return `Reset link expires in ${props.expiresInMinutes} minutes`;
|
|
251
|
+
}
|
|
252
|
+
function render2(props) {
|
|
253
|
+
const userName = escapeHtml(props.userName);
|
|
254
|
+
const resetUrl = escapeHtml(props.resetUrl);
|
|
255
|
+
const brandName = escapeHtml(props.brandName);
|
|
256
|
+
const expires = String(props.expiresInMinutes);
|
|
257
|
+
const body = `
|
|
258
|
+
<h2 style="margin:0 0 16px;font-size:22px;color:${palette.heading};">Reset your password</h2>
|
|
259
|
+
<p style="margin:0 0 16px;font-size:15px;color:${palette.body};line-height:1.6;">
|
|
260
|
+
Hi ${userName}, we received a request to reset the password on your ${brandName} account.
|
|
261
|
+
Click the button below to choose a new one.
|
|
262
|
+
</p>
|
|
263
|
+
<a href="${resetUrl}" style="display:inline-block;background:${palette.gradient};color:#ffffff;text-decoration:none;border-radius:8px;padding:12px 24px;font-size:15px;font-weight:600;margin:0 0 24px;">
|
|
264
|
+
Reset password →
|
|
265
|
+
</a>
|
|
266
|
+
<p style="margin:0 0 16px;font-size:14px;color:${palette.body};line-height:1.6;">
|
|
267
|
+
This link expires in <strong>${expires} minutes</strong>. If it's expired, request a new one
|
|
268
|
+
from the sign-in page.
|
|
269
|
+
</p>
|
|
270
|
+
<p style="margin:0;font-size:13px;color:${palette.muted};line-height:1.5;">
|
|
271
|
+
If this wasn't you, you can safely ignore this email — your password won't change unless
|
|
272
|
+
you click the link above. For added safety, review recent sign-ins on your account.
|
|
273
|
+
</p>
|
|
274
|
+
`;
|
|
275
|
+
return baseLayout({
|
|
276
|
+
brandName: props.brandName,
|
|
277
|
+
preview: preview2(props),
|
|
278
|
+
body
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
var passwordResetEmail = { subject: subject2, preview: preview2, render: render2 };
|
|
282
|
+
|
|
283
|
+
// src/templates/receipt.tsx
|
|
284
|
+
function subject3(props) {
|
|
285
|
+
return `Receipt for ${props.invoiceNumber}`;
|
|
286
|
+
}
|
|
287
|
+
function preview3(props) {
|
|
288
|
+
return `Payment of ${props.amount} ${props.currency} received`;
|
|
289
|
+
}
|
|
290
|
+
function render3(props) {
|
|
291
|
+
const customerName = escapeHtml(props.customerName);
|
|
292
|
+
const invoiceNumber = escapeHtml(props.invoiceNumber);
|
|
293
|
+
const amount = escapeHtml(props.amount);
|
|
294
|
+
const currency = escapeHtml(props.currency);
|
|
295
|
+
const periodStart = escapeHtml(props.periodStart);
|
|
296
|
+
const periodEnd = escapeHtml(props.periodEnd);
|
|
297
|
+
const downloadUrl = escapeHtml(props.downloadUrl);
|
|
298
|
+
const brandName = escapeHtml(props.brandName);
|
|
299
|
+
const body = `
|
|
300
|
+
<h2 style="margin:0 0 16px;font-size:22px;color:${palette.heading};">Payment received ✓</h2>
|
|
301
|
+
<p style="margin:0 0 16px;font-size:15px;color:${palette.body};line-height:1.6;">
|
|
302
|
+
Hi ${customerName}, thank you for your payment. Here's a summary of your latest invoice from
|
|
303
|
+
${brandName}.
|
|
304
|
+
</p>
|
|
305
|
+
<table width="100%" cellpadding="0" cellspacing="0" style="margin:0 0 24px;border-collapse:collapse;border:1px solid ${palette.border};border-radius:8px;overflow:hidden;">
|
|
306
|
+
<tr>
|
|
307
|
+
<td style="padding:12px 16px;border-bottom:1px solid ${palette.border};font-size:13px;color:${palette.muted};">Invoice number</td>
|
|
308
|
+
<td style="padding:12px 16px;border-bottom:1px solid ${palette.border};font-size:14px;color:${palette.heading};text-align:right;font-weight:600;">${invoiceNumber}</td>
|
|
309
|
+
</tr>
|
|
310
|
+
<tr>
|
|
311
|
+
<td style="padding:12px 16px;border-bottom:1px solid ${palette.border};font-size:13px;color:${palette.muted};">Billing period</td>
|
|
312
|
+
<td style="padding:12px 16px;border-bottom:1px solid ${palette.border};font-size:14px;color:${palette.heading};text-align:right;">${periodStart} — ${periodEnd}</td>
|
|
313
|
+
</tr>
|
|
314
|
+
<tr>
|
|
315
|
+
<td style="padding:12px 16px;font-size:13px;color:${palette.muted};">Amount paid</td>
|
|
316
|
+
<td style="padding:12px 16px;font-size:14px;color:#16a34a;text-align:right;font-weight:700;">${amount} ${currency}</td>
|
|
317
|
+
</tr>
|
|
318
|
+
</table>
|
|
319
|
+
<a href="${downloadUrl}" style="display:inline-block;background:#0f172a;color:#ffffff;text-decoration:none;border-radius:8px;padding:12px 24px;font-size:15px;font-weight:600;margin:0 0 24px;">
|
|
320
|
+
Download PDF receipt →
|
|
321
|
+
</a>
|
|
322
|
+
<p style="margin:0;font-size:13px;color:${palette.muted};">
|
|
323
|
+
Keep this receipt for your records. Questions about this charge? Reply to this email and the
|
|
324
|
+
${brandName} billing team will help.
|
|
325
|
+
</p>
|
|
326
|
+
`;
|
|
327
|
+
return baseLayout({
|
|
328
|
+
brandName: props.brandName,
|
|
329
|
+
preview: preview3(props),
|
|
330
|
+
body
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
var receiptEmail = { subject: subject3, preview: preview3, render: render3 };
|
|
334
|
+
|
|
335
|
+
// src/templates/welcome.tsx
|
|
336
|
+
function subject4(props) {
|
|
337
|
+
return `Welcome to ${props.brandName}`;
|
|
338
|
+
}
|
|
339
|
+
function preview4(_props) {
|
|
340
|
+
return "Get started with your account";
|
|
341
|
+
}
|
|
342
|
+
function render4(props) {
|
|
343
|
+
const userName = escapeHtml(props.userName);
|
|
344
|
+
const loginUrl = escapeHtml(props.loginUrl);
|
|
345
|
+
const brandName = escapeHtml(props.brandName);
|
|
346
|
+
const body = `
|
|
347
|
+
<h2 style="margin:0 0 16px;font-size:22px;color:${palette.heading};">Welcome, ${userName}!</h2>
|
|
348
|
+
<p style="margin:0 0 16px;font-size:15px;color:${palette.body};line-height:1.6;">
|
|
349
|
+
We're glad to have you with ${brandName}. Your account is ready — sign in to
|
|
350
|
+
explore the dashboard, set up your workspace, and invite the rest of your team.
|
|
351
|
+
</p>
|
|
352
|
+
<a href="${loginUrl}" style="display:inline-block;background:${palette.gradient};color:#ffffff;text-decoration:none;border-radius:8px;padding:12px 24px;font-size:15px;font-weight:600;margin:0 0 24px;">
|
|
353
|
+
Get Started →
|
|
354
|
+
</a>
|
|
355
|
+
<p style="margin:0;font-size:13px;color:${palette.muted};">
|
|
356
|
+
Need help? Reply to this email and a real human from the ${brandName} team will get back to you.
|
|
357
|
+
</p>
|
|
358
|
+
`;
|
|
359
|
+
return baseLayout({
|
|
360
|
+
brandName: props.brandName,
|
|
361
|
+
preview: preview4(props),
|
|
362
|
+
body
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
var welcomeEmail = { subject: subject4, preview: preview4, render: render4 };
|
|
366
|
+
|
|
367
|
+
// src/templates/index.ts
|
|
368
|
+
var REACT_EMAIL_TEMPLATES = {
|
|
369
|
+
welcome: {
|
|
370
|
+
id: "welcome-react",
|
|
371
|
+
label: "Welcome (React Email)",
|
|
372
|
+
description: "Account onboarding welcome message",
|
|
373
|
+
fileName: "welcome-react-email.html",
|
|
374
|
+
...welcomeEmail
|
|
375
|
+
},
|
|
376
|
+
passwordReset: {
|
|
377
|
+
id: "password-reset",
|
|
378
|
+
label: "Password Reset",
|
|
379
|
+
description: "Secure password reset link with expiry",
|
|
380
|
+
fileName: "password-reset-email.html",
|
|
381
|
+
...passwordResetEmail
|
|
382
|
+
},
|
|
383
|
+
invitation: {
|
|
384
|
+
id: "invitation",
|
|
385
|
+
label: "Invitation",
|
|
386
|
+
description: "Workspace invitation with role and expiry",
|
|
387
|
+
fileName: "invitation-email.html",
|
|
388
|
+
...invitationEmail
|
|
389
|
+
},
|
|
390
|
+
receipt: {
|
|
391
|
+
id: "receipt",
|
|
392
|
+
label: "Receipt",
|
|
393
|
+
description: "Payment receipt with billing period and PDF link",
|
|
394
|
+
fileName: "receipt-email.html",
|
|
395
|
+
...receiptEmail
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
// src/index.ts
|
|
400
|
+
var FROM = process.env.EMAIL_FROM ?? getBrandMailFrom();
|
|
401
|
+
var EMAIL_TEMPLATE_CATALOG = [
|
|
402
|
+
{
|
|
403
|
+
id: "welcome",
|
|
404
|
+
label: "Welcome",
|
|
405
|
+
description: "Workspace provisioning welcome email",
|
|
406
|
+
sendHelper: "sendWelcomeEmail",
|
|
407
|
+
fileName: "welcome-email.html"
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
id: "order-confirmation",
|
|
411
|
+
label: "Order Confirmation",
|
|
412
|
+
description: "Commerce order receipt with line items",
|
|
413
|
+
sendHelper: "sendOrderConfirmationEmail",
|
|
414
|
+
fileName: "order-confirmation-email.html"
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
id: "magic-link",
|
|
418
|
+
label: "Magic Link",
|
|
419
|
+
description: "Passwordless sign-in link",
|
|
420
|
+
sendHelper: "sendMagicLinkEmail",
|
|
421
|
+
fileName: "magic-link-email.html"
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
id: "email-change",
|
|
425
|
+
label: "Email Change",
|
|
426
|
+
description: "Confirm a new account email address",
|
|
427
|
+
sendHelper: "sendEmailChangeEmail",
|
|
428
|
+
fileName: "email-change-email.html"
|
|
429
|
+
},
|
|
430
|
+
{
|
|
431
|
+
id: "contact-form-received",
|
|
432
|
+
label: "Contact Form Received",
|
|
433
|
+
description: "Contact form acknowledgement",
|
|
434
|
+
sendHelper: "sendContactFormReceivedEmail",
|
|
435
|
+
fileName: "contact-form-received-email.html"
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
id: "license-created",
|
|
439
|
+
label: "License Created",
|
|
440
|
+
description: "License key delivery email",
|
|
441
|
+
sendHelper: "sendLicenseCreatedEmail",
|
|
442
|
+
fileName: "license-created-email.html"
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
id: "welcome-react",
|
|
446
|
+
label: "Welcome (React Email)",
|
|
447
|
+
description: "Account onboarding welcome message",
|
|
448
|
+
sendHelper: "sendWelcomeReactEmail",
|
|
449
|
+
fileName: "welcome-react-email.html"
|
|
450
|
+
},
|
|
451
|
+
{
|
|
452
|
+
id: "password-reset",
|
|
453
|
+
label: "Password Reset",
|
|
454
|
+
description: "Secure password reset link with expiry",
|
|
455
|
+
sendHelper: "sendPasswordResetEmail",
|
|
456
|
+
fileName: "password-reset-email.html"
|
|
457
|
+
},
|
|
458
|
+
{
|
|
459
|
+
id: "invitation",
|
|
460
|
+
label: "Invitation",
|
|
461
|
+
description: "Workspace invitation with role and expiry",
|
|
462
|
+
sendHelper: "sendInvitationEmail",
|
|
463
|
+
fileName: "invitation-email.html"
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
id: "receipt",
|
|
467
|
+
label: "Receipt",
|
|
468
|
+
description: "Payment receipt with billing period and PDF link",
|
|
469
|
+
sendHelper: "sendReceiptEmail",
|
|
470
|
+
fileName: "receipt-email.html"
|
|
471
|
+
}
|
|
472
|
+
];
|
|
473
|
+
async function send(opts) {
|
|
474
|
+
const provider = getEmailProvider();
|
|
475
|
+
return provider.send({
|
|
476
|
+
...opts,
|
|
477
|
+
from: FROM
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
function baseLayout2(content, previewText = "") {
|
|
481
|
+
return `<!DOCTYPE html>
|
|
482
|
+
<html lang="en">
|
|
483
|
+
<head>
|
|
484
|
+
<meta charset="UTF-8" />
|
|
485
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
486
|
+
<meta name="x-apple-disable-message-reformatting" />
|
|
487
|
+
<title>${brand.name}</title>
|
|
488
|
+
<!--[if mso]><noscript><xml><o:OfficeDocumentSettings><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml></noscript><![endif]-->
|
|
489
|
+
</head>
|
|
490
|
+
<body style="margin:0;padding:0;background:#f8fafc;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;">
|
|
491
|
+
${previewText ? `<div style="display:none;max-height:0;overflow:hidden;">${previewText} \u200C‌ </div>` : ""}
|
|
492
|
+
<table width="100%" cellpadding="0" cellspacing="0" role="presentation" style="background:#f8fafc;">
|
|
493
|
+
<tr><td align="center" style="padding:40px 16px;">
|
|
494
|
+
<table width="600" cellpadding="0" cellspacing="0" role="presentation" style="background:#ffffff;border-radius:12px;border:1px solid #e2e8f0;overflow:hidden;max-width:600px;width:100%;">
|
|
495
|
+
<!-- Header -->
|
|
496
|
+
<tr><td style="background:${colors3.primary["500"]};padding:32px 40px;">
|
|
497
|
+
<h1 style="margin:0;color:#ffffff;font-size:24px;font-weight:700;letter-spacing:-0.5px;">${brand.name}</h1>
|
|
498
|
+
</td></tr>
|
|
499
|
+
<!-- Body -->
|
|
500
|
+
<tr><td style="padding:40px;">
|
|
501
|
+
${content}
|
|
502
|
+
</td></tr>
|
|
503
|
+
<!-- Footer -->
|
|
504
|
+
<tr><td style="padding:24px 40px;border-top:1px solid #e2e8f0;background:#f8fafc;">
|
|
505
|
+
<p style="margin:0;font-size:12px;color:#94a3b8;text-align:center;">
|
|
506
|
+
\xA9 ${(/* @__PURE__ */ new Date()).getFullYear()} ${brand.nameFullEn} \xB7
|
|
507
|
+
<a href="${getBrandOrigin2("landing")}/privacy" style="color:#94a3b8;">Privacy</a> \xB7
|
|
508
|
+
<a href="${getBrandOrigin2("landing")}/unsubscribe" style="color:#94a3b8;">Unsubscribe</a>
|
|
509
|
+
</p>
|
|
510
|
+
</td></tr>
|
|
511
|
+
</table>
|
|
512
|
+
</td></tr>
|
|
513
|
+
</table>
|
|
514
|
+
</body>
|
|
515
|
+
</html>`;
|
|
516
|
+
}
|
|
517
|
+
async function sendWelcomeEmail(opts) {
|
|
518
|
+
const dashboardUrl = opts.dashboardUrl ?? getBrandOrigin2("app");
|
|
519
|
+
const html = baseLayout2(
|
|
520
|
+
`
|
|
521
|
+
<h2 style="margin:0 0 16px;font-size:22px;color:#0f172a;">Welcome to ${brand.name}, ${opts.firstName}!</h2>
|
|
522
|
+
<p style="margin:0 0 16px;font-size:15px;color:#475569;line-height:1.6;">
|
|
523
|
+
Your workspace <strong>${opts.orgName}</strong> is ready. You can now invite team members,
|
|
524
|
+
create API keys, and start building with the ${brand.name} platform.
|
|
525
|
+
</p>
|
|
526
|
+
<a href="${dashboardUrl}" style="display:inline-block;background:${colors3.primary["500"]};color:#ffffff;text-decoration:none;border-radius:8px;padding:12px 24px;font-size:15px;font-weight:600;margin:0 0 24px;">
|
|
527
|
+
Open Dashboard \u2192
|
|
528
|
+
</a>
|
|
529
|
+
<p style="margin:0;font-size:13px;color:#94a3b8;">
|
|
530
|
+
If you have questions, reply to this email or visit our <a href="${getBrandOrigin2("docs")}" style="color:${colors3.primary["500"]};">documentation</a>.
|
|
531
|
+
</p>
|
|
532
|
+
`,
|
|
533
|
+
`Welcome to ${brand.name} \u2014 ${opts.orgName} is ready`
|
|
534
|
+
);
|
|
535
|
+
return send({
|
|
536
|
+
to: opts.to,
|
|
537
|
+
subject: `Welcome to ${brand.name} \u2014 ${opts.orgName} is ready`,
|
|
538
|
+
html,
|
|
539
|
+
tags: [{ name: "type", value: "welcome" }]
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
async function sendOrderConfirmationEmail(opts) {
|
|
543
|
+
const formattedTotal = new Intl.NumberFormat(opts.locale ?? "en-US", {
|
|
544
|
+
style: "currency",
|
|
545
|
+
currency: opts.currency ?? "USD"
|
|
546
|
+
}).format(opts.totalAmount / 100);
|
|
547
|
+
const itemsHtml = opts.items.map(
|
|
548
|
+
(item) => `<tr>
|
|
549
|
+
<td style="padding:12px 0;border-bottom:1px solid #e2e8f0;color:#0f172a;font-size:15px;">${item.productId}</td>
|
|
550
|
+
<td style="padding:12px 0;border-bottom:1px solid #e2e8f0;color:#475569;font-size:15px;text-align:right;">x${item.quantity}</td>
|
|
551
|
+
</tr>`
|
|
552
|
+
).join("");
|
|
553
|
+
const html = baseLayout2(
|
|
554
|
+
`
|
|
555
|
+
<h2 style="margin:0 0 16px;font-size:22px;color:#0f172a;">Order Confirmed!</h2>
|
|
556
|
+
<p style="margin:0 0 16px;font-size:15px;color:#475569;line-height:1.6;">
|
|
557
|
+
Thank you for your order <strong>#${opts.orderId}</strong>. We've received your payment of
|
|
558
|
+
<strong>${formattedTotal}</strong> and your order is now being processed.
|
|
559
|
+
</p>
|
|
560
|
+
<table width="100%" cellpadding="0" cellspacing="0" style="margin:0 0 24px;border-collapse:collapse;">
|
|
561
|
+
${itemsHtml}
|
|
562
|
+
</table>
|
|
563
|
+
<a href="${getBrandOrigin2("app")}/orders/${opts.orderId}" style="display:inline-block;background:${colors3.primary["500"]};color:#ffffff;text-decoration:none;border-radius:8px;padding:12px 24px;font-size:15px;font-weight:600;margin:0 0 24px;">
|
|
564
|
+
View Order Details \u2192
|
|
565
|
+
</a>
|
|
566
|
+
`,
|
|
567
|
+
`Order #${opts.orderId} Confirmed`
|
|
568
|
+
);
|
|
569
|
+
return send({
|
|
570
|
+
to: opts.to,
|
|
571
|
+
subject: `Order #${opts.orderId} Confirmed`,
|
|
572
|
+
html,
|
|
573
|
+
tags: [{ name: "type", value: "order_confirmation" }]
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
async function sendMagicLinkEmail(opts) {
|
|
577
|
+
const html = baseLayout2(
|
|
578
|
+
`
|
|
579
|
+
<h2 style="margin:0 0 16px;font-size:22px;color:#0f172a;">Sign in to ${brand.name}</h2>
|
|
580
|
+
<p style="margin:0 0 16px;font-size:15px;color:#475569;line-height:1.6;">
|
|
581
|
+
Click the button below to sign in securely. No password required.
|
|
582
|
+
</p>
|
|
583
|
+
<a href="${opts.magicLinkUrl}" style="display:inline-block;background:${colors3.primary["500"]};color:#ffffff;text-decoration:none;border-radius:8px;padding:12px 24px;font-size:16px;font-weight:600;margin:0 0 24px;text-align:center;width:100%;max-width:280px;">
|
|
584
|
+
\u2728 Sign In Automatically
|
|
585
|
+
</a>
|
|
586
|
+
<p style="margin:0;font-size:12px;color:#94a3b8;line-height:1.4;">
|
|
587
|
+
If you did not request this email, you can safely ignore it.
|
|
588
|
+
This link expires in 15 minutes.
|
|
589
|
+
</p>
|
|
590
|
+
`,
|
|
591
|
+
`Click here to sign in to your ${brand.name} account`
|
|
592
|
+
);
|
|
593
|
+
return send({
|
|
594
|
+
to: opts.to,
|
|
595
|
+
subject: `Sign in to ${brand.name} \u2728`,
|
|
596
|
+
html,
|
|
597
|
+
tags: [{ name: "type", value: "magic_link" }]
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
async function sendEmailChangeEmail(opts) {
|
|
601
|
+
const recipientEmail = opts.newEmail ?? opts.to;
|
|
602
|
+
const html = baseLayout2(
|
|
603
|
+
`
|
|
604
|
+
<h2 style="margin:0 0 16px;font-size:22px;color:#0f172a;">Confirm your new email address</h2>
|
|
605
|
+
<p style="margin:0 0 16px;font-size:15px;color:#475569;line-height:1.6;">
|
|
606
|
+
We received a request to change the email on your ${brand.name} account to
|
|
607
|
+
<strong>${recipientEmail}</strong>. Click the button below to confirm. This link expires in 1 hour.
|
|
608
|
+
</p>
|
|
609
|
+
<a href="${opts.confirmUrl}" style="display:inline-block;background:${colors3.primary["500"]};color:#ffffff;text-decoration:none;border-radius:8px;padding:12px 24px;font-size:16px;font-weight:600;margin:0 0 24px;text-align:center;width:100%;max-width:280px;">
|
|
610
|
+
Confirm email change \u2192
|
|
611
|
+
</a>
|
|
612
|
+
<p style="margin:0;font-size:12px;color:#94a3b8;line-height:1.4;">
|
|
613
|
+
If you did not request this change you can safely ignore this email \u2014 your address will not be updated.
|
|
614
|
+
</p>
|
|
615
|
+
`,
|
|
616
|
+
"Confirm your new Nebutra email address"
|
|
617
|
+
);
|
|
618
|
+
return send({
|
|
619
|
+
to: opts.to,
|
|
620
|
+
subject: "Confirm your new Nebutra email address",
|
|
621
|
+
html,
|
|
622
|
+
tags: [{ name: "type", value: "email_change_verification" }]
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
async function sendContactFormReceivedEmail(opts) {
|
|
626
|
+
const html = baseLayout2(
|
|
627
|
+
`
|
|
628
|
+
<h2 style="margin:0 0 16px;font-size:22px;color:#0f172a;">Message Received</h2>
|
|
629
|
+
<p style="margin:0 0 16px;font-size:15px;color:#475569;line-height:1.6;">
|
|
630
|
+
Hi ${opts.name},
|
|
631
|
+
</p>
|
|
632
|
+
<p style="margin:0 0 16px;font-size:15px;color:#475569;line-height:1.6;">
|
|
633
|
+
Thank you for contacting us regarding "<strong>${opts.subject}</strong>".
|
|
634
|
+
We've received your request and our team will get back to you within 1-2 business days.
|
|
635
|
+
</p>
|
|
636
|
+
<p style="margin:0;font-size:15px;color:#475569;line-height:1.6;">
|
|
637
|
+
Best regards,<br/>The ${brand.name} Team
|
|
638
|
+
</p>
|
|
639
|
+
`,
|
|
640
|
+
"We have received your message"
|
|
641
|
+
);
|
|
642
|
+
return send({
|
|
643
|
+
to: opts.to,
|
|
644
|
+
subject: `We've received your message - ${brand.name}`,
|
|
645
|
+
html,
|
|
646
|
+
tags: [{ name: "type", value: "contact_receipt" }]
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
async function sendLicenseCreatedEmail(opts) {
|
|
650
|
+
const html = baseLayout2(
|
|
651
|
+
`
|
|
652
|
+
<h2 style="margin:0 0 16px;font-size:22px;color:#0f172a;">Welcome to ${brand.name}-Sailor! \u{1F680}</h2>
|
|
653
|
+
<p style="margin:0 0 16px;font-size:15px;color:#475569;line-height:1.6;">
|
|
654
|
+
Hi ${opts.firstName}, your <strong>${opts.tier}</strong> license has been successfully generated.
|
|
655
|
+
</p>
|
|
656
|
+
<p style="margin:0 0 16px;font-size:15px;color:#475569;line-height:1.6;">
|
|
657
|
+
Your official License Key is:
|
|
658
|
+
</p>
|
|
659
|
+
<div style="background:#0f172a;border-radius:8px;padding:16px 20px;margin:0 0 24px;overflow-x:auto;">
|
|
660
|
+
<code style="color:${colors3.accent["500"]};font-family:'Courier New',monospace;font-size:13px;word-break:break-all;">${opts.licenseKey}</code>
|
|
661
|
+
</div>
|
|
662
|
+
<p style="margin:0 0 16px;font-size:15px;color:#475569;line-height:1.6;">
|
|
663
|
+
To unlock the premium capabilities of the ${brand.name} CLI locally, run the following command in your terminal:
|
|
664
|
+
</p>
|
|
665
|
+
<div style="background:#f1f5f9;border:1px solid #e2e8f0;border-radius:6px;padding:12px;margin:0 0 24px;">
|
|
666
|
+
<code style="color:#334155;font-family:'Courier New',monospace;font-size:13px;">nebutra license activate ${opts.licenseKey}</code>
|
|
667
|
+
</div>
|
|
668
|
+
<p style="margin:0;font-size:13px;color:#94a3b8;">
|
|
669
|
+
If you have questions, reply to this email or visit our <a href="${getBrandOrigin2("docs")}" style="color:${colors3.primary["500"]};">documentation</a>.
|
|
670
|
+
</p>
|
|
671
|
+
`,
|
|
672
|
+
`Your ${brand.name} License Key is Ready`
|
|
673
|
+
);
|
|
674
|
+
return send({
|
|
675
|
+
to: opts.to,
|
|
676
|
+
subject: `Your ${brand.name} License Key is Ready (${opts.tier})`,
|
|
677
|
+
html,
|
|
678
|
+
tags: [{ name: "type", value: "license_created" }]
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
async function sendWelcomeReactEmail(opts) {
|
|
682
|
+
return send({
|
|
683
|
+
to: opts.to,
|
|
684
|
+
subject: welcomeEmail.subject(opts),
|
|
685
|
+
html: welcomeEmail.render(opts),
|
|
686
|
+
tags: [{ name: "type", value: "welcome_react" }]
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
async function sendPasswordResetEmail(opts) {
|
|
690
|
+
return send({
|
|
691
|
+
to: opts.to,
|
|
692
|
+
subject: passwordResetEmail.subject(opts),
|
|
693
|
+
html: passwordResetEmail.render(opts),
|
|
694
|
+
tags: [{ name: "type", value: "password_reset" }]
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
async function sendInvitationEmail(opts) {
|
|
698
|
+
return send({
|
|
699
|
+
to: opts.to,
|
|
700
|
+
subject: invitationEmail.subject(opts),
|
|
701
|
+
html: invitationEmail.render(opts),
|
|
702
|
+
tags: [{ name: "type", value: "invitation" }]
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
async function sendReceiptEmail(opts) {
|
|
706
|
+
return send({
|
|
707
|
+
to: opts.to,
|
|
708
|
+
subject: receiptEmail.subject(opts),
|
|
709
|
+
html: receiptEmail.render(opts),
|
|
710
|
+
tags: [{ name: "type", value: "receipt" }]
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
export {
|
|
714
|
+
ConsoleEmailProvider,
|
|
715
|
+
EMAIL_TEMPLATE_CATALOG,
|
|
716
|
+
NodemailerEmailProvider,
|
|
717
|
+
REACT_EMAIL_TEMPLATES,
|
|
718
|
+
ResendEmailProvider,
|
|
719
|
+
getEmailProvider,
|
|
720
|
+
invitationEmail,
|
|
721
|
+
passwordResetEmail,
|
|
722
|
+
receiptEmail,
|
|
723
|
+
render as renderInvitationEmail,
|
|
724
|
+
render2 as renderPasswordResetEmail,
|
|
725
|
+
render3 as renderReceiptEmail,
|
|
726
|
+
render4 as renderWelcomeEmail,
|
|
727
|
+
resetEmailProvider,
|
|
728
|
+
sendContactFormReceivedEmail,
|
|
729
|
+
sendEmailChangeEmail,
|
|
730
|
+
sendInvitationEmail,
|
|
731
|
+
sendLicenseCreatedEmail,
|
|
732
|
+
sendMagicLinkEmail,
|
|
733
|
+
sendOrderConfirmationEmail,
|
|
734
|
+
sendPasswordResetEmail,
|
|
735
|
+
sendReceiptEmail,
|
|
736
|
+
sendWelcomeEmail,
|
|
737
|
+
sendWelcomeReactEmail,
|
|
738
|
+
welcomeEmail
|
|
739
|
+
};
|
|
740
|
+
//# sourceMappingURL=index.js.map
|