@nebutra/email 2.0.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +6 -6
- package/.turbo/turbo-test.log +7 -7
- package/.turbo/turbo-typecheck.log +1 -1
- package/CHANGELOG.md +7 -0
- package/README.md +3 -6
- package/dist/index.d.ts +5 -31
- package/dist/index.js +4 -53
- package/dist/index.js.map +1 -1
- package/package.json +4 -12
- package/src/__tests__/provider.test.ts +0 -9
- package/src/index.ts +5 -8
- package/src/provider.ts +3 -15
- package/tsup.config.ts +1 -1
- package/src/providers/nodemailer.ts +0 -73
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @nebutra/email@
|
|
2
|
+
> @nebutra/email@3.0.0 build /home/runner/work/Nebutra-Sailor/Nebutra-Sailor/packages/integrations/email
|
|
3
3
|
> tsup
|
|
4
4
|
|
|
5
5
|
[34mCLI[39m Building entry: src/index.ts
|
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
[34mCLI[39m Target: es2022
|
|
10
10
|
[34mCLI[39m Cleaning output folder
|
|
11
11
|
[34mESM[39m Build start
|
|
12
|
-
[32mESM[39m [1mdist/index.js [22m[
|
|
13
|
-
[32mESM[39m [1mdist/index.js.map [22m[
|
|
14
|
-
[32mESM[39m ⚡️ Build success in
|
|
12
|
+
[32mESM[39m [1mdist/index.js [22m[32m26.53 KB[39m
|
|
13
|
+
[32mESM[39m [1mdist/index.js.map [22m[32m50.57 KB[39m
|
|
14
|
+
[32mESM[39m ⚡️ Build success in 96ms
|
|
15
15
|
[34mDTS[39m Build start
|
|
16
|
-
[32mDTS[39m ⚡️ Build success in
|
|
17
|
-
[32mDTS[39m [1mdist/index.d.ts [22m[
|
|
16
|
+
[32mDTS[39m ⚡️ Build success in 17154ms
|
|
17
|
+
[32mDTS[39m [1mdist/index.d.ts [22m[32m13.30 KB[39m
|
package/.turbo/turbo-test.log
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
|
|
2
|
-
> @nebutra/email@
|
|
2
|
+
> @nebutra/email@3.0.0 test /home/runner/work/Nebutra-Sailor/Nebutra-Sailor/packages/integrations/email
|
|
3
3
|
> vitest run
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
[1m[30m[46m RUN [49m[39m[22m [36mv4.1.4 [39m[90m/home/runner/work/Nebutra-Sailor/Nebutra-Sailor/packages/integrations/email[39m
|
|
7
7
|
|
|
8
|
-
[32m✓[39m src/__tests__/provider.test.ts [2m([22m[
|
|
9
|
-
[32m✓[39m src/__tests__/email-contract.test.ts [2m([22m[2m2 tests[22m[2m)[22m[32m
|
|
10
|
-
[32m✓[39m src/templates/__tests__/templates.test.ts [2m([22m[2m13 tests[22m[2m)[22m[32m
|
|
8
|
+
[32m✓[39m src/__tests__/provider.test.ts [2m([22m[2m9 tests[22m[2m)[22m[32m 168[2mms[22m[39m
|
|
9
|
+
[32m✓[39m src/__tests__/email-contract.test.ts [2m([22m[2m2 tests[22m[2m)[22m[32m 39[2mms[22m[39m
|
|
10
|
+
[32m✓[39m src/templates/__tests__/templates.test.ts [2m([22m[2m13 tests[22m[2m)[22m[32m 75[2mms[22m[39m
|
|
11
11
|
|
|
12
12
|
[2m Test Files [22m [1m[32m3 passed[39m[22m[90m (3)[39m
|
|
13
|
-
[2m Tests [22m [1m[
|
|
14
|
-
[2m Start at [22m
|
|
15
|
-
[2m Duration [22m 1.
|
|
13
|
+
[2m Tests [22m [1m[32m24 passed[39m[22m[90m (24)[39m
|
|
14
|
+
[2m Start at [22m 08:50:44
|
|
15
|
+
[2m Duration [22m 1.70s[2m (transform 1.43s, setup 0ms, import 1.94s, tests 282ms, environment 0ms)[22m
|
|
16
16
|
|
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Multi-provider transactional email for Nebutra products.
|
|
4
4
|
|
|
5
|
-
The package auto-detects Resend
|
|
6
|
-
|
|
5
|
+
The package auto-detects Resend or a console development provider, then sends
|
|
6
|
+
branded HTML templates through a single API.
|
|
7
7
|
|
|
8
8
|
## Installation
|
|
9
9
|
|
|
@@ -55,10 +55,8 @@ await sendApiKeyCreatedEmail({
|
|
|
55
55
|
|
|
56
56
|
| Environment Variable | Description |
|
|
57
57
|
|---------------------|-------------|
|
|
58
|
-
| `EMAIL_PROVIDER` | Optional explicit provider: `resend
|
|
58
|
+
| `EMAIL_PROVIDER` | Optional explicit provider: `resend` or `console` |
|
|
59
59
|
| `RESEND_API_KEY` | Resend API key from https://resend.com/api-keys |
|
|
60
|
-
| `SMTP_HOST` | Enables Nodemailer SMTP provider when Resend is not configured |
|
|
61
|
-
| `SMTP_PORT` / `SMTP_USER` / `SMTP_PASS` / `SMTP_SECURE` | SMTP provider settings |
|
|
62
60
|
| `EMAIL_FROM` | Verified sender address (default: `Nebutra <noreply@nebutra.com>`) |
|
|
63
61
|
|
|
64
62
|
## Providers
|
|
@@ -66,7 +64,6 @@ await sendApiKeyCreatedEmail({
|
|
|
66
64
|
| Provider | Selection |
|
|
67
65
|
| --- | --- |
|
|
68
66
|
| `resend` | `EMAIL_PROVIDER=resend` or `RESEND_API_KEY` present |
|
|
69
|
-
| `nodemailer` | `EMAIL_PROVIDER=nodemailer` or `SMTP_HOST` present |
|
|
70
67
|
| `console` | Explicit `EMAIL_PROVIDER=console` or development/test fallback |
|
|
71
68
|
|
|
72
69
|
## License
|
package/dist/index.d.ts
CHANGED
|
@@ -4,8 +4,7 @@
|
|
|
4
4
|
* Supports multiple email providers via auto-detection:
|
|
5
5
|
* 1. EMAIL_PROVIDER env var (explicit)
|
|
6
6
|
* 2. RESEND_API_KEY → Resend
|
|
7
|
-
* 3.
|
|
8
|
-
* 4. Fallback → Console (dev/test)
|
|
7
|
+
* 3. Fallback → Console (dev/test)
|
|
9
8
|
*/
|
|
10
9
|
interface SendOptions {
|
|
11
10
|
to: string | string[];
|
|
@@ -25,7 +24,7 @@ interface EmailProvider {
|
|
|
25
24
|
readonly name: string;
|
|
26
25
|
send(opts: SendOptions): Promise<SendResult>;
|
|
27
26
|
}
|
|
28
|
-
type EmailProviderType = "resend" | "
|
|
27
|
+
type EmailProviderType = "resend" | "console";
|
|
29
28
|
declare function getEmailProvider(): EmailProvider;
|
|
30
29
|
declare function resetEmailProvider(): void;
|
|
31
30
|
|
|
@@ -39,29 +38,6 @@ declare class ConsoleEmailProvider implements EmailProvider {
|
|
|
39
38
|
send(opts: SendOptions): Promise<SendResult>;
|
|
40
39
|
}
|
|
41
40
|
|
|
42
|
-
/**
|
|
43
|
-
* Nodemailer email provider — SMTP-based, self-hosted fallback.
|
|
44
|
-
* Requires SMTP_HOST (+ optional SMTP_PORT, SMTP_USER, SMTP_PASS, SMTP_SECURE).
|
|
45
|
-
*/
|
|
46
|
-
|
|
47
|
-
interface NodemailerConfig {
|
|
48
|
-
host: string;
|
|
49
|
-
port: number;
|
|
50
|
-
secure: boolean;
|
|
51
|
-
auth?: {
|
|
52
|
-
user: string;
|
|
53
|
-
pass: string;
|
|
54
|
-
} | undefined;
|
|
55
|
-
}
|
|
56
|
-
declare class NodemailerEmailProvider implements EmailProvider {
|
|
57
|
-
readonly name: "nodemailer";
|
|
58
|
-
private config;
|
|
59
|
-
private transporter;
|
|
60
|
-
constructor(config: NodemailerConfig);
|
|
61
|
-
private getTransporter;
|
|
62
|
-
send(opts: SendOptions): Promise<SendResult>;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
41
|
/**
|
|
66
42
|
* Resend email provider — production-grade transactional email.
|
|
67
43
|
* Requires RESEND_API_KEY environment variable.
|
|
@@ -240,10 +216,9 @@ declare const REACT_EMAIL_TEMPLATES: {
|
|
|
240
216
|
* @nebutra/email — Multi-provider transactional email
|
|
241
217
|
*
|
|
242
218
|
* Provider-agnostic email system with auto-detection:
|
|
243
|
-
* 1. EMAIL_PROVIDER env var (explicit: "resend" | "
|
|
219
|
+
* 1. EMAIL_PROVIDER env var (explicit: "resend" | "console")
|
|
244
220
|
* 2. RESEND_API_KEY present → Resend
|
|
245
|
-
* 3.
|
|
246
|
-
* 4. Fallback → Console (dev/test, no API key needed)
|
|
221
|
+
* 3. Fallback → Console (dev/test, no API key needed)
|
|
247
222
|
*
|
|
248
223
|
* Usage:
|
|
249
224
|
* import { sendWelcomeEmail, sendApiKeyCreatedEmail } from "@nebutra/email";
|
|
@@ -253,7 +228,6 @@ declare const REACT_EMAIL_TEMPLATES: {
|
|
|
253
228
|
* EMAIL_PROVIDER — explicit provider override (optional)
|
|
254
229
|
* EMAIL_FROM — verified sender (e.g. getBrandMailFrom() example)
|
|
255
230
|
* RESEND_API_KEY — for Resend provider
|
|
256
|
-
* SMTP_HOST — for Nodemailer provider (+ SMTP_PORT, SMTP_USER, SMTP_PASS, SMTP_SECURE)
|
|
257
231
|
*/
|
|
258
232
|
|
|
259
233
|
interface EmailTemplateCatalogEntry {
|
|
@@ -412,4 +386,4 @@ declare function sendReceiptEmail(opts: {
|
|
|
412
386
|
to: string;
|
|
413
387
|
} & ReceiptEmailProps): Promise<SendResult>;
|
|
414
388
|
|
|
415
|
-
export { ConsoleEmailProvider, EMAIL_TEMPLATE_CATALOG, type EmailProvider, type EmailProviderType, type EmailTemplateCatalogEntry, type InvitationEmailProps,
|
|
389
|
+
export { ConsoleEmailProvider, EMAIL_TEMPLATE_CATALOG, type EmailProvider, type EmailProviderType, type EmailTemplateCatalogEntry, type InvitationEmailProps, type PasswordResetEmailProps, REACT_EMAIL_TEMPLATES, type ReactEmailTemplate, type ReceiptEmailProps, type RenderedEmail, ResendEmailProvider, type SendOptions, type SendResult, type WelcomeEmailProps, getEmailProvider, invitationEmail, passwordResetEmail, receiptEmail, render$2 as renderInvitationEmail, render$1 as renderPasswordResetEmail, render$3 as renderReceiptEmail, render as renderWelcomeEmail, resetEmailProvider, sendContactFormReceivedEmail, sendEmailChangeEmail, sendInvitationEmail, sendLicenseCreatedEmail, sendMagicLinkEmail, sendOrderConfirmationEmail, sendPasswordResetEmail, sendReceiptEmail, sendWelcomeEmail, sendWelcomeReactEmail, welcomeEmail };
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import { brand, colors as colors3 } from "@nebutra/brand/metadata";
|
|
3
|
-
import { getBrandMailFrom, getBrandOrigin as getBrandOrigin2 } from "@nebutra/brand/metadata-helpers";
|
|
3
|
+
import { getBrandMailFrom, getBrandOrigin as getBrandOrigin2, getDocsUrl } from "@nebutra/brand/metadata-helpers";
|
|
4
4
|
|
|
5
5
|
// src/providers/console.ts
|
|
6
6
|
var counter = 0;
|
|
@@ -35,46 +35,6 @@ ${separator}`,
|
|
|
35
35
|
}
|
|
36
36
|
};
|
|
37
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
38
|
// src/providers/resend.ts
|
|
79
39
|
import { Resend } from "resend";
|
|
80
40
|
var ResendEmailProvider = class {
|
|
@@ -106,24 +66,16 @@ var ResendEmailProvider = class {
|
|
|
106
66
|
var cachedProvider;
|
|
107
67
|
function detectProviderType() {
|
|
108
68
|
const explicit = process.env.EMAIL_PROVIDER;
|
|
109
|
-
if (explicit === "resend" || explicit === "
|
|
69
|
+
if (explicit === "resend" || explicit === "console") {
|
|
110
70
|
return explicit;
|
|
111
71
|
}
|
|
112
72
|
if (process.env.RESEND_API_KEY) return "resend";
|
|
113
|
-
if (process.env.SMTP_HOST) return "nodemailer";
|
|
114
73
|
return "console";
|
|
115
74
|
}
|
|
116
75
|
function createProvider(type) {
|
|
117
76
|
switch (type) {
|
|
118
77
|
case "resend":
|
|
119
78
|
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
79
|
case "console":
|
|
128
80
|
return new ConsoleEmailProvider();
|
|
129
81
|
}
|
|
@@ -527,7 +479,7 @@ async function sendWelcomeEmail(opts) {
|
|
|
527
479
|
Open Dashboard \u2192
|
|
528
480
|
</a>
|
|
529
481
|
<p style="margin:0;font-size:13px;color:#94a3b8;">
|
|
530
|
-
If you have questions, reply to this email or visit our <a href="${
|
|
482
|
+
If you have questions, reply to this email or visit our <a href="${getDocsUrl()}" style="color:${colors3.primary["500"]};">documentation</a>.
|
|
531
483
|
</p>
|
|
532
484
|
`,
|
|
533
485
|
`Welcome to ${brand.name} \u2014 ${opts.orgName} is ready`
|
|
@@ -666,7 +618,7 @@ async function sendLicenseCreatedEmail(opts) {
|
|
|
666
618
|
<code style="color:#334155;font-family:'Courier New',monospace;font-size:13px;">nebutra license activate ${opts.licenseKey}</code>
|
|
667
619
|
</div>
|
|
668
620
|
<p style="margin:0;font-size:13px;color:#94a3b8;">
|
|
669
|
-
If you have questions, reply to this email or visit our <a href="${
|
|
621
|
+
If you have questions, reply to this email or visit our <a href="${getDocsUrl()}" style="color:${colors3.primary["500"]};">documentation</a>.
|
|
670
622
|
</p>
|
|
671
623
|
`,
|
|
672
624
|
`Your ${brand.name} License Key is Ready`
|
|
@@ -713,7 +665,6 @@ async function sendReceiptEmail(opts) {
|
|
|
713
665
|
export {
|
|
714
666
|
ConsoleEmailProvider,
|
|
715
667
|
EMAIL_TEMPLATE_CATALOG,
|
|
716
|
-
NodemailerEmailProvider,
|
|
717
668
|
REACT_EMAIL_TEMPLATES,
|
|
718
669
|
ResendEmailProvider,
|
|
719
670
|
getEmailProvider,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/providers/console.ts","../src/providers/nodemailer.ts","../src/providers/resend.ts","../src/provider.ts","../src/templates/invitation.tsx","../src/templates/_layout.ts","../src/templates/password-reset.tsx","../src/templates/receipt.tsx","../src/templates/welcome.tsx","../src/templates/index.ts"],"sourcesContent":["/**\n * @nebutra/email — Multi-provider transactional email\n *\n * Provider-agnostic email system with auto-detection:\n * 1. EMAIL_PROVIDER env var (explicit: \"resend\" | \"nodemailer\" | \"console\")\n * 2. RESEND_API_KEY present → Resend\n * 3. SMTP_HOST present → Nodemailer (SMTP)\n * 4. Fallback → Console (dev/test, no API key needed)\n *\n * Usage:\n * import { sendWelcomeEmail, sendApiKeyCreatedEmail } from \"@nebutra/email\";\n * await sendWelcomeEmail({ to: \"user@example.com\", orgName: \"Acme Corp\" });\n *\n * Environment variables:\n * EMAIL_PROVIDER — explicit provider override (optional)\n * EMAIL_FROM — verified sender (e.g. getBrandMailFrom() example)\n * RESEND_API_KEY — for Resend provider\n * SMTP_HOST — for Nodemailer provider (+ SMTP_PORT, SMTP_USER, SMTP_PASS, SMTP_SECURE)\n */\n\nimport { brand, colors } from \"@nebutra/brand/metadata\";\nimport { getBrandMailFrom, getBrandOrigin } from \"@nebutra/brand/metadata-helpers\";\nimport { getEmailProvider, type SendResult } from \"./provider\";\n\nexport type { EmailProvider, EmailProviderType, SendOptions, SendResult } from \"./provider\";\nexport { getEmailProvider, resetEmailProvider } from \"./provider\";\nexport { ConsoleEmailProvider } from \"./providers/console\";\nexport { NodemailerEmailProvider } from \"./providers/nodemailer\";\nexport { ResendEmailProvider } from \"./providers/resend\";\n\nconst FROM = process.env.EMAIL_FROM ?? getBrandMailFrom();\n\n// ── Types ──────────────────────────────────────────────────────────────────\n\nexport interface EmailTemplateCatalogEntry {\n id: string;\n label: string;\n description: string;\n sendHelper: string;\n fileName: string;\n}\n\nexport const EMAIL_TEMPLATE_CATALOG = [\n {\n id: \"welcome\",\n label: \"Welcome\",\n description: \"Workspace provisioning welcome email\",\n sendHelper: \"sendWelcomeEmail\",\n fileName: \"welcome-email.html\",\n },\n {\n id: \"order-confirmation\",\n label: \"Order Confirmation\",\n description: \"Commerce order receipt with line items\",\n sendHelper: \"sendOrderConfirmationEmail\",\n fileName: \"order-confirmation-email.html\",\n },\n {\n id: \"magic-link\",\n label: \"Magic Link\",\n description: \"Passwordless sign-in link\",\n sendHelper: \"sendMagicLinkEmail\",\n fileName: \"magic-link-email.html\",\n },\n {\n id: \"email-change\",\n label: \"Email Change\",\n description: \"Confirm a new account email address\",\n sendHelper: \"sendEmailChangeEmail\",\n fileName: \"email-change-email.html\",\n },\n {\n id: \"contact-form-received\",\n label: \"Contact Form Received\",\n description: \"Contact form acknowledgement\",\n sendHelper: \"sendContactFormReceivedEmail\",\n fileName: \"contact-form-received-email.html\",\n },\n {\n id: \"license-created\",\n label: \"License Created\",\n description: \"License key delivery email\",\n sendHelper: \"sendLicenseCreatedEmail\",\n fileName: \"license-created-email.html\",\n },\n {\n id: \"welcome-react\",\n label: \"Welcome (React Email)\",\n description: \"Account onboarding welcome message\",\n sendHelper: \"sendWelcomeReactEmail\",\n fileName: \"welcome-react-email.html\",\n },\n {\n id: \"password-reset\",\n label: \"Password Reset\",\n description: \"Secure password reset link with expiry\",\n sendHelper: \"sendPasswordResetEmail\",\n fileName: \"password-reset-email.html\",\n },\n {\n id: \"invitation\",\n label: \"Invitation\",\n description: \"Workspace invitation with role and expiry\",\n sendHelper: \"sendInvitationEmail\",\n fileName: \"invitation-email.html\",\n },\n {\n id: \"receipt\",\n label: \"Receipt\",\n description: \"Payment receipt with billing period and PDF link\",\n sendHelper: \"sendReceiptEmail\",\n fileName: \"receipt-email.html\",\n },\n] as const satisfies readonly EmailTemplateCatalogEntry[];\n\n// ── Core send helper ───────────────────────────────────────────────────────\n\nasync function send(opts: {\n to: string | string[];\n subject: string;\n html: string;\n replyTo?: string;\n tags?: { name: string; value: string }[];\n}): Promise<SendResult> {\n const provider = getEmailProvider();\n return provider.send({\n ...opts,\n from: FROM,\n });\n}\n\n// ── Templates ──────────────────────────────────────────────────────────────\n\nfunction baseLayout(content: string, previewText = \"\"): string {\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n <meta name=\"x-apple-disable-message-reformatting\" />\n <title>${brand.name}</title>\n <!--[if mso]><noscript><xml><o:OfficeDocumentSettings><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml></noscript><![endif]-->\n</head>\n<body style=\"margin:0;padding:0;background:#f8fafc;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;\">\n ${previewText ? `<div style=\"display:none;max-height:0;overflow:hidden;\">${previewText} ‌ </div>` : \"\"}\n <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#f8fafc;\">\n <tr><td align=\"center\" style=\"padding:40px 16px;\">\n <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%;\">\n <!-- Header -->\n <tr><td style=\"background:${colors.primary[\"500\"]};padding:32px 40px;\">\n <h1 style=\"margin:0;color:#ffffff;font-size:24px;font-weight:700;letter-spacing:-0.5px;\">${brand.name}</h1>\n </td></tr>\n <!-- Body -->\n <tr><td style=\"padding:40px;\">\n ${content}\n </td></tr>\n <!-- Footer -->\n <tr><td style=\"padding:24px 40px;border-top:1px solid #e2e8f0;background:#f8fafc;\">\n <p style=\"margin:0;font-size:12px;color:#94a3b8;text-align:center;\">\n © ${new Date().getFullYear()} ${brand.nameFullEn} ·\n <a href=\"${getBrandOrigin(\"landing\")}/privacy\" style=\"color:#94a3b8;\">Privacy</a> ·\n <a href=\"${getBrandOrigin(\"landing\")}/unsubscribe\" style=\"color:#94a3b8;\">Unsubscribe</a>\n </p>\n </td></tr>\n </table>\n </td></tr>\n </table>\n</body>\n</html>`;\n}\n\n// ── Email senders ──────────────────────────────────────────────────────────\n\n/**\n * Welcome email sent when a new organization is provisioned.\n */\nexport async function sendWelcomeEmail(opts: {\n to: string;\n firstName: string;\n orgName: string;\n dashboardUrl?: string;\n}): Promise<SendResult> {\n const dashboardUrl = opts.dashboardUrl ?? getBrandOrigin(\"app\");\n\n const html = baseLayout(\n `\n <h2 style=\"margin:0 0 16px;font-size:22px;color:#0f172a;\">Welcome to ${brand.name}, ${opts.firstName}!</h2>\n <p style=\"margin:0 0 16px;font-size:15px;color:#475569;line-height:1.6;\">\n Your workspace <strong>${opts.orgName}</strong> is ready. You can now invite team members,\n create API keys, and start building with the ${brand.name} platform.\n </p>\n <a href=\"${dashboardUrl}\" style=\"display:inline-block;background:${colors.primary[\"500\"]};color:#ffffff;text-decoration:none;border-radius:8px;padding:12px 24px;font-size:15px;font-weight:600;margin:0 0 24px;\">\n Open Dashboard →\n </a>\n <p style=\"margin:0;font-size:13px;color:#94a3b8;\">\n If you have questions, reply to this email or visit our <a href=\"${getBrandOrigin(\"docs\")}\" style=\"color:${colors.primary[\"500\"]};\">documentation</a>.\n </p>\n `,\n `Welcome to ${brand.name} — ${opts.orgName} is ready`,\n );\n\n return send({\n to: opts.to,\n subject: `Welcome to ${brand.name} — ${opts.orgName} is ready`,\n html,\n tags: [{ name: \"type\", value: \"welcome\" }],\n });\n}\n\n/**\n * Order confirmation email sent when an order successfully completes.\n */\nexport async function sendOrderConfirmationEmail(opts: {\n to: string;\n orderId: string;\n /** Order total in minor units (integer cents). */\n totalAmount: number;\n /** ISO 4217 currency code (default \"USD\"). */\n currency?: string;\n /** BCP 47 locale tag (default \"en-US\"). */\n locale?: string;\n items: Array<{ productId: string; quantity: number | string }>;\n}): Promise<SendResult> {\n const formattedTotal = new Intl.NumberFormat(opts.locale ?? \"en-US\", {\n style: \"currency\",\n currency: opts.currency ?? \"USD\",\n }).format(opts.totalAmount / 100);\n\n const itemsHtml = opts.items\n .map(\n (item) =>\n `<tr>\n <td style=\"padding:12px 0;border-bottom:1px solid #e2e8f0;color:#0f172a;font-size:15px;\">${item.productId}</td>\n <td style=\"padding:12px 0;border-bottom:1px solid #e2e8f0;color:#475569;font-size:15px;text-align:right;\">x${item.quantity}</td>\n </tr>`,\n )\n .join(\"\");\n\n const html = baseLayout(\n `\n <h2 style=\"margin:0 0 16px;font-size:22px;color:#0f172a;\">Order Confirmed!</h2>\n <p style=\"margin:0 0 16px;font-size:15px;color:#475569;line-height:1.6;\">\n Thank you for your order <strong>#${opts.orderId}</strong>. We've received your payment of\n <strong>${formattedTotal}</strong> and your order is now being processed.\n </p>\n <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin:0 0 24px;border-collapse:collapse;\">\n ${itemsHtml}\n </table>\n <a href=\"${getBrandOrigin(\"app\")}/orders/${opts.orderId}\" style=\"display:inline-block;background:${colors.primary[\"500\"]};color:#ffffff;text-decoration:none;border-radius:8px;padding:12px 24px;font-size:15px;font-weight:600;margin:0 0 24px;\">\n View Order Details →\n </a>\n `,\n `Order #${opts.orderId} Confirmed`,\n );\n\n return send({\n to: opts.to,\n subject: `Order #${opts.orderId} Confirmed`,\n html,\n tags: [{ name: \"type\", value: \"order_confirmation\" }],\n });\n}\n\n/**\n * Shipfast-style Magic Link Email for 1-click Authentication.\n */\nexport async function sendMagicLinkEmail(opts: {\n to: string;\n magicLinkUrl: string;\n}): Promise<SendResult> {\n const html = baseLayout(\n `\n <h2 style=\"margin:0 0 16px;font-size:22px;color:#0f172a;\">Sign in to ${brand.name}</h2>\n <p style=\"margin:0 0 16px;font-size:15px;color:#475569;line-height:1.6;\">\n Click the button below to sign in securely. No password required.\n </p>\n <a href=\"${opts.magicLinkUrl}\" style=\"display:inline-block;background:${colors.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;\">\n ✨ Sign In Automatically\n </a>\n <p style=\"margin:0;font-size:12px;color:#94a3b8;line-height:1.4;\">\n If you did not request this email, you can safely ignore it.\n This link expires in 15 minutes.\n </p>\n `,\n `Click here to sign in to your ${brand.name} account`,\n );\n\n return send({\n to: opts.to,\n subject: `Sign in to ${brand.name} ✨`,\n html,\n tags: [{ name: \"type\", value: \"magic_link\" }],\n });\n}\n\n/**\n * Email-change verification sent to the *new* address (TODO #126).\n * Confirming the link flips the account email.\n */\nexport async function sendEmailChangeEmail(opts: {\n to: string;\n confirmUrl: string;\n /** Optional display name for the new address (defaults to `to`). */\n newEmail?: string;\n}): Promise<SendResult> {\n const recipientEmail = opts.newEmail ?? opts.to;\n const html = baseLayout(\n `\n <h2 style=\"margin:0 0 16px;font-size:22px;color:#0f172a;\">Confirm your new email address</h2>\n <p style=\"margin:0 0 16px;font-size:15px;color:#475569;line-height:1.6;\">\n We received a request to change the email on your ${brand.name} account to\n <strong>${recipientEmail}</strong>. Click the button below to confirm. This link expires in 1 hour.\n </p>\n <a href=\"${opts.confirmUrl}\" style=\"display:inline-block;background:${colors.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;\">\n Confirm email change →\n </a>\n <p style=\"margin:0;font-size:12px;color:#94a3b8;line-height:1.4;\">\n If you did not request this change you can safely ignore this email — your address will not be updated.\n </p>\n `,\n \"Confirm your new Nebutra email address\",\n );\n\n return send({\n to: opts.to,\n subject: \"Confirm your new Nebutra email address\",\n html,\n tags: [{ name: \"type\", value: \"email_change_verification\" }],\n });\n}\n\n/**\n * Contact Form Receipt (Sent to the User)\n */\nexport async function sendContactFormReceivedEmail(opts: {\n to: string;\n name: string;\n subject: string;\n}): Promise<SendResult> {\n const html = baseLayout(\n `\n <h2 style=\"margin:0 0 16px;font-size:22px;color:#0f172a;\">Message Received</h2>\n <p style=\"margin:0 0 16px;font-size:15px;color:#475569;line-height:1.6;\">\n Hi ${opts.name},\n </p>\n <p style=\"margin:0 0 16px;font-size:15px;color:#475569;line-height:1.6;\">\n Thank you for contacting us regarding \"<strong>${opts.subject}</strong>\". \n We've received your request and our team will get back to you within 1-2 business days.\n </p>\n <p style=\"margin:0;font-size:15px;color:#475569;line-height:1.6;\">\n Best regards,<br/>The ${brand.name} Team\n </p>\n `,\n \"We have received your message\",\n );\n\n return send({\n to: opts.to,\n subject: `We've received your message - ${brand.name}`,\n html,\n tags: [{ name: \"type\", value: \"contact_receipt\" }],\n });\n}\n\n// ── Billing Lifecycle Emails ───────────────────────────────────────────────\n\n/**\n * License created email sent when a user claims their OPC/STARTUP license.\n */\nexport async function sendLicenseCreatedEmail(opts: {\n to: string;\n firstName: string;\n licenseKey: string;\n tier: string;\n}): Promise<SendResult> {\n const html = baseLayout(\n `\n <h2 style=\"margin:0 0 16px;font-size:22px;color:#0f172a;\">Welcome to ${brand.name}-Sailor! 🚀</h2>\n <p style=\"margin:0 0 16px;font-size:15px;color:#475569;line-height:1.6;\">\n Hi ${opts.firstName}, your <strong>${opts.tier}</strong> license has been successfully generated.\n </p>\n <p style=\"margin:0 0 16px;font-size:15px;color:#475569;line-height:1.6;\">\n Your official License Key is:\n </p>\n <div style=\"background:#0f172a;border-radius:8px;padding:16px 20px;margin:0 0 24px;overflow-x:auto;\">\n <code style=\"color:${colors.accent[\"500\"]};font-family:'Courier New',monospace;font-size:13px;word-break:break-all;\">${opts.licenseKey}</code>\n </div>\n <p style=\"margin:0 0 16px;font-size:15px;color:#475569;line-height:1.6;\">\n To unlock the premium capabilities of the ${brand.name} CLI locally, run the following command in your terminal:\n </p>\n <div style=\"background:#f1f5f9;border:1px solid #e2e8f0;border-radius:6px;padding:12px;margin:0 0 24px;\">\n <code style=\"color:#334155;font-family:'Courier New',monospace;font-size:13px;\">nebutra license activate ${opts.licenseKey}</code>\n </div>\n <p style=\"margin:0;font-size:13px;color:#94a3b8;\">\n If you have questions, reply to this email or visit our <a href=\"${getBrandOrigin(\"docs\")}\" style=\"color:${colors.primary[\"500\"]};\">documentation</a>.\n </p>\n `,\n `Your ${brand.name} License Key is Ready`,\n );\n\n return send({\n to: opts.to,\n subject: `Your ${brand.name} License Key is Ready (${opts.tier})`,\n html,\n tags: [{ name: \"type\", value: \"license_created\" }],\n });\n}\n\n// ── React Email-style templates (welcome, password-reset, invitation, receipt) ─\n\nimport {\n type InvitationEmailProps,\n invitationEmail,\n type PasswordResetEmailProps,\n passwordResetEmail,\n type ReceiptEmailProps,\n receiptEmail,\n type WelcomeEmailProps,\n welcomeEmail,\n} from \"./templates\";\n\nexport {\n type InvitationEmailProps,\n invitationEmail,\n type PasswordResetEmailProps,\n passwordResetEmail,\n REACT_EMAIL_TEMPLATES,\n type ReactEmailTemplate,\n type ReceiptEmailProps,\n type RenderedEmail,\n receiptEmail,\n renderInvitationEmail,\n renderPasswordResetEmail,\n renderReceiptEmail,\n renderWelcomeEmail,\n type WelcomeEmailProps,\n welcomeEmail,\n} from \"./templates\";\n\n/**\n * Send the React Email-style welcome message. Mirrors `WelcomeEmailProps`\n * exactly so callers can compose messages without depending on the legacy\n * `sendWelcomeEmail` signature.\n */\nexport async function sendWelcomeReactEmail(\n opts: { to: string } & WelcomeEmailProps,\n): Promise<SendResult> {\n return send({\n to: opts.to,\n subject: welcomeEmail.subject(opts),\n html: welcomeEmail.render(opts),\n tags: [{ name: \"type\", value: \"welcome_react\" }],\n });\n}\n\n/**\n * Send a password reset email with a tokenized link and expiry note.\n */\nexport async function sendPasswordResetEmail(\n opts: { to: string } & PasswordResetEmailProps,\n): Promise<SendResult> {\n return send({\n to: opts.to,\n subject: passwordResetEmail.subject(opts),\n html: passwordResetEmail.render(opts),\n tags: [{ name: \"type\", value: \"password_reset\" }],\n });\n}\n\n/**\n * Send a workspace invitation with role and accept link.\n */\nexport async function sendInvitationEmail(\n opts: { to: string } & InvitationEmailProps,\n): Promise<SendResult> {\n return send({\n to: opts.to,\n subject: invitationEmail.subject(opts),\n html: invitationEmail.render(opts),\n tags: [{ name: \"type\", value: \"invitation\" }],\n });\n}\n\n/**\n * Send a payment receipt with billing period and PDF download URL.\n */\nexport async function sendReceiptEmail(\n opts: { to: string } & ReceiptEmailProps,\n): Promise<SendResult> {\n return send({\n to: opts.to,\n subject: receiptEmail.subject(opts),\n html: receiptEmail.render(opts),\n tags: [{ name: \"type\", value: \"receipt\" }],\n });\n}\n","/**\n * Console email provider — prints email details to stdout.\n * Perfect for local development and testing without API keys.\n */\n\nimport type { EmailProvider, SendOptions, SendResult } from \"../provider\";\n\nlet counter = 0;\n\nexport class ConsoleEmailProvider implements EmailProvider {\n readonly name = \"console\" as const;\n\n async send(opts: SendOptions): Promise<SendResult> {\n counter += 1;\n const id = `console_${Date.now()}_${counter}`;\n const recipients = Array.isArray(opts.to) ? opts.to.join(\", \") : opts.to;\n const separator = \"─\".repeat(60);\n\n const lines = [\n `\\n${separator}`,\n `📧 Email (console provider)`,\n `${separator}`,\n `From: ${opts.from}`,\n `To: ${recipients}`,\n `Subject: ${opts.subject}`,\n ];\n\n if (opts.replyTo) {\n lines.push(`Reply-To: ${opts.replyTo}`);\n }\n\n if (opts.tags?.length) {\n const tagStr = opts.tags.map((t) => `${t.name}=${t.value}`).join(\", \");\n lines.push(`Tags: ${tagStr}`);\n }\n\n lines.push(`ID: ${id}`);\n lines.push(`${separator}\\n`);\n\n process.stdout.write(lines.join(\"\\n\"));\n\n return { id };\n }\n}\n","/**\n * Nodemailer email provider — SMTP-based, self-hosted fallback.\n * Requires SMTP_HOST (+ optional SMTP_PORT, SMTP_USER, SMTP_PASS, SMTP_SECURE).\n */\n\nimport type { EmailProvider, SendOptions, SendResult } from \"../provider\";\n\nexport interface NodemailerConfig {\n host: string;\n port: number;\n secure: boolean;\n auth?:\n | {\n user: string;\n pass: string;\n }\n | undefined;\n}\n\nexport class NodemailerEmailProvider implements EmailProvider {\n readonly name = \"nodemailer\" as const;\n private config: NodemailerConfig;\n private transporter: unknown;\n\n constructor(config: NodemailerConfig) {\n if (!config.host) {\n throw new Error(\"SMTP_HOST is required for Nodemailer provider\");\n }\n this.config = config;\n }\n\n private async getTransporter() {\n if (this.transporter) return this.transporter;\n\n // Lazy import to avoid requiring nodemailer when using other providers\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const nodemailer = await (import(\"nodemailer\" as string) as Promise<{\n createTransport(opts: Record<string, unknown>): {\n sendMail(opts: Record<string, unknown>): Promise<{ messageId: string }>;\n };\n }>);\n this.transporter = nodemailer.createTransport({\n host: this.config.host,\n port: this.config.port,\n secure: this.config.secure,\n ...(this.config.auth ? { auth: this.config.auth } : {}),\n });\n return this.transporter;\n }\n\n async send(opts: SendOptions): Promise<SendResult> {\n const transporter = (await this.getTransporter()) as {\n sendMail(opts: Record<string, unknown>): Promise<{ messageId: string }>;\n };\n\n const info = await transporter.sendMail({\n from: opts.from,\n to: Array.isArray(opts.to) ? opts.to.join(\", \") : opts.to,\n subject: opts.subject,\n html: opts.html,\n ...(opts.replyTo ? { replyTo: opts.replyTo } : {}),\n ...(opts.tags?.length\n ? {\n headers: {\n \"X-Email-Tags\": opts.tags.map((t) => `${t.name}=${t.value}`).join(\", \"),\n },\n }\n : {}),\n });\n\n return { id: info.messageId };\n }\n}\n","/**\n * Resend email provider — production-grade transactional email.\n * Requires RESEND_API_KEY environment variable.\n */\n\nimport { Resend } from \"resend\";\nimport type { EmailProvider, SendOptions, SendResult } from \"../provider\";\n\nexport class ResendEmailProvider implements EmailProvider {\n readonly name = \"resend\" as const;\n private client: Resend;\n\n constructor(apiKey: string) {\n if (!apiKey) {\n throw new Error(\"RESEND_API_KEY is required for Resend provider\");\n }\n this.client = new Resend(apiKey);\n }\n\n async send(opts: SendOptions): Promise<SendResult> {\n const { data, error } = await this.client.emails.send({\n from: opts.from,\n to: Array.isArray(opts.to) ? opts.to : [opts.to],\n subject: opts.subject,\n html: opts.html,\n ...(opts.replyTo ? { replyTo: opts.replyTo } : {}),\n ...(opts.tags ? { tags: opts.tags } : {}),\n });\n\n if (error) {\n throw new Error(`Resend send failed: ${error.message}`);\n }\n\n return { id: data?.id ?? \"unknown\" };\n }\n}\n","/**\n * @nebutra/email — Provider abstraction\n *\n * Supports multiple email providers via auto-detection:\n * 1. EMAIL_PROVIDER env var (explicit)\n * 2. RESEND_API_KEY → Resend\n * 3. SMTP_HOST → Nodemailer\n * 4. Fallback → Console (dev/test)\n */\n\n// ── Interface ────────────────────────────────────────────────────────────────\n\nexport interface SendOptions {\n to: string | string[];\n subject: string;\n html: string;\n from: string;\n replyTo?: string;\n tags?: { name: string; value: string }[];\n}\n\nexport interface SendResult {\n id: string;\n}\n\nexport interface EmailProvider {\n readonly name: string;\n send(opts: SendOptions): Promise<SendResult>;\n}\n\n// ── Provider types ───────────────────────────────────────────────────────────\n\nexport type EmailProviderType = \"resend\" | \"nodemailer\" | \"console\";\n\n// ── Factory ──────────────────────────────────────────────────────────────────\n\nimport { ConsoleEmailProvider } from \"./providers/console\";\nimport { NodemailerEmailProvider } from \"./providers/nodemailer\";\nimport { ResendEmailProvider } from \"./providers/resend\";\n\nlet cachedProvider: EmailProvider | undefined;\n\nfunction detectProviderType(): EmailProviderType {\n const explicit = process.env.EMAIL_PROVIDER;\n if (explicit === \"resend\" || explicit === \"nodemailer\" || explicit === \"console\") {\n return explicit;\n }\n\n if (process.env.RESEND_API_KEY) return \"resend\";\n if (process.env.SMTP_HOST) return \"nodemailer\";\n return \"console\";\n}\n\nfunction createProvider(type: EmailProviderType): EmailProvider {\n switch (type) {\n case \"resend\":\n return new ResendEmailProvider(process.env.RESEND_API_KEY!);\n case \"nodemailer\":\n return new NodemailerEmailProvider({\n host: process.env.SMTP_HOST!,\n port: Number(process.env.SMTP_PORT ?? \"587\"),\n secure: process.env.SMTP_SECURE === \"true\",\n auth: process.env.SMTP_USER\n ? { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS ?? \"\" }\n : undefined,\n });\n case \"console\":\n return new ConsoleEmailProvider();\n }\n}\n\nexport function getEmailProvider(): EmailProvider {\n if (cachedProvider) return cachedProvider;\n const type = detectProviderType();\n cachedProvider = createProvider(type);\n return cachedProvider;\n}\n\nexport function resetEmailProvider(): void {\n cachedProvider = undefined;\n}\n","/**\n * InvitationEmail — workspace member invitation with role and expiry.\n *\n * react-email is not installed; this module exposes the same\n * `{ subject, preview, render }` contract using a plain template literal so the\n * catalog can be migrated to React Email later without changing call sites.\n */\n\nimport { colors } from \"@nebutra/brand/metadata\";\nimport { baseLayout, escapeHtml, palette } from \"./_layout\";\n\nexport interface InvitationEmailProps {\n inviterName: string;\n organizationName: string;\n role: string;\n acceptUrl: string;\n expiresAt: string;\n brandName: string;\n}\n\nexport function subject(props: InvitationEmailProps): string {\n return `${props.inviterName} invited you to join ${props.organizationName}`;\n}\n\nexport function preview(props: InvitationEmailProps): string {\n return `You've been invited to ${props.organizationName} as ${props.role}`;\n}\n\nexport function render(props: InvitationEmailProps): string {\n const inviterName = escapeHtml(props.inviterName);\n const organizationName = escapeHtml(props.organizationName);\n const role = escapeHtml(props.role);\n const acceptUrl = escapeHtml(props.acceptUrl);\n const expiresAt = escapeHtml(props.expiresAt);\n const brandName = escapeHtml(props.brandName);\n\n const body = `\n <h2 style=\"margin:0 0 16px;font-size:22px;color:${palette.heading};\">You're invited to ${organizationName}</h2>\n <p style=\"margin:0 0 16px;font-size:15px;color:${palette.body};line-height:1.6;\">\n <strong>${inviterName}</strong> has invited you to collaborate on\n <strong>${organizationName}</strong>.\n </p>\n <p style=\"margin:0 0 24px;font-size:14px;color:${palette.body};\">\n Role:\n <span style=\"display:inline-block;background:#e0f2fe;color:${colors.primary[\"500\"]};padding:4px 10px;border-radius:999px;font-size:13px;font-weight:600;margin-left:6px;\">\n ${role}\n </span>\n </p>\n <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;\">\n Accept invitation →\n </a>\n <p style=\"margin:0 0 12px;font-size:13px;color:${palette.muted};\">\n This invitation expires on <strong>${expiresAt}</strong>.\n </p>\n <p style=\"margin:0;font-size:13px;color:${palette.muted};\">\n Sent by ${brandName} on behalf of ${organizationName}. If you weren't expecting this, you can\n safely ignore the message.\n </p>\n `;\n\n return baseLayout({\n brandName: props.brandName,\n preview: preview(props),\n body,\n });\n}\n\nexport const invitationEmail = { subject, preview, render } as const;\n","import { colors } from \"@nebutra/brand/metadata\";\nimport { getBrandOrigin } from \"@nebutra/brand/metadata-helpers\";\n/**\n * Shared HTML layout for the React Email-style template catalog.\n *\n * NOTE: react-email is NOT installed in this workspace. This module ships a\n * plain template literal layout instead so the templates remain framework-free\n * and can be rendered without a React runtime. The contract still mirrors a\n * future React Email migration: each template module exports\n * `{ subject, preview, render }` and the renderer returns a complete HTML\n * document.\n */\n\nexport interface RenderedEmail {\n html: string;\n subject: string;\n preview: string;\n}\n\n/**\n * Email clients do not load app CSS variables. These are identity-locked hex\n * snapshots (VI brand-primary header), not product-chrome skins. Keep in sync with\n * brand assets — do not invent per-template blues.\n */\nconst BRAND_HEADER = colors.primary[\"500\"];\nconst COLOR_BG = \"#f8fafc\";\nconst COLOR_BORDER = \"#e2e8f0\";\nconst COLOR_HEADING = \"#0f172a\";\nconst COLOR_BODY = \"#475569\";\nconst COLOR_MUTED = \"#94a3b8\";\n\nexport interface BaseLayoutOptions {\n brandName: string;\n preview: string;\n body: string;\n}\n\n/**\n * Wrap a body fragment with the standard brand-style transactional shell.\n */\nexport function baseLayout({ brandName, preview, body }: BaseLayoutOptions): string {\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n <meta name=\"x-apple-disable-message-reformatting\" />\n <title>${escapeHtml(brandName)}</title>\n</head>\n<body style=\"margin:0;padding:0;background:${COLOR_BG};font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;\">\n <div style=\"display:none;max-height:0;overflow:hidden;\">${escapeHtml(preview)} ‌ </div>\n <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:${COLOR_BG};\">\n <tr><td align=\"center\" style=\"padding:40px 16px;\">\n <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%;\">\n <tr><td style=\"background:${BRAND_HEADER};padding:32px 40px;\">\n <h1 style=\"margin:0;color:#ffffff;font-size:24px;font-weight:700;letter-spacing:-0.5px;\">${escapeHtml(brandName)}</h1>\n </td></tr>\n <tr><td style=\"padding:40px;\">\n ${body}\n </td></tr>\n <tr><td style=\"padding:24px 40px;border-top:1px solid ${COLOR_BORDER};background:${COLOR_BG};\">\n <p style=\"margin:0;font-size:12px;color:${COLOR_MUTED};text-align:center;\">\n © ${new Date().getFullYear()} ${escapeHtml(brandName)} ·\n <a href=\"${getBrandOrigin(\"landing\")}/privacy\" style=\"color:${COLOR_MUTED};\">Privacy</a> ·\n <a href=\"${getBrandOrigin(\"landing\")}/unsubscribe\" style=\"color:${COLOR_MUTED};\">Unsubscribe</a>\n </p>\n </td></tr>\n </table>\n </td></tr>\n </table>\n</body>\n</html>`;\n}\n\nexport const palette = {\n /** @deprecated alias of solid identity header — not a product skin */\n gradient: BRAND_HEADER,\n header: BRAND_HEADER,\n background: COLOR_BG,\n border: COLOR_BORDER,\n heading: COLOR_HEADING,\n body: COLOR_BODY,\n muted: COLOR_MUTED,\n} as const;\n\n/**\n * Conservative HTML escape for user-supplied template props.\n *\n * Templates accept structured props from trusted backend code, but escaping is\n * still required because some props (e.g. organization names, invoice numbers)\n * may originate from end-user input.\n */\nexport function escapeHtml(value: string): string {\n return value\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\")\n .replace(/'/g, \"'\");\n}\n","/**\n * PasswordResetEmail — secure password reset link with expiry.\n *\n * react-email is not installed; this module exposes the same\n * `{ subject, preview, render }` contract using a plain template literal so the\n * catalog can be migrated to React Email later without changing call sites.\n */\n\nimport { baseLayout, escapeHtml, palette } from \"./_layout\";\n\nexport interface PasswordResetEmailProps {\n userName: string;\n resetUrl: string;\n expiresInMinutes: number;\n brandName: string;\n}\n\nexport function subject(props: PasswordResetEmailProps): string {\n return `Reset your ${props.brandName} password`;\n}\n\nexport function preview(props: PasswordResetEmailProps): string {\n return `Reset link expires in ${props.expiresInMinutes} minutes`;\n}\n\nexport function render(props: PasswordResetEmailProps): string {\n const userName = escapeHtml(props.userName);\n const resetUrl = escapeHtml(props.resetUrl);\n const brandName = escapeHtml(props.brandName);\n const expires = String(props.expiresInMinutes);\n\n const body = `\n <h2 style=\"margin:0 0 16px;font-size:22px;color:${palette.heading};\">Reset your password</h2>\n <p style=\"margin:0 0 16px;font-size:15px;color:${palette.body};line-height:1.6;\">\n Hi ${userName}, we received a request to reset the password on your ${brandName} account.\n Click the button below to choose a new one.\n </p>\n <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;\">\n Reset password →\n </a>\n <p style=\"margin:0 0 16px;font-size:14px;color:${palette.body};line-height:1.6;\">\n This link expires in <strong>${expires} minutes</strong>. If it's expired, request a new one\n from the sign-in page.\n </p>\n <p style=\"margin:0;font-size:13px;color:${palette.muted};line-height:1.5;\">\n If this wasn't you, you can safely ignore this email — your password won't change unless\n you click the link above. For added safety, review recent sign-ins on your account.\n </p>\n `;\n\n return baseLayout({\n brandName: props.brandName,\n preview: preview(props),\n body,\n });\n}\n\nexport const passwordResetEmail = { subject, preview, render } as const;\n","/**\n * ReceiptEmail — payment confirmation with billing period and invoice download.\n *\n * react-email is not installed; this module exposes the same\n * `{ subject, preview, render }` contract using a plain template literal so the\n * catalog can be migrated to React Email later without changing call sites.\n */\n\nimport { baseLayout, escapeHtml, palette } from \"./_layout\";\n\nexport interface ReceiptEmailProps {\n customerName: string;\n invoiceNumber: string;\n amount: string;\n currency: string;\n periodStart: string;\n periodEnd: string;\n downloadUrl: string;\n brandName: string;\n}\n\nexport function subject(props: ReceiptEmailProps): string {\n return `Receipt for ${props.invoiceNumber}`;\n}\n\nexport function preview(props: ReceiptEmailProps): string {\n return `Payment of ${props.amount} ${props.currency} received`;\n}\n\nexport function render(props: ReceiptEmailProps): string {\n const customerName = escapeHtml(props.customerName);\n const invoiceNumber = escapeHtml(props.invoiceNumber);\n const amount = escapeHtml(props.amount);\n const currency = escapeHtml(props.currency);\n const periodStart = escapeHtml(props.periodStart);\n const periodEnd = escapeHtml(props.periodEnd);\n const downloadUrl = escapeHtml(props.downloadUrl);\n const brandName = escapeHtml(props.brandName);\n\n const body = `\n <h2 style=\"margin:0 0 16px;font-size:22px;color:${palette.heading};\">Payment received ✓</h2>\n <p style=\"margin:0 0 16px;font-size:15px;color:${palette.body};line-height:1.6;\">\n Hi ${customerName}, thank you for your payment. Here's a summary of your latest invoice from\n ${brandName}.\n </p>\n <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;\">\n <tr>\n <td style=\"padding:12px 16px;border-bottom:1px solid ${palette.border};font-size:13px;color:${palette.muted};\">Invoice number</td>\n <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>\n </tr>\n <tr>\n <td style=\"padding:12px 16px;border-bottom:1px solid ${palette.border};font-size:13px;color:${palette.muted};\">Billing period</td>\n <td style=\"padding:12px 16px;border-bottom:1px solid ${palette.border};font-size:14px;color:${palette.heading};text-align:right;\">${periodStart} — ${periodEnd}</td>\n </tr>\n <tr>\n <td style=\"padding:12px 16px;font-size:13px;color:${palette.muted};\">Amount paid</td>\n <td style=\"padding:12px 16px;font-size:14px;color:#16a34a;text-align:right;font-weight:700;\">${amount} ${currency}</td>\n </tr>\n </table>\n <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;\">\n Download PDF receipt →\n </a>\n <p style=\"margin:0;font-size:13px;color:${palette.muted};\">\n Keep this receipt for your records. Questions about this charge? Reply to this email and the\n ${brandName} billing team will help.\n </p>\n `;\n\n return baseLayout({\n brandName: props.brandName,\n preview: preview(props),\n body,\n });\n}\n\nexport const receiptEmail = { subject, preview, render } as const;\n","/**\n * WelcomeEmail — onboarding welcome message for new accounts.\n *\n * react-email is not installed; this module exposes the same\n * `{ subject, preview, render }` contract using a plain template literal so the\n * catalog can be migrated to React Email later without changing call sites.\n */\n\nimport { baseLayout, escapeHtml, palette } from \"./_layout\";\n\nexport interface WelcomeEmailProps {\n userName: string;\n loginUrl: string;\n brandName: string;\n}\n\nexport function subject(props: WelcomeEmailProps): string {\n return `Welcome to ${props.brandName}`;\n}\n\nexport function preview(_props: WelcomeEmailProps): string {\n return \"Get started with your account\";\n}\n\nexport function render(props: WelcomeEmailProps): string {\n const userName = escapeHtml(props.userName);\n const loginUrl = escapeHtml(props.loginUrl);\n const brandName = escapeHtml(props.brandName);\n\n const body = `\n <h2 style=\"margin:0 0 16px;font-size:22px;color:${palette.heading};\">Welcome, ${userName}!</h2>\n <p style=\"margin:0 0 16px;font-size:15px;color:${palette.body};line-height:1.6;\">\n We're glad to have you with ${brandName}. Your account is ready — sign in to\n explore the dashboard, set up your workspace, and invite the rest of your team.\n </p>\n <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;\">\n Get Started →\n </a>\n <p style=\"margin:0;font-size:13px;color:${palette.muted};\">\n Need help? Reply to this email and a real human from the ${brandName} team will get back to you.\n </p>\n `;\n\n return baseLayout({\n brandName: props.brandName,\n preview: preview(props),\n body,\n });\n}\n\nexport const welcomeEmail = { subject, preview, render } as const;\n","/**\n * React Email-style template catalog.\n *\n * Each template module exports `{ subject, preview, render }` plus a typed\n * Props interface. They render to a complete HTML document via plain template\n * literals (react-email is not installed in this workspace; see AGENTS.md).\n *\n * The legacy `send*Email` helpers in `../index.ts` remain the canonical way to\n * dispatch transactional email. These template modules are the new React-Email\n * shaped surface intended for the mail-preview app and future migration.\n */\n\nexport type { RenderedEmail } from \"./_layout\";\nexport {\n type InvitationEmailProps,\n invitationEmail,\n preview as invitationEmailPreview,\n render as renderInvitationEmail,\n subject as invitationEmailSubject,\n} from \"./invitation\";\nexport {\n type PasswordResetEmailProps,\n passwordResetEmail,\n preview as passwordResetEmailPreview,\n render as renderPasswordResetEmail,\n subject as passwordResetEmailSubject,\n} from \"./password-reset\";\n\nexport {\n preview as receiptEmailPreview,\n type ReceiptEmailProps,\n receiptEmail,\n render as renderReceiptEmail,\n subject as receiptEmailSubject,\n} from \"./receipt\";\nexport {\n preview as welcomeEmailPreview,\n render as renderWelcomeEmail,\n subject as welcomeEmailSubject,\n type WelcomeEmailProps,\n welcomeEmail,\n} from \"./welcome\";\n\nimport { invitationEmail } from \"./invitation\";\nimport { passwordResetEmail } from \"./password-reset\";\nimport { receiptEmail } from \"./receipt\";\nimport { welcomeEmail } from \"./welcome\";\n\n/**\n * Stable shape of a registered template entry for the React Email-style\n * catalog. Distinct from `EMAIL_TEMPLATE_CATALOG` in `../index.ts` which tracks\n * the legacy `send*Email` helper surface.\n */\nexport interface ReactEmailTemplate<P> {\n id: string;\n label: string;\n description: string;\n fileName: string;\n subject: (props: P) => string;\n preview: (props: P) => string;\n render: (props: P) => string;\n}\n\nexport const REACT_EMAIL_TEMPLATES = {\n welcome: {\n id: \"welcome-react\",\n label: \"Welcome (React Email)\",\n description: \"Account onboarding welcome message\",\n fileName: \"welcome-react-email.html\",\n ...welcomeEmail,\n },\n passwordReset: {\n id: \"password-reset\",\n label: \"Password Reset\",\n description: \"Secure password reset link with expiry\",\n fileName: \"password-reset-email.html\",\n ...passwordResetEmail,\n },\n invitation: {\n id: \"invitation\",\n label: \"Invitation\",\n description: \"Workspace invitation with role and expiry\",\n fileName: \"invitation-email.html\",\n ...invitationEmail,\n },\n receipt: {\n id: \"receipt\",\n label: \"Receipt\",\n description: \"Payment receipt with billing period and PDF link\",\n fileName: \"receipt-email.html\",\n ...receiptEmail,\n },\n} as const;\n"],"mappings":"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","names":["colors","getBrandOrigin","colors","preview","colors","subject","preview","render","subject","preview","render","subject","preview","render","baseLayout","colors","getBrandOrigin"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/providers/console.ts","../src/providers/resend.ts","../src/provider.ts","../src/templates/invitation.tsx","../src/templates/_layout.ts","../src/templates/password-reset.tsx","../src/templates/receipt.tsx","../src/templates/welcome.tsx","../src/templates/index.ts"],"sourcesContent":["/**\n * @nebutra/email — Multi-provider transactional email\n *\n * Provider-agnostic email system with auto-detection:\n * 1. EMAIL_PROVIDER env var (explicit: \"resend\" | \"console\")\n * 2. RESEND_API_KEY present → Resend\n * 3. Fallback → Console (dev/test, no API key needed)\n *\n * Usage:\n * import { sendWelcomeEmail, sendApiKeyCreatedEmail } from \"@nebutra/email\";\n * await sendWelcomeEmail({ to: \"user@example.com\", orgName: \"Acme Corp\" });\n *\n * Environment variables:\n * EMAIL_PROVIDER — explicit provider override (optional)\n * EMAIL_FROM — verified sender (e.g. getBrandMailFrom() example)\n * RESEND_API_KEY — for Resend provider\n */\n\nimport { brand, colors } from \"@nebutra/brand/metadata\";\nimport { getBrandMailFrom, getBrandOrigin, getDocsUrl } from \"@nebutra/brand/metadata-helpers\";\nimport { getEmailProvider, type SendResult } from \"./provider\";\n\nexport type { EmailProvider, EmailProviderType, SendOptions, SendResult } from \"./provider\";\nexport { getEmailProvider, resetEmailProvider } from \"./provider\";\nexport { ConsoleEmailProvider } from \"./providers/console\";\nexport { ResendEmailProvider } from \"./providers/resend\";\n\nconst FROM = process.env.EMAIL_FROM ?? getBrandMailFrom();\n\n// ── Types ──────────────────────────────────────────────────────────────────\n\nexport interface EmailTemplateCatalogEntry {\n id: string;\n label: string;\n description: string;\n sendHelper: string;\n fileName: string;\n}\n\nexport const EMAIL_TEMPLATE_CATALOG = [\n {\n id: \"welcome\",\n label: \"Welcome\",\n description: \"Workspace provisioning welcome email\",\n sendHelper: \"sendWelcomeEmail\",\n fileName: \"welcome-email.html\",\n },\n {\n id: \"order-confirmation\",\n label: \"Order Confirmation\",\n description: \"Commerce order receipt with line items\",\n sendHelper: \"sendOrderConfirmationEmail\",\n fileName: \"order-confirmation-email.html\",\n },\n {\n id: \"magic-link\",\n label: \"Magic Link\",\n description: \"Passwordless sign-in link\",\n sendHelper: \"sendMagicLinkEmail\",\n fileName: \"magic-link-email.html\",\n },\n {\n id: \"email-change\",\n label: \"Email Change\",\n description: \"Confirm a new account email address\",\n sendHelper: \"sendEmailChangeEmail\",\n fileName: \"email-change-email.html\",\n },\n {\n id: \"contact-form-received\",\n label: \"Contact Form Received\",\n description: \"Contact form acknowledgement\",\n sendHelper: \"sendContactFormReceivedEmail\",\n fileName: \"contact-form-received-email.html\",\n },\n {\n id: \"license-created\",\n label: \"License Created\",\n description: \"License key delivery email\",\n sendHelper: \"sendLicenseCreatedEmail\",\n fileName: \"license-created-email.html\",\n },\n {\n id: \"welcome-react\",\n label: \"Welcome (React Email)\",\n description: \"Account onboarding welcome message\",\n sendHelper: \"sendWelcomeReactEmail\",\n fileName: \"welcome-react-email.html\",\n },\n {\n id: \"password-reset\",\n label: \"Password Reset\",\n description: \"Secure password reset link with expiry\",\n sendHelper: \"sendPasswordResetEmail\",\n fileName: \"password-reset-email.html\",\n },\n {\n id: \"invitation\",\n label: \"Invitation\",\n description: \"Workspace invitation with role and expiry\",\n sendHelper: \"sendInvitationEmail\",\n fileName: \"invitation-email.html\",\n },\n {\n id: \"receipt\",\n label: \"Receipt\",\n description: \"Payment receipt with billing period and PDF link\",\n sendHelper: \"sendReceiptEmail\",\n fileName: \"receipt-email.html\",\n },\n] as const satisfies readonly EmailTemplateCatalogEntry[];\n\n// ── Core send helper ───────────────────────────────────────────────────────\n\nasync function send(opts: {\n to: string | string[];\n subject: string;\n html: string;\n replyTo?: string;\n tags?: { name: string; value: string }[];\n}): Promise<SendResult> {\n const provider = getEmailProvider();\n return provider.send({\n ...opts,\n from: FROM,\n });\n}\n\n// ── Templates ──────────────────────────────────────────────────────────────\n\nfunction baseLayout(content: string, previewText = \"\"): string {\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n <meta name=\"x-apple-disable-message-reformatting\" />\n <title>${brand.name}</title>\n <!--[if mso]><noscript><xml><o:OfficeDocumentSettings><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml></noscript><![endif]-->\n</head>\n<body style=\"margin:0;padding:0;background:#f8fafc;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;\">\n ${previewText ? `<div style=\"display:none;max-height:0;overflow:hidden;\">${previewText} ‌ </div>` : \"\"}\n <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#f8fafc;\">\n <tr><td align=\"center\" style=\"padding:40px 16px;\">\n <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%;\">\n <!-- Header -->\n <tr><td style=\"background:${colors.primary[\"500\"]};padding:32px 40px;\">\n <h1 style=\"margin:0;color:#ffffff;font-size:24px;font-weight:700;letter-spacing:-0.5px;\">${brand.name}</h1>\n </td></tr>\n <!-- Body -->\n <tr><td style=\"padding:40px;\">\n ${content}\n </td></tr>\n <!-- Footer -->\n <tr><td style=\"padding:24px 40px;border-top:1px solid #e2e8f0;background:#f8fafc;\">\n <p style=\"margin:0;font-size:12px;color:#94a3b8;text-align:center;\">\n © ${new Date().getFullYear()} ${brand.nameFullEn} ·\n <a href=\"${getBrandOrigin(\"landing\")}/privacy\" style=\"color:#94a3b8;\">Privacy</a> ·\n <a href=\"${getBrandOrigin(\"landing\")}/unsubscribe\" style=\"color:#94a3b8;\">Unsubscribe</a>\n </p>\n </td></tr>\n </table>\n </td></tr>\n </table>\n</body>\n</html>`;\n}\n\n// ── Email senders ──────────────────────────────────────────────────────────\n\n/**\n * Welcome email sent when a new organization is provisioned.\n */\nexport async function sendWelcomeEmail(opts: {\n to: string;\n firstName: string;\n orgName: string;\n dashboardUrl?: string;\n}): Promise<SendResult> {\n const dashboardUrl = opts.dashboardUrl ?? getBrandOrigin(\"app\");\n\n const html = baseLayout(\n `\n <h2 style=\"margin:0 0 16px;font-size:22px;color:#0f172a;\">Welcome to ${brand.name}, ${opts.firstName}!</h2>\n <p style=\"margin:0 0 16px;font-size:15px;color:#475569;line-height:1.6;\">\n Your workspace <strong>${opts.orgName}</strong> is ready. You can now invite team members,\n create API keys, and start building with the ${brand.name} platform.\n </p>\n <a href=\"${dashboardUrl}\" style=\"display:inline-block;background:${colors.primary[\"500\"]};color:#ffffff;text-decoration:none;border-radius:8px;padding:12px 24px;font-size:15px;font-weight:600;margin:0 0 24px;\">\n Open Dashboard →\n </a>\n <p style=\"margin:0;font-size:13px;color:#94a3b8;\">\n If you have questions, reply to this email or visit our <a href=\"${getDocsUrl()}\" style=\"color:${colors.primary[\"500\"]};\">documentation</a>.\n </p>\n `,\n `Welcome to ${brand.name} — ${opts.orgName} is ready`,\n );\n\n return send({\n to: opts.to,\n subject: `Welcome to ${brand.name} — ${opts.orgName} is ready`,\n html,\n tags: [{ name: \"type\", value: \"welcome\" }],\n });\n}\n\n/**\n * Order confirmation email sent when an order successfully completes.\n */\nexport async function sendOrderConfirmationEmail(opts: {\n to: string;\n orderId: string;\n /** Order total in minor units (integer cents). */\n totalAmount: number;\n /** ISO 4217 currency code (default \"USD\"). */\n currency?: string;\n /** BCP 47 locale tag (default \"en-US\"). */\n locale?: string;\n items: Array<{ productId: string; quantity: number | string }>;\n}): Promise<SendResult> {\n const formattedTotal = new Intl.NumberFormat(opts.locale ?? \"en-US\", {\n style: \"currency\",\n currency: opts.currency ?? \"USD\",\n }).format(opts.totalAmount / 100);\n\n const itemsHtml = opts.items\n .map(\n (item) =>\n `<tr>\n <td style=\"padding:12px 0;border-bottom:1px solid #e2e8f0;color:#0f172a;font-size:15px;\">${item.productId}</td>\n <td style=\"padding:12px 0;border-bottom:1px solid #e2e8f0;color:#475569;font-size:15px;text-align:right;\">x${item.quantity}</td>\n </tr>`,\n )\n .join(\"\");\n\n const html = baseLayout(\n `\n <h2 style=\"margin:0 0 16px;font-size:22px;color:#0f172a;\">Order Confirmed!</h2>\n <p style=\"margin:0 0 16px;font-size:15px;color:#475569;line-height:1.6;\">\n Thank you for your order <strong>#${opts.orderId}</strong>. We've received your payment of\n <strong>${formattedTotal}</strong> and your order is now being processed.\n </p>\n <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin:0 0 24px;border-collapse:collapse;\">\n ${itemsHtml}\n </table>\n <a href=\"${getBrandOrigin(\"app\")}/orders/${opts.orderId}\" style=\"display:inline-block;background:${colors.primary[\"500\"]};color:#ffffff;text-decoration:none;border-radius:8px;padding:12px 24px;font-size:15px;font-weight:600;margin:0 0 24px;\">\n View Order Details →\n </a>\n `,\n `Order #${opts.orderId} Confirmed`,\n );\n\n return send({\n to: opts.to,\n subject: `Order #${opts.orderId} Confirmed`,\n html,\n tags: [{ name: \"type\", value: \"order_confirmation\" }],\n });\n}\n\n/**\n * Shipfast-style Magic Link Email for 1-click Authentication.\n */\nexport async function sendMagicLinkEmail(opts: {\n to: string;\n magicLinkUrl: string;\n}): Promise<SendResult> {\n const html = baseLayout(\n `\n <h2 style=\"margin:0 0 16px;font-size:22px;color:#0f172a;\">Sign in to ${brand.name}</h2>\n <p style=\"margin:0 0 16px;font-size:15px;color:#475569;line-height:1.6;\">\n Click the button below to sign in securely. No password required.\n </p>\n <a href=\"${opts.magicLinkUrl}\" style=\"display:inline-block;background:${colors.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;\">\n ✨ Sign In Automatically\n </a>\n <p style=\"margin:0;font-size:12px;color:#94a3b8;line-height:1.4;\">\n If you did not request this email, you can safely ignore it.\n This link expires in 15 minutes.\n </p>\n `,\n `Click here to sign in to your ${brand.name} account`,\n );\n\n return send({\n to: opts.to,\n subject: `Sign in to ${brand.name} ✨`,\n html,\n tags: [{ name: \"type\", value: \"magic_link\" }],\n });\n}\n\n/**\n * Email-change verification sent to the *new* address (TODO #126).\n * Confirming the link flips the account email.\n */\nexport async function sendEmailChangeEmail(opts: {\n to: string;\n confirmUrl: string;\n /** Optional display name for the new address (defaults to `to`). */\n newEmail?: string;\n}): Promise<SendResult> {\n const recipientEmail = opts.newEmail ?? opts.to;\n const html = baseLayout(\n `\n <h2 style=\"margin:0 0 16px;font-size:22px;color:#0f172a;\">Confirm your new email address</h2>\n <p style=\"margin:0 0 16px;font-size:15px;color:#475569;line-height:1.6;\">\n We received a request to change the email on your ${brand.name} account to\n <strong>${recipientEmail}</strong>. Click the button below to confirm. This link expires in 1 hour.\n </p>\n <a href=\"${opts.confirmUrl}\" style=\"display:inline-block;background:${colors.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;\">\n Confirm email change →\n </a>\n <p style=\"margin:0;font-size:12px;color:#94a3b8;line-height:1.4;\">\n If you did not request this change you can safely ignore this email — your address will not be updated.\n </p>\n `,\n \"Confirm your new Nebutra email address\",\n );\n\n return send({\n to: opts.to,\n subject: \"Confirm your new Nebutra email address\",\n html,\n tags: [{ name: \"type\", value: \"email_change_verification\" }],\n });\n}\n\n/**\n * Contact Form Receipt (Sent to the User)\n */\nexport async function sendContactFormReceivedEmail(opts: {\n to: string;\n name: string;\n subject: string;\n}): Promise<SendResult> {\n const html = baseLayout(\n `\n <h2 style=\"margin:0 0 16px;font-size:22px;color:#0f172a;\">Message Received</h2>\n <p style=\"margin:0 0 16px;font-size:15px;color:#475569;line-height:1.6;\">\n Hi ${opts.name},\n </p>\n <p style=\"margin:0 0 16px;font-size:15px;color:#475569;line-height:1.6;\">\n Thank you for contacting us regarding \"<strong>${opts.subject}</strong>\". \n We've received your request and our team will get back to you within 1-2 business days.\n </p>\n <p style=\"margin:0;font-size:15px;color:#475569;line-height:1.6;\">\n Best regards,<br/>The ${brand.name} Team\n </p>\n `,\n \"We have received your message\",\n );\n\n return send({\n to: opts.to,\n subject: `We've received your message - ${brand.name}`,\n html,\n tags: [{ name: \"type\", value: \"contact_receipt\" }],\n });\n}\n\n// ── Billing Lifecycle Emails ───────────────────────────────────────────────\n\n/**\n * License created email sent when a user claims their OPC/STARTUP license.\n */\nexport async function sendLicenseCreatedEmail(opts: {\n to: string;\n firstName: string;\n licenseKey: string;\n tier: string;\n}): Promise<SendResult> {\n const html = baseLayout(\n `\n <h2 style=\"margin:0 0 16px;font-size:22px;color:#0f172a;\">Welcome to ${brand.name}-Sailor! 🚀</h2>\n <p style=\"margin:0 0 16px;font-size:15px;color:#475569;line-height:1.6;\">\n Hi ${opts.firstName}, your <strong>${opts.tier}</strong> license has been successfully generated.\n </p>\n <p style=\"margin:0 0 16px;font-size:15px;color:#475569;line-height:1.6;\">\n Your official License Key is:\n </p>\n <div style=\"background:#0f172a;border-radius:8px;padding:16px 20px;margin:0 0 24px;overflow-x:auto;\">\n <code style=\"color:${colors.accent[\"500\"]};font-family:'Courier New',monospace;font-size:13px;word-break:break-all;\">${opts.licenseKey}</code>\n </div>\n <p style=\"margin:0 0 16px;font-size:15px;color:#475569;line-height:1.6;\">\n To unlock the premium capabilities of the ${brand.name} CLI locally, run the following command in your terminal:\n </p>\n <div style=\"background:#f1f5f9;border:1px solid #e2e8f0;border-radius:6px;padding:12px;margin:0 0 24px;\">\n <code style=\"color:#334155;font-family:'Courier New',monospace;font-size:13px;\">nebutra license activate ${opts.licenseKey}</code>\n </div>\n <p style=\"margin:0;font-size:13px;color:#94a3b8;\">\n If you have questions, reply to this email or visit our <a href=\"${getDocsUrl()}\" style=\"color:${colors.primary[\"500\"]};\">documentation</a>.\n </p>\n `,\n `Your ${brand.name} License Key is Ready`,\n );\n\n return send({\n to: opts.to,\n subject: `Your ${brand.name} License Key is Ready (${opts.tier})`,\n html,\n tags: [{ name: \"type\", value: \"license_created\" }],\n });\n}\n\n// ── React Email-style templates (welcome, password-reset, invitation, receipt) ─\n\nimport {\n type InvitationEmailProps,\n invitationEmail,\n type PasswordResetEmailProps,\n passwordResetEmail,\n type ReceiptEmailProps,\n receiptEmail,\n type WelcomeEmailProps,\n welcomeEmail,\n} from \"./templates\";\n\nexport {\n type InvitationEmailProps,\n invitationEmail,\n type PasswordResetEmailProps,\n passwordResetEmail,\n REACT_EMAIL_TEMPLATES,\n type ReactEmailTemplate,\n type ReceiptEmailProps,\n type RenderedEmail,\n receiptEmail,\n renderInvitationEmail,\n renderPasswordResetEmail,\n renderReceiptEmail,\n renderWelcomeEmail,\n type WelcomeEmailProps,\n welcomeEmail,\n} from \"./templates\";\n\n/**\n * Send the React Email-style welcome message. Mirrors `WelcomeEmailProps`\n * exactly so callers can compose messages without depending on the legacy\n * `sendWelcomeEmail` signature.\n */\nexport async function sendWelcomeReactEmail(\n opts: { to: string } & WelcomeEmailProps,\n): Promise<SendResult> {\n return send({\n to: opts.to,\n subject: welcomeEmail.subject(opts),\n html: welcomeEmail.render(opts),\n tags: [{ name: \"type\", value: \"welcome_react\" }],\n });\n}\n\n/**\n * Send a password reset email with a tokenized link and expiry note.\n */\nexport async function sendPasswordResetEmail(\n opts: { to: string } & PasswordResetEmailProps,\n): Promise<SendResult> {\n return send({\n to: opts.to,\n subject: passwordResetEmail.subject(opts),\n html: passwordResetEmail.render(opts),\n tags: [{ name: \"type\", value: \"password_reset\" }],\n });\n}\n\n/**\n * Send a workspace invitation with role and accept link.\n */\nexport async function sendInvitationEmail(\n opts: { to: string } & InvitationEmailProps,\n): Promise<SendResult> {\n return send({\n to: opts.to,\n subject: invitationEmail.subject(opts),\n html: invitationEmail.render(opts),\n tags: [{ name: \"type\", value: \"invitation\" }],\n });\n}\n\n/**\n * Send a payment receipt with billing period and PDF download URL.\n */\nexport async function sendReceiptEmail(\n opts: { to: string } & ReceiptEmailProps,\n): Promise<SendResult> {\n return send({\n to: opts.to,\n subject: receiptEmail.subject(opts),\n html: receiptEmail.render(opts),\n tags: [{ name: \"type\", value: \"receipt\" }],\n });\n}\n","/**\n * Console email provider — prints email details to stdout.\n * Perfect for local development and testing without API keys.\n */\n\nimport type { EmailProvider, SendOptions, SendResult } from \"../provider\";\n\nlet counter = 0;\n\nexport class ConsoleEmailProvider implements EmailProvider {\n readonly name = \"console\" as const;\n\n async send(opts: SendOptions): Promise<SendResult> {\n counter += 1;\n const id = `console_${Date.now()}_${counter}`;\n const recipients = Array.isArray(opts.to) ? opts.to.join(\", \") : opts.to;\n const separator = \"─\".repeat(60);\n\n const lines = [\n `\\n${separator}`,\n `📧 Email (console provider)`,\n `${separator}`,\n `From: ${opts.from}`,\n `To: ${recipients}`,\n `Subject: ${opts.subject}`,\n ];\n\n if (opts.replyTo) {\n lines.push(`Reply-To: ${opts.replyTo}`);\n }\n\n if (opts.tags?.length) {\n const tagStr = opts.tags.map((t) => `${t.name}=${t.value}`).join(\", \");\n lines.push(`Tags: ${tagStr}`);\n }\n\n lines.push(`ID: ${id}`);\n lines.push(`${separator}\\n`);\n\n process.stdout.write(lines.join(\"\\n\"));\n\n return { id };\n }\n}\n","/**\n * Resend email provider — production-grade transactional email.\n * Requires RESEND_API_KEY environment variable.\n */\n\nimport { Resend } from \"resend\";\nimport type { EmailProvider, SendOptions, SendResult } from \"../provider\";\n\nexport class ResendEmailProvider implements EmailProvider {\n readonly name = \"resend\" as const;\n private client: Resend;\n\n constructor(apiKey: string) {\n if (!apiKey) {\n throw new Error(\"RESEND_API_KEY is required for Resend provider\");\n }\n this.client = new Resend(apiKey);\n }\n\n async send(opts: SendOptions): Promise<SendResult> {\n const { data, error } = await this.client.emails.send({\n from: opts.from,\n to: Array.isArray(opts.to) ? opts.to : [opts.to],\n subject: opts.subject,\n html: opts.html,\n ...(opts.replyTo ? { replyTo: opts.replyTo } : {}),\n ...(opts.tags ? { tags: opts.tags } : {}),\n });\n\n if (error) {\n throw new Error(`Resend send failed: ${error.message}`);\n }\n\n return { id: data?.id ?? \"unknown\" };\n }\n}\n","/**\n * @nebutra/email — Provider abstraction\n *\n * Supports multiple email providers via auto-detection:\n * 1. EMAIL_PROVIDER env var (explicit)\n * 2. RESEND_API_KEY → Resend\n * 3. Fallback → Console (dev/test)\n */\n\n// ── Interface ────────────────────────────────────────────────────────────────\n\nexport interface SendOptions {\n to: string | string[];\n subject: string;\n html: string;\n from: string;\n replyTo?: string;\n tags?: { name: string; value: string }[];\n}\n\nexport interface SendResult {\n id: string;\n}\n\nexport interface EmailProvider {\n readonly name: string;\n send(opts: SendOptions): Promise<SendResult>;\n}\n\n// ── Provider types ───────────────────────────────────────────────────────────\n\nexport type EmailProviderType = \"resend\" | \"console\";\n\n// ── Factory ──────────────────────────────────────────────────────────────────\n\nimport { ConsoleEmailProvider } from \"./providers/console\";\nimport { ResendEmailProvider } from \"./providers/resend\";\n\nlet cachedProvider: EmailProvider | undefined;\n\nfunction detectProviderType(): EmailProviderType {\n const explicit = process.env.EMAIL_PROVIDER;\n if (explicit === \"resend\" || explicit === \"console\") {\n return explicit;\n }\n\n if (process.env.RESEND_API_KEY) return \"resend\";\n return \"console\";\n}\n\nfunction createProvider(type: EmailProviderType): EmailProvider {\n switch (type) {\n case \"resend\":\n return new ResendEmailProvider(process.env.RESEND_API_KEY!);\n case \"console\":\n return new ConsoleEmailProvider();\n }\n}\n\nexport function getEmailProvider(): EmailProvider {\n if (cachedProvider) return cachedProvider;\n const type = detectProviderType();\n cachedProvider = createProvider(type);\n return cachedProvider;\n}\n\nexport function resetEmailProvider(): void {\n cachedProvider = undefined;\n}\n","/**\n * InvitationEmail — workspace member invitation with role and expiry.\n *\n * react-email is not installed; this module exposes the same\n * `{ subject, preview, render }` contract using a plain template literal so the\n * catalog can be migrated to React Email later without changing call sites.\n */\n\nimport { colors } from \"@nebutra/brand/metadata\";\nimport { baseLayout, escapeHtml, palette } from \"./_layout\";\n\nexport interface InvitationEmailProps {\n inviterName: string;\n organizationName: string;\n role: string;\n acceptUrl: string;\n expiresAt: string;\n brandName: string;\n}\n\nexport function subject(props: InvitationEmailProps): string {\n return `${props.inviterName} invited you to join ${props.organizationName}`;\n}\n\nexport function preview(props: InvitationEmailProps): string {\n return `You've been invited to ${props.organizationName} as ${props.role}`;\n}\n\nexport function render(props: InvitationEmailProps): string {\n const inviterName = escapeHtml(props.inviterName);\n const organizationName = escapeHtml(props.organizationName);\n const role = escapeHtml(props.role);\n const acceptUrl = escapeHtml(props.acceptUrl);\n const expiresAt = escapeHtml(props.expiresAt);\n const brandName = escapeHtml(props.brandName);\n\n const body = `\n <h2 style=\"margin:0 0 16px;font-size:22px;color:${palette.heading};\">You're invited to ${organizationName}</h2>\n <p style=\"margin:0 0 16px;font-size:15px;color:${palette.body};line-height:1.6;\">\n <strong>${inviterName}</strong> has invited you to collaborate on\n <strong>${organizationName}</strong>.\n </p>\n <p style=\"margin:0 0 24px;font-size:14px;color:${palette.body};\">\n Role:\n <span style=\"display:inline-block;background:#e0f2fe;color:${colors.primary[\"500\"]};padding:4px 10px;border-radius:999px;font-size:13px;font-weight:600;margin-left:6px;\">\n ${role}\n </span>\n </p>\n <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;\">\n Accept invitation →\n </a>\n <p style=\"margin:0 0 12px;font-size:13px;color:${palette.muted};\">\n This invitation expires on <strong>${expiresAt}</strong>.\n </p>\n <p style=\"margin:0;font-size:13px;color:${palette.muted};\">\n Sent by ${brandName} on behalf of ${organizationName}. If you weren't expecting this, you can\n safely ignore the message.\n </p>\n `;\n\n return baseLayout({\n brandName: props.brandName,\n preview: preview(props),\n body,\n });\n}\n\nexport const invitationEmail = { subject, preview, render } as const;\n","import { colors } from \"@nebutra/brand/metadata\";\nimport { getBrandOrigin } from \"@nebutra/brand/metadata-helpers\";\n/**\n * Shared HTML layout for the React Email-style template catalog.\n *\n * NOTE: react-email is NOT installed in this workspace. This module ships a\n * plain template literal layout instead so the templates remain framework-free\n * and can be rendered without a React runtime. The contract still mirrors a\n * future React Email migration: each template module exports\n * `{ subject, preview, render }` and the renderer returns a complete HTML\n * document.\n */\n\nexport interface RenderedEmail {\n html: string;\n subject: string;\n preview: string;\n}\n\n/**\n * Email clients do not load app CSS variables. These are identity-locked hex\n * snapshots (VI brand-primary header), not product-chrome skins. Keep in sync with\n * brand assets — do not invent per-template blues.\n */\nconst BRAND_HEADER = colors.primary[\"500\"];\nconst COLOR_BG = \"#f8fafc\";\nconst COLOR_BORDER = \"#e2e8f0\";\nconst COLOR_HEADING = \"#0f172a\";\nconst COLOR_BODY = \"#475569\";\nconst COLOR_MUTED = \"#94a3b8\";\n\nexport interface BaseLayoutOptions {\n brandName: string;\n preview: string;\n body: string;\n}\n\n/**\n * Wrap a body fragment with the standard brand-style transactional shell.\n */\nexport function baseLayout({ brandName, preview, body }: BaseLayoutOptions): string {\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n <meta name=\"x-apple-disable-message-reformatting\" />\n <title>${escapeHtml(brandName)}</title>\n</head>\n<body style=\"margin:0;padding:0;background:${COLOR_BG};font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;\">\n <div style=\"display:none;max-height:0;overflow:hidden;\">${escapeHtml(preview)} ‌ </div>\n <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:${COLOR_BG};\">\n <tr><td align=\"center\" style=\"padding:40px 16px;\">\n <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%;\">\n <tr><td style=\"background:${BRAND_HEADER};padding:32px 40px;\">\n <h1 style=\"margin:0;color:#ffffff;font-size:24px;font-weight:700;letter-spacing:-0.5px;\">${escapeHtml(brandName)}</h1>\n </td></tr>\n <tr><td style=\"padding:40px;\">\n ${body}\n </td></tr>\n <tr><td style=\"padding:24px 40px;border-top:1px solid ${COLOR_BORDER};background:${COLOR_BG};\">\n <p style=\"margin:0;font-size:12px;color:${COLOR_MUTED};text-align:center;\">\n © ${new Date().getFullYear()} ${escapeHtml(brandName)} ·\n <a href=\"${getBrandOrigin(\"landing\")}/privacy\" style=\"color:${COLOR_MUTED};\">Privacy</a> ·\n <a href=\"${getBrandOrigin(\"landing\")}/unsubscribe\" style=\"color:${COLOR_MUTED};\">Unsubscribe</a>\n </p>\n </td></tr>\n </table>\n </td></tr>\n </table>\n</body>\n</html>`;\n}\n\nexport const palette = {\n /** @deprecated alias of solid identity header — not a product skin */\n gradient: BRAND_HEADER,\n header: BRAND_HEADER,\n background: COLOR_BG,\n border: COLOR_BORDER,\n heading: COLOR_HEADING,\n body: COLOR_BODY,\n muted: COLOR_MUTED,\n} as const;\n\n/**\n * Conservative HTML escape for user-supplied template props.\n *\n * Templates accept structured props from trusted backend code, but escaping is\n * still required because some props (e.g. organization names, invoice numbers)\n * may originate from end-user input.\n */\nexport function escapeHtml(value: string): string {\n return value\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\")\n .replace(/'/g, \"'\");\n}\n","/**\n * PasswordResetEmail — secure password reset link with expiry.\n *\n * react-email is not installed; this module exposes the same\n * `{ subject, preview, render }` contract using a plain template literal so the\n * catalog can be migrated to React Email later without changing call sites.\n */\n\nimport { baseLayout, escapeHtml, palette } from \"./_layout\";\n\nexport interface PasswordResetEmailProps {\n userName: string;\n resetUrl: string;\n expiresInMinutes: number;\n brandName: string;\n}\n\nexport function subject(props: PasswordResetEmailProps): string {\n return `Reset your ${props.brandName} password`;\n}\n\nexport function preview(props: PasswordResetEmailProps): string {\n return `Reset link expires in ${props.expiresInMinutes} minutes`;\n}\n\nexport function render(props: PasswordResetEmailProps): string {\n const userName = escapeHtml(props.userName);\n const resetUrl = escapeHtml(props.resetUrl);\n const brandName = escapeHtml(props.brandName);\n const expires = String(props.expiresInMinutes);\n\n const body = `\n <h2 style=\"margin:0 0 16px;font-size:22px;color:${palette.heading};\">Reset your password</h2>\n <p style=\"margin:0 0 16px;font-size:15px;color:${palette.body};line-height:1.6;\">\n Hi ${userName}, we received a request to reset the password on your ${brandName} account.\n Click the button below to choose a new one.\n </p>\n <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;\">\n Reset password →\n </a>\n <p style=\"margin:0 0 16px;font-size:14px;color:${palette.body};line-height:1.6;\">\n This link expires in <strong>${expires} minutes</strong>. If it's expired, request a new one\n from the sign-in page.\n </p>\n <p style=\"margin:0;font-size:13px;color:${palette.muted};line-height:1.5;\">\n If this wasn't you, you can safely ignore this email — your password won't change unless\n you click the link above. For added safety, review recent sign-ins on your account.\n </p>\n `;\n\n return baseLayout({\n brandName: props.brandName,\n preview: preview(props),\n body,\n });\n}\n\nexport const passwordResetEmail = { subject, preview, render } as const;\n","/**\n * ReceiptEmail — payment confirmation with billing period and invoice download.\n *\n * react-email is not installed; this module exposes the same\n * `{ subject, preview, render }` contract using a plain template literal so the\n * catalog can be migrated to React Email later without changing call sites.\n */\n\nimport { baseLayout, escapeHtml, palette } from \"./_layout\";\n\nexport interface ReceiptEmailProps {\n customerName: string;\n invoiceNumber: string;\n amount: string;\n currency: string;\n periodStart: string;\n periodEnd: string;\n downloadUrl: string;\n brandName: string;\n}\n\nexport function subject(props: ReceiptEmailProps): string {\n return `Receipt for ${props.invoiceNumber}`;\n}\n\nexport function preview(props: ReceiptEmailProps): string {\n return `Payment of ${props.amount} ${props.currency} received`;\n}\n\nexport function render(props: ReceiptEmailProps): string {\n const customerName = escapeHtml(props.customerName);\n const invoiceNumber = escapeHtml(props.invoiceNumber);\n const amount = escapeHtml(props.amount);\n const currency = escapeHtml(props.currency);\n const periodStart = escapeHtml(props.periodStart);\n const periodEnd = escapeHtml(props.periodEnd);\n const downloadUrl = escapeHtml(props.downloadUrl);\n const brandName = escapeHtml(props.brandName);\n\n const body = `\n <h2 style=\"margin:0 0 16px;font-size:22px;color:${palette.heading};\">Payment received ✓</h2>\n <p style=\"margin:0 0 16px;font-size:15px;color:${palette.body};line-height:1.6;\">\n Hi ${customerName}, thank you for your payment. Here's a summary of your latest invoice from\n ${brandName}.\n </p>\n <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;\">\n <tr>\n <td style=\"padding:12px 16px;border-bottom:1px solid ${palette.border};font-size:13px;color:${palette.muted};\">Invoice number</td>\n <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>\n </tr>\n <tr>\n <td style=\"padding:12px 16px;border-bottom:1px solid ${palette.border};font-size:13px;color:${palette.muted};\">Billing period</td>\n <td style=\"padding:12px 16px;border-bottom:1px solid ${palette.border};font-size:14px;color:${palette.heading};text-align:right;\">${periodStart} — ${periodEnd}</td>\n </tr>\n <tr>\n <td style=\"padding:12px 16px;font-size:13px;color:${palette.muted};\">Amount paid</td>\n <td style=\"padding:12px 16px;font-size:14px;color:#16a34a;text-align:right;font-weight:700;\">${amount} ${currency}</td>\n </tr>\n </table>\n <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;\">\n Download PDF receipt →\n </a>\n <p style=\"margin:0;font-size:13px;color:${palette.muted};\">\n Keep this receipt for your records. Questions about this charge? Reply to this email and the\n ${brandName} billing team will help.\n </p>\n `;\n\n return baseLayout({\n brandName: props.brandName,\n preview: preview(props),\n body,\n });\n}\n\nexport const receiptEmail = { subject, preview, render } as const;\n","/**\n * WelcomeEmail — onboarding welcome message for new accounts.\n *\n * react-email is not installed; this module exposes the same\n * `{ subject, preview, render }` contract using a plain template literal so the\n * catalog can be migrated to React Email later without changing call sites.\n */\n\nimport { baseLayout, escapeHtml, palette } from \"./_layout\";\n\nexport interface WelcomeEmailProps {\n userName: string;\n loginUrl: string;\n brandName: string;\n}\n\nexport function subject(props: WelcomeEmailProps): string {\n return `Welcome to ${props.brandName}`;\n}\n\nexport function preview(_props: WelcomeEmailProps): string {\n return \"Get started with your account\";\n}\n\nexport function render(props: WelcomeEmailProps): string {\n const userName = escapeHtml(props.userName);\n const loginUrl = escapeHtml(props.loginUrl);\n const brandName = escapeHtml(props.brandName);\n\n const body = `\n <h2 style=\"margin:0 0 16px;font-size:22px;color:${palette.heading};\">Welcome, ${userName}!</h2>\n <p style=\"margin:0 0 16px;font-size:15px;color:${palette.body};line-height:1.6;\">\n We're glad to have you with ${brandName}. Your account is ready — sign in to\n explore the dashboard, set up your workspace, and invite the rest of your team.\n </p>\n <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;\">\n Get Started →\n </a>\n <p style=\"margin:0;font-size:13px;color:${palette.muted};\">\n Need help? Reply to this email and a real human from the ${brandName} team will get back to you.\n </p>\n `;\n\n return baseLayout({\n brandName: props.brandName,\n preview: preview(props),\n body,\n });\n}\n\nexport const welcomeEmail = { subject, preview, render } as const;\n","/**\n * React Email-style template catalog.\n *\n * Each template module exports `{ subject, preview, render }` plus a typed\n * Props interface. They render to a complete HTML document via plain template\n * literals (react-email is not installed in this workspace; see AGENTS.md).\n *\n * The legacy `send*Email` helpers in `../index.ts` remain the canonical way to\n * dispatch transactional email. These template modules are the new React-Email\n * shaped surface intended for the mail-preview app and future migration.\n */\n\nexport type { RenderedEmail } from \"./_layout\";\nexport {\n type InvitationEmailProps,\n invitationEmail,\n preview as invitationEmailPreview,\n render as renderInvitationEmail,\n subject as invitationEmailSubject,\n} from \"./invitation\";\nexport {\n type PasswordResetEmailProps,\n passwordResetEmail,\n preview as passwordResetEmailPreview,\n render as renderPasswordResetEmail,\n subject as passwordResetEmailSubject,\n} from \"./password-reset\";\n\nexport {\n preview as receiptEmailPreview,\n type ReceiptEmailProps,\n receiptEmail,\n render as renderReceiptEmail,\n subject as receiptEmailSubject,\n} from \"./receipt\";\nexport {\n preview as welcomeEmailPreview,\n render as renderWelcomeEmail,\n subject as welcomeEmailSubject,\n type WelcomeEmailProps,\n welcomeEmail,\n} from \"./welcome\";\n\nimport { invitationEmail } from \"./invitation\";\nimport { passwordResetEmail } from \"./password-reset\";\nimport { receiptEmail } from \"./receipt\";\nimport { welcomeEmail } from \"./welcome\";\n\n/**\n * Stable shape of a registered template entry for the React Email-style\n * catalog. Distinct from `EMAIL_TEMPLATE_CATALOG` in `../index.ts` which tracks\n * the legacy `send*Email` helper surface.\n */\nexport interface ReactEmailTemplate<P> {\n id: string;\n label: string;\n description: string;\n fileName: string;\n subject: (props: P) => string;\n preview: (props: P) => string;\n render: (props: P) => string;\n}\n\nexport const REACT_EMAIL_TEMPLATES = {\n welcome: {\n id: \"welcome-react\",\n label: \"Welcome (React Email)\",\n description: \"Account onboarding welcome message\",\n fileName: \"welcome-react-email.html\",\n ...welcomeEmail,\n },\n passwordReset: {\n id: \"password-reset\",\n label: \"Password Reset\",\n description: \"Secure password reset link with expiry\",\n fileName: \"password-reset-email.html\",\n ...passwordResetEmail,\n },\n invitation: {\n id: \"invitation\",\n label: \"Invitation\",\n description: \"Workspace invitation with role and expiry\",\n fileName: \"invitation-email.html\",\n ...invitationEmail,\n },\n receipt: {\n id: \"receipt\",\n label: \"Receipt\",\n description: \"Payment receipt with billing period and PDF link\",\n fileName: \"receipt-email.html\",\n ...receiptEmail,\n },\n} as const;\n"],"mappings":"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","names":["colors","getBrandOrigin","colors","preview","colors","subject","preview","render","subject","preview","render","subject","preview","render","baseLayout","colors","getBrandOrigin"]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nebutra/email",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Multi-provider transactional email (Resend,
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"description": "Multi-provider transactional email (Resend, Console)",
|
|
5
5
|
"private": false,
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"type": "module",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"graph": "core",
|
|
11
11
|
"featureId": "email",
|
|
12
12
|
"category": "integrations",
|
|
13
|
-
"summary": "Multi-provider transactional email (Resend,
|
|
13
|
+
"summary": "Multi-provider transactional email (Resend, Console)"
|
|
14
14
|
},
|
|
15
15
|
"main": "./dist/index.js",
|
|
16
16
|
"exports": {
|
|
@@ -21,15 +21,7 @@
|
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"resend": "^4.0.1",
|
|
24
|
-
"@nebutra/brand": "
|
|
25
|
-
},
|
|
26
|
-
"peerDependencies": {
|
|
27
|
-
"nodemailer": "^9.1.0"
|
|
28
|
-
},
|
|
29
|
-
"peerDependenciesMeta": {
|
|
30
|
-
"nodemailer": {
|
|
31
|
-
"optional": true
|
|
32
|
-
}
|
|
24
|
+
"@nebutra/brand": "3.0.0"
|
|
33
25
|
},
|
|
34
26
|
"devDependencies": {
|
|
35
27
|
"tsup": "^8.5.0",
|
|
@@ -93,7 +93,6 @@ describe("getEmailProvider (factory)", () => {
|
|
|
93
93
|
process.env = { ...originalEnv };
|
|
94
94
|
delete process.env.EMAIL_PROVIDER;
|
|
95
95
|
delete process.env.RESEND_API_KEY;
|
|
96
|
-
delete process.env.SMTP_HOST;
|
|
97
96
|
// Reset cached provider between tests
|
|
98
97
|
const { resetEmailProvider } = await import("../provider");
|
|
99
98
|
resetEmailProvider();
|
|
@@ -125,12 +124,4 @@ describe("getEmailProvider (factory)", () => {
|
|
|
125
124
|
const provider = getEmailProvider();
|
|
126
125
|
expect(provider.name).toBe("resend");
|
|
127
126
|
});
|
|
128
|
-
|
|
129
|
-
it("returns nodemailer provider when SMTP_HOST is set", async () => {
|
|
130
|
-
process.env.SMTP_HOST = "smtp.example.com";
|
|
131
|
-
const { resetEmailProvider, getEmailProvider } = await import("../provider");
|
|
132
|
-
resetEmailProvider();
|
|
133
|
-
const provider = getEmailProvider();
|
|
134
|
-
expect(provider.name).toBe("nodemailer");
|
|
135
|
-
});
|
|
136
127
|
});
|
package/src/index.ts
CHANGED
|
@@ -2,10 +2,9 @@
|
|
|
2
2
|
* @nebutra/email — Multi-provider transactional email
|
|
3
3
|
*
|
|
4
4
|
* Provider-agnostic email system with auto-detection:
|
|
5
|
-
* 1. EMAIL_PROVIDER env var (explicit: "resend" | "
|
|
5
|
+
* 1. EMAIL_PROVIDER env var (explicit: "resend" | "console")
|
|
6
6
|
* 2. RESEND_API_KEY present → Resend
|
|
7
|
-
* 3.
|
|
8
|
-
* 4. Fallback → Console (dev/test, no API key needed)
|
|
7
|
+
* 3. Fallback → Console (dev/test, no API key needed)
|
|
9
8
|
*
|
|
10
9
|
* Usage:
|
|
11
10
|
* import { sendWelcomeEmail, sendApiKeyCreatedEmail } from "@nebutra/email";
|
|
@@ -15,17 +14,15 @@
|
|
|
15
14
|
* EMAIL_PROVIDER — explicit provider override (optional)
|
|
16
15
|
* EMAIL_FROM — verified sender (e.g. getBrandMailFrom() example)
|
|
17
16
|
* RESEND_API_KEY — for Resend provider
|
|
18
|
-
* SMTP_HOST — for Nodemailer provider (+ SMTP_PORT, SMTP_USER, SMTP_PASS, SMTP_SECURE)
|
|
19
17
|
*/
|
|
20
18
|
|
|
21
19
|
import { brand, colors } from "@nebutra/brand/metadata";
|
|
22
|
-
import { getBrandMailFrom, getBrandOrigin } from "@nebutra/brand/metadata-helpers";
|
|
20
|
+
import { getBrandMailFrom, getBrandOrigin, getDocsUrl } from "@nebutra/brand/metadata-helpers";
|
|
23
21
|
import { getEmailProvider, type SendResult } from "./provider";
|
|
24
22
|
|
|
25
23
|
export type { EmailProvider, EmailProviderType, SendOptions, SendResult } from "./provider";
|
|
26
24
|
export { getEmailProvider, resetEmailProvider } from "./provider";
|
|
27
25
|
export { ConsoleEmailProvider } from "./providers/console";
|
|
28
|
-
export { NodemailerEmailProvider } from "./providers/nodemailer";
|
|
29
26
|
export { ResendEmailProvider } from "./providers/resend";
|
|
30
27
|
|
|
31
28
|
const FROM = process.env.EMAIL_FROM ?? getBrandMailFrom();
|
|
@@ -193,7 +190,7 @@ export async function sendWelcomeEmail(opts: {
|
|
|
193
190
|
Open Dashboard →
|
|
194
191
|
</a>
|
|
195
192
|
<p style="margin:0;font-size:13px;color:#94a3b8;">
|
|
196
|
-
If you have questions, reply to this email or visit our <a href="${
|
|
193
|
+
If you have questions, reply to this email or visit our <a href="${getDocsUrl()}" style="color:${colors.primary["500"]};">documentation</a>.
|
|
197
194
|
</p>
|
|
198
195
|
`,
|
|
199
196
|
`Welcome to ${brand.name} — ${opts.orgName} is ready`,
|
|
@@ -392,7 +389,7 @@ export async function sendLicenseCreatedEmail(opts: {
|
|
|
392
389
|
<code style="color:#334155;font-family:'Courier New',monospace;font-size:13px;">nebutra license activate ${opts.licenseKey}</code>
|
|
393
390
|
</div>
|
|
394
391
|
<p style="margin:0;font-size:13px;color:#94a3b8;">
|
|
395
|
-
If you have questions, reply to this email or visit our <a href="${
|
|
392
|
+
If you have questions, reply to this email or visit our <a href="${getDocsUrl()}" style="color:${colors.primary["500"]};">documentation</a>.
|
|
396
393
|
</p>
|
|
397
394
|
`,
|
|
398
395
|
`Your ${brand.name} License Key is Ready`,
|
package/src/provider.ts
CHANGED
|
@@ -4,8 +4,7 @@
|
|
|
4
4
|
* Supports multiple email providers via auto-detection:
|
|
5
5
|
* 1. EMAIL_PROVIDER env var (explicit)
|
|
6
6
|
* 2. RESEND_API_KEY → Resend
|
|
7
|
-
* 3.
|
|
8
|
-
* 4. Fallback → Console (dev/test)
|
|
7
|
+
* 3. Fallback → Console (dev/test)
|
|
9
8
|
*/
|
|
10
9
|
|
|
11
10
|
// ── Interface ────────────────────────────────────────────────────────────────
|
|
@@ -30,24 +29,22 @@ export interface EmailProvider {
|
|
|
30
29
|
|
|
31
30
|
// ── Provider types ───────────────────────────────────────────────────────────
|
|
32
31
|
|
|
33
|
-
export type EmailProviderType = "resend" | "
|
|
32
|
+
export type EmailProviderType = "resend" | "console";
|
|
34
33
|
|
|
35
34
|
// ── Factory ──────────────────────────────────────────────────────────────────
|
|
36
35
|
|
|
37
36
|
import { ConsoleEmailProvider } from "./providers/console";
|
|
38
|
-
import { NodemailerEmailProvider } from "./providers/nodemailer";
|
|
39
37
|
import { ResendEmailProvider } from "./providers/resend";
|
|
40
38
|
|
|
41
39
|
let cachedProvider: EmailProvider | undefined;
|
|
42
40
|
|
|
43
41
|
function detectProviderType(): EmailProviderType {
|
|
44
42
|
const explicit = process.env.EMAIL_PROVIDER;
|
|
45
|
-
if (explicit === "resend" || explicit === "
|
|
43
|
+
if (explicit === "resend" || explicit === "console") {
|
|
46
44
|
return explicit;
|
|
47
45
|
}
|
|
48
46
|
|
|
49
47
|
if (process.env.RESEND_API_KEY) return "resend";
|
|
50
|
-
if (process.env.SMTP_HOST) return "nodemailer";
|
|
51
48
|
return "console";
|
|
52
49
|
}
|
|
53
50
|
|
|
@@ -55,15 +52,6 @@ function createProvider(type: EmailProviderType): EmailProvider {
|
|
|
55
52
|
switch (type) {
|
|
56
53
|
case "resend":
|
|
57
54
|
return new ResendEmailProvider(process.env.RESEND_API_KEY!);
|
|
58
|
-
case "nodemailer":
|
|
59
|
-
return new NodemailerEmailProvider({
|
|
60
|
-
host: process.env.SMTP_HOST!,
|
|
61
|
-
port: Number(process.env.SMTP_PORT ?? "587"),
|
|
62
|
-
secure: process.env.SMTP_SECURE === "true",
|
|
63
|
-
auth: process.env.SMTP_USER
|
|
64
|
-
? { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS ?? "" }
|
|
65
|
-
: undefined,
|
|
66
|
-
});
|
|
67
55
|
case "console":
|
|
68
56
|
return new ConsoleEmailProvider();
|
|
69
57
|
}
|
package/tsup.config.ts
CHANGED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Nodemailer email provider — SMTP-based, self-hosted fallback.
|
|
3
|
-
* Requires SMTP_HOST (+ optional SMTP_PORT, SMTP_USER, SMTP_PASS, SMTP_SECURE).
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { EmailProvider, SendOptions, SendResult } from "../provider";
|
|
7
|
-
|
|
8
|
-
export interface NodemailerConfig {
|
|
9
|
-
host: string;
|
|
10
|
-
port: number;
|
|
11
|
-
secure: boolean;
|
|
12
|
-
auth?:
|
|
13
|
-
| {
|
|
14
|
-
user: string;
|
|
15
|
-
pass: string;
|
|
16
|
-
}
|
|
17
|
-
| undefined;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export class NodemailerEmailProvider implements EmailProvider {
|
|
21
|
-
readonly name = "nodemailer" as const;
|
|
22
|
-
private config: NodemailerConfig;
|
|
23
|
-
private transporter: unknown;
|
|
24
|
-
|
|
25
|
-
constructor(config: NodemailerConfig) {
|
|
26
|
-
if (!config.host) {
|
|
27
|
-
throw new Error("SMTP_HOST is required for Nodemailer provider");
|
|
28
|
-
}
|
|
29
|
-
this.config = config;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
private async getTransporter() {
|
|
33
|
-
if (this.transporter) return this.transporter;
|
|
34
|
-
|
|
35
|
-
// Lazy import to avoid requiring nodemailer when using other providers
|
|
36
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
37
|
-
const nodemailer = await (import("nodemailer" as string) as Promise<{
|
|
38
|
-
createTransport(opts: Record<string, unknown>): {
|
|
39
|
-
sendMail(opts: Record<string, unknown>): Promise<{ messageId: string }>;
|
|
40
|
-
};
|
|
41
|
-
}>);
|
|
42
|
-
this.transporter = nodemailer.createTransport({
|
|
43
|
-
host: this.config.host,
|
|
44
|
-
port: this.config.port,
|
|
45
|
-
secure: this.config.secure,
|
|
46
|
-
...(this.config.auth ? { auth: this.config.auth } : {}),
|
|
47
|
-
});
|
|
48
|
-
return this.transporter;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
async send(opts: SendOptions): Promise<SendResult> {
|
|
52
|
-
const transporter = (await this.getTransporter()) as {
|
|
53
|
-
sendMail(opts: Record<string, unknown>): Promise<{ messageId: string }>;
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
const info = await transporter.sendMail({
|
|
57
|
-
from: opts.from,
|
|
58
|
-
to: Array.isArray(opts.to) ? opts.to.join(", ") : opts.to,
|
|
59
|
-
subject: opts.subject,
|
|
60
|
-
html: opts.html,
|
|
61
|
-
...(opts.replyTo ? { replyTo: opts.replyTo } : {}),
|
|
62
|
-
...(opts.tags?.length
|
|
63
|
-
? {
|
|
64
|
-
headers: {
|
|
65
|
-
"X-Email-Tags": opts.tags.map((t) => `${t.name}=${t.value}`).join(", "),
|
|
66
|
-
},
|
|
67
|
-
}
|
|
68
|
-
: {}),
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
return { id: info.messageId };
|
|
72
|
-
}
|
|
73
|
-
}
|