@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/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)}&nbsp;&zwnj;&nbsp;</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
+ &copy; ${(/* @__PURE__ */ new Date()).getFullYear()} ${escapeHtml(brandName)} &middot;
176
+ <a href="${getBrandOrigin("landing")}/privacy" style="color:${COLOR_MUTED};">Privacy</a> &middot;
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
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 &rarr;
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 &rarr;
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 &mdash; 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 &check;</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} &mdash; ${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 &rarr;
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 &mdash; 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 &rarr;
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}&nbsp;\u200C&zwnj;&nbsp;</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