@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/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 ?? `&copy; ${(/* @__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