@parsrun/email 0.1.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/README.md +145 -0
- package/dist/index.d.ts +103 -0
- package/dist/index.js +1192 -0
- package/dist/index.js.map +1 -0
- package/dist/providers/console.d.ts +47 -0
- package/dist/providers/console.js +126 -0
- package/dist/providers/console.js.map +1 -0
- package/dist/providers/index.d.ts +6 -0
- package/dist/providers/index.js +621 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/postmark.d.ts +47 -0
- package/dist/providers/postmark.js +202 -0
- package/dist/providers/postmark.js.map +1 -0
- package/dist/providers/resend.d.ts +47 -0
- package/dist/providers/resend.js +174 -0
- package/dist/providers/resend.js.map +1 -0
- package/dist/providers/sendgrid.d.ts +47 -0
- package/dist/providers/sendgrid.js +193 -0
- package/dist/providers/sendgrid.js.map +1 -0
- package/dist/templates/index.d.ts +114 -0
- package/dist/templates/index.js +415 -0
- package/dist/templates/index.js.map +1 -0
- package/dist/types.d.ts +186 -0
- package/dist/types.js +50 -0
- package/dist/types.js.map +1 -0
- package/package.json +80 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1192 @@
|
|
|
1
|
+
// src/types.ts
|
|
2
|
+
import {
|
|
3
|
+
type,
|
|
4
|
+
emailAddress,
|
|
5
|
+
emailAttachment,
|
|
6
|
+
sendEmailOptions,
|
|
7
|
+
sendTemplateEmailOptions,
|
|
8
|
+
emailSendResult,
|
|
9
|
+
smtpConfig,
|
|
10
|
+
resendConfig,
|
|
11
|
+
sendgridConfig,
|
|
12
|
+
sesConfig,
|
|
13
|
+
postmarkConfig,
|
|
14
|
+
emailConfig
|
|
15
|
+
} from "@parsrun/types";
|
|
16
|
+
var EmailError = class extends Error {
|
|
17
|
+
constructor(message, code, cause) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.code = code;
|
|
20
|
+
this.cause = cause;
|
|
21
|
+
this.name = "EmailError";
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
var EmailErrorCodes = {
|
|
25
|
+
INVALID_CONFIG: "INVALID_CONFIG",
|
|
26
|
+
INVALID_RECIPIENT: "INVALID_RECIPIENT",
|
|
27
|
+
INVALID_CONTENT: "INVALID_CONTENT",
|
|
28
|
+
SEND_FAILED: "SEND_FAILED",
|
|
29
|
+
RATE_LIMITED: "RATE_LIMITED",
|
|
30
|
+
PROVIDER_ERROR: "PROVIDER_ERROR",
|
|
31
|
+
TEMPLATE_ERROR: "TEMPLATE_ERROR",
|
|
32
|
+
ATTACHMENT_ERROR: "ATTACHMENT_ERROR"
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// src/providers/resend.ts
|
|
36
|
+
var ResendProvider = class {
|
|
37
|
+
type = "resend";
|
|
38
|
+
apiKey;
|
|
39
|
+
fromEmail;
|
|
40
|
+
fromName;
|
|
41
|
+
baseUrl = "https://api.resend.com";
|
|
42
|
+
constructor(config) {
|
|
43
|
+
this.apiKey = config.apiKey;
|
|
44
|
+
this.fromEmail = config.fromEmail;
|
|
45
|
+
this.fromName = config.fromName;
|
|
46
|
+
}
|
|
47
|
+
formatAddress(address) {
|
|
48
|
+
if (typeof address === "string") {
|
|
49
|
+
return address;
|
|
50
|
+
}
|
|
51
|
+
if (address.name) {
|
|
52
|
+
return `${address.name} <${address.email}>`;
|
|
53
|
+
}
|
|
54
|
+
return address.email;
|
|
55
|
+
}
|
|
56
|
+
formatAddresses(addresses) {
|
|
57
|
+
if (Array.isArray(addresses)) {
|
|
58
|
+
return addresses.map((a) => this.formatAddress(a));
|
|
59
|
+
}
|
|
60
|
+
return [this.formatAddress(addresses)];
|
|
61
|
+
}
|
|
62
|
+
async send(options) {
|
|
63
|
+
const from = options.from ? this.formatAddress(options.from) : this.fromName ? `${this.fromName} <${this.fromEmail}>` : this.fromEmail;
|
|
64
|
+
const payload = {
|
|
65
|
+
from,
|
|
66
|
+
to: this.formatAddresses(options.to),
|
|
67
|
+
subject: options.subject
|
|
68
|
+
};
|
|
69
|
+
if (options.html) payload["html"] = options.html;
|
|
70
|
+
if (options.text) payload["text"] = options.text;
|
|
71
|
+
if (options.replyTo) payload["reply_to"] = this.formatAddress(options.replyTo);
|
|
72
|
+
if (options.cc) payload["cc"] = this.formatAddresses(options.cc);
|
|
73
|
+
if (options.bcc) payload["bcc"] = this.formatAddresses(options.bcc);
|
|
74
|
+
if (options.headers) payload["headers"] = options.headers;
|
|
75
|
+
if (options.tags) payload["tags"] = Object.entries(options.tags).map(([name, value]) => ({ name, value }));
|
|
76
|
+
if (options.scheduledAt) payload["scheduled_at"] = options.scheduledAt.toISOString();
|
|
77
|
+
if (options.attachments && options.attachments.length > 0) {
|
|
78
|
+
payload["attachments"] = options.attachments.map((att) => ({
|
|
79
|
+
filename: att.filename,
|
|
80
|
+
content: typeof att.content === "string" ? att.content : this.uint8ArrayToBase64(att.content),
|
|
81
|
+
content_type: att.contentType
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
const response = await fetch(`${this.baseUrl}/emails`, {
|
|
86
|
+
method: "POST",
|
|
87
|
+
headers: {
|
|
88
|
+
"Authorization": `Bearer ${this.apiKey}`,
|
|
89
|
+
"Content-Type": "application/json"
|
|
90
|
+
},
|
|
91
|
+
body: JSON.stringify(payload)
|
|
92
|
+
});
|
|
93
|
+
const data = await response.json();
|
|
94
|
+
if (!response.ok) {
|
|
95
|
+
return {
|
|
96
|
+
success: false,
|
|
97
|
+
error: data.message || `HTTP ${response.status}`,
|
|
98
|
+
data
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
success: true,
|
|
103
|
+
messageId: data.id,
|
|
104
|
+
data
|
|
105
|
+
};
|
|
106
|
+
} catch (err) {
|
|
107
|
+
throw new EmailError(
|
|
108
|
+
`Resend send failed: ${err instanceof Error ? err.message : "Unknown error"}`,
|
|
109
|
+
EmailErrorCodes.SEND_FAILED,
|
|
110
|
+
err
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async sendBatch(options) {
|
|
115
|
+
const results = [];
|
|
116
|
+
let successful = 0;
|
|
117
|
+
let failed = 0;
|
|
118
|
+
for (const email of options.emails) {
|
|
119
|
+
try {
|
|
120
|
+
const result = await this.send(email);
|
|
121
|
+
results.push(result);
|
|
122
|
+
if (result.success) {
|
|
123
|
+
successful++;
|
|
124
|
+
} else {
|
|
125
|
+
failed++;
|
|
126
|
+
if (options.stopOnError) break;
|
|
127
|
+
}
|
|
128
|
+
} catch (err) {
|
|
129
|
+
failed++;
|
|
130
|
+
results.push({
|
|
131
|
+
success: false,
|
|
132
|
+
error: err instanceof Error ? err.message : "Unknown error"
|
|
133
|
+
});
|
|
134
|
+
if (options.stopOnError) break;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
total: options.emails.length,
|
|
139
|
+
successful,
|
|
140
|
+
failed,
|
|
141
|
+
results
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
async verify() {
|
|
145
|
+
try {
|
|
146
|
+
const response = await fetch(`${this.baseUrl}/domains`, {
|
|
147
|
+
headers: {
|
|
148
|
+
"Authorization": `Bearer ${this.apiKey}`
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
return response.ok;
|
|
152
|
+
} catch {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
uint8ArrayToBase64(data) {
|
|
157
|
+
let binary = "";
|
|
158
|
+
for (let i = 0; i < data.length; i++) {
|
|
159
|
+
const byte = data[i];
|
|
160
|
+
if (byte !== void 0) {
|
|
161
|
+
binary += String.fromCharCode(byte);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return btoa(binary);
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
function createResendProvider(config) {
|
|
168
|
+
return new ResendProvider(config);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// src/providers/sendgrid.ts
|
|
172
|
+
var SendGridProvider = class {
|
|
173
|
+
type = "sendgrid";
|
|
174
|
+
apiKey;
|
|
175
|
+
fromEmail;
|
|
176
|
+
fromName;
|
|
177
|
+
baseUrl = "https://api.sendgrid.com/v3";
|
|
178
|
+
constructor(config) {
|
|
179
|
+
this.apiKey = config.apiKey;
|
|
180
|
+
this.fromEmail = config.fromEmail;
|
|
181
|
+
this.fromName = config.fromName;
|
|
182
|
+
}
|
|
183
|
+
formatAddress(address) {
|
|
184
|
+
if (typeof address === "string") {
|
|
185
|
+
return { email: address };
|
|
186
|
+
}
|
|
187
|
+
return address.name ? { email: address.email, name: address.name } : { email: address.email };
|
|
188
|
+
}
|
|
189
|
+
formatAddresses(addresses) {
|
|
190
|
+
if (Array.isArray(addresses)) {
|
|
191
|
+
return addresses.map((a) => this.formatAddress(a));
|
|
192
|
+
}
|
|
193
|
+
return [this.formatAddress(addresses)];
|
|
194
|
+
}
|
|
195
|
+
async send(options) {
|
|
196
|
+
const from = options.from ? this.formatAddress(options.from) : this.fromName ? { email: this.fromEmail, name: this.fromName } : { email: this.fromEmail };
|
|
197
|
+
const personalization = {
|
|
198
|
+
to: this.formatAddresses(options.to)
|
|
199
|
+
};
|
|
200
|
+
if (options.cc) {
|
|
201
|
+
personalization.cc = this.formatAddresses(options.cc);
|
|
202
|
+
}
|
|
203
|
+
if (options.bcc) {
|
|
204
|
+
personalization.bcc = this.formatAddresses(options.bcc);
|
|
205
|
+
}
|
|
206
|
+
if (options.headers) {
|
|
207
|
+
personalization.headers = options.headers;
|
|
208
|
+
}
|
|
209
|
+
const payload = {
|
|
210
|
+
personalizations: [personalization],
|
|
211
|
+
from,
|
|
212
|
+
subject: options.subject,
|
|
213
|
+
content: []
|
|
214
|
+
};
|
|
215
|
+
const content = [];
|
|
216
|
+
if (options.text) {
|
|
217
|
+
content.push({ type: "text/plain", value: options.text });
|
|
218
|
+
}
|
|
219
|
+
if (options.html) {
|
|
220
|
+
content.push({ type: "text/html", value: options.html });
|
|
221
|
+
}
|
|
222
|
+
payload["content"] = content;
|
|
223
|
+
if (options.replyTo) {
|
|
224
|
+
payload["reply_to"] = this.formatAddress(options.replyTo);
|
|
225
|
+
}
|
|
226
|
+
if (options.attachments && options.attachments.length > 0) {
|
|
227
|
+
payload["attachments"] = options.attachments.map((att) => ({
|
|
228
|
+
filename: att.filename,
|
|
229
|
+
content: typeof att.content === "string" ? att.content : this.uint8ArrayToBase64(att.content),
|
|
230
|
+
type: att.contentType,
|
|
231
|
+
content_id: att.contentId,
|
|
232
|
+
disposition: att.contentId ? "inline" : "attachment"
|
|
233
|
+
}));
|
|
234
|
+
}
|
|
235
|
+
if (options.scheduledAt) {
|
|
236
|
+
payload["send_at"] = Math.floor(options.scheduledAt.getTime() / 1e3);
|
|
237
|
+
}
|
|
238
|
+
try {
|
|
239
|
+
const response = await fetch(`${this.baseUrl}/mail/send`, {
|
|
240
|
+
method: "POST",
|
|
241
|
+
headers: {
|
|
242
|
+
"Authorization": `Bearer ${this.apiKey}`,
|
|
243
|
+
"Content-Type": "application/json"
|
|
244
|
+
},
|
|
245
|
+
body: JSON.stringify(payload)
|
|
246
|
+
});
|
|
247
|
+
if (response.status === 202) {
|
|
248
|
+
const messageId = response.headers.get("x-message-id");
|
|
249
|
+
return {
|
|
250
|
+
success: true,
|
|
251
|
+
messageId: messageId ?? void 0
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
const data = await response.json().catch(() => ({}));
|
|
255
|
+
const errorMessage = data.errors?.[0]?.message || `HTTP ${response.status}`;
|
|
256
|
+
return {
|
|
257
|
+
success: false,
|
|
258
|
+
error: errorMessage,
|
|
259
|
+
data
|
|
260
|
+
};
|
|
261
|
+
} catch (err) {
|
|
262
|
+
throw new EmailError(
|
|
263
|
+
`SendGrid send failed: ${err instanceof Error ? err.message : "Unknown error"}`,
|
|
264
|
+
EmailErrorCodes.SEND_FAILED,
|
|
265
|
+
err
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
async sendBatch(options) {
|
|
270
|
+
const results = [];
|
|
271
|
+
let successful = 0;
|
|
272
|
+
let failed = 0;
|
|
273
|
+
for (const email of options.emails) {
|
|
274
|
+
try {
|
|
275
|
+
const result = await this.send(email);
|
|
276
|
+
results.push(result);
|
|
277
|
+
if (result.success) {
|
|
278
|
+
successful++;
|
|
279
|
+
} else {
|
|
280
|
+
failed++;
|
|
281
|
+
if (options.stopOnError) break;
|
|
282
|
+
}
|
|
283
|
+
} catch (err) {
|
|
284
|
+
failed++;
|
|
285
|
+
results.push({
|
|
286
|
+
success: false,
|
|
287
|
+
error: err instanceof Error ? err.message : "Unknown error"
|
|
288
|
+
});
|
|
289
|
+
if (options.stopOnError) break;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return {
|
|
293
|
+
total: options.emails.length,
|
|
294
|
+
successful,
|
|
295
|
+
failed,
|
|
296
|
+
results
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
async verify() {
|
|
300
|
+
try {
|
|
301
|
+
const response = await fetch(`${this.baseUrl}/scopes`, {
|
|
302
|
+
headers: {
|
|
303
|
+
"Authorization": `Bearer ${this.apiKey}`
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
return response.ok;
|
|
307
|
+
} catch {
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
uint8ArrayToBase64(data) {
|
|
312
|
+
let binary = "";
|
|
313
|
+
for (let i = 0; i < data.length; i++) {
|
|
314
|
+
const byte = data[i];
|
|
315
|
+
if (byte !== void 0) {
|
|
316
|
+
binary += String.fromCharCode(byte);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return btoa(binary);
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
function createSendGridProvider(config) {
|
|
323
|
+
return new SendGridProvider(config);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// src/providers/postmark.ts
|
|
327
|
+
var PostmarkProvider = class {
|
|
328
|
+
type = "postmark";
|
|
329
|
+
apiKey;
|
|
330
|
+
fromEmail;
|
|
331
|
+
fromName;
|
|
332
|
+
baseUrl = "https://api.postmarkapp.com";
|
|
333
|
+
constructor(config) {
|
|
334
|
+
this.apiKey = config.apiKey;
|
|
335
|
+
this.fromEmail = config.fromEmail;
|
|
336
|
+
this.fromName = config.fromName;
|
|
337
|
+
}
|
|
338
|
+
formatAddress(address) {
|
|
339
|
+
if (typeof address === "string") {
|
|
340
|
+
return address;
|
|
341
|
+
}
|
|
342
|
+
if (address.name) {
|
|
343
|
+
return `${address.name} <${address.email}>`;
|
|
344
|
+
}
|
|
345
|
+
return address.email;
|
|
346
|
+
}
|
|
347
|
+
formatAddresses(addresses) {
|
|
348
|
+
if (Array.isArray(addresses)) {
|
|
349
|
+
return addresses.map((a) => this.formatAddress(a)).join(",");
|
|
350
|
+
}
|
|
351
|
+
return this.formatAddress(addresses);
|
|
352
|
+
}
|
|
353
|
+
async send(options) {
|
|
354
|
+
const from = options.from ? this.formatAddress(options.from) : this.fromName ? `${this.fromName} <${this.fromEmail}>` : this.fromEmail;
|
|
355
|
+
const payload = {
|
|
356
|
+
From: from,
|
|
357
|
+
To: this.formatAddresses(options.to),
|
|
358
|
+
Subject: options.subject
|
|
359
|
+
};
|
|
360
|
+
if (options.html) payload["HtmlBody"] = options.html;
|
|
361
|
+
if (options.text) payload["TextBody"] = options.text;
|
|
362
|
+
if (options.replyTo) payload["ReplyTo"] = this.formatAddress(options.replyTo);
|
|
363
|
+
if (options.cc) payload["Cc"] = this.formatAddresses(options.cc);
|
|
364
|
+
if (options.bcc) payload["Bcc"] = this.formatAddresses(options.bcc);
|
|
365
|
+
if (options.headers) {
|
|
366
|
+
payload["Headers"] = Object.entries(options.headers).map(([Name, Value]) => ({ Name, Value }));
|
|
367
|
+
}
|
|
368
|
+
if (options.tags) {
|
|
369
|
+
const tagEntries = Object.entries(options.tags);
|
|
370
|
+
if (tagEntries.length > 0 && tagEntries[0]) {
|
|
371
|
+
payload["Tag"] = tagEntries[0][1];
|
|
372
|
+
}
|
|
373
|
+
payload["Metadata"] = options.tags;
|
|
374
|
+
}
|
|
375
|
+
if (options.attachments && options.attachments.length > 0) {
|
|
376
|
+
payload["Attachments"] = options.attachments.map((att) => ({
|
|
377
|
+
Name: att.filename,
|
|
378
|
+
Content: typeof att.content === "string" ? att.content : this.uint8ArrayToBase64(att.content),
|
|
379
|
+
ContentType: att.contentType || "application/octet-stream",
|
|
380
|
+
ContentID: att.contentId
|
|
381
|
+
}));
|
|
382
|
+
}
|
|
383
|
+
try {
|
|
384
|
+
const response = await fetch(`${this.baseUrl}/email`, {
|
|
385
|
+
method: "POST",
|
|
386
|
+
headers: {
|
|
387
|
+
"X-Postmark-Server-Token": this.apiKey,
|
|
388
|
+
"Content-Type": "application/json",
|
|
389
|
+
"Accept": "application/json"
|
|
390
|
+
},
|
|
391
|
+
body: JSON.stringify(payload)
|
|
392
|
+
});
|
|
393
|
+
const data = await response.json();
|
|
394
|
+
if (!response.ok || data.ErrorCode) {
|
|
395
|
+
return {
|
|
396
|
+
success: false,
|
|
397
|
+
error: data.Message || `HTTP ${response.status}`,
|
|
398
|
+
data
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
return {
|
|
402
|
+
success: true,
|
|
403
|
+
messageId: data.MessageID,
|
|
404
|
+
data
|
|
405
|
+
};
|
|
406
|
+
} catch (err) {
|
|
407
|
+
throw new EmailError(
|
|
408
|
+
`Postmark send failed: ${err instanceof Error ? err.message : "Unknown error"}`,
|
|
409
|
+
EmailErrorCodes.SEND_FAILED,
|
|
410
|
+
err
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
async sendBatch(options) {
|
|
415
|
+
const batchPayload = options.emails.map((email) => {
|
|
416
|
+
const from = email.from ? this.formatAddress(email.from) : this.fromName ? `${this.fromName} <${this.fromEmail}>` : this.fromEmail;
|
|
417
|
+
const item = {
|
|
418
|
+
From: from,
|
|
419
|
+
To: this.formatAddresses(email.to),
|
|
420
|
+
Subject: email.subject
|
|
421
|
+
};
|
|
422
|
+
if (email.html) item["HtmlBody"] = email.html;
|
|
423
|
+
if (email.text) item["TextBody"] = email.text;
|
|
424
|
+
if (email.replyTo) item["ReplyTo"] = this.formatAddress(email.replyTo);
|
|
425
|
+
if (email.cc) item["Cc"] = this.formatAddresses(email.cc);
|
|
426
|
+
if (email.bcc) item["Bcc"] = this.formatAddresses(email.bcc);
|
|
427
|
+
return item;
|
|
428
|
+
});
|
|
429
|
+
try {
|
|
430
|
+
const response = await fetch(`${this.baseUrl}/email/batch`, {
|
|
431
|
+
method: "POST",
|
|
432
|
+
headers: {
|
|
433
|
+
"X-Postmark-Server-Token": this.apiKey,
|
|
434
|
+
"Content-Type": "application/json",
|
|
435
|
+
"Accept": "application/json"
|
|
436
|
+
},
|
|
437
|
+
body: JSON.stringify(batchPayload)
|
|
438
|
+
});
|
|
439
|
+
const data = await response.json();
|
|
440
|
+
const results = data.map((item) => ({
|
|
441
|
+
success: !item.ErrorCode,
|
|
442
|
+
messageId: item.MessageID,
|
|
443
|
+
error: item.ErrorCode ? item.Message : void 0,
|
|
444
|
+
data: item
|
|
445
|
+
}));
|
|
446
|
+
const successful = results.filter((r) => r.success).length;
|
|
447
|
+
const failed = results.filter((r) => !r.success).length;
|
|
448
|
+
return {
|
|
449
|
+
total: options.emails.length,
|
|
450
|
+
successful,
|
|
451
|
+
failed,
|
|
452
|
+
results
|
|
453
|
+
};
|
|
454
|
+
} catch (err) {
|
|
455
|
+
throw new EmailError(
|
|
456
|
+
`Postmark batch send failed: ${err instanceof Error ? err.message : "Unknown error"}`,
|
|
457
|
+
EmailErrorCodes.SEND_FAILED,
|
|
458
|
+
err
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
async verify() {
|
|
463
|
+
try {
|
|
464
|
+
const response = await fetch(`${this.baseUrl}/server`, {
|
|
465
|
+
headers: {
|
|
466
|
+
"X-Postmark-Server-Token": this.apiKey,
|
|
467
|
+
"Accept": "application/json"
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
return response.ok;
|
|
471
|
+
} catch {
|
|
472
|
+
return false;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
uint8ArrayToBase64(data) {
|
|
476
|
+
let binary = "";
|
|
477
|
+
for (let i = 0; i < data.length; i++) {
|
|
478
|
+
const byte = data[i];
|
|
479
|
+
if (byte !== void 0) {
|
|
480
|
+
binary += String.fromCharCode(byte);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return btoa(binary);
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
function createPostmarkProvider(config) {
|
|
487
|
+
return new PostmarkProvider(config);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// src/providers/console.ts
|
|
491
|
+
var ConsoleProvider = class {
|
|
492
|
+
type = "console";
|
|
493
|
+
fromEmail;
|
|
494
|
+
fromName;
|
|
495
|
+
messageCounter = 0;
|
|
496
|
+
constructor(config) {
|
|
497
|
+
this.fromEmail = config.fromEmail;
|
|
498
|
+
this.fromName = config.fromName;
|
|
499
|
+
}
|
|
500
|
+
formatAddress(address) {
|
|
501
|
+
if (typeof address === "string") {
|
|
502
|
+
return address;
|
|
503
|
+
}
|
|
504
|
+
if (address.name) {
|
|
505
|
+
return `${address.name} <${address.email}>`;
|
|
506
|
+
}
|
|
507
|
+
return address.email;
|
|
508
|
+
}
|
|
509
|
+
formatAddresses(addresses) {
|
|
510
|
+
if (Array.isArray(addresses)) {
|
|
511
|
+
return addresses.map((a) => this.formatAddress(a)).join(", ");
|
|
512
|
+
}
|
|
513
|
+
return this.formatAddress(addresses);
|
|
514
|
+
}
|
|
515
|
+
async send(options) {
|
|
516
|
+
this.messageCounter++;
|
|
517
|
+
const messageId = `console-${Date.now()}-${this.messageCounter}`;
|
|
518
|
+
const from = options.from ? this.formatAddress(options.from) : this.fromName ? `${this.fromName} <${this.fromEmail}>` : this.fromEmail;
|
|
519
|
+
const separator = "\u2500".repeat(60);
|
|
520
|
+
console.log(`
|
|
521
|
+
${separator}`);
|
|
522
|
+
console.log("\u{1F4E7} EMAIL (Console Provider)");
|
|
523
|
+
console.log(separator);
|
|
524
|
+
console.log(`Message ID: ${messageId}`);
|
|
525
|
+
console.log(`From: ${from}`);
|
|
526
|
+
console.log(`To: ${this.formatAddresses(options.to)}`);
|
|
527
|
+
if (options.cc) {
|
|
528
|
+
console.log(`CC: ${this.formatAddresses(options.cc)}`);
|
|
529
|
+
}
|
|
530
|
+
if (options.bcc) {
|
|
531
|
+
console.log(`BCC: ${this.formatAddresses(options.bcc)}`);
|
|
532
|
+
}
|
|
533
|
+
if (options.replyTo) {
|
|
534
|
+
console.log(`Reply-To: ${this.formatAddress(options.replyTo)}`);
|
|
535
|
+
}
|
|
536
|
+
console.log(`Subject: ${options.subject}`);
|
|
537
|
+
if (options.headers) {
|
|
538
|
+
console.log(`Headers: ${JSON.stringify(options.headers)}`);
|
|
539
|
+
}
|
|
540
|
+
if (options.tags) {
|
|
541
|
+
console.log(`Tags: ${JSON.stringify(options.tags)}`);
|
|
542
|
+
}
|
|
543
|
+
if (options.scheduledAt) {
|
|
544
|
+
console.log(`Scheduled: ${options.scheduledAt.toISOString()}`);
|
|
545
|
+
}
|
|
546
|
+
if (options.attachments && options.attachments.length > 0) {
|
|
547
|
+
console.log(`Attachments:`);
|
|
548
|
+
for (const att of options.attachments) {
|
|
549
|
+
const size = typeof att.content === "string" ? att.content.length : att.content.length;
|
|
550
|
+
console.log(` - ${att.filename} (${att.contentType || "unknown"}, ${size} bytes)`);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
console.log(separator);
|
|
554
|
+
if (options.text) {
|
|
555
|
+
console.log("TEXT CONTENT:");
|
|
556
|
+
console.log(options.text);
|
|
557
|
+
}
|
|
558
|
+
if (options.html) {
|
|
559
|
+
console.log("HTML CONTENT:");
|
|
560
|
+
console.log(options.html);
|
|
561
|
+
}
|
|
562
|
+
console.log(`${separator}
|
|
563
|
+
`);
|
|
564
|
+
return {
|
|
565
|
+
success: true,
|
|
566
|
+
messageId
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
async sendBatch(options) {
|
|
570
|
+
const results = [];
|
|
571
|
+
let successful = 0;
|
|
572
|
+
let failed = 0;
|
|
573
|
+
console.log(`
|
|
574
|
+
\u{1F4EC} BATCH EMAIL (${options.emails.length} emails)`);
|
|
575
|
+
for (const email of options.emails) {
|
|
576
|
+
try {
|
|
577
|
+
const result = await this.send(email);
|
|
578
|
+
results.push(result);
|
|
579
|
+
if (result.success) {
|
|
580
|
+
successful++;
|
|
581
|
+
} else {
|
|
582
|
+
failed++;
|
|
583
|
+
if (options.stopOnError) break;
|
|
584
|
+
}
|
|
585
|
+
} catch (err) {
|
|
586
|
+
failed++;
|
|
587
|
+
results.push({
|
|
588
|
+
success: false,
|
|
589
|
+
error: err instanceof Error ? err.message : "Unknown error"
|
|
590
|
+
});
|
|
591
|
+
if (options.stopOnError) break;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
console.log(`\u{1F4EC} BATCH COMPLETE: ${successful} sent, ${failed} failed
|
|
595
|
+
`);
|
|
596
|
+
return {
|
|
597
|
+
total: options.emails.length,
|
|
598
|
+
successful,
|
|
599
|
+
failed,
|
|
600
|
+
results
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
async verify() {
|
|
604
|
+
console.log("\u{1F4E7} Console email provider verified (always returns true)");
|
|
605
|
+
return true;
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
function createConsoleProvider(config) {
|
|
609
|
+
return new ConsoleProvider(config);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// src/templates/index.ts
|
|
613
|
+
function renderTemplate(template, data) {
|
|
614
|
+
return template.replace(/\{\{(\w+(?:\.\w+)*)\}\}/g, (_, path) => {
|
|
615
|
+
const keys = path.split(".");
|
|
616
|
+
let value = data;
|
|
617
|
+
for (const key of keys) {
|
|
618
|
+
if (value && typeof value === "object" && key in value) {
|
|
619
|
+
value = value[key];
|
|
620
|
+
} else {
|
|
621
|
+
return `{{${path}}}`;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
return String(value ?? "");
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
function wrapEmailHtml(content, options) {
|
|
628
|
+
const { brandName = "Pars", brandColor = "#0070f3", footerText } = options ?? {};
|
|
629
|
+
return `<!DOCTYPE html>
|
|
630
|
+
<html lang="en">
|
|
631
|
+
<head>
|
|
632
|
+
<meta charset="UTF-8">
|
|
633
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
634
|
+
<title>${brandName}</title>
|
|
635
|
+
<style>
|
|
636
|
+
body {
|
|
637
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
638
|
+
line-height: 1.6;
|
|
639
|
+
color: #333;
|
|
640
|
+
margin: 0;
|
|
641
|
+
padding: 0;
|
|
642
|
+
background-color: #f5f5f5;
|
|
643
|
+
}
|
|
644
|
+
.container {
|
|
645
|
+
max-width: 600px;
|
|
646
|
+
margin: 0 auto;
|
|
647
|
+
padding: 40px 20px;
|
|
648
|
+
}
|
|
649
|
+
.card {
|
|
650
|
+
background: #ffffff;
|
|
651
|
+
border-radius: 8px;
|
|
652
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
|
653
|
+
padding: 40px;
|
|
654
|
+
}
|
|
655
|
+
.header {
|
|
656
|
+
text-align: center;
|
|
657
|
+
margin-bottom: 32px;
|
|
658
|
+
}
|
|
659
|
+
.brand {
|
|
660
|
+
font-size: 24px;
|
|
661
|
+
font-weight: 700;
|
|
662
|
+
color: ${brandColor};
|
|
663
|
+
}
|
|
664
|
+
.content {
|
|
665
|
+
margin-bottom: 32px;
|
|
666
|
+
}
|
|
667
|
+
.code-box {
|
|
668
|
+
background: #f8f9fa;
|
|
669
|
+
border: 2px dashed #dee2e6;
|
|
670
|
+
border-radius: 8px;
|
|
671
|
+
padding: 24px;
|
|
672
|
+
text-align: center;
|
|
673
|
+
margin: 24px 0;
|
|
674
|
+
}
|
|
675
|
+
.code {
|
|
676
|
+
font-size: 36px;
|
|
677
|
+
font-weight: 700;
|
|
678
|
+
letter-spacing: 8px;
|
|
679
|
+
color: ${brandColor};
|
|
680
|
+
font-family: 'SF Mono', Monaco, 'Courier New', monospace;
|
|
681
|
+
}
|
|
682
|
+
.button {
|
|
683
|
+
display: inline-block;
|
|
684
|
+
background: ${brandColor};
|
|
685
|
+
color: #ffffff !important;
|
|
686
|
+
text-decoration: none;
|
|
687
|
+
padding: 14px 32px;
|
|
688
|
+
border-radius: 6px;
|
|
689
|
+
font-weight: 600;
|
|
690
|
+
margin: 16px 0;
|
|
691
|
+
}
|
|
692
|
+
.button:hover {
|
|
693
|
+
opacity: 0.9;
|
|
694
|
+
}
|
|
695
|
+
.footer {
|
|
696
|
+
text-align: center;
|
|
697
|
+
color: #666;
|
|
698
|
+
font-size: 13px;
|
|
699
|
+
margin-top: 32px;
|
|
700
|
+
padding-top: 24px;
|
|
701
|
+
border-top: 1px solid #eee;
|
|
702
|
+
}
|
|
703
|
+
.footer a {
|
|
704
|
+
color: ${brandColor};
|
|
705
|
+
}
|
|
706
|
+
.text-muted {
|
|
707
|
+
color: #666;
|
|
708
|
+
font-size: 14px;
|
|
709
|
+
}
|
|
710
|
+
.text-center {
|
|
711
|
+
text-align: center;
|
|
712
|
+
}
|
|
713
|
+
h1 {
|
|
714
|
+
font-size: 24px;
|
|
715
|
+
margin: 0 0 16px 0;
|
|
716
|
+
color: #111;
|
|
717
|
+
}
|
|
718
|
+
p {
|
|
719
|
+
margin: 0 0 16px 0;
|
|
720
|
+
}
|
|
721
|
+
</style>
|
|
722
|
+
</head>
|
|
723
|
+
<body>
|
|
724
|
+
<div class="container">
|
|
725
|
+
<div class="card">
|
|
726
|
+
<div class="header">
|
|
727
|
+
<div class="brand">${brandName}</div>
|
|
728
|
+
</div>
|
|
729
|
+
<div class="content">
|
|
730
|
+
${content}
|
|
731
|
+
</div>
|
|
732
|
+
<div class="footer">
|
|
733
|
+
${footerText ?? `© ${(/* @__PURE__ */ new Date()).getFullYear()} ${brandName}. All rights reserved.`}
|
|
734
|
+
</div>
|
|
735
|
+
</div>
|
|
736
|
+
</div>
|
|
737
|
+
</body>
|
|
738
|
+
</html>`;
|
|
739
|
+
}
|
|
740
|
+
var otpTemplate = {
|
|
741
|
+
name: "otp",
|
|
742
|
+
subject: "Your verification code: {{code}}",
|
|
743
|
+
html: `
|
|
744
|
+
<h1>Your verification code</h1>
|
|
745
|
+
<p>Use the following code to verify your identity:</p>
|
|
746
|
+
<div class="code-box">
|
|
747
|
+
<span class="code">{{code}}</span>
|
|
748
|
+
</div>
|
|
749
|
+
<p class="text-muted">This code expires in {{expiresInMinutes}} minutes.</p>
|
|
750
|
+
<p class="text-muted">If you didn't request this code, you can safely ignore this email.</p>
|
|
751
|
+
`,
|
|
752
|
+
text: `Your verification code: {{code}}
|
|
753
|
+
|
|
754
|
+
This code expires in {{expiresInMinutes}} minutes.
|
|
755
|
+
|
|
756
|
+
If you didn't request this code, you can safely ignore this email.`
|
|
757
|
+
};
|
|
758
|
+
function renderOTPEmail(data) {
|
|
759
|
+
const templateData = {
|
|
760
|
+
...data,
|
|
761
|
+
expiresInMinutes: data.expiresInMinutes ?? 10
|
|
762
|
+
};
|
|
763
|
+
return {
|
|
764
|
+
subject: renderTemplate(otpTemplate.subject, templateData),
|
|
765
|
+
html: wrapEmailHtml(renderTemplate(otpTemplate.html, templateData), {
|
|
766
|
+
brandName: data.brandName,
|
|
767
|
+
brandColor: data.brandColor
|
|
768
|
+
}),
|
|
769
|
+
text: renderTemplate(otpTemplate.text ?? "", templateData)
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
var magicLinkTemplate = {
|
|
773
|
+
name: "magic-link",
|
|
774
|
+
subject: "Sign in to {{brandName}}",
|
|
775
|
+
html: `
|
|
776
|
+
<h1>Sign in to your account</h1>
|
|
777
|
+
<p>Click the button below to securely sign in to your account:</p>
|
|
778
|
+
<div class="text-center">
|
|
779
|
+
<a href="{{url}}" class="button">Sign In</a>
|
|
780
|
+
</div>
|
|
781
|
+
<p class="text-muted">This link expires in {{expiresInMinutes}} minutes.</p>
|
|
782
|
+
<p class="text-muted">If you didn't request this link, you can safely ignore this email.</p>
|
|
783
|
+
<p class="text-muted" style="margin-top: 24px; font-size: 12px;">
|
|
784
|
+
If the button doesn't work, copy and paste this URL into your browser:<br>
|
|
785
|
+
<a href="{{url}}">{{url}}</a>
|
|
786
|
+
</p>
|
|
787
|
+
`,
|
|
788
|
+
text: `Sign in to {{brandName}}
|
|
789
|
+
|
|
790
|
+
Click this link to sign in:
|
|
791
|
+
{{url}}
|
|
792
|
+
|
|
793
|
+
This link expires in {{expiresInMinutes}} minutes.
|
|
794
|
+
|
|
795
|
+
If you didn't request this link, you can safely ignore this email.`
|
|
796
|
+
};
|
|
797
|
+
function renderMagicLinkEmail(data) {
|
|
798
|
+
const templateData = {
|
|
799
|
+
...data,
|
|
800
|
+
brandName: data.brandName ?? "Pars",
|
|
801
|
+
expiresInMinutes: data.expiresInMinutes ?? 15
|
|
802
|
+
};
|
|
803
|
+
return {
|
|
804
|
+
subject: renderTemplate(magicLinkTemplate.subject, templateData),
|
|
805
|
+
html: wrapEmailHtml(renderTemplate(magicLinkTemplate.html, templateData), {
|
|
806
|
+
brandName: templateData.brandName,
|
|
807
|
+
brandColor: data.brandColor
|
|
808
|
+
}),
|
|
809
|
+
text: renderTemplate(magicLinkTemplate.text ?? "", templateData)
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
var verificationTemplate = {
|
|
813
|
+
name: "verification",
|
|
814
|
+
subject: "Verify your email address",
|
|
815
|
+
html: `
|
|
816
|
+
<h1>Verify your email</h1>
|
|
817
|
+
<p>Hi{{#name}} {{name}}{{/name}},</p>
|
|
818
|
+
<p>Please verify your email address by clicking the button below:</p>
|
|
819
|
+
<div class="text-center">
|
|
820
|
+
<a href="{{url}}" class="button">Verify Email</a>
|
|
821
|
+
</div>
|
|
822
|
+
<p class="text-muted">This link expires in {{expiresInHours}} hours.</p>
|
|
823
|
+
<p class="text-muted" style="margin-top: 24px; font-size: 12px;">
|
|
824
|
+
If the button doesn't work, copy and paste this URL into your browser:<br>
|
|
825
|
+
<a href="{{url}}">{{url}}</a>
|
|
826
|
+
</p>
|
|
827
|
+
`,
|
|
828
|
+
text: `Verify your email address
|
|
829
|
+
|
|
830
|
+
Hi{{#name}} {{name}}{{/name}},
|
|
831
|
+
|
|
832
|
+
Please verify your email address by clicking this link:
|
|
833
|
+
{{url}}
|
|
834
|
+
|
|
835
|
+
This link expires in {{expiresInHours}} hours.`
|
|
836
|
+
};
|
|
837
|
+
function renderVerificationEmail(data) {
|
|
838
|
+
const templateData = {
|
|
839
|
+
...data,
|
|
840
|
+
expiresInHours: data.expiresInHours ?? 24
|
|
841
|
+
};
|
|
842
|
+
let html = verificationTemplate.html.replace(/\{\{#name\}\}(.*?)\{\{\/name\}\}/gs, data.name ? "$1" : "");
|
|
843
|
+
let text = (verificationTemplate.text ?? "").replace(/\{\{#name\}\}(.*?)\{\{\/name\}\}/gs, data.name ? "$1" : "");
|
|
844
|
+
html = renderTemplate(html, templateData);
|
|
845
|
+
text = renderTemplate(text, templateData);
|
|
846
|
+
return {
|
|
847
|
+
subject: renderTemplate(verificationTemplate.subject, templateData),
|
|
848
|
+
html: wrapEmailHtml(html, {
|
|
849
|
+
brandName: data.brandName,
|
|
850
|
+
brandColor: data.brandColor
|
|
851
|
+
}),
|
|
852
|
+
text
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
var welcomeTemplate = {
|
|
856
|
+
name: "welcome",
|
|
857
|
+
subject: "Welcome to {{brandName}}!",
|
|
858
|
+
html: `
|
|
859
|
+
<h1>Welcome to {{brandName}}!</h1>
|
|
860
|
+
<p>Hi{{#name}} {{name}}{{/name}},</p>
|
|
861
|
+
<p>Thank you for joining us. We're excited to have you on board!</p>
|
|
862
|
+
<p>Your account is now ready to use.</p>
|
|
863
|
+
{{#loginUrl}}
|
|
864
|
+
<div class="text-center">
|
|
865
|
+
<a href="{{loginUrl}}" class="button">Go to Dashboard</a>
|
|
866
|
+
</div>
|
|
867
|
+
{{/loginUrl}}
|
|
868
|
+
<p>If you have any questions, feel free to reach out to our support team.</p>
|
|
869
|
+
<p>Best regards,<br>The {{brandName}} Team</p>
|
|
870
|
+
`,
|
|
871
|
+
text: `Welcome to {{brandName}}!
|
|
872
|
+
|
|
873
|
+
Hi{{#name}} {{name}}{{/name}},
|
|
874
|
+
|
|
875
|
+
Thank you for joining us. We're excited to have you on board!
|
|
876
|
+
|
|
877
|
+
Your account is now ready to use.
|
|
878
|
+
|
|
879
|
+
{{#loginUrl}}Go to your dashboard: {{loginUrl}}{{/loginUrl}}
|
|
880
|
+
|
|
881
|
+
If you have any questions, feel free to reach out to our support team.
|
|
882
|
+
|
|
883
|
+
Best regards,
|
|
884
|
+
The {{brandName}} Team`
|
|
885
|
+
};
|
|
886
|
+
function renderWelcomeEmail(data) {
|
|
887
|
+
const brandName = data.brandName ?? "Pars";
|
|
888
|
+
const templateData = { ...data, brandName };
|
|
889
|
+
let html = welcomeTemplate.html.replace(/\{\{#name\}\}(.*?)\{\{\/name\}\}/gs, data.name ? "$1" : "").replace(/\{\{#loginUrl\}\}([\s\S]*?)\{\{\/loginUrl\}\}/g, data.loginUrl ? "$1" : "");
|
|
890
|
+
let text = (welcomeTemplate.text ?? "").replace(/\{\{#name\}\}(.*?)\{\{\/name\}\}/gs, data.name ? "$1" : "").replace(/\{\{#loginUrl\}\}([\s\S]*?)\{\{\/loginUrl\}\}/g, data.loginUrl ? "$1" : "");
|
|
891
|
+
html = renderTemplate(html, templateData);
|
|
892
|
+
text = renderTemplate(text, templateData);
|
|
893
|
+
return {
|
|
894
|
+
subject: renderTemplate(welcomeTemplate.subject, templateData),
|
|
895
|
+
html: wrapEmailHtml(html, {
|
|
896
|
+
brandName,
|
|
897
|
+
brandColor: data.brandColor
|
|
898
|
+
}),
|
|
899
|
+
text
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
var passwordResetTemplate = {
|
|
903
|
+
name: "password-reset",
|
|
904
|
+
subject: "Reset your password",
|
|
905
|
+
html: `
|
|
906
|
+
<h1>Reset your password</h1>
|
|
907
|
+
<p>We received a request to reset your password. Click the button below to choose a new password:</p>
|
|
908
|
+
<div class="text-center">
|
|
909
|
+
<a href="{{url}}" class="button">Reset Password</a>
|
|
910
|
+
</div>
|
|
911
|
+
<p class="text-muted">This link expires in {{expiresInMinutes}} minutes.</p>
|
|
912
|
+
<p class="text-muted">If you didn't request a password reset, you can safely ignore this email. Your password will remain unchanged.</p>
|
|
913
|
+
<p class="text-muted" style="margin-top: 24px; font-size: 12px;">
|
|
914
|
+
If the button doesn't work, copy and paste this URL into your browser:<br>
|
|
915
|
+
<a href="{{url}}">{{url}}</a>
|
|
916
|
+
</p>
|
|
917
|
+
`,
|
|
918
|
+
text: `Reset your password
|
|
919
|
+
|
|
920
|
+
We received a request to reset your password. Click this link to choose a new password:
|
|
921
|
+
{{url}}
|
|
922
|
+
|
|
923
|
+
This link expires in {{expiresInMinutes}} minutes.
|
|
924
|
+
|
|
925
|
+
If you didn't request a password reset, you can safely ignore this email. Your password will remain unchanged.`
|
|
926
|
+
};
|
|
927
|
+
function renderPasswordResetEmail(data) {
|
|
928
|
+
const templateData = {
|
|
929
|
+
...data,
|
|
930
|
+
expiresInMinutes: data.expiresInMinutes ?? 60
|
|
931
|
+
};
|
|
932
|
+
return {
|
|
933
|
+
subject: renderTemplate(passwordResetTemplate.subject, templateData),
|
|
934
|
+
html: wrapEmailHtml(renderTemplate(passwordResetTemplate.html, templateData), {
|
|
935
|
+
brandName: data.brandName,
|
|
936
|
+
brandColor: data.brandColor
|
|
937
|
+
}),
|
|
938
|
+
text: renderTemplate(passwordResetTemplate.text ?? "", templateData)
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
var invitationTemplate = {
|
|
942
|
+
name: "invitation",
|
|
943
|
+
subject: "{{#inviterName}}{{inviterName}} invited you to join {{/inviterName}}{{organizationName}}",
|
|
944
|
+
html: `
|
|
945
|
+
<h1>You're invited!</h1>
|
|
946
|
+
{{#inviterName}}
|
|
947
|
+
<p><strong>{{inviterName}}</strong> has invited you to join <strong>{{organizationName}}</strong>{{#role}} as a {{role}}{{/role}}.</p>
|
|
948
|
+
{{/inviterName}}
|
|
949
|
+
{{^inviterName}}
|
|
950
|
+
<p>You've been invited to join <strong>{{organizationName}}</strong>{{#role}} as a {{role}}{{/role}}.</p>
|
|
951
|
+
{{/inviterName}}
|
|
952
|
+
<div class="text-center">
|
|
953
|
+
<a href="{{url}}" class="button">Accept Invitation</a>
|
|
954
|
+
</div>
|
|
955
|
+
<p class="text-muted">This invitation expires in {{expiresInDays}} days.</p>
|
|
956
|
+
<p class="text-muted" style="margin-top: 24px; font-size: 12px;">
|
|
957
|
+
If the button doesn't work, copy and paste this URL into your browser:<br>
|
|
958
|
+
<a href="{{url}}">{{url}}</a>
|
|
959
|
+
</p>
|
|
960
|
+
`,
|
|
961
|
+
text: `You're invited to join {{organizationName}}!
|
|
962
|
+
|
|
963
|
+
{{#inviterName}}{{inviterName}} has invited you to join {{organizationName}}{{#role}} as a {{role}}{{/role}}.{{/inviterName}}
|
|
964
|
+
{{^inviterName}}You've been invited to join {{organizationName}}{{#role}} as a {{role}}{{/role}}.{{/inviterName}}
|
|
965
|
+
|
|
966
|
+
Accept the invitation:
|
|
967
|
+
{{url}}
|
|
968
|
+
|
|
969
|
+
This invitation expires in {{expiresInDays}} days.`
|
|
970
|
+
};
|
|
971
|
+
function renderInvitationEmail(data) {
|
|
972
|
+
const templateData = {
|
|
973
|
+
...data,
|
|
974
|
+
organizationName: data.organizationName ?? "the team",
|
|
975
|
+
expiresInDays: data.expiresInDays ?? 7
|
|
976
|
+
};
|
|
977
|
+
let html = invitationTemplate.html.replace(/\{\{#inviterName\}\}([\s\S]*?)\{\{\/inviterName\}\}/g, data.inviterName ? "$1" : "").replace(/\{\{\^inviterName\}\}([\s\S]*?)\{\{\/inviterName\}\}/g, data.inviterName ? "" : "$1").replace(/\{\{#role\}\}([\s\S]*?)\{\{\/role\}\}/g, data.role ? "$1" : "");
|
|
978
|
+
let text = (invitationTemplate.text ?? "").replace(/\{\{#inviterName\}\}([\s\S]*?)\{\{\/inviterName\}\}/g, data.inviterName ? "$1" : "").replace(/\{\{\^inviterName\}\}([\s\S]*?)\{\{\/inviterName\}\}/g, data.inviterName ? "" : "$1").replace(/\{\{#role\}\}([\s\S]*?)\{\{\/role\}\}/g, data.role ? "$1" : "");
|
|
979
|
+
let subject = invitationTemplate.subject.replace(/\{\{#inviterName\}\}([\s\S]*?)\{\{\/inviterName\}\}/g, data.inviterName ? "$1" : "You're invited to join ");
|
|
980
|
+
html = renderTemplate(html, templateData);
|
|
981
|
+
text = renderTemplate(text, templateData);
|
|
982
|
+
subject = renderTemplate(subject, templateData);
|
|
983
|
+
return {
|
|
984
|
+
subject,
|
|
985
|
+
html: wrapEmailHtml(html, {
|
|
986
|
+
brandName: data.brandName,
|
|
987
|
+
brandColor: data.brandColor
|
|
988
|
+
}),
|
|
989
|
+
text
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
var templates = {
|
|
993
|
+
otp: otpTemplate,
|
|
994
|
+
magicLink: magicLinkTemplate,
|
|
995
|
+
verification: verificationTemplate,
|
|
996
|
+
welcome: welcomeTemplate,
|
|
997
|
+
passwordReset: passwordResetTemplate,
|
|
998
|
+
invitation: invitationTemplate
|
|
999
|
+
};
|
|
1000
|
+
var renderFunctions = {
|
|
1001
|
+
otp: renderOTPEmail,
|
|
1002
|
+
magicLink: renderMagicLinkEmail,
|
|
1003
|
+
verification: renderVerificationEmail,
|
|
1004
|
+
welcome: renderWelcomeEmail,
|
|
1005
|
+
passwordReset: renderPasswordResetEmail,
|
|
1006
|
+
invitation: renderInvitationEmail
|
|
1007
|
+
};
|
|
1008
|
+
|
|
1009
|
+
// src/index.ts
|
|
1010
|
+
var EmailService = class {
|
|
1011
|
+
provider;
|
|
1012
|
+
debug;
|
|
1013
|
+
constructor(config) {
|
|
1014
|
+
this.debug = config.debug ?? false;
|
|
1015
|
+
this.provider = this.createProvider(config);
|
|
1016
|
+
}
|
|
1017
|
+
createProvider(config) {
|
|
1018
|
+
const providerConfig = {
|
|
1019
|
+
apiKey: config.apiKey,
|
|
1020
|
+
fromEmail: config.fromEmail,
|
|
1021
|
+
fromName: config.fromName,
|
|
1022
|
+
options: config.providerOptions
|
|
1023
|
+
};
|
|
1024
|
+
switch (config.provider) {
|
|
1025
|
+
case "resend":
|
|
1026
|
+
return new ResendProvider(providerConfig);
|
|
1027
|
+
case "sendgrid":
|
|
1028
|
+
return new SendGridProvider(providerConfig);
|
|
1029
|
+
case "postmark":
|
|
1030
|
+
return new PostmarkProvider(providerConfig);
|
|
1031
|
+
case "console":
|
|
1032
|
+
return new ConsoleProvider(providerConfig);
|
|
1033
|
+
default:
|
|
1034
|
+
throw new EmailError(
|
|
1035
|
+
`Unknown email provider: ${config.provider}`,
|
|
1036
|
+
EmailErrorCodes.INVALID_CONFIG
|
|
1037
|
+
);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
/**
|
|
1041
|
+
* Get the provider type
|
|
1042
|
+
*/
|
|
1043
|
+
get providerType() {
|
|
1044
|
+
return this.provider.type;
|
|
1045
|
+
}
|
|
1046
|
+
/**
|
|
1047
|
+
* Send a single email
|
|
1048
|
+
*/
|
|
1049
|
+
async send(options) {
|
|
1050
|
+
if (this.debug) {
|
|
1051
|
+
console.log("[Email] Sending email:", {
|
|
1052
|
+
to: options.to,
|
|
1053
|
+
subject: options.subject,
|
|
1054
|
+
provider: this.provider.type
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1057
|
+
const result = await this.provider.send(options);
|
|
1058
|
+
if (this.debug) {
|
|
1059
|
+
console.log("[Email] Result:", result);
|
|
1060
|
+
}
|
|
1061
|
+
return result;
|
|
1062
|
+
}
|
|
1063
|
+
/**
|
|
1064
|
+
* Send multiple emails
|
|
1065
|
+
*/
|
|
1066
|
+
async sendBatch(options) {
|
|
1067
|
+
if (this.debug) {
|
|
1068
|
+
console.log("[Email] Sending batch:", {
|
|
1069
|
+
count: options.emails.length,
|
|
1070
|
+
provider: this.provider.type
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
if (this.provider.sendBatch) {
|
|
1074
|
+
const result = await this.provider.sendBatch(options);
|
|
1075
|
+
if (this.debug) {
|
|
1076
|
+
console.log("[Email] Batch result:", {
|
|
1077
|
+
total: result.total,
|
|
1078
|
+
successful: result.successful,
|
|
1079
|
+
failed: result.failed
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
return result;
|
|
1083
|
+
}
|
|
1084
|
+
const results = [];
|
|
1085
|
+
let successful = 0;
|
|
1086
|
+
let failed = 0;
|
|
1087
|
+
for (const email of options.emails) {
|
|
1088
|
+
try {
|
|
1089
|
+
const result = await this.send(email);
|
|
1090
|
+
results.push(result);
|
|
1091
|
+
if (result.success) {
|
|
1092
|
+
successful++;
|
|
1093
|
+
} else {
|
|
1094
|
+
failed++;
|
|
1095
|
+
if (options.stopOnError) break;
|
|
1096
|
+
}
|
|
1097
|
+
} catch (err) {
|
|
1098
|
+
failed++;
|
|
1099
|
+
results.push({
|
|
1100
|
+
success: false,
|
|
1101
|
+
error: err instanceof Error ? err.message : "Unknown error"
|
|
1102
|
+
});
|
|
1103
|
+
if (options.stopOnError) break;
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
return {
|
|
1107
|
+
total: options.emails.length,
|
|
1108
|
+
successful,
|
|
1109
|
+
failed,
|
|
1110
|
+
results
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
/**
|
|
1114
|
+
* Verify provider configuration
|
|
1115
|
+
*/
|
|
1116
|
+
async verify() {
|
|
1117
|
+
if (this.provider.verify) {
|
|
1118
|
+
return this.provider.verify();
|
|
1119
|
+
}
|
|
1120
|
+
return true;
|
|
1121
|
+
}
|
|
1122
|
+
};
|
|
1123
|
+
function createEmailService(config) {
|
|
1124
|
+
return new EmailService(config);
|
|
1125
|
+
}
|
|
1126
|
+
function createEmailProvider(type2, config) {
|
|
1127
|
+
switch (type2) {
|
|
1128
|
+
case "resend":
|
|
1129
|
+
return new ResendProvider(config);
|
|
1130
|
+
case "sendgrid":
|
|
1131
|
+
return new SendGridProvider(config);
|
|
1132
|
+
case "postmark":
|
|
1133
|
+
return new PostmarkProvider(config);
|
|
1134
|
+
case "console":
|
|
1135
|
+
return new ConsoleProvider(config);
|
|
1136
|
+
default:
|
|
1137
|
+
throw new EmailError(
|
|
1138
|
+
`Unknown email provider: ${type2}`,
|
|
1139
|
+
EmailErrorCodes.INVALID_CONFIG
|
|
1140
|
+
);
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
var index_default = {
|
|
1144
|
+
EmailService,
|
|
1145
|
+
createEmailService,
|
|
1146
|
+
createEmailProvider
|
|
1147
|
+
};
|
|
1148
|
+
export {
|
|
1149
|
+
ConsoleProvider,
|
|
1150
|
+
EmailError,
|
|
1151
|
+
EmailErrorCodes,
|
|
1152
|
+
EmailService,
|
|
1153
|
+
PostmarkProvider,
|
|
1154
|
+
ResendProvider,
|
|
1155
|
+
SendGridProvider,
|
|
1156
|
+
createConsoleProvider,
|
|
1157
|
+
createEmailProvider,
|
|
1158
|
+
createEmailService,
|
|
1159
|
+
createPostmarkProvider,
|
|
1160
|
+
createResendProvider,
|
|
1161
|
+
createSendGridProvider,
|
|
1162
|
+
index_default as default,
|
|
1163
|
+
emailAddress,
|
|
1164
|
+
emailAttachment,
|
|
1165
|
+
emailConfig,
|
|
1166
|
+
emailSendResult,
|
|
1167
|
+
invitationTemplate,
|
|
1168
|
+
magicLinkTemplate,
|
|
1169
|
+
otpTemplate,
|
|
1170
|
+
passwordResetTemplate,
|
|
1171
|
+
postmarkConfig,
|
|
1172
|
+
renderFunctions,
|
|
1173
|
+
renderInvitationEmail,
|
|
1174
|
+
renderMagicLinkEmail,
|
|
1175
|
+
renderOTPEmail,
|
|
1176
|
+
renderPasswordResetEmail,
|
|
1177
|
+
renderTemplate,
|
|
1178
|
+
renderVerificationEmail,
|
|
1179
|
+
renderWelcomeEmail,
|
|
1180
|
+
resendConfig,
|
|
1181
|
+
sendEmailOptions,
|
|
1182
|
+
sendTemplateEmailOptions,
|
|
1183
|
+
sendgridConfig,
|
|
1184
|
+
sesConfig,
|
|
1185
|
+
smtpConfig,
|
|
1186
|
+
templates,
|
|
1187
|
+
type,
|
|
1188
|
+
verificationTemplate,
|
|
1189
|
+
welcomeTemplate,
|
|
1190
|
+
wrapEmailHtml
|
|
1191
|
+
};
|
|
1192
|
+
//# sourceMappingURL=index.js.map
|