@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.
@@ -0,0 +1,621 @@
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
+ export {
612
+ ConsoleProvider,
613
+ PostmarkProvider,
614
+ ResendProvider,
615
+ SendGridProvider,
616
+ createConsoleProvider,
617
+ createPostmarkProvider,
618
+ createResendProvider,
619
+ createSendGridProvider
620
+ };
621
+ //# sourceMappingURL=index.js.map