@shogo-ai/email 1.4.1

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.
@@ -0,0 +1,1059 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/server.ts
31
+ var server_exports = {};
32
+ __export(server_exports, {
33
+ createEmail: () => createEmail,
34
+ createEmailOptional: () => createEmailOptional
35
+ });
36
+ module.exports = __toCommonJS(server_exports);
37
+
38
+ // src/types.ts
39
+ function formatEmailAddress(address) {
40
+ if (typeof address === "string") {
41
+ return address;
42
+ }
43
+ return address.name ? `"${address.name}" <${address.email}>` : address.email;
44
+ }
45
+ var EmailError = class _EmailError extends Error {
46
+ constructor(code, message, cause) {
47
+ super(message);
48
+ this.name = "EmailError";
49
+ this.code = code;
50
+ this.cause = cause;
51
+ }
52
+ static configMissing(field) {
53
+ return new _EmailError("config_missing", `Missing required config: ${field}`);
54
+ }
55
+ static templateNotFound(name) {
56
+ return new _EmailError("template_not_found", `Template not found: ${name}`);
57
+ }
58
+ static providerNotConfigured() {
59
+ return new _EmailError("provider_not_configured", "Email provider is not configured");
60
+ }
61
+ };
62
+
63
+ // src/providers/smtp.ts
64
+ var SmtpProvider = class {
65
+ // nodemailer.Transporter - lazy loaded
66
+ constructor(config) {
67
+ this.validateConfig(config);
68
+ this.config = config;
69
+ }
70
+ /**
71
+ * Validate required config fields
72
+ */
73
+ validateConfig(config) {
74
+ const required = ["host", "port", "user", "password"];
75
+ for (const field of required) {
76
+ if (!config[field]) {
77
+ throw EmailError.configMissing(`smtp.${field}`);
78
+ }
79
+ }
80
+ }
81
+ /**
82
+ * Lazy-load nodemailer and create transporter
83
+ */
84
+ async getTransporter() {
85
+ if (this.transporter) {
86
+ return this.transporter;
87
+ }
88
+ const nodemailer = await import("nodemailer");
89
+ this.transporter = nodemailer.createTransport({
90
+ host: this.config.host,
91
+ port: this.config.port,
92
+ secure: this.config.secure ?? this.config.port === 465,
93
+ auth: {
94
+ user: this.config.user,
95
+ pass: this.config.password
96
+ }
97
+ });
98
+ return this.transporter;
99
+ }
100
+ /**
101
+ * Format recipients for nodemailer
102
+ */
103
+ formatRecipients(addresses) {
104
+ const list = Array.isArray(addresses) ? addresses : [addresses];
105
+ return list.map(formatEmailAddress).join(", ");
106
+ }
107
+ /**
108
+ * Strip HTML tags to create plain text version
109
+ */
110
+ stripHtml(html) {
111
+ return html.replace(/<style[^>]*>.*?<\/style>/gi, "").replace(/<script[^>]*>.*?<\/script>/gi, "").replace(/<[^>]+>/g, "").replace(/\s+/g, " ").trim();
112
+ }
113
+ /**
114
+ * Check if provider is configured
115
+ */
116
+ isConfigured() {
117
+ return !!(this.config.host && this.config.port && this.config.user && this.config.password);
118
+ }
119
+ /**
120
+ * Send an email via SMTP
121
+ */
122
+ async send(params) {
123
+ const { to, subject, html, text, from, replyTo, cc, bcc } = params;
124
+ if (!from) {
125
+ throw EmailError.configMissing("from address");
126
+ }
127
+ try {
128
+ const transporter = await this.getTransporter();
129
+ const mailOptions = {
130
+ from: formatEmailAddress(from),
131
+ to: this.formatRecipients(to),
132
+ subject,
133
+ html,
134
+ text: text || this.stripHtml(html)
135
+ };
136
+ if (replyTo) {
137
+ mailOptions.replyTo = formatEmailAddress(replyTo);
138
+ }
139
+ if (cc) {
140
+ mailOptions.cc = this.formatRecipients(cc);
141
+ }
142
+ if (bcc) {
143
+ mailOptions.bcc = this.formatRecipients(bcc);
144
+ }
145
+ const info = await transporter.sendMail(mailOptions);
146
+ return {
147
+ success: true,
148
+ messageId: info.messageId
149
+ };
150
+ } catch (error) {
151
+ const errorMessage = error.message || "Failed to send email";
152
+ const errorCode = this.mapErrorCode(error);
153
+ return {
154
+ success: false,
155
+ error: `[${errorCode}] ${errorMessage}`
156
+ };
157
+ }
158
+ }
159
+ /**
160
+ * Map nodemailer error to error code
161
+ */
162
+ mapErrorCode(error) {
163
+ const code = error.code || error.errno || "";
164
+ const message = (error.message || "").toLowerCase();
165
+ if (code === "ECONNREFUSED" || code === "ENOTFOUND" || message.includes("connection")) {
166
+ return "connection_error";
167
+ }
168
+ if (code === "EAUTH" || message.includes("auth") || message.includes("credentials")) {
169
+ return "authentication_error";
170
+ }
171
+ if (message.includes("recipient") || message.includes("mailbox") || message.includes("not found")) {
172
+ return "invalid_recipient";
173
+ }
174
+ return "send_failed";
175
+ }
176
+ };
177
+ function createSmtpProviderFromEnv() {
178
+ const host = process.env.SMTP_HOST;
179
+ const port = process.env.SMTP_PORT;
180
+ const user = process.env.SMTP_USER;
181
+ const password = process.env.SMTP_PASSWORD;
182
+ if (!host || !port || !user || !password) {
183
+ return null;
184
+ }
185
+ return new SmtpProvider({
186
+ host,
187
+ port: parseInt(port, 10),
188
+ user,
189
+ password,
190
+ secure: process.env.SMTP_SECURE === "true" ? true : void 0
191
+ });
192
+ }
193
+
194
+ // src/providers/ses.ts
195
+ var SesProvider = class {
196
+ // SESClient - lazy loaded
197
+ constructor(config) {
198
+ this.validateConfig(config);
199
+ this.config = config;
200
+ }
201
+ /**
202
+ * Validate required config fields
203
+ */
204
+ validateConfig(config) {
205
+ if (!config.region) {
206
+ throw EmailError.configMissing("ses.region");
207
+ }
208
+ }
209
+ /**
210
+ * Lazy-load AWS SDK and create client
211
+ */
212
+ async getClient() {
213
+ if (this.client) {
214
+ return this.client;
215
+ }
216
+ const { SESClient } = await import("@aws-sdk/client-ses");
217
+ const clientConfig = {
218
+ region: this.config.region
219
+ };
220
+ if (this.config.accessKeyId && this.config.secretAccessKey) {
221
+ clientConfig.credentials = {
222
+ accessKeyId: this.config.accessKeyId,
223
+ secretAccessKey: this.config.secretAccessKey
224
+ };
225
+ }
226
+ this.client = new SESClient(clientConfig);
227
+ return this.client;
228
+ }
229
+ /**
230
+ * Format recipients as array of strings
231
+ */
232
+ formatRecipients(addresses) {
233
+ const list = Array.isArray(addresses) ? addresses : [addresses];
234
+ return list.map(
235
+ (addr) => typeof addr === "string" ? addr : addr.email
236
+ );
237
+ }
238
+ /**
239
+ * Strip HTML tags to create plain text version
240
+ */
241
+ stripHtml(html) {
242
+ return html.replace(/<style[^>]*>.*?<\/style>/gi, "").replace(/<script[^>]*>.*?<\/script>/gi, "").replace(/<[^>]+>/g, "").replace(/\s+/g, " ").trim();
243
+ }
244
+ /**
245
+ * Check if provider is configured
246
+ */
247
+ isConfigured() {
248
+ return !!this.config.region;
249
+ }
250
+ /**
251
+ * Send an email via SES
252
+ */
253
+ async send(params) {
254
+ const { to, subject, html, text, from, replyTo, cc, bcc } = params;
255
+ if (!from) {
256
+ throw EmailError.configMissing("from address");
257
+ }
258
+ try {
259
+ const client = await this.getClient();
260
+ const { SendEmailCommand } = await import("@aws-sdk/client-ses");
261
+ const destination = {
262
+ ToAddresses: this.formatRecipients(to)
263
+ };
264
+ if (cc) {
265
+ destination.CcAddresses = this.formatRecipients(cc);
266
+ }
267
+ if (bcc) {
268
+ destination.BccAddresses = this.formatRecipients(bcc);
269
+ }
270
+ const command = new SendEmailCommand({
271
+ Source: formatEmailAddress(from),
272
+ Destination: destination,
273
+ Message: {
274
+ Subject: {
275
+ Data: subject,
276
+ Charset: "UTF-8"
277
+ },
278
+ Body: {
279
+ Html: {
280
+ Data: html,
281
+ Charset: "UTF-8"
282
+ },
283
+ Text: {
284
+ Data: text || this.stripHtml(html),
285
+ Charset: "UTF-8"
286
+ }
287
+ }
288
+ },
289
+ ReplyToAddresses: replyTo ? [formatEmailAddress(replyTo)] : void 0
290
+ });
291
+ const response = await client.send(command);
292
+ return {
293
+ success: true,
294
+ messageId: response.MessageId
295
+ };
296
+ } catch (error) {
297
+ const errorMessage = error.message || "Failed to send email";
298
+ return {
299
+ success: false,
300
+ error: `[ses_error] ${errorMessage}`
301
+ };
302
+ }
303
+ }
304
+ };
305
+ function createSesProviderFromEnv() {
306
+ const region = process.env.SES_REGION || process.env.AWS_REGION;
307
+ if (!region) {
308
+ return null;
309
+ }
310
+ return new SesProvider({
311
+ region,
312
+ accessKeyId: process.env.AWS_ACCESS_KEY_ID,
313
+ secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
314
+ });
315
+ }
316
+
317
+ // src/providers/oci-email.ts
318
+ var OciEmailProvider = class {
319
+ constructor(config) {
320
+ this.fromAddress = config.fromAddress;
321
+ this.smtp = new SmtpProvider({
322
+ host: config.host,
323
+ port: config.port,
324
+ user: config.user,
325
+ password: config.password,
326
+ secure: config.port === 465
327
+ });
328
+ }
329
+ isConfigured() {
330
+ return this.smtp.isConfigured();
331
+ }
332
+ async send(params) {
333
+ const effective = {
334
+ ...params,
335
+ from: params.from ?? this.fromAddress
336
+ };
337
+ return this.smtp.send(effective);
338
+ }
339
+ };
340
+ function createOciEmailProviderFromEnv() {
341
+ const host = process.env.OCI_EMAIL_SMTP_HOST;
342
+ const user = process.env.OCI_EMAIL_SMTP_USER;
343
+ const password = process.env.OCI_EMAIL_SMTP_PASS;
344
+ if (!host || !user || !password) {
345
+ return null;
346
+ }
347
+ const port = parseInt(process.env.OCI_EMAIL_SMTP_PORT || "587", 10);
348
+ return new OciEmailProvider({
349
+ host,
350
+ port,
351
+ user,
352
+ password,
353
+ fromAddress: process.env.OCI_EMAIL_FROM_ADDRESS
354
+ });
355
+ }
356
+
357
+ // src/providers/index.ts
358
+ function createProviderFromEnv() {
359
+ const explicitProvider = process.env.EMAIL_PROVIDER;
360
+ if (explicitProvider === "smtp") {
361
+ return createSmtpProviderFromEnv();
362
+ }
363
+ if (explicitProvider === "ses") {
364
+ return createSesProviderFromEnv();
365
+ }
366
+ if (explicitProvider === "oci-email") {
367
+ return createOciEmailProviderFromEnv();
368
+ }
369
+ if (process.env.OCI_EMAIL_SMTP_HOST) {
370
+ return createOciEmailProviderFromEnv();
371
+ }
372
+ if (process.env.SMTP_HOST) {
373
+ return createSmtpProviderFromEnv();
374
+ }
375
+ if (process.env.SES_REGION || process.env.AWS_REGION) {
376
+ return createSesProviderFromEnv();
377
+ }
378
+ return null;
379
+ }
380
+
381
+ // src/templates.ts
382
+ function interpolate(template, data) {
383
+ return template.replace(/\{\{(\w+(?:\.\w+)*)\}\}/g, (match, path) => {
384
+ const value = getNestedValue(data, path);
385
+ return value !== void 0 ? String(value) : match;
386
+ });
387
+ }
388
+ function getNestedValue(obj, path) {
389
+ const keys = path.split(".");
390
+ let current = obj;
391
+ for (const key of keys) {
392
+ if (current === null || current === void 0) {
393
+ return void 0;
394
+ }
395
+ current = current[key];
396
+ }
397
+ return current;
398
+ }
399
+ function htmlToText(html) {
400
+ return html.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "").replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "").replace(/<br\s*\/?>/gi, "\n").replace(/<\/p>/gi, "\n\n").replace(/<\/div>/gi, "\n").replace(/<\/h[1-6]>/gi, "\n\n").replace(/<\/li>/gi, "\n").replace(/<[^>]+>/g, "").replace(/&nbsp;/gi, " ").replace(/&amp;/gi, "&").replace(/&lt;/gi, "<").replace(/&gt;/gi, ">").replace(/&quot;/gi, '"').replace(/&#39;/gi, "'").replace(/[ \t]+/g, " ").replace(/\n{3,}/g, "\n\n").trim();
401
+ }
402
+ var EmailTemplateRegistry = class {
403
+ constructor() {
404
+ this.templates = {};
405
+ }
406
+ /**
407
+ * Register a template
408
+ */
409
+ register(template) {
410
+ this.templates[template.name] = template;
411
+ }
412
+ /**
413
+ * Get a template by name
414
+ */
415
+ get(name) {
416
+ return this.templates[name];
417
+ }
418
+ /**
419
+ * Check if a template exists
420
+ */
421
+ has(name) {
422
+ return name in this.templates;
423
+ }
424
+ /**
425
+ * Render a template with data
426
+ */
427
+ render(name, data) {
428
+ const template = this.templates[name];
429
+ if (!template) {
430
+ throw EmailError.templateNotFound(name);
431
+ }
432
+ const mergedData = { ...template.defaults, ...data };
433
+ const allData = {
434
+ ...mergedData,
435
+ currentYear: (/* @__PURE__ */ new Date()).getFullYear()
436
+ };
437
+ const subject = interpolate(template.subject, allData);
438
+ const html = interpolate(template.html, allData);
439
+ const text = template.text ? interpolate(template.text, allData) : htmlToText(html);
440
+ return { subject, html, text };
441
+ }
442
+ /**
443
+ * List all registered template names
444
+ */
445
+ list() {
446
+ return Object.keys(this.templates);
447
+ }
448
+ };
449
+
450
+ // src/templates/_layout.ts
451
+ var EMAIL_CONSTANTS = {
452
+ APP_NAME: "Shogo",
453
+ APP_URL: "https://shogo.ai",
454
+ SUPPORT_EMAIL: "support@shogo.ai",
455
+ LOGO_URL: "https://shogo.ai/assets/images/shogo-logo-email.png",
456
+ LOGO_DARK_URL: "https://shogo.ai/assets/images/shogo-logo-dark-email.png",
457
+ FOOTER_TEXT: "&copy; {{currentYear}} Shogo. All rights reserved"
458
+ };
459
+ var LIGHT_STYLES = `
460
+ body, html { margin: 0; padding: 0; }
461
+ .email-body {
462
+ font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
463
+ background-color: #fafafa;
464
+ color: #2a2929;
465
+ line-height: 1.6;
466
+ }
467
+ .email-wrapper { padding: 40px 20px; }
468
+ .email-container {
469
+ max-width: 560px;
470
+ margin: 0 auto;
471
+ background: #ffffff;
472
+ border-radius: 16px;
473
+ border: 1px solid #e5e7eb;
474
+ overflow: hidden;
475
+ box-shadow: 0 1px 3px rgba(0,0,0,0.04);
476
+ }
477
+ .email-header {
478
+ background: #ffffff;
479
+ padding: 24px 32px;
480
+ border-bottom: 1px solid #e5e7eb;
481
+ }
482
+ .email-logo {
483
+ font-size: 18px;
484
+ font-weight: 700;
485
+ color: #2a2929;
486
+ text-decoration: none;
487
+ letter-spacing: -0.3px;
488
+ }
489
+ .email-content { padding: 32px; }
490
+ .email-h1 {
491
+ font-size: 22px;
492
+ font-weight: 700;
493
+ color: #2a2929;
494
+ margin: 0 0 16px;
495
+ }
496
+ .email-text {
497
+ font-size: 15px;
498
+ color: #555;
499
+ margin: 0 0 16px;
500
+ }
501
+ .email-muted {
502
+ font-size: 13px;
503
+ color: #888;
504
+ margin: 0 0 12px;
505
+ }
506
+ .email-btn {
507
+ display: inline-block;
508
+ padding: 12px 32px;
509
+ background: #e8853d;
510
+ color: #ffffff;
511
+ text-decoration: none;
512
+ font-weight: 600;
513
+ font-size: 14px;
514
+ border-radius: 9999px;
515
+ margin: 8px 0 16px;
516
+ }
517
+ .email-btn-outline {
518
+ display: inline-block;
519
+ padding: 12px 32px;
520
+ background: #ffffff;
521
+ color: #2a2929;
522
+ text-decoration: none;
523
+ font-weight: 600;
524
+ font-size: 14px;
525
+ border-radius: 9999px;
526
+ border: 1px solid #d4d4d4;
527
+ margin: 8px 0 16px;
528
+ }
529
+ .email-btn-danger {
530
+ display: inline-block;
531
+ padding: 12px 32px;
532
+ background: #ef4444;
533
+ color: #ffffff;
534
+ text-decoration: none;
535
+ font-weight: 600;
536
+ font-size: 14px;
537
+ border-radius: 9999px;
538
+ margin: 8px 0 16px;
539
+ }
540
+ .email-divider {
541
+ border: none;
542
+ border-top: 1px solid #f1f5f9;
543
+ margin: 24px 0;
544
+ }
545
+ .email-footer {
546
+ padding: 20px 32px;
547
+ background: #ffffff;
548
+ border-top: 1px solid #e5e7eb;
549
+ text-align: center;
550
+ }
551
+ .email-footer-text {
552
+ font-size: 12px;
553
+ color: #888;
554
+ margin: 0;
555
+ }
556
+ .email-badge {
557
+ display: inline-block;
558
+ padding: 4px 10px;
559
+ background: #fff7ed;
560
+ color: #e8853d;
561
+ font-size: 12px;
562
+ font-weight: 600;
563
+ border-radius: 9999px;
564
+ border: 1px solid rgba(232,133,61,0.15);
565
+ }
566
+ .email-detail-row {
567
+ display: flex;
568
+ justify-content: space-between;
569
+ padding: 8px 0;
570
+ border-bottom: 1px solid #f4f4f5;
571
+ }
572
+ .email-detail-label {
573
+ font-size: 13px;
574
+ color: #888;
575
+ }
576
+ .email-detail-value {
577
+ font-size: 13px;
578
+ font-weight: 600;
579
+ color: #2a2929;
580
+ }
581
+ .email-logo-dark { display: none !important; }`;
582
+ var DARK_STYLES = `
583
+ @media (prefers-color-scheme: dark) {
584
+ .email-body { background-color: #1a1a1e; color: #e4e4e7; }
585
+ .email-container { background: #27272a; border-color: #3f3f46; box-shadow: 0 1px 4px rgba(0,0,0,0.3); }
586
+ .email-header { background: #27272a; border-bottom-color: #3f3f46; }
587
+ .email-logo { color: #fafafa; }
588
+ .email-h1 { color: #fafafa; }
589
+ .email-text { color: #d4d4d8; }
590
+ .email-muted { color: #a1a1aa; }
591
+ .email-btn-outline { background: #3f3f46; color: #fafafa; border-color: #52525b; }
592
+ .email-divider { border-top-color: #3f3f46; }
593
+ .email-footer { background: #27272a; border-top-color: #3f3f46; }
594
+ .email-footer-text { color: #71717a; }
595
+ .email-badge { background: rgba(232,133,61,0.15); color: #f0a970; border-color: rgba(232,133,61,0.25); }
596
+ .email-detail-row { border-bottom-color: #3f3f46; }
597
+ .email-detail-label { color: #a1a1aa; }
598
+ .email-detail-value { color: #fafafa; }
599
+ .email-logo-light { display: none !important; }
600
+ .email-logo-dark { display: block !important; }
601
+ }`;
602
+ var STYLES = `
603
+ <style>${LIGHT_STYLES}
604
+ ${DARK_STYLES}
605
+ </style>`;
606
+ var DARK_STYLE_OVERRIDES = DARK_STYLES.replace(
607
+ "@media (prefers-color-scheme: dark) {",
608
+ ""
609
+ ).replace(/\}\s*$/, "");
610
+ function wrapInLayout(content) {
611
+ return `<!DOCTYPE html>
612
+ <html lang="en">
613
+ <head>
614
+ <meta charset="UTF-8">
615
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
616
+ ${STYLES}
617
+ </head>
618
+ <body class="email-body">
619
+ <div class="email-wrapper">
620
+ <div class="email-container">
621
+ <div class="email-header">
622
+ <a href="${EMAIL_CONSTANTS.APP_URL}" style="display:inline-block;text-decoration:none;">
623
+ <img class="email-logo-light" src="${EMAIL_CONSTANTS.LOGO_URL}" alt="${EMAIL_CONSTANTS.APP_NAME}" width="169" height="58" style="display:block;height:40px;width:auto;" />
624
+ <img class="email-logo-dark" src="${EMAIL_CONSTANTS.LOGO_DARK_URL}" alt="${EMAIL_CONSTANTS.APP_NAME}" width="169" height="58" style="display:none;height:40px;width:auto;" />
625
+ </a>
626
+ </div>
627
+ <div class="email-content">
628
+ ${content}
629
+ </div>
630
+ <div class="email-footer">
631
+ <p class="email-footer-text">&copy; {{currentYear}} Shogo. All rights reserved</p>
632
+ </div>
633
+ </div>
634
+ </div>
635
+ </body>
636
+ </html>`;
637
+ }
638
+
639
+ // src/templates/auth/welcome.ts
640
+ var welcomeTemplate = {
641
+ name: "welcome",
642
+ subject: "Welcome to {{appName}}!",
643
+ html: wrapInLayout(`
644
+ <h1 class="email-h1">Welcome to {{appName}}!</h1>
645
+ <p class="email-text">Hi {{name}},</p>
646
+ <p class="email-text">
647
+ Thanks for signing up. You now have access to build, deploy, and
648
+ collaborate on AI-powered apps \u2014 all from one place.
649
+ </p>
650
+ <a href="{{loginUrl}}" class="email-btn" style="color:#ffffff;text-decoration:none;">Get Started</a>
651
+ <hr class="email-divider">
652
+ <p class="email-muted">
653
+ If you have any questions, just reply to this email \u2014 we're happy to help.
654
+ </p>
655
+ `),
656
+ defaults: {
657
+ appName: EMAIL_CONSTANTS.APP_NAME,
658
+ loginUrl: EMAIL_CONSTANTS.APP_URL
659
+ }
660
+ };
661
+
662
+ // src/templates/auth/password-reset.ts
663
+ var passwordResetTemplate = {
664
+ name: "password-reset",
665
+ subject: "Reset your {{appName}} password",
666
+ html: wrapInLayout(`
667
+ <h1 class="email-h1">Reset Your Password</h1>
668
+ <p class="email-text">
669
+ We received a request to reset the password for your account.
670
+ Click the button below to choose a new one:
671
+ </p>
672
+ <a href="{{resetUrl}}" class="email-btn" style="color:#ffffff;text-decoration:none;">Reset Password</a>
673
+ <hr class="email-divider">
674
+ <p class="email-muted">This link expires in {{expiresIn}}.</p>
675
+ <p class="email-muted">
676
+ If you didn't request this, you can safely ignore this email.
677
+ Your password will remain unchanged.
678
+ </p>
679
+ `),
680
+ defaults: {
681
+ appName: EMAIL_CONSTANTS.APP_NAME,
682
+ expiresIn: "1 hour"
683
+ }
684
+ };
685
+
686
+ // src/templates/auth/email-verification.ts
687
+ var emailVerificationTemplate = {
688
+ name: "email-verification",
689
+ subject: "Verify your email for {{appName}}",
690
+ html: wrapInLayout(`
691
+ <h1 class="email-h1">Verify Your Email</h1>
692
+ <p class="email-text">
693
+ Please confirm your email address by clicking the button below.
694
+ This helps us keep your account secure.
695
+ </p>
696
+ <a href="{{verifyUrl}}" class="email-btn" style="color:#ffffff;text-decoration:none;">Verify Email</a>
697
+ <hr class="email-divider">
698
+ <p class="email-muted">This link expires in {{expiresIn}}.</p>
699
+ <p class="email-muted">
700
+ If you didn't create an account, you can safely ignore this email.
701
+ </p>
702
+ `),
703
+ defaults: {
704
+ appName: EMAIL_CONSTANTS.APP_NAME,
705
+ expiresIn: "24 hours"
706
+ }
707
+ };
708
+
709
+ // src/templates/invitation/workspace-invite.ts
710
+ var workspaceInviteTemplate = {
711
+ name: "workspace-invite",
712
+ subject: "{{inviterName}} invited you to {{workspaceName}}",
713
+ html: wrapInLayout(`
714
+ <h1 class="email-h1">You're Invited!</h1>
715
+ <p class="email-text">
716
+ <strong>{{inviterName}}</strong> has invited you to join the
717
+ <strong>{{workspaceName}}</strong> workspace as
718
+ <span class="email-badge">{{role}}</span>.
719
+ </p>
720
+ <p class="email-text">
721
+ Accept the invitation to start collaborating on projects, share
722
+ resources, and build together.
723
+ </p>
724
+ <a href="{{acceptUrl}}" class="email-btn" style="color:#ffffff;text-decoration:none;">Accept Invitation</a>
725
+ <hr class="email-divider">
726
+ <p class="email-muted">
727
+ If you weren't expecting this, you can safely ignore this email.
728
+ </p>
729
+ `),
730
+ defaults: {
731
+ appName: EMAIL_CONSTANTS.APP_NAME,
732
+ role: "Editor"
733
+ }
734
+ };
735
+
736
+ // src/templates/invitation/project-invite.ts
737
+ var projectInviteTemplate = {
738
+ name: "project-invite",
739
+ subject: "{{inviterName}} invited you to {{projectName}}",
740
+ html: wrapInLayout(`
741
+ <h1 class="email-h1">Project Invitation</h1>
742
+ <p class="email-text">
743
+ <strong>{{inviterName}}</strong> has invited you to the
744
+ <strong>{{projectName}}</strong> project as
745
+ <span class="email-badge">{{role}}</span>.
746
+ </p>
747
+ <a href="{{acceptUrl}}" class="email-btn" style="color:#ffffff;text-decoration:none;">Accept Invitation</a>
748
+ <hr class="email-divider">
749
+ <p class="email-muted">
750
+ If you weren't expecting this, you can safely ignore this email.
751
+ </p>
752
+ `),
753
+ defaults: {
754
+ appName: EMAIL_CONSTANTS.APP_NAME,
755
+ role: "Editor"
756
+ }
757
+ };
758
+
759
+ // src/templates/invitation/invite-accepted.ts
760
+ var inviteAcceptedTemplate = {
761
+ name: "invite-accepted",
762
+ subject: "{{inviteeName}} accepted your invitation to {{resourceName}}",
763
+ html: wrapInLayout(`
764
+ <h1 class="email-h1">Invitation Accepted</h1>
765
+ <p class="email-text">
766
+ <strong>{{inviteeName}}</strong> ({{inviteeEmail}}) has accepted your
767
+ invitation to join <strong>{{resourceName}}</strong>.
768
+ </p>
769
+ <p class="email-text">
770
+ They now have access to the {{resourceType}} and can start collaborating.
771
+ </p>
772
+ <a href="{{dashboardUrl}}" class="email-btn-outline">View Team</a>
773
+ `),
774
+ defaults: {
775
+ appName: EMAIL_CONSTANTS.APP_NAME,
776
+ resourceType: "workspace"
777
+ }
778
+ };
779
+
780
+ // src/templates/billing/plan-upgraded.ts
781
+ var planUpgradedTemplate = {
782
+ name: "plan-upgraded",
783
+ subject: "{{workspaceName}} has been upgraded to {{planName}}",
784
+ html: wrapInLayout(`
785
+ <h1 class="email-h1">Plan Upgraded!</h1>
786
+ <p class="email-text">
787
+ Your workspace <strong>{{workspaceName}}</strong> is now on the
788
+ <span class="email-badge">{{planName}}</span> plan.
789
+ </p>
790
+ <table width="100%" cellpadding="0" cellspacing="0">
791
+ <tr>
792
+ <td class="email-detail-label">Plan</td>
793
+ <td class="email-detail-value" align="right">{{planName}}</td>
794
+ </tr>
795
+ <tr>
796
+ <td class="email-detail-label">Billing</td>
797
+ <td class="email-detail-value" align="right">{{billingInterval}}</td>
798
+ </tr>
799
+ <tr>
800
+ <td class="email-detail-label">Seats</td>
801
+ <td class="email-detail-value" align="right">{{seats}}</td>
802
+ </tr>
803
+ <tr>
804
+ <td class="email-detail-label">Monthly included usage</td>
805
+ <td class="email-detail-value" align="right">{{includedUsdTotal}}</td>
806
+ </tr>
807
+ </table>
808
+ <p class="email-text" style="margin-top:18px;">
809
+ Every AI request is billed at the provider's raw cost plus a flat 20% markup \u2014
810
+ no credits, no unit conversions. You can add or remove seats anytime from
811
+ Settings &rarr; Billing; we prorate the change automatically.
812
+ </p>
813
+ <br>
814
+ <a href="{{dashboardUrl}}" class="email-btn" style="color:#ffffff;text-decoration:none;">Go to Dashboard</a>
815
+ <hr class="email-divider">
816
+ <p class="email-muted">
817
+ Manage your subscription anytime from Settings &rarr; Billing.
818
+ </p>
819
+ `),
820
+ defaults: {
821
+ appName: EMAIL_CONSTANTS.APP_NAME,
822
+ billingInterval: "Monthly",
823
+ seats: "1"
824
+ }
825
+ };
826
+
827
+ // src/templates/billing/payment-receipt.ts
828
+ var paymentReceiptTemplate = {
829
+ name: "payment-receipt",
830
+ subject: "Payment receipt for {{workspaceName}}",
831
+ html: wrapInLayout(`
832
+ <h1 class="email-h1">Payment Received</h1>
833
+ <p class="email-text">
834
+ Thank you! Here's your receipt for <strong>{{workspaceName}}</strong>.
835
+ </p>
836
+ <table width="100%" cellpadding="0" cellspacing="0">
837
+ <tr>
838
+ <td class="email-detail-label">Plan</td>
839
+ <td class="email-detail-value" align="right">{{planName}}</td>
840
+ </tr>
841
+ <tr>
842
+ <td class="email-detail-label">Amount</td>
843
+ <td class="email-detail-value" align="right">{{currency}}{{amount}}</td>
844
+ </tr>
845
+ <tr>
846
+ <td class="email-detail-label">Date</td>
847
+ <td class="email-detail-value" align="right">{{invoiceDate}}</td>
848
+ </tr>
849
+ </table>
850
+ <br>
851
+ <a href="{{invoiceUrl}}" class="email-btn-outline">View Invoice</a>
852
+ <hr class="email-divider">
853
+ <p class="email-muted">
854
+ If you have billing questions, reply to this email or contact support.
855
+ </p>
856
+ `),
857
+ defaults: {
858
+ appName: EMAIL_CONSTANTS.APP_NAME,
859
+ currency: "$"
860
+ }
861
+ };
862
+
863
+ // src/templates/billing/payment-failed.ts
864
+ var paymentFailedTemplate = {
865
+ name: "payment-failed",
866
+ subject: "Payment failed for {{workspaceName}}",
867
+ html: wrapInLayout(`
868
+ <h1 class="email-h1">Payment Failed</h1>
869
+ <p class="email-text">
870
+ We were unable to process the payment of <strong>{{currency}}{{amount}}</strong>
871
+ for <strong>{{workspaceName}}</strong> ({{planName}} plan).
872
+ </p>
873
+ <p class="email-text">
874
+ Please update your payment method to keep your plan active and avoid
875
+ any interruption to your workspace.
876
+ </p>
877
+ <a href="{{retryUrl}}" class="email-btn-danger" style="color:#ffffff;text-decoration:none;">Update Payment Method</a>
878
+ <hr class="email-divider">
879
+ <p class="email-muted">
880
+ If you believe this is a mistake, please reply to this email or
881
+ contact support.
882
+ </p>
883
+ `),
884
+ defaults: {
885
+ appName: EMAIL_CONSTANTS.APP_NAME,
886
+ currency: "$"
887
+ }
888
+ };
889
+
890
+ // src/templates/workspace/member-joined.ts
891
+ var memberJoinedTemplate = {
892
+ name: "member-joined",
893
+ subject: "{{memberName}} joined {{workspaceName}}",
894
+ html: wrapInLayout(`
895
+ <h1 class="email-h1">New Team Member</h1>
896
+ <p class="email-text">
897
+ <strong>{{memberName}}</strong> ({{memberEmail}}) has joined
898
+ <strong>{{workspaceName}}</strong> as
899
+ <span class="email-badge">{{role}}</span>.
900
+ </p>
901
+ <a href="{{dashboardUrl}}" class="email-btn-outline">View Team</a>
902
+ `),
903
+ defaults: {
904
+ appName: EMAIL_CONSTANTS.APP_NAME,
905
+ role: "Editor"
906
+ }
907
+ };
908
+
909
+ // src/templates/workspace/member-removed.ts
910
+ var memberRemovedTemplate = {
911
+ name: "member-removed",
912
+ subject: "You have been removed from {{workspaceName}}",
913
+ html: wrapInLayout(`
914
+ <h1 class="email-h1">Workspace Access Removed</h1>
915
+ <p class="email-text">
916
+ You no longer have access to the <strong>{{workspaceName}}</strong>
917
+ workspace. All associated projects and data in that workspace are no
918
+ longer accessible from your account.
919
+ </p>
920
+ <hr class="email-divider">
921
+ <p class="email-muted">
922
+ If you believe this was a mistake, please contact the workspace owner
923
+ or reply to this email.
924
+ </p>
925
+ `),
926
+ defaults: {
927
+ appName: EMAIL_CONSTANTS.APP_NAME
928
+ }
929
+ };
930
+
931
+ // src/templates/workspace/account-deleted.ts
932
+ var accountDeletedTemplate = {
933
+ name: "account-deleted",
934
+ subject: "Your {{appName}} account has been deleted",
935
+ html: wrapInLayout(`
936
+ <h1 class="email-h1">Account Deleted</h1>
937
+ <p class="email-text">
938
+ Your {{appName}} account associated with <strong>{{email}}</strong>
939
+ has been permanently deleted. All your data, workspaces, and projects
940
+ have been removed.
941
+ </p>
942
+ <p class="email-text">
943
+ If you ever want to come back, you can create a new account at any
944
+ time.
945
+ </p>
946
+ <hr class="email-divider">
947
+ <p class="email-muted">
948
+ If you didn't request this deletion, please contact us immediately
949
+ by replying to this email.
950
+ </p>
951
+ `),
952
+ defaults: {
953
+ appName: EMAIL_CONSTANTS.APP_NAME
954
+ }
955
+ };
956
+
957
+ // src/templates/index.ts
958
+ var allTemplates = [
959
+ welcomeTemplate,
960
+ passwordResetTemplate,
961
+ emailVerificationTemplate,
962
+ workspaceInviteTemplate,
963
+ projectInviteTemplate,
964
+ inviteAcceptedTemplate,
965
+ planUpgradedTemplate,
966
+ paymentReceiptTemplate,
967
+ paymentFailedTemplate,
968
+ memberJoinedTemplate,
969
+ memberRemovedTemplate,
970
+ accountDeletedTemplate
971
+ ];
972
+ function createShogoTemplateRegistry() {
973
+ const registry = new EmailTemplateRegistry();
974
+ for (const template of allTemplates) {
975
+ registry.register(template);
976
+ }
977
+ return registry;
978
+ }
979
+
980
+ // src/server.ts
981
+ var EmailService = class {
982
+ constructor(provider, templates, defaultFrom) {
983
+ this.provider = provider;
984
+ this.templates = templates;
985
+ this.defaultFrom = defaultFrom;
986
+ }
987
+ /**
988
+ * Check if the service is configured and ready
989
+ */
990
+ isConfigured() {
991
+ return this.provider.isConfigured();
992
+ }
993
+ /**
994
+ * Register a custom email template
995
+ */
996
+ registerTemplate(template) {
997
+ this.templates.register(template);
998
+ }
999
+ /**
1000
+ * Send a raw email
1001
+ */
1002
+ async send(params) {
1003
+ const emailParams = {
1004
+ ...params,
1005
+ from: params.from ?? this.defaultFrom
1006
+ };
1007
+ return this.provider.send(emailParams);
1008
+ }
1009
+ /**
1010
+ * Send a templated email
1011
+ */
1012
+ async sendTemplate(params) {
1013
+ const { template: templateName, data, ...rest } = params;
1014
+ const { subject, html, text } = this.templates.render(templateName, data);
1015
+ return this.send({
1016
+ ...rest,
1017
+ subject,
1018
+ html,
1019
+ text
1020
+ });
1021
+ }
1022
+ };
1023
+ function createEmail(options = {}) {
1024
+ const { config, defaultFrom, includeBuiltins = true } = options;
1025
+ let provider = null;
1026
+ if (config) {
1027
+ if (config.provider === "smtp" && config.smtp) {
1028
+ provider = new SmtpProvider(config.smtp);
1029
+ } else if (config.provider === "ses" && config.ses) {
1030
+ provider = new SesProvider(config.ses);
1031
+ } else if (config.provider === "oci-email" && config.ociEmail) {
1032
+ provider = new OciEmailProvider(config.ociEmail);
1033
+ }
1034
+ } else {
1035
+ provider = createProviderFromEnv();
1036
+ }
1037
+ if (!provider) {
1038
+ throw EmailError.providerNotConfigured();
1039
+ }
1040
+ const from = defaultFrom ?? config?.defaultFrom ?? process.env.EMAIL_FROM ?? process.env.SMTP_FROM_EMAIL ?? process.env.SES_FROM_EMAIL ?? process.env.OCI_EMAIL_FROM_ADDRESS;
1041
+ if (!from) {
1042
+ throw EmailError.configMissing("defaultFrom (or EMAIL_FROM env var)");
1043
+ }
1044
+ const templates = includeBuiltins ? createShogoTemplateRegistry() : new EmailTemplateRegistry();
1045
+ return new EmailService(provider, templates, from);
1046
+ }
1047
+ function createEmailOptional(options = {}) {
1048
+ try {
1049
+ return createEmail(options);
1050
+ } catch {
1051
+ return null;
1052
+ }
1053
+ }
1054
+ // Annotate the CommonJS export names for ESM import in node:
1055
+ 0 && (module.exports = {
1056
+ createEmail,
1057
+ createEmailOptional
1058
+ });
1059
+ //# sourceMappingURL=server.cjs.map