@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.
- package/LICENSE +21 -0
- package/README.md +29 -0
- package/dist/chunk-VPWVZHAC.js +882 -0
- package/dist/index.cjs +923 -0
- package/dist/index.d.cts +136 -0
- package/dist/index.d.ts +136 -0
- package/dist/index.js +37 -0
- package/dist/server.cjs +1059 -0
- package/dist/server.d.cts +109 -0
- package/dist/server.d.ts +109 -0
- package/dist/server.js +404 -0
- package/dist/types-CCHryvHe.d.cts +196 -0
- package/dist/types-CCHryvHe.d.ts +196 -0
- package/package.json +74 -0
package/dist/server.cjs
ADDED
|
@@ -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(/ /gi, " ").replace(/&/gi, "&").replace(/</gi, "<").replace(/>/gi, ">").replace(/"/gi, '"').replace(/'/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: "© {{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">© {{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 → 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 → 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
|