@kumix/email 0.1.0 → 0.1.2

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 CHANGED
@@ -170,6 +170,10 @@ await email.sendEmail({
170
170
  });
171
171
  ```
172
172
 
173
+ > `nodemailer` is loaded via a dynamic `import()` and is a Node-oriented package. In Deno, install
174
+ > it through an `npm:` specifier (`deno run --allow-env --allow-net ...` with
175
+ > `npm:nodemailer@^9` resolvable). Bun and Node.js resolve it from `node_modules` automatically.
176
+
173
177
  ### Browser
174
178
 
175
179
  Use manual config — no env vars. Resend only, since Nodemailer requires Node.js APIs.
@@ -286,6 +290,33 @@ await email.sendEmail({
286
290
  });
287
291
  ```
288
292
 
293
+ ### Priority & Scheduled Delivery
294
+
295
+ Both providers support a numeric `priority` (1 = highest … 5 = lowest). It is forwarded to
296
+ Nodemailer as the `priority` field plus an `X-Priority` header, and to Resend as an `X-Priority`
297
+ header.
298
+
299
+ ```typescript
300
+ await email.sendEmail({
301
+ to: "oncall@example.com",
302
+ subject: "Incident",
303
+ html: "<p>High priority</p>",
304
+ priority: 1, // 1..5
305
+ });
306
+ ```
307
+
308
+ Resend also supports deferred delivery via `scheduledAt` (an ISO 8601 timestamp). It is forwarded
309
+ as Resend's `scheduled_at` field. Nodemailer ignores this option.
310
+
311
+ ```typescript
312
+ await email.sendEmail({
313
+ to: "user@example.com",
314
+ subject: "Scheduled",
315
+ html: "<p>Sent later</p>",
316
+ scheduledAt: new Date("2030-01-01T09:00:00Z"),
317
+ });
318
+ ```
319
+
289
320
  ## Runtime Compatibility
290
321
 
291
322
  | Feature | Node.js | Bun | CF Workers | Deno | Browser |
@@ -335,13 +366,13 @@ await email.sendEmail({
335
366
 
336
367
  - `renderEmailTemplate(Component, props)` — Render React component to HTML string
337
368
  - `htmlToText(html)` — Convert HTML to plain text
338
- - `isValidEmail(email)` — Validate email format (RFC 5322)
369
+ - `isValidEmail(email)` — Validate common email format (permissive sanity check, not full RFC 5322)
339
370
  - `validateEmails(emails)` — Validate single or multiple emails
340
371
  - `filterValidEmails(emails)` — Filter invalid emails from a list
341
- - `formatEmailAddress(name, email)` — Format as `Name <email>`
372
+ - `formatEmailAddress(name, email)` — Format as `"Name" <email>` (display name is quoted/escaped per RFC 5322; CR/LF stripped to prevent header injection)
342
373
  - `extractEmail(formatted)` — Extract email from formatted address
343
374
  - `extractDisplayName(formatted)` — Extract display name from formatted address
344
- - `sanitizeHtml(html)` — Strip dangerous HTML (scripts, event handlers)
375
+ - `sanitizeHtml(html)` — Coarse cleanup that strips `<script>`/`<style>`/`<iframe>`/`<object>`/`<embed>`/`<form>`/`<svg>` blocks, `on*` event handlers, and `javascript:` URLs. ⚠️ **NOT a security sanitizer** — for untrusted input use a purpose-built sanitizer (e.g. DOMPurify)
345
376
  - `truncateText(text, maxLength, ellipsis?)` — Truncate with ellipsis
346
377
  - `generatePreviewText(html, maxLength?)` — Generate email preview text
347
378
  - `generateUnsubscribeLink(baseUrl, email, token?)` — Create unsubscribe URL
@@ -357,14 +388,21 @@ await email.sendEmail({
357
388
 
358
389
  ```typescript
359
390
  import type {
391
+ // Config shapes
360
392
  EmailConfig,
361
393
  ResendConfig,
362
394
  NodemailerConfig,
363
- SendEmailOptions,
364
- EmailResult,
395
+ BaseEmailConfig,
365
396
  EmailProvider,
366
397
  EnvRecord,
367
- // ... and more
398
+ // Sending
399
+ SendEmailOptions,
400
+ EmailAttachment,
401
+ EmailResult,
402
+ EmailValidationResult,
403
+ IEmailProvider,
404
+ ConfigValidationResult,
405
+ EmailTemplateData,
368
406
  } from "@kumix/email";
369
407
  ```
370
408
 
@@ -379,8 +417,12 @@ import type {
379
417
  | `KUMIX_EMAIL_SMTP_HOST` | Nodemailer | Yes |
380
418
  | `KUMIX_EMAIL_SMTP_PORT` | Nodemailer | Yes |
381
419
  | `KUMIX_EMAIL_SMTP_SECURE` | Nodemailer | No |
382
- | `KUMIX_EMAIL_SMTP_USER` | Nodemailer | Yes |
383
- | `KUMIX_EMAIL_SMTP_PASS` | Nodemailer | Yes |
420
+
421
+ `KUMIX_EMAIL_SMTP_SECURE` enables TLS when set to `true`, `1`, or `yes` (case-insensitive); any
422
+ other value (or unset) leaves the connection unsecured. Defaults to off — set it explicitly for
423
+ port 465 (implicit TLS).
424
+ | `KUMIX_EMAIL_SMTP_USER` | Nodemailer | Yes |
425
+ | `KUMIX_EMAIL_SMTP_PASS` | Nodemailer | Yes |
384
426
 
385
427
  Legacy env vars (`RESEND_API_KEY`, `SMTP_HOST`, etc.) are also supported for backward compatibility.
386
428
 
package/dist/helpers.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import React from "react";
1
+ import { ComponentType } from "react";
2
2
 
3
3
  //#region src/helpers.d.ts
4
4
  /**
@@ -17,12 +17,13 @@ import React from "react";
17
17
  * console.log(html); // "<h1>Welcome John!</h1>"
18
18
  * ```
19
19
  */
20
- declare function renderEmailTemplate<T extends Record<string, unknown>>(Template: React.ComponentType<T>, props: T): Promise<string>;
20
+ declare function renderEmailTemplate<T extends Record<string, unknown>>(Template: ComponentType<T>, props: T): Promise<string>;
21
21
  /**
22
22
  * Generate plain text from HTML
23
23
  *
24
- * Converts HTML content to plain text by removing tags and normalizing whitespace.
25
- * Useful for creating text versions of HTML emails.
24
+ * Converts HTML content to plain text by removing tags and normalizing
25
+ * whitespace. Block-level boundaries (`<br>`, `</p>`, headings) are preserved
26
+ * as newlines in the output.
26
27
  *
27
28
  * @param html - HTML string to convert
28
29
  * @returns Plain text version of the HTML
@@ -30,7 +31,7 @@ declare function renderEmailTemplate<T extends Record<string, unknown>>(Template
30
31
  * ```typescript
31
32
  * const html = '<h1>Hello</h1><p>Welcome to our <strong>platform</strong>!</p>';
32
33
  * const text = htmlToText(html);
33
- * console.log(text); // "Hello Welcome to our platform!"
34
+ * // "Hello\n\nWelcome to our platform!"
34
35
  * ```
35
36
  */
36
37
  declare function htmlToText(html: string): string;
@@ -79,10 +80,20 @@ declare function validateEmails(emails: string | string[]): boolean;
79
80
  * ```
80
81
  */
81
82
  declare function filterValidEmails(emails: string[]): string[];
83
+ /**
84
+ * Strip CR/LF from an email header value to prevent SMTP header injection.
85
+ *
86
+ * @param value - Raw header/recipient value
87
+ * @returns The value with all `\r` and `\n` removed
88
+ */
89
+ declare function stripCrlf(value: string): string;
82
90
  /**
83
91
  * Format email address with display name
84
92
  *
85
- * Combines a display name with an email address in the standard format.
93
+ * Combines a display name with an email address in the standard `"Name" <email>`
94
+ * format. The display name is quoted and any embedded quotes/backslashes are
95
+ * escaped; CR/LF characters are stripped from both parts to prevent SMTP header
96
+ * injection.
86
97
  *
87
98
  * @param name - Display name for the email address
88
99
  * @param email - Email address
@@ -129,13 +140,18 @@ declare function extractEmail(formattedAddress: string): string;
129
140
  */
130
141
  declare function extractDisplayName(formattedAddress: string): string;
131
142
  /**
132
- * Sanitize HTML content for email
133
- *
134
- * Removes potentially dangerous HTML elements and attributes while preserving
135
- * email-safe formatting.
136
- *
137
- * @param html - HTML content to sanitize
138
- * @returns Sanitized HTML safe for email
143
+ * Strip some unsafe constructs from HTML (basic, NOT a security boundary).
144
+ *
145
+ * ⚠️ This function is a coarse cleanup pass for trusted-or-low-risk email
146
+ * content. It is **not** a sanitizer. It removes `<script>`/`<style>`/
147
+ * `<iframe>`/`<object>`/`<embed>`/`<form>` blocks, on* event handlers (with
148
+ * and without quotes), and obvious `javascript:` URLs. It will NOT defeat a
149
+ * motivated attacker HTML entity-encoding, unquoted attributes, SVG/CS
150
+ * injection, and many other bypasses exist. For content from untrusted
151
+ * sources, use a purpose-built sanitizer (e.g. DOMPurify) before rendering.
152
+ *
153
+ * @param html - HTML content to clean up
154
+ * @returns HTML with the dangerous constructs above removed
139
155
  * @example
140
156
  * ```typescript
141
157
  * const unsafeHtml = '<script>alert("xss")</script><p>Safe content</p>';
@@ -321,5 +337,5 @@ declare function getMimeType(filename: string): string;
321
337
  */
322
338
  declare function formatFileSize(bytes: number, decimals?: number): string;
323
339
  //#endregion
324
- export { addUtmParams, chunkEmails, deduplicateEmails, extractDisplayName, extractEmail, filterValidEmails, formatEmailAddress, formatFileSize, generatePreviewText, generateTrackingPixel, generateUnsubscribeLink, getMimeType, htmlToText, isValidEmail, parseEmailList, renderEmailTemplate, sanitizeHtml, truncateText, validateEmails };
340
+ export { addUtmParams, chunkEmails, deduplicateEmails, extractDisplayName, extractEmail, filterValidEmails, formatEmailAddress, formatFileSize, generatePreviewText, generateTrackingPixel, generateUnsubscribeLink, getMimeType, htmlToText, isValidEmail, parseEmailList, renderEmailTemplate, sanitizeHtml, stripCrlf, truncateText, validateEmails };
325
341
  //# sourceMappingURL=helpers.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"helpers.d.ts","names":[],"sources":["../src/helpers.ts"],"mappings":";;;;;;AAsEuC;AA6BvC;;;;AAA0C;AAoB1C;;;;AAAwD;AAmBxD;;iBA1FsB,mBAAA,WAA8B,MAAA,mBAClD,QAAA,EAAU,KAAA,CAAM,aAAA,CAAc,CAAA,GAC9B,KAAA,EAAO,CAAA,GACN,OAAA;;AAuF+C;AAsBlD;;;;AAA8D;AAqB9D;;;;AAAqD;AAqBrD;;;iBApIgB,UAAA,CAAW,IAAY;AAoIoB;AAwB3D;;;;AAAyC;AA8BzC;;;;;;;;AAtD2D,iBAvG3C,YAAA,CAAa,KAAa;AAiL1C;;;;AAAyE;AA4BzE;;;;;;;;AAAsF;AA5BtF,iBA7JgB,cAAA,CAAe,MAAyB;;;;;;;;AAwNnC;AA2BrB;;;;;;iBAhOgB,iBAAA,CAAkB,MAAgB;;;;;;AAwO/C;AA8BH;;;;AAAkD;AAqBlD;;;iBArQgB,kBAAA,CAAmB,IAAA,UAAc,KAAa;AAqQZ;AA6BlD;;;;AAAqE;AA0BrE;;;;AAA4C;AAwD5C;;;;AAAkE;AA/GhB,iBAhPlC,YAAA,CAAa,gBAAwB;;;;;;;;;;;;;;;;;iBAqBrC,kBAAA,CAAmB,gBAAwB;;;;;;;;;;;;;;;;iBAwB3C,YAAA,CAAa,IAAY;;;;;;;;;;;;;;;;;iBA8BzB,YAAA,CAAa,IAAA,UAAc,SAAA,UAAmB,QAAA;;;;;;;;;;;;;;;;iBAoB9C,mBAAA,CAAoB,IAAA,UAAc,SAAuB;;;;;;;;;;;;;;;;;;;;iBA4BzD,uBAAA,CAAwB,OAAA,UAAiB,KAAA,UAAe,KAAA;;;;;;;;;;;;;;;;;;;;iBA4BxD,qBAAA,CACd,OAAA,UACA,OAAA,UACA,WAAA;;;;;;;;;;;;;;;;;;;iBA2Bc,YAAA,CACd,GAAA,UACA,SAAA;EACE,MAAA;EACA,MAAA;EACA,QAAA;EACA,IAAA;EACA,OAAA;AAAA;;;;;;;;;;;;;;iBA+BY,cAAA,CAAe,WAAmB;;;;;;;;;;;;;;;iBAqBlC,iBAAA,CAAkB,MAAgB;;;;;;;;;;;;;;;;;;iBA6BlC,WAAA,CAAY,MAAA,YAAkB,SAAuB;;;;;;;;;;;;;;;iBA0BrD,WAAA,CAAY,QAAgB;;;;;;;;;;;;;;;;iBAwD5B,cAAA,CAAe,KAAA,UAAe,QAAoB"}
1
+ {"version":3,"file":"helpers.d.ts","names":[],"sources":["../src/helpers.ts"],"mappings":";;;;;AA4EuC;AAsCvC;;;;AAA0C;AAoB1C;;;;AAAwD;AAmBxD;;;iBA1GsB,mBAAA,WAA8B,MAAA,mBAClD,QAAA,EAAU,aAAA,CAAc,CAAA,GACxB,KAAA,EAAO,CAAA,GACN,OAAA;AAuG+C;AAclD;;;;AAAuC;AAqBvC;;;;AAA8D;AA4B9D;;;;AAAqD;AA/DH,iBA7ElC,UAAA,CAAW,IAAY;;;;AAiKoB;AA6B3D;;;;AAAyC;AAyCzC;;;;;iBAjMgB,YAAA,CAAa,KAAa;;;AAiM4C;AA2BtF;;;;AAAyE;AA4BzE;;;;;;iBApOgB,cAAA,CAAe,MAAyB;;AAoO8B;AA4BtF;;;;;;;;AAGqB;AA2BrB;;;iBA3QgB,iBAAA,CAAkB,MAAgB;;;;;;;iBAclC,SAAA,CAAU,KAAa;;AAqQpC;AA8BH;;;;AAAkD;AAqBlD;;;;AAAkD;AA6BlD;;;;AAAqE;iBAhUrD,kBAAA,CAAmB,IAAA,UAAc,KAAa;;;;AA0VlB;AAwD5C;;;;AAAkE;;;;;;;;iBAtXlD,YAAA,CAAa,gBAAwB;;;;;;;;;;;;;;;;;iBAqBrC,kBAAA,CAAmB,gBAAwB;;;;;;;;;;;;;;;;;;;;;iBA6B3C,YAAA,CAAa,IAAY;;;;;;;;;;;;;;;;;iBAyCzB,YAAA,CAAa,IAAA,UAAc,SAAA,UAAmB,QAAA;;;;;;;;;;;;;;;;iBA2B9C,mBAAA,CAAoB,IAAA,UAAc,SAAuB;;;;;;;;;;;;;;;;;;;;iBA4BzD,uBAAA,CAAwB,OAAA,UAAiB,KAAA,UAAe,KAAA;;;;;;;;;;;;;;;;;;;;iBA4BxD,qBAAA,CACd,OAAA,UACA,OAAA,UACA,WAAA;;;;;;;;;;;;;;;;;;;iBA2Bc,YAAA,CACd,GAAA,UACA,SAAA;EACE,MAAA;EACA,MAAA;EACA,QAAA;EACA,IAAA;EACA,OAAA;AAAA;;;;;;;;;;;;;;iBA+BY,cAAA,CAAe,WAAmB;;;;;;;;;;;;;;;iBAqBlC,iBAAA,CAAkB,MAAgB;;;;;;;;;;;;;;;;;;iBA6BlC,WAAA,CAAY,MAAA,YAAkB,SAAuB;;;;;;;;;;;;;;;iBA0BrD,WAAA,CAAY,QAAgB;;;;;;;;;;;;;;;;iBAwD5B,cAAA,CAAe,KAAA,UAAe,QAAoB"}
package/dist/helpers.js CHANGED
@@ -1,31 +1,5 @@
1
- import React from "react";
2
- import { render } from "react-email";
3
1
  //#region src/helpers.ts
4
2
  /**
5
- * @fileoverview Email Helper Utilities
6
- *
7
- * This module provides utility functions for email operations including:
8
- * - Template rendering and text conversion
9
- * - Email address validation and formatting
10
- * - Content processing and sanitization
11
- * - Attachment handling
12
- * - Email list management
13
- *
14
- * Import from '@kumix/email/helpers' to access these utilities.
15
- *
16
- * @example
17
- * ```typescript
18
- * import {
19
- * renderEmailTemplate,
20
- * htmlToText,
21
- * isValidEmail,
22
- * formatEmailAddress,
23
- * sanitizeHtml,
24
- * generateUnsubscribeLink
25
- * } from '@kumix/email/helpers';
26
- * ```
27
- */
28
- /**
29
3
  * Render React component to HTML string
30
4
  *
31
5
  * Converts a React email template component to HTML string that can be sent via email.
@@ -42,13 +16,16 @@ import { render } from "react-email";
42
16
  * ```
43
17
  */
44
18
  async function renderEmailTemplate(Template, props) {
45
- return render(React.createElement(Template, props));
19
+ const { createElement } = await import("react");
20
+ const { render } = await import("react-email");
21
+ return render(createElement(Template, props));
46
22
  }
47
23
  /**
48
24
  * Generate plain text from HTML
49
25
  *
50
- * Converts HTML content to plain text by removing tags and normalizing whitespace.
51
- * Useful for creating text versions of HTML emails.
26
+ * Converts HTML content to plain text by removing tags and normalizing
27
+ * whitespace. Block-level boundaries (`<br>`, `</p>`, headings) are preserved
28
+ * as newlines in the output.
52
29
  *
53
30
  * @param html - HTML string to convert
54
31
  * @returns Plain text version of the HTML
@@ -56,11 +33,13 @@ async function renderEmailTemplate(Template, props) {
56
33
  * ```typescript
57
34
  * const html = '<h1>Hello</h1><p>Welcome to our <strong>platform</strong>!</p>';
58
35
  * const text = htmlToText(html);
59
- * console.log(text); // "Hello Welcome to our platform!"
36
+ * // "Hello\n\nWelcome to our platform!"
60
37
  * ```
61
38
  */
62
39
  function htmlToText(html) {
63
- return html.replace(/<br\s*\/?>/gi, "\n").replace(/<\/p>/gi, "\n\n").replace(/<\/h[1-6]>/gi, "\n\n").replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").replace(/\n\s+/g, "\n").trim();
40
+ const NEWLINE = "\0N\0";
41
+ const DOUBLE_NEWLINE = "\0D\0";
42
+ return html.replace(/<br\s*\/?>/gi, NEWLINE).replace(/<\/p>/gi, DOUBLE_NEWLINE).replace(/<\/h[1-6]>/gi, DOUBLE_NEWLINE).replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim().split(DOUBLE_NEWLINE).map((segment) => segment.split(NEWLINE).join("\n").trim()).filter((segment) => segment.length > 0).join("\n\n");
64
43
  }
65
44
  /**
66
45
  * Validate email address format
@@ -114,9 +93,21 @@ function filterValidEmails(emails) {
114
93
  return emails.filter((email) => isValidEmail(email.trim()));
115
94
  }
116
95
  /**
96
+ * Strip CR/LF from an email header value to prevent SMTP header injection.
97
+ *
98
+ * @param value - Raw header/recipient value
99
+ * @returns The value with all `\r` and `\n` removed
100
+ */
101
+ function stripCrlf(value) {
102
+ return value.replace(/[\r\n]/g, "");
103
+ }
104
+ /**
117
105
  * Format email address with display name
118
106
  *
119
- * Combines a display name with an email address in the standard format.
107
+ * Combines a display name with an email address in the standard `"Name" <email>`
108
+ * format. The display name is quoted and any embedded quotes/backslashes are
109
+ * escaped; CR/LF characters are stripped from both parts to prevent SMTP header
110
+ * injection.
120
111
  *
121
112
  * @param name - Display name for the email address
122
113
  * @param email - Email address
@@ -128,7 +119,9 @@ function filterValidEmails(emails) {
128
119
  * ```
129
120
  */
130
121
  function formatEmailAddress(name, email) {
131
- return `${name} <${email}>`;
122
+ const safeName = name.replace(/[\r\n]/g, "");
123
+ const safeEmail = email.replace(/[\r\n]/g, "");
124
+ return `"${safeName.replace(/\\|"/g, (ch) => `\\${ch}`)}" <${safeEmail}>`;
132
125
  }
133
126
  /**
134
127
  * Extract email address from formatted string
@@ -171,13 +164,18 @@ function extractDisplayName(formattedAddress) {
171
164
  return match ? match[1].trim().replace(/^["']|["']$/g, "") : "";
172
165
  }
173
166
  /**
174
- * Sanitize HTML content for email
175
- *
176
- * Removes potentially dangerous HTML elements and attributes while preserving
177
- * email-safe formatting.
178
- *
179
- * @param html - HTML content to sanitize
180
- * @returns Sanitized HTML safe for email
167
+ * Strip some unsafe constructs from HTML (basic, NOT a security boundary).
168
+ *
169
+ * ⚠️ This function is a coarse cleanup pass for trusted-or-low-risk email
170
+ * content. It is **not** a sanitizer. It removes `<script>`/`<style>`/
171
+ * `<iframe>`/`<object>`/`<embed>`/`<form>` blocks, on* event handlers (with
172
+ * and without quotes), and obvious `javascript:` URLs. It will NOT defeat a
173
+ * motivated attacker HTML entity-encoding, unquoted attributes, SVG/CS
174
+ * injection, and many other bypasses exist. For content from untrusted
175
+ * sources, use a purpose-built sanitizer (e.g. DOMPurify) before rendering.
176
+ *
177
+ * @param html - HTML content to clean up
178
+ * @returns HTML with the dangerous constructs above removed
181
179
  * @example
182
180
  * ```typescript
183
181
  * const unsafeHtml = '<script>alert("xss")<\/script><p>Safe content</p>';
@@ -186,10 +184,10 @@ function extractDisplayName(formattedAddress) {
186
184
  * ```
187
185
  */
188
186
  function sanitizeHtml(html) {
189
- let sanitized = html.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, "");
190
- sanitized = sanitized.replace(/\s*on\w+\s*=\s*["'][^"']*["']/gi, "");
191
- sanitized = sanitized.replace(/\s*javascript\s*:/gi, "");
192
- sanitized = sanitized.replace(/\s*style\s*=\s*["'][^"']*expression\([^"']*\)["']/gi, "");
187
+ let sanitized = html.replace(/<(script|style|iframe|object|embed|form|svg)\b[^<]*(?:(?!<\/\1>)<[^<]*)*<\/\1>/gi, "");
188
+ sanitized = sanitized.replace(/\s+on\w+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi, "");
189
+ sanitized = sanitized.replace(/(href|src|xlink:href|action|formaction)\s*=\s*["']?\s*javascript:/gi, "$1=\"\"");
190
+ sanitized = sanitized.replace(/\s*style\s*=\s*["'][^"']*expression\([^"']*\)[^"']*["']/gi, "");
193
191
  return sanitized;
194
192
  }
195
193
  /**
@@ -209,7 +207,9 @@ function sanitizeHtml(html) {
209
207
  * ```
210
208
  */
211
209
  function truncateText(text, maxLength, ellipsis = "...") {
210
+ if (maxLength <= 0) return "";
212
211
  if (text.length <= maxLength) return text;
212
+ if (maxLength <= ellipsis.length) return text.slice(0, maxLength);
213
213
  return text.slice(0, maxLength - ellipsis.length) + ellipsis;
214
214
  }
215
215
  /**
@@ -428,7 +428,9 @@ function getMimeType(filename) {
428
428
  * ```
429
429
  */
430
430
  function formatFileSize(bytes, decimals = 2) {
431
+ if (!Number.isFinite(bytes)) return "0 Bytes";
431
432
  if (bytes === 0) return "0 Bytes";
433
+ if (bytes < 0) return `-${formatFileSize(-bytes, decimals)}`;
432
434
  const k = 1024;
433
435
  const dm = decimals < 0 ? 0 : decimals;
434
436
  const sizes = [
@@ -438,10 +440,10 @@ function formatFileSize(bytes, decimals = 2) {
438
440
  "GB",
439
441
  "TB"
440
442
  ];
441
- const i = Math.floor(Math.log(bytes) / Math.log(k));
443
+ const i = Math.max(0, Math.min(Math.floor(Math.log(bytes) / Math.log(k)), sizes.length - 1));
442
444
  return `${(bytes / k ** i).toFixed(dm)} ${sizes[i]}`;
443
445
  }
444
446
  //#endregion
445
- export { addUtmParams, chunkEmails, deduplicateEmails, extractDisplayName, extractEmail, filterValidEmails, formatEmailAddress, formatFileSize, generatePreviewText, generateTrackingPixel, generateUnsubscribeLink, getMimeType, htmlToText, isValidEmail, parseEmailList, renderEmailTemplate, sanitizeHtml, truncateText, validateEmails };
447
+ export { addUtmParams, chunkEmails, deduplicateEmails, extractDisplayName, extractEmail, filterValidEmails, formatEmailAddress, formatFileSize, generatePreviewText, generateTrackingPixel, generateUnsubscribeLink, getMimeType, htmlToText, isValidEmail, parseEmailList, renderEmailTemplate, sanitizeHtml, stripCrlf, truncateText, validateEmails };
446
448
 
447
449
  //# sourceMappingURL=helpers.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"helpers.js","names":[],"sources":["../src/helpers.ts"],"sourcesContent":["/**\n * @fileoverview Email Helper Utilities\n *\n * This module provides utility functions for email operations including:\n * - Template rendering and text conversion\n * - Email address validation and formatting\n * - Content processing and sanitization\n * - Attachment handling\n * - Email list management\n *\n * Import from '@kumix/email/helpers' to access these utilities.\n *\n * @example\n * ```typescript\n * import {\n * renderEmailTemplate,\n * htmlToText,\n * isValidEmail,\n * formatEmailAddress,\n * sanitizeHtml,\n * generateUnsubscribeLink\n * } from '@kumix/email/helpers';\n * ```\n */\n\nimport React from \"react\";\nimport { render } from \"react-email\";\n\n// =============================================================================\n// TEMPLATE RENDERING\n// =============================================================================\n\n/**\n * Render React component to HTML string\n *\n * Converts a React email template component to HTML string that can be sent via email.\n *\n * @param Template - React component to render\n * @param props - Props to pass to the component\n * @returns Promise resolving to HTML string\n * @example\n * ```typescript\n * const WelcomeEmail = ({ name }: { name: string }) => <h1>Welcome {name}!</h1>;\n *\n * const html = await renderEmailTemplate(WelcomeEmail, { name: 'John' });\n * console.log(html); // \"<h1>Welcome John!</h1>\"\n * ```\n */\nexport async function renderEmailTemplate<T extends Record<string, unknown>>(\n Template: React.ComponentType<T>,\n props: T,\n): Promise<string> {\n return render(React.createElement(Template, props));\n}\n\n/**\n * Generate plain text from HTML\n *\n * Converts HTML content to plain text by removing tags and normalizing whitespace.\n * Useful for creating text versions of HTML emails.\n *\n * @param html - HTML string to convert\n * @returns Plain text version of the HTML\n * @example\n * ```typescript\n * const html = '<h1>Hello</h1><p>Welcome to our <strong>platform</strong>!</p>';\n * const text = htmlToText(html);\n * console.log(text); // \"Hello Welcome to our platform!\"\n * ```\n */\nexport function htmlToText(html: string): string {\n return html\n .replace(/<br\\s*\\/?>/gi, \"\\n\") // Convert <br> to newlines\n .replace(/<\\/p>/gi, \"\\n\\n\") // Convert </p> to double newlines\n .replace(/<\\/h[1-6]>/gi, \"\\n\\n\") // Convert heading endings to double newlines\n .replace(/<[^>]*>/g, \" \") // Remove all other HTML tags\n .replace(/\\s+/g, \" \") // Normalize whitespace\n .replace(/\\n\\s+/g, \"\\n\") // Remove spaces after newlines\n .trim();\n}\n\n// =============================================================================\n// EMAIL VALIDATION\n// =============================================================================\n\n/**\n * Validate email address format\n *\n * Checks if an email address has a valid format using RFC 5322 compliant regex.\n *\n * @param email - Email address to validate\n * @returns true if email format is valid, false otherwise\n * @example\n * ```typescript\n * console.log(isValidEmail('user@example.com')); // true\n * console.log(isValidEmail('invalid-email')); // false\n * console.log(isValidEmail('user+tag@example.co.uk')); // true\n * ```\n */\nexport function isValidEmail(email: string): boolean {\n const emailRegex =\n /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+$/;\n return emailRegex.test(email);\n}\n\n/**\n * Validate multiple email addresses\n *\n * Validates an array of email addresses or a single email address.\n *\n * @param emails - Single email or array of emails to validate\n * @returns true if all emails are valid, false if any are invalid\n * @example\n * ```typescript\n * console.log(validateEmails('user@example.com')); // true\n * console.log(validateEmails(['user1@example.com', 'user2@example.com'])); // true\n * console.log(validateEmails(['valid@example.com', 'invalid-email'])); // false\n * ```\n */\nexport function validateEmails(emails: string | string[]): boolean {\n const emailArray = Array.isArray(emails) ? emails : [emails];\n return emailArray.every((email) => isValidEmail(email.trim()));\n}\n\n/**\n * Filter valid emails from a list\n *\n * Returns only the valid email addresses from a list, filtering out invalid ones.\n *\n * @param emails - Array of email addresses to filter\n * @returns Array containing only valid email addresses\n * @example\n * ```typescript\n * const emails = ['valid@example.com', 'invalid-email', 'another@example.com'];\n * const validEmails = filterValidEmails(emails);\n * console.log(validEmails); // ['valid@example.com', 'another@example.com']\n * ```\n */\nexport function filterValidEmails(emails: string[]): string[] {\n return emails.filter((email) => isValidEmail(email.trim()));\n}\n\n// =============================================================================\n// EMAIL FORMATTING\n// =============================================================================\n\n/**\n * Format email address with display name\n *\n * Combines a display name with an email address in the standard format.\n *\n * @param name - Display name for the email address\n * @param email - Email address\n * @returns Formatted email address string\n * @example\n * ```typescript\n * const formatted = formatEmailAddress('John Doe', 'john@example.com');\n * console.log(formatted); // \"John Doe <john@example.com>\"\n * ```\n */\nexport function formatEmailAddress(name: string, email: string): string {\n // Don't escape quotes - keep them as is for standard email format\n return `${name} <${email}>`;\n}\n\n/**\n * Extract email address from formatted string\n *\n * Extracts the email address from a formatted \"Name <email>\" string.\n *\n * @param formattedAddress - Formatted email address string\n * @returns Just the email address part\n * @example\n * ```typescript\n * const email = extractEmail('John Doe <john@example.com>');\n * console.log(email); // \"john@example.com\"\n *\n * const email2 = extractEmail('simple@example.com');\n * console.log(email2); // \"simple@example.com\"\n * ```\n */\nexport function extractEmail(formattedAddress: string): string {\n const match = formattedAddress.match(/<([^>]*)>/);\n return match ? match[1] : formattedAddress.trim();\n}\n\n/**\n * Extract display name from formatted email address\n *\n * Extracts the display name from a formatted \"Name <email>\" string.\n *\n * @param formattedAddress - Formatted email address string\n * @returns Display name or empty string if none found\n * @example\n * ```typescript\n * const name = extractDisplayName('John Doe <john@example.com>');\n * console.log(name); // \"John Doe\"\n *\n * const name2 = extractDisplayName('simple@example.com');\n * console.log(name2); // \"\"\n * ```\n */\nexport function extractDisplayName(formattedAddress: string): string {\n const match = formattedAddress.match(/^(.+?)\\s*<[^>]*>$/);\n return match ? match[1].trim().replace(/^[\"']|[\"']$/g, \"\") : \"\";\n}\n\n// =============================================================================\n// CONTENT PROCESSING\n// =============================================================================\n\n/**\n * Sanitize HTML content for email\n *\n * Removes potentially dangerous HTML elements and attributes while preserving\n * email-safe formatting.\n *\n * @param html - HTML content to sanitize\n * @returns Sanitized HTML safe for email\n * @example\n * ```typescript\n * const unsafeHtml = '<script>alert(\"xss\")</script><p>Safe content</p>';\n * const safeHtml = sanitizeHtml(unsafeHtml);\n * console.log(safeHtml); // \"<p>Safe content</p>\"\n * ```\n */\nexport function sanitizeHtml(html: string): string {\n // Remove script tags and their content\n let sanitized = html.replace(/<script\\b[^<]*(?:(?!<\\/script>)<[^<]*)*<\\/script>/gi, \"\");\n\n // Remove dangerous attributes\n sanitized = sanitized.replace(/\\s*on\\w+\\s*=\\s*[\"'][^\"']*[\"']/gi, \"\"); // onclick, onload, etc.\n sanitized = sanitized.replace(/\\s*javascript\\s*:/gi, \"\"); // javascript: urls\n\n // Remove style attributes that could be dangerous\n sanitized = sanitized.replace(/\\s*style\\s*=\\s*[\"'][^\"']*expression\\([^\"']*\\)[\"']/gi, \"\");\n\n return sanitized;\n}\n\n/**\n * Truncate text with ellipsis\n *\n * Truncates text to a specified length and adds ellipsis if needed.\n *\n * @param text - Text to truncate\n * @param maxLength - Maximum length before truncation\n * @param ellipsis - String to append when truncated (default: '...')\n * @returns Truncated text\n * @example\n * ```typescript\n * const text = 'This is a very long email subject line';\n * const truncated = truncateText(text, 20);\n * console.log(truncated); // \"This is a very long...\"\n * ```\n */\nexport function truncateText(text: string, maxLength: number, ellipsis: string = \"...\"): string {\n if (text.length <= maxLength) return text;\n return text.slice(0, maxLength - ellipsis.length) + ellipsis;\n}\n\n/**\n * Generate email preview text\n *\n * Creates a preview text from HTML content, suitable for email preview panes.\n *\n * @param html - HTML content to generate preview from\n * @param maxLength - Maximum length of preview text (default: 150)\n * @returns Preview text\n * @example\n * ```typescript\n * const html = '<h1>Welcome!</h1><p>Thank you for joining our platform.</p>';\n * const preview = generatePreviewText(html);\n * console.log(preview); // \"Welcome! Thank you for joining our platform.\"\n * ```\n */\nexport function generatePreviewText(html: string, maxLength: number = 150): string {\n const text = htmlToText(html);\n return truncateText(text, maxLength);\n}\n\n// =============================================================================\n// URL AND LINK UTILITIES\n// =============================================================================\n\n/**\n * Generate unsubscribe link\n *\n * Creates an unsubscribe URL with proper parameters.\n *\n * @param baseUrl - Base URL for unsubscribe endpoint\n * @param email - Email address to unsubscribe\n * @param token - Optional security token\n * @returns Complete unsubscribe URL\n * @example\n * ```typescript\n * const link = generateUnsubscribeLink(\n * 'https://example.com/unsubscribe',\n * 'user@example.com',\n * 'secure-token-123'\n * );\n * console.log(link); // \"https://example.com/unsubscribe?email=user%40example.com&token=secure-token-123\"\n * ```\n */\nexport function generateUnsubscribeLink(baseUrl: string, email: string, token?: string): string {\n const url = new URL(baseUrl);\n url.searchParams.set(\"email\", email);\n if (token) {\n url.searchParams.set(\"token\", token);\n }\n return url.toString();\n}\n\n/**\n * Generate tracking pixel URL\n *\n * Creates a tracking pixel URL for email open tracking.\n *\n * @param baseUrl - Base URL for tracking endpoint\n * @param emailId - Unique identifier for the email\n * @param recipientId - Unique identifier for the recipient\n * @returns Tracking pixel URL\n * @example\n * ```typescript\n * const pixelUrl = generateTrackingPixel(\n * 'https://example.com/track',\n * 'email-123',\n * 'user-456'\n * );\n * // Use in email: <img src=\"${pixelUrl}\" width=\"1\" height=\"1\" />\n * ```\n */\nexport function generateTrackingPixel(\n baseUrl: string,\n emailId: string,\n recipientId: string,\n): string {\n const url = new URL(baseUrl);\n url.searchParams.set(\"email_id\", emailId);\n url.searchParams.set(\"recipient_id\", recipientId);\n url.searchParams.set(\"t\", Date.now().toString());\n return url.toString();\n}\n\n/**\n * Add UTM parameters to URL\n *\n * Adds UTM tracking parameters to a URL for campaign tracking.\n *\n * @param url - Original URL\n * @param utmParams - UTM parameters object\n * @returns URL with UTM parameters added\n * @example\n * ```typescript\n * const trackedUrl = addUtmParams('https://example.com/product', {\n * source: 'email',\n * medium: 'newsletter',\n * campaign: 'summer_sale',\n * content: 'header_button'\n * });\n * ```\n */\nexport function addUtmParams(\n url: string,\n utmParams: {\n source?: string;\n medium?: string;\n campaign?: string;\n term?: string;\n content?: string;\n },\n): string {\n const urlObj = new URL(url);\n\n if (utmParams.source) urlObj.searchParams.set(\"utm_source\", utmParams.source);\n if (utmParams.medium) urlObj.searchParams.set(\"utm_medium\", utmParams.medium);\n if (utmParams.campaign) urlObj.searchParams.set(\"utm_campaign\", utmParams.campaign);\n if (utmParams.term) urlObj.searchParams.set(\"utm_term\", utmParams.term);\n if (utmParams.content) urlObj.searchParams.set(\"utm_content\", utmParams.content);\n\n return urlObj.toString();\n}\n\n// =============================================================================\n// EMAIL LIST UTILITIES\n// =============================================================================\n\n/**\n * Parse email list from string\n *\n * Parses a comma or semicolon separated string of email addresses.\n *\n * @param emailString - String containing multiple email addresses\n * @returns Array of trimmed email addresses\n * @example\n * ```typescript\n * const emails = parseEmailList('user1@example.com, user2@example.com; user3@example.com');\n * console.log(emails); // ['user1@example.com', 'user2@example.com', 'user3@example.com']\n * ```\n */\nexport function parseEmailList(emailString: string): string[] {\n return emailString\n .split(/[,;]/)\n .map((email) => email.trim())\n .filter((email) => email.length > 0);\n}\n\n/**\n * Deduplicate email list\n *\n * Removes duplicate email addresses from a list (case-insensitive).\n *\n * @param emails - Array of email addresses\n * @returns Array with duplicates removed\n * @example\n * ```typescript\n * const emails = ['user@example.com', 'USER@EXAMPLE.COM', 'other@example.com'];\n * const unique = deduplicateEmails(emails);\n * console.log(unique); // ['user@example.com', 'other@example.com']\n * ```\n */\nexport function deduplicateEmails(emails: string[]): string[] {\n const seen = new Set<string>();\n return emails.filter((email) => {\n const normalized = email.toLowerCase().trim();\n if (seen.has(normalized)) {\n return false;\n }\n seen.add(normalized);\n return true;\n });\n}\n\n/**\n * Chunk email list for batch sending\n *\n * Splits a large email list into smaller chunks for batch processing.\n *\n * @param emails - Array of email addresses\n * @param chunkSize - Size of each chunk (default: 100)\n * @returns Array of email chunks\n * @example\n * ```typescript\n * const emails = ['user1@example.com', 'user2@example.com', ...]; // 250 emails\n * const chunks = chunkEmails(emails, 100);\n * console.log(chunks.length); // 3 chunks\n * console.log(chunks[0].length); // 100\n * console.log(chunks[2].length); // 50\n * ```\n */\nexport function chunkEmails(emails: string[], chunkSize: number = 100): string[][] {\n const chunks: string[][] = [];\n for (let i = 0; i < emails.length; i += chunkSize) {\n chunks.push(emails.slice(i, i + chunkSize));\n }\n return chunks;\n}\n\n// =============================================================================\n// ATTACHMENT UTILITIES\n// =============================================================================\n\n/**\n * Get MIME type from file extension\n *\n * Returns the appropriate MIME type for common file extensions.\n *\n * @param filename - Filename or extension\n * @returns MIME type string\n * @example\n * ```typescript\n * console.log(getMimeType('document.pdf')); // 'application/pdf'\n * console.log(getMimeType('image.jpg')); // 'image/jpeg'\n * console.log(getMimeType('.png')); // 'image/png'\n * ```\n */\nexport function getMimeType(filename: string): string {\n const ext = filename.toLowerCase().split(\".\").pop() || \"\";\n\n const mimeTypes: Record<string, string> = {\n // Images\n jpg: \"image/jpeg\",\n jpeg: \"image/jpeg\",\n png: \"image/png\",\n gif: \"image/gif\",\n webp: \"image/webp\",\n svg: \"image/svg+xml\",\n\n // Documents\n pdf: \"application/pdf\",\n doc: \"application/msword\",\n docx: \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\",\n xls: \"application/vnd.ms-excel\",\n xlsx: \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\n ppt: \"application/vnd.ms-powerpoint\",\n pptx: \"application/vnd.openxmlformats-officedocument.presentationml.presentation\",\n\n // Text\n txt: \"text/plain\",\n csv: \"text/csv\",\n html: \"text/html\",\n css: \"text/css\",\n js: \"text/javascript\",\n json: \"application/json\",\n xml: \"application/xml\",\n\n // Archives\n zip: \"application/zip\",\n rar: \"application/x-rar-compressed\",\n \"7z\": \"application/x-7z-compressed\",\n tar: \"application/x-tar\",\n gz: \"application/gzip\",\n };\n\n return mimeTypes[ext] || \"application/octet-stream\";\n}\n\n/**\n * Format file size for display\n *\n * Converts file size in bytes to human-readable format.\n *\n * @param bytes - File size in bytes\n * @param decimals - Number of decimal places (default: 2)\n * @returns Formatted file size string\n * @example\n * ```typescript\n * console.log(formatFileSize(1024)); // \"1.00 KB\"\n * console.log(formatFileSize(1048576)); // \"1.00 MB\"\n * console.log(formatFileSize(1073741824, 1)); // \"1.0 GB\"\n * ```\n */\nexport function formatFileSize(bytes: number, decimals: number = 2): string {\n if (bytes === 0) return \"0 Bytes\";\n\n const k = 1024;\n const dm = decimals < 0 ? 0 : decimals;\n const sizes = [\"Bytes\", \"KB\", \"MB\", \"GB\", \"TB\"];\n\n const i = Math.floor(Math.log(bytes) / Math.log(k));\n\n return `${(bytes / k ** i).toFixed(dm)} ${sizes[i]}`;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"helpers.js","names":[],"sources":["../src/helpers.ts"],"sourcesContent":["/**\n * @fileoverview Email Helper Utilities\n *\n * This module provides utility functions for email operations including:\n * - Template rendering and text conversion\n * - Email address validation and formatting\n * - Content processing and sanitization\n * - Attachment handling\n * - Email list management\n *\n * Import from '@kumix/email/helpers' to access these utilities.\n *\n * @example\n * ```typescript\n * import {\n * renderEmailTemplate,\n * htmlToText,\n * isValidEmail,\n * formatEmailAddress,\n * sanitizeHtml,\n * generateUnsubscribeLink\n * } from '@kumix/email/helpers';\n * ```\n */\n\nimport type { ComponentType } from \"react\";\n\n// =============================================================================\n// TEMPLATE RENDERING\n// =============================================================================\n\n/**\n * Render React component to HTML string\n *\n * Converts a React email template component to HTML string that can be sent via email.\n *\n * @param Template - React component to render\n * @param props - Props to pass to the component\n * @returns Promise resolving to HTML string\n * @example\n * ```typescript\n * const WelcomeEmail = ({ name }: { name: string }) => <h1>Welcome {name}!</h1>;\n *\n * const html = await renderEmailTemplate(WelcomeEmail, { name: 'John' });\n * console.log(html); // \"<h1>Welcome John!</h1>\"\n * ```\n */\nexport async function renderEmailTemplate<T extends Record<string, unknown>>(\n Template: ComponentType<T>,\n props: T,\n): Promise<string> {\n // Dynamic-import react/react-email so they are only loaded when a caller\n // actually renders a template. These are optional peers; a static top-level\n // import would crash module load for consumers that only use plain-HTML\n // sends (or only Nodemailer).\n const { createElement } = await import(\"react\");\n const { render } = await import(\"react-email\");\n return render(createElement(Template, props));\n}\n\n/**\n * Generate plain text from HTML\n *\n * Converts HTML content to plain text by removing tags and normalizing\n * whitespace. Block-level boundaries (`<br>`, `</p>`, headings) are preserved\n * as newlines in the output.\n *\n * @param html - HTML string to convert\n * @returns Plain text version of the HTML\n * @example\n * ```typescript\n * const html = '<h1>Hello</h1><p>Welcome to our <strong>platform</strong>!</p>';\n * const text = htmlToText(html);\n * // \"Hello\\n\\nWelcome to our platform!\"\n * ```\n */\nexport function htmlToText(html: string): string {\n // Insert newline placeholders for block boundaries BEFORE the whitespace\n // normalization pass. Previously `.replace(/\\s+/g, \" \")` ran before any\n // newline logic and collapsed the inserted `\\n`s back into spaces, so the\n // function always returned a single line despite its docs and tests.\n const NEWLINE = \"\\u0000N\\u0000\";\n const DOUBLE_NEWLINE = \"\\u0000D\\u0000\";\n return html\n .replace(/<br\\s*\\/?>/gi, NEWLINE)\n .replace(/<\\/p>/gi, DOUBLE_NEWLINE)\n .replace(/<\\/h[1-6]>/gi, DOUBLE_NEWLINE)\n .replace(/<[^>]*>/g, \" \") // Remove all other HTML tags\n .replace(/\\s+/g, \" \") // Normalize whitespace (placeholders are non-whitespace)\n .trim()\n .split(DOUBLE_NEWLINE)\n .map((segment) => segment.split(NEWLINE).join(\"\\n\").trim())\n .filter((segment) => segment.length > 0)\n .join(\"\\n\\n\");\n}\n\n// =============================================================================\n// EMAIL VALIDATION\n// =============================================================================\n\n/**\n * Validate email address format\n *\n * Checks if an email address has a valid format using RFC 5322 compliant regex.\n *\n * @param email - Email address to validate\n * @returns true if email format is valid, false otherwise\n * @example\n * ```typescript\n * console.log(isValidEmail('user@example.com')); // true\n * console.log(isValidEmail('invalid-email')); // false\n * console.log(isValidEmail('user+tag@example.co.uk')); // true\n * ```\n */\nexport function isValidEmail(email: string): boolean {\n const emailRegex =\n /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+$/;\n return emailRegex.test(email);\n}\n\n/**\n * Validate multiple email addresses\n *\n * Validates an array of email addresses or a single email address.\n *\n * @param emails - Single email or array of emails to validate\n * @returns true if all emails are valid, false if any are invalid\n * @example\n * ```typescript\n * console.log(validateEmails('user@example.com')); // true\n * console.log(validateEmails(['user1@example.com', 'user2@example.com'])); // true\n * console.log(validateEmails(['valid@example.com', 'invalid-email'])); // false\n * ```\n */\nexport function validateEmails(emails: string | string[]): boolean {\n const emailArray = Array.isArray(emails) ? emails : [emails];\n return emailArray.every((email) => isValidEmail(email.trim()));\n}\n\n/**\n * Filter valid emails from a list\n *\n * Returns only the valid email addresses from a list, filtering out invalid ones.\n *\n * @param emails - Array of email addresses to filter\n * @returns Array containing only valid email addresses\n * @example\n * ```typescript\n * const emails = ['valid@example.com', 'invalid-email', 'another@example.com'];\n * const validEmails = filterValidEmails(emails);\n * console.log(validEmails); // ['valid@example.com', 'another@example.com']\n * ```\n */\nexport function filterValidEmails(emails: string[]): string[] {\n return emails.filter((email) => isValidEmail(email.trim()));\n}\n\n// =============================================================================\n// EMAIL FORMATTING\n// =============================================================================\n\n/**\n * Strip CR/LF from an email header value to prevent SMTP header injection.\n *\n * @param value - Raw header/recipient value\n * @returns The value with all `\\r` and `\\n` removed\n */\nexport function stripCrlf(value: string): string {\n return value.replace(/[\\r\\n]/g, \"\");\n}\n\n/**\n * Format email address with display name\n *\n * Combines a display name with an email address in the standard `\"Name\" <email>`\n * format. The display name is quoted and any embedded quotes/backslashes are\n * escaped; CR/LF characters are stripped from both parts to prevent SMTP header\n * injection.\n *\n * @param name - Display name for the email address\n * @param email - Email address\n * @returns Formatted email address string\n * @example\n * ```typescript\n * const formatted = formatEmailAddress('John Doe', 'john@example.com');\n * console.log(formatted); // \"John Doe <john@example.com>\"\n * ```\n */\nexport function formatEmailAddress(name: string, email: string): string {\n // Strip CR/LF from both halves to prevent header injection. A newline in\n // either `name` or `email` lets a malicious value inject extra headers\n // (BCC, additional recipients, etc.) when concatenated into a `from` field.\n const safeName = name.replace(/[\\r\\n]/g, \"\");\n const safeEmail = email.replace(/[\\r\\n]/g, \"\");\n // Escape backslashes and double quotes per RFC 5322 atom/quoted-string rules\n // and wrap the name in quotes so special characters survive the wire.\n const escapedName = safeName.replace(/\\\\|\"/g, (ch) => `\\\\${ch}`);\n return `\"${escapedName}\" <${safeEmail}>`;\n}\n\n/**\n * Extract email address from formatted string\n *\n * Extracts the email address from a formatted \"Name <email>\" string.\n *\n * @param formattedAddress - Formatted email address string\n * @returns Just the email address part\n * @example\n * ```typescript\n * const email = extractEmail('John Doe <john@example.com>');\n * console.log(email); // \"john@example.com\"\n *\n * const email2 = extractEmail('simple@example.com');\n * console.log(email2); // \"simple@example.com\"\n * ```\n */\nexport function extractEmail(formattedAddress: string): string {\n const match = formattedAddress.match(/<([^>]*)>/);\n return match ? match[1] : formattedAddress.trim();\n}\n\n/**\n * Extract display name from formatted email address\n *\n * Extracts the display name from a formatted \"Name <email>\" string.\n *\n * @param formattedAddress - Formatted email address string\n * @returns Display name or empty string if none found\n * @example\n * ```typescript\n * const name = extractDisplayName('John Doe <john@example.com>');\n * console.log(name); // \"John Doe\"\n *\n * const name2 = extractDisplayName('simple@example.com');\n * console.log(name2); // \"\"\n * ```\n */\nexport function extractDisplayName(formattedAddress: string): string {\n const match = formattedAddress.match(/^(.+?)\\s*<[^>]*>$/);\n return match ? match[1].trim().replace(/^[\"']|[\"']$/g, \"\") : \"\";\n}\n\n// =============================================================================\n// CONTENT PROCESSING\n// =============================================================================\n\n/**\n * Strip some unsafe constructs from HTML (basic, NOT a security boundary).\n *\n * ⚠️ This function is a coarse cleanup pass for trusted-or-low-risk email\n * content. It is **not** a sanitizer. It removes `<script>`/`<style>`/\n * `<iframe>`/`<object>`/`<embed>`/`<form>` blocks, on* event handlers (with\n * and without quotes), and obvious `javascript:` URLs. It will NOT defeat a\n * motivated attacker — HTML entity-encoding, unquoted attributes, SVG/CS\n * injection, and many other bypasses exist. For content from untrusted\n * sources, use a purpose-built sanitizer (e.g. DOMPurify) before rendering.\n *\n * @param html - HTML content to clean up\n * @returns HTML with the dangerous constructs above removed\n * @example\n * ```typescript\n * const unsafeHtml = '<script>alert(\"xss\")</script><p>Safe content</p>';\n * const safeHtml = sanitizeHtml(unsafeHtml);\n * console.log(safeHtml); // \"<p>Safe content</p>\"\n * ```\n */\nexport function sanitizeHtml(html: string): string {\n // Remove entire blocks of dangerous tags (script, style, iframe, object,\n // embed, form, svg). Previous version only stripped <script> and let\n // <iframe>, <object>, <embed>, <svg onload=...>, and <style> through.\n let sanitized = html.replace(\n /<(script|style|iframe|object|embed|form|svg)\\b[^<]*(?:(?!<\\/\\1>)<[^<]*)*<\\/\\1>/gi,\n \"\",\n );\n\n // Remove event handler attributes: handle both quoted (`onclick=\"...\"`) and\n // unquoted (`onclick=foo`) forms. The previous regex only matched quoted values.\n sanitized = sanitized.replace(/\\s+on\\w+\\s*=\\s*(?:\"[^\"]*\"|'[^']*'|[^\\s>]+)/gi, \"\");\n\n // Remove `javascript:` URLs in href/src/etc. Also catch common encodings.\n sanitized = sanitized.replace(\n /(href|src|xlink:href|action|formaction)\\s*=\\s*[\"']?\\s*javascript:/gi,\n '$1=\"\"',\n );\n\n // Remove `style` attributes containing `expression(...)` (legacy IE CSS expression XSS)\n sanitized = sanitized.replace(/\\s*style\\s*=\\s*[\"'][^\"']*expression\\([^\"']*\\)[^\"']*[\"']/gi, \"\");\n\n return sanitized;\n}\n\n/**\n * Truncate text with ellipsis\n *\n * Truncates text to a specified length and adds ellipsis if needed.\n *\n * @param text - Text to truncate\n * @param maxLength - Maximum length before truncation\n * @param ellipsis - String to append when truncated (default: '...')\n * @returns Truncated text\n * @example\n * ```typescript\n * const text = 'This is a very long email subject line';\n * const truncated = truncateText(text, 20);\n * console.log(truncated); // \"This is a very long...\"\n * ```\n */\nexport function truncateText(text: string, maxLength: number, ellipsis: string = \"...\"): string {\n if (maxLength <= 0) return \"\";\n if (text.length <= maxLength) return text;\n // When `maxLength` is shorter than the ellipsis itself, fall back to a hard\n // slice so the result never exceeds the requested length. Previously\n // `truncateText(\"hello\", 2)` returned `\"hell...\"` (longer than `maxLength`).\n if (maxLength <= ellipsis.length) {\n return text.slice(0, maxLength);\n }\n return text.slice(0, maxLength - ellipsis.length) + ellipsis;\n}\n\n/**\n * Generate email preview text\n *\n * Creates a preview text from HTML content, suitable for email preview panes.\n *\n * @param html - HTML content to generate preview from\n * @param maxLength - Maximum length of preview text (default: 150)\n * @returns Preview text\n * @example\n * ```typescript\n * const html = '<h1>Welcome!</h1><p>Thank you for joining our platform.</p>';\n * const preview = generatePreviewText(html);\n * console.log(preview); // \"Welcome! Thank you for joining our platform.\"\n * ```\n */\nexport function generatePreviewText(html: string, maxLength: number = 150): string {\n const text = htmlToText(html);\n return truncateText(text, maxLength);\n}\n\n// =============================================================================\n// URL AND LINK UTILITIES\n// =============================================================================\n\n/**\n * Generate unsubscribe link\n *\n * Creates an unsubscribe URL with proper parameters.\n *\n * @param baseUrl - Base URL for unsubscribe endpoint\n * @param email - Email address to unsubscribe\n * @param token - Optional security token\n * @returns Complete unsubscribe URL\n * @example\n * ```typescript\n * const link = generateUnsubscribeLink(\n * 'https://example.com/unsubscribe',\n * 'user@example.com',\n * 'secure-token-123'\n * );\n * console.log(link); // \"https://example.com/unsubscribe?email=user%40example.com&token=secure-token-123\"\n * ```\n */\nexport function generateUnsubscribeLink(baseUrl: string, email: string, token?: string): string {\n const url = new URL(baseUrl);\n url.searchParams.set(\"email\", email);\n if (token) {\n url.searchParams.set(\"token\", token);\n }\n return url.toString();\n}\n\n/**\n * Generate tracking pixel URL\n *\n * Creates a tracking pixel URL for email open tracking.\n *\n * @param baseUrl - Base URL for tracking endpoint\n * @param emailId - Unique identifier for the email\n * @param recipientId - Unique identifier for the recipient\n * @returns Tracking pixel URL\n * @example\n * ```typescript\n * const pixelUrl = generateTrackingPixel(\n * 'https://example.com/track',\n * 'email-123',\n * 'user-456'\n * );\n * // Use in email: <img src=\"${pixelUrl}\" width=\"1\" height=\"1\" />\n * ```\n */\nexport function generateTrackingPixel(\n baseUrl: string,\n emailId: string,\n recipientId: string,\n): string {\n const url = new URL(baseUrl);\n url.searchParams.set(\"email_id\", emailId);\n url.searchParams.set(\"recipient_id\", recipientId);\n url.searchParams.set(\"t\", Date.now().toString());\n return url.toString();\n}\n\n/**\n * Add UTM parameters to URL\n *\n * Adds UTM tracking parameters to a URL for campaign tracking.\n *\n * @param url - Original URL\n * @param utmParams - UTM parameters object\n * @returns URL with UTM parameters added\n * @example\n * ```typescript\n * const trackedUrl = addUtmParams('https://example.com/product', {\n * source: 'email',\n * medium: 'newsletter',\n * campaign: 'summer_sale',\n * content: 'header_button'\n * });\n * ```\n */\nexport function addUtmParams(\n url: string,\n utmParams: {\n source?: string;\n medium?: string;\n campaign?: string;\n term?: string;\n content?: string;\n },\n): string {\n const urlObj = new URL(url);\n\n if (utmParams.source) urlObj.searchParams.set(\"utm_source\", utmParams.source);\n if (utmParams.medium) urlObj.searchParams.set(\"utm_medium\", utmParams.medium);\n if (utmParams.campaign) urlObj.searchParams.set(\"utm_campaign\", utmParams.campaign);\n if (utmParams.term) urlObj.searchParams.set(\"utm_term\", utmParams.term);\n if (utmParams.content) urlObj.searchParams.set(\"utm_content\", utmParams.content);\n\n return urlObj.toString();\n}\n\n// =============================================================================\n// EMAIL LIST UTILITIES\n// =============================================================================\n\n/**\n * Parse email list from string\n *\n * Parses a comma or semicolon separated string of email addresses.\n *\n * @param emailString - String containing multiple email addresses\n * @returns Array of trimmed email addresses\n * @example\n * ```typescript\n * const emails = parseEmailList('user1@example.com, user2@example.com; user3@example.com');\n * console.log(emails); // ['user1@example.com', 'user2@example.com', 'user3@example.com']\n * ```\n */\nexport function parseEmailList(emailString: string): string[] {\n return emailString\n .split(/[,;]/)\n .map((email) => email.trim())\n .filter((email) => email.length > 0);\n}\n\n/**\n * Deduplicate email list\n *\n * Removes duplicate email addresses from a list (case-insensitive).\n *\n * @param emails - Array of email addresses\n * @returns Array with duplicates removed\n * @example\n * ```typescript\n * const emails = ['user@example.com', 'USER@EXAMPLE.COM', 'other@example.com'];\n * const unique = deduplicateEmails(emails);\n * console.log(unique); // ['user@example.com', 'other@example.com']\n * ```\n */\nexport function deduplicateEmails(emails: string[]): string[] {\n const seen = new Set<string>();\n return emails.filter((email) => {\n const normalized = email.toLowerCase().trim();\n if (seen.has(normalized)) {\n return false;\n }\n seen.add(normalized);\n return true;\n });\n}\n\n/**\n * Chunk email list for batch sending\n *\n * Splits a large email list into smaller chunks for batch processing.\n *\n * @param emails - Array of email addresses\n * @param chunkSize - Size of each chunk (default: 100)\n * @returns Array of email chunks\n * @example\n * ```typescript\n * const emails = ['user1@example.com', 'user2@example.com', ...]; // 250 emails\n * const chunks = chunkEmails(emails, 100);\n * console.log(chunks.length); // 3 chunks\n * console.log(chunks[0].length); // 100\n * console.log(chunks[2].length); // 50\n * ```\n */\nexport function chunkEmails(emails: string[], chunkSize: number = 100): string[][] {\n const chunks: string[][] = [];\n for (let i = 0; i < emails.length; i += chunkSize) {\n chunks.push(emails.slice(i, i + chunkSize));\n }\n return chunks;\n}\n\n// =============================================================================\n// ATTACHMENT UTILITIES\n// =============================================================================\n\n/**\n * Get MIME type from file extension\n *\n * Returns the appropriate MIME type for common file extensions.\n *\n * @param filename - Filename or extension\n * @returns MIME type string\n * @example\n * ```typescript\n * console.log(getMimeType('document.pdf')); // 'application/pdf'\n * console.log(getMimeType('image.jpg')); // 'image/jpeg'\n * console.log(getMimeType('.png')); // 'image/png'\n * ```\n */\nexport function getMimeType(filename: string): string {\n const ext = filename.toLowerCase().split(\".\").pop() || \"\";\n\n const mimeTypes: Record<string, string> = {\n // Images\n jpg: \"image/jpeg\",\n jpeg: \"image/jpeg\",\n png: \"image/png\",\n gif: \"image/gif\",\n webp: \"image/webp\",\n svg: \"image/svg+xml\",\n\n // Documents\n pdf: \"application/pdf\",\n doc: \"application/msword\",\n docx: \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\",\n xls: \"application/vnd.ms-excel\",\n xlsx: \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\n ppt: \"application/vnd.ms-powerpoint\",\n pptx: \"application/vnd.openxmlformats-officedocument.presentationml.presentation\",\n\n // Text\n txt: \"text/plain\",\n csv: \"text/csv\",\n html: \"text/html\",\n css: \"text/css\",\n js: \"text/javascript\",\n json: \"application/json\",\n xml: \"application/xml\",\n\n // Archives\n zip: \"application/zip\",\n rar: \"application/x-rar-compressed\",\n \"7z\": \"application/x-7z-compressed\",\n tar: \"application/x-tar\",\n gz: \"application/gzip\",\n };\n\n return mimeTypes[ext] || \"application/octet-stream\";\n}\n\n/**\n * Format file size for display\n *\n * Converts file size in bytes to human-readable format.\n *\n * @param bytes - File size in bytes\n * @param decimals - Number of decimal places (default: 2)\n * @returns Formatted file size string\n * @example\n * ```typescript\n * console.log(formatFileSize(1024)); // \"1.00 KB\"\n * console.log(formatFileSize(1048576)); // \"1.00 MB\"\n * console.log(formatFileSize(1073741824, 1)); // \"1.0 GB\"\n * ```\n */\nexport function formatFileSize(bytes: number, decimals: number = 2): string {\n if (!Number.isFinite(bytes)) return \"0 Bytes\";\n if (bytes === 0) return \"0 Bytes\";\n if (bytes < 0) return `-${formatFileSize(-bytes, decimals)}`;\n\n const k = 1024;\n const dm = decimals < 0 ? 0 : decimals;\n const sizes = [\"Bytes\", \"KB\", \"MB\", \"GB\", \"TB\"];\n\n // Floor the index (and clamp) so fractional bytes (e.g. 0.5) don't pick a\n // negative index and produce `\"512 undefined\"`.\n const i = Math.max(0, Math.min(Math.floor(Math.log(bytes) / Math.log(k)), sizes.length - 1));\n\n return `${(bytes / k ** i).toFixed(dm)} ${sizes[i]}`;\n}\n"],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -141,63 +141,6 @@ interface IEmailProvider {
141
141
  /** Validate provider configuration */
142
142
  validateConfig?(): Promise<EmailValidationResult>;
143
143
  }
144
- /**
145
- * Email template options
146
- * @public
147
- */
148
- interface EmailTemplateOptions {
149
- /** Template name or ID */
150
- template: string;
151
- /** Template data/variables */
152
- data: EmailTemplateData;
153
- /** Recipient email address(es) */
154
- to: string | string[];
155
- /** CC recipient email address(es) */
156
- cc?: string | string[];
157
- /** BCC recipient email address(es) */
158
- bcc?: string | string[];
159
- /** Email subject (if not defined in template) */
160
- subject?: string;
161
- /** Email attachments */
162
- attachments?: EmailAttachment[];
163
- /** Custom email headers */
164
- headers?: Record<string, string>;
165
- /** Email tags for tracking and analytics */
166
- tags?: Record<string, string>;
167
- }
168
- /**
169
- * Bulk email sending options
170
- * @public
171
- */
172
- interface BulkEmailOptions {
173
- /** Array of email sending options */
174
- emails: SendEmailOptions[];
175
- /** Batch size for sending (default: 100) */
176
- batchSize?: number;
177
- /** Delay between batches in milliseconds (default: 1000) */
178
- batchDelay?: number;
179
- }
180
- /**
181
- * Bulk email sending result
182
- * @public
183
- */
184
- interface BulkEmailResult {
185
- /** Whether all emails were processed */
186
- success: boolean;
187
- /** Total number of emails processed */
188
- total: number;
189
- /** Number of successfully sent emails */
190
- sent: number;
191
- /** Number of failed emails */
192
- failed: number;
193
- /** Array of individual email results */
194
- results: EmailResult[];
195
- /** Array of errors for failed emails */
196
- errors: Array<{
197
- index: number;
198
- error: string;
199
- }>;
200
- }
201
144
  /**
202
145
  * Email configuration validation result
203
146
  * @public
@@ -210,51 +153,6 @@ interface ConfigValidationResult {
210
153
  /** Array of validation errors */
211
154
  errors: string[];
212
155
  }
213
- /**
214
- * Email analytics data
215
- * @public
216
- */
217
- interface EmailAnalytics {
218
- /** Number of emails sent */
219
- sent: number;
220
- /** Number of emails delivered */
221
- delivered: number;
222
- /** Number of emails opened */
223
- opened: number;
224
- /** Number of emails clicked */
225
- clicked: number;
226
- /** Number of emails bounced */
227
- bounced: number;
228
- /** Number of emails marked as spam */
229
- spam: number;
230
- /** Delivery rate percentage */
231
- deliveryRate: number;
232
- /** Open rate percentage */
233
- openRate: number;
234
- /** Click rate percentage */
235
- clickRate: number;
236
- }
237
- /**
238
- * Email webhook event types
239
- * @public
240
- */
241
- type EmailWebhookEvent = "sent" | "delivered" | "opened" | "clicked" | "bounced" | "spam" | "unsubscribed";
242
- /**
243
- * Email webhook payload
244
- * @public
245
- */
246
- interface EmailWebhookPayload {
247
- /** Event type */
248
- event: EmailWebhookEvent;
249
- /** Message ID */
250
- messageId: string;
251
- /** Recipient email */
252
- email: string;
253
- /** Timestamp of the event */
254
- timestamp: Date;
255
- /** Additional event data */
256
- data?: Record<string, unknown>;
257
- }
258
156
  //#endregion
259
157
  //#region src/config.d.ts
260
158
  /**
@@ -280,6 +178,11 @@ declare class EmailService {
280
178
  private createProvider;
281
179
  sendEmail(options: SendEmailOptions): Promise<EmailResult>;
282
180
  sendTemplate<T extends EmailTemplateData>(Template: React.ComponentType<T>, props: T, options: Omit<SendEmailOptions, "html" | "text">): Promise<EmailResult>;
181
+ /**
182
+ * Returns a deep copy of the current configuration. A shallow clone would
183
+ * share the nested `from` / `smtp` / `auth` objects (which contain SMTP
184
+ * credentials), letting callers accidentally mutate the service's config.
185
+ */
283
186
  getConfig(): EmailConfig;
284
187
  updateConfig(config: EmailConfig): void;
285
188
  validateConfig(): Promise<EmailValidationResult>;
@@ -338,7 +241,7 @@ declare class NodemailerProvider implements IEmailProvider {
338
241
  * ```
339
242
  */
340
243
  declare class ResendProvider implements IEmailProvider {
341
- private resend;
244
+ private _client;
342
245
  private config;
343
246
  /**
344
247
  * Create a new Resend provider instance
@@ -346,6 +249,7 @@ declare class ResendProvider implements IEmailProvider {
346
249
  * @throws Error if API key is missing
347
250
  */
348
251
  constructor(config: ResendConfig);
252
+ private getClient;
349
253
  /**
350
254
  * Validate Resend configuration
351
255
  * @returns Promise resolving to validation result
@@ -397,5 +301,5 @@ declare class ResendProvider implements IEmailProvider {
397
301
  send(options: SendEmailOptions): Promise<EmailResult>;
398
302
  }
399
303
  //#endregion
400
- export { type BulkEmailOptions, type BulkEmailResult, type ConfigValidationResult, type EmailAnalytics, type EmailAttachment, type EmailConfig, type EmailProvider, type EmailResult, EmailService, type EmailTemplateData, type EmailTemplateOptions, type EmailValidationResult, type EmailWebhookEvent, type EmailWebhookPayload, type EnvRecord, type IEmailProvider, type NodemailerConfig, NodemailerProvider, type ResendConfig, ResendProvider, type SendEmailOptions, createEmail, createNodemailer, createResend, getConfiguredProvider, getEmailEnvVars, hasEmailConfig, isEmailConfigured, loadEmailConfig, loadNodemailerConfig, loadResendConfig, validateEmailEnvVars, validateNodemailerEnvVars, validateResendEnvVars };
304
+ export { type BaseEmailConfig, type ConfigValidationResult, type EmailAttachment, type EmailConfig, type EmailProvider, type EmailResult, EmailService, type EmailTemplateData, type EmailValidationResult, type EnvRecord, type IEmailProvider, type NodemailerConfig, NodemailerProvider, type ResendConfig, ResendProvider, type SendEmailOptions, createEmail, createNodemailer, createResend, getConfiguredProvider, getEmailEnvVars, hasEmailConfig, isEmailConfigured, loadEmailConfig, loadNodemailerConfig, loadResendConfig, validateEmailEnvVars, validateNodemailerEnvVars, validateResendEnvVars };
401
305
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/types.ts","../src/config.ts","../src/services.ts","../src/factory.ts","../src/providers/nodemailer.ts","../src/providers/resend.ts"],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/types.ts","../src/config.ts","../src/services.ts","../src/factory.ts","../src/providers/nodemailer.ts","../src/providers/resend.ts"],"mappings":"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"}
package/dist/index.js CHANGED
@@ -1,5 +1,4 @@
1
- import { htmlToText, renderEmailTemplate } from "./helpers.js";
2
- import { Resend } from "resend";
1
+ import { formatEmailAddress, htmlToText, renderEmailTemplate, stripCrlf } from "./helpers.js";
3
2
  //#region src/config.ts
4
3
  function getDefaultEnv() {
5
4
  if (typeof process !== "undefined" && process?.env) return process.env;
@@ -10,10 +9,10 @@ function resolveEnv(env) {
10
9
  function loadResendConfig(env) {
11
10
  const e = resolveEnv(env);
12
11
  if (!e) return null;
13
- const apiKey = e.KUMIX_EMAIL_RESEND_API_KEY;
14
- const fromName = e.KUMIX_EMAIL_FROM_NAME;
15
- const fromEmail = e.KUMIX_EMAIL_FROM_EMAIL;
16
- const replyTo = e.KUMIX_EMAIL_REPLY_TO;
12
+ const apiKey = e.KUMIX_EMAIL_RESEND_API_KEY ?? e.RESEND_API_KEY;
13
+ const fromName = (e.KUMIX_EMAIL_FROM_NAME ?? e.EMAIL_FROM_NAME)?.trim();
14
+ const fromEmail = (e.KUMIX_EMAIL_FROM_EMAIL ?? e.EMAIL_FROM_EMAIL)?.trim();
15
+ const replyTo = (e.KUMIX_EMAIL_REPLY_TO ?? e.EMAIL_REPLY_TO)?.trim();
17
16
  if (!apiKey || !fromName || !fromEmail) return null;
18
17
  return {
19
18
  provider: "resend",
@@ -28,15 +27,18 @@ function loadResendConfig(env) {
28
27
  function loadNodemailerConfig(env) {
29
28
  const e = resolveEnv(env);
30
29
  if (!e) return null;
31
- const host = e.KUMIX_EMAIL_SMTP_HOST;
32
- const port = e.KUMIX_EMAIL_SMTP_PORT;
33
- const secure = e.KUMIX_EMAIL_SMTP_SECURE;
34
- const user = e.KUMIX_EMAIL_SMTP_USER;
35
- const pass = e.KUMIX_EMAIL_SMTP_PASS;
36
- const fromName = e.KUMIX_EMAIL_FROM_NAME;
37
- const fromEmail = e.KUMIX_EMAIL_FROM_EMAIL;
38
- const replyTo = e.KUMIX_EMAIL_REPLY_TO;
30
+ const host = (e.KUMIX_EMAIL_SMTP_HOST ?? e.SMTP_HOST)?.trim();
31
+ const port = e.KUMIX_EMAIL_SMTP_PORT ?? e.SMTP_PORT;
32
+ const secure = e.KUMIX_EMAIL_SMTP_SECURE ?? e.SMTP_SECURE;
33
+ const user = e.KUMIX_EMAIL_SMTP_USER ?? e.SMTP_USER;
34
+ const pass = e.KUMIX_EMAIL_SMTP_PASS ?? e.SMTP_PASS;
35
+ const fromName = (e.KUMIX_EMAIL_FROM_NAME ?? e.EMAIL_FROM_NAME)?.trim();
36
+ const fromEmail = (e.KUMIX_EMAIL_FROM_EMAIL ?? e.EMAIL_FROM_EMAIL)?.trim();
37
+ const replyTo = (e.KUMIX_EMAIL_REPLY_TO ?? e.EMAIL_REPLY_TO)?.trim();
39
38
  if (!host || !port || !user || !pass || !fromName || !fromEmail) return null;
39
+ const parsedPort = parseInt(port, 10);
40
+ if (Number.isNaN(parsedPort) || parsedPort < 1 || parsedPort > 65535) return null;
41
+ const secureNorm = typeof secure === "string" ? secure.toLowerCase() : "";
40
42
  return {
41
43
  provider: "nodemailer",
42
44
  from: {
@@ -46,8 +48,8 @@ function loadNodemailerConfig(env) {
46
48
  replyTo,
47
49
  smtp: {
48
50
  host,
49
- port: parseInt(port, 10),
50
- secure: secure === "true",
51
+ port: parsedPort,
52
+ secure: secureNorm === "true" || secureNorm === "1" || secureNorm === "yes",
51
53
  auth: {
52
54
  user,
53
55
  pass
@@ -131,6 +133,7 @@ function validateEmailEnvVars(env) {
131
133
  }
132
134
  //#endregion
133
135
  //#region src/providers/nodemailer.ts
136
+ /** biome-ignore-all lint/suspicious/noExplicitAny: We need to use any to avoid type errors. */
134
137
  var NodemailerProvider = class {
135
138
  config;
136
139
  _transporter = null;
@@ -187,18 +190,24 @@ var NodemailerProvider = class {
187
190
  }
188
191
  async send(options) {
189
192
  try {
193
+ const transporter = await this.getTransporter();
194
+ const headers = {};
195
+ if (options.headers) for (const [k, v] of Object.entries(options.headers)) headers[k] = String(v).replace(/[\r\n]/g, "");
196
+ if (options.tags) for (const [name, value] of Object.entries(options.tags)) headers[`X-Tag-${name.replace(/[\r\n]/g, "")}`] = String(value).replace(/[\r\n]/g, "");
197
+ if (options.priority) headers["X-Priority"] = String(options.priority);
190
198
  return {
191
199
  success: true,
192
- messageId: (await (await this.getTransporter()).sendMail({
193
- from: `${this.config.from.name} <${this.config.from.email}>`,
194
- to: Array.isArray(options.to) ? options.to.join(", ") : options.to,
195
- cc: options.cc ? Array.isArray(options.cc) ? options.cc.join(", ") : options.cc : void 0,
196
- bcc: options.bcc ? Array.isArray(options.bcc) ? options.bcc.join(", ") : options.bcc : void 0,
197
- subject: options.subject,
200
+ messageId: (await transporter.sendMail({
201
+ from: formatEmailAddress(this.config.from.name, this.config.from.email),
202
+ to: (Array.isArray(options.to) ? options.to : [options.to]).map(stripCrlf).join(", "),
203
+ cc: options.cc ? (Array.isArray(options.cc) ? options.cc : [options.cc]).map(stripCrlf).join(", ") : void 0,
204
+ bcc: options.bcc ? (Array.isArray(options.bcc) ? options.bcc : [options.bcc]).map(stripCrlf).join(", ") : void 0,
205
+ subject: options.subject.replace(/[\r\n]/g, ""),
198
206
  html: options.html,
199
207
  text: options.text,
200
- replyTo: this.config.replyTo,
201
- headers: options.headers,
208
+ priority: options.priority === void 0 ? void 0 : options.priority <= 2 ? "high" : options.priority >= 4 ? "low" : "normal",
209
+ replyTo: this.config.replyTo ? stripCrlf(this.config.replyTo) : void 0,
210
+ headers: Object.keys(headers).length > 0 ? headers : void 0,
202
211
  attachments: options.attachments?.map((a) => ({
203
212
  filename: a.filename,
204
213
  content: a.content,
@@ -248,7 +257,7 @@ var NodemailerProvider = class {
248
257
  * ```
249
258
  */
250
259
  var ResendProvider = class {
251
- resend;
260
+ _client = null;
252
261
  config;
253
262
  /**
254
263
  * Create a new Resend provider instance
@@ -258,7 +267,12 @@ var ResendProvider = class {
258
267
  constructor(config) {
259
268
  this.config = config;
260
269
  if (!config.apiKey) throw new Error("Resend API key is required");
261
- this.resend = new Resend(config.apiKey);
270
+ }
271
+ async getClient() {
272
+ if (this._client) return this._client;
273
+ const { Resend } = await import("resend");
274
+ this._client = new Resend(this.config.apiKey);
275
+ return this._client;
262
276
  }
263
277
  /**
264
278
  * Validate Resend configuration
@@ -331,17 +345,27 @@ var ResendProvider = class {
331
345
  */
332
346
  async send(options) {
333
347
  try {
334
- const emailData = {
335
- from: `${this.config.from.name} <${this.config.from.email}>`,
336
- to: Array.isArray(options.to) ? options.to : [options.to],
337
- subject: options.subject,
348
+ const base = {
349
+ from: formatEmailAddress(this.config.from.name, this.config.from.email),
350
+ to: (Array.isArray(options.to) ? options.to : [options.to]).map(stripCrlf),
351
+ subject: options.subject.replace(/[\r\n]/g, "")
352
+ };
353
+ const emailData = options.html ? {
354
+ ...base,
355
+ html: options.html,
356
+ ...options.text ? { text: options.text } : {}
357
+ } : {
358
+ ...base,
338
359
  text: options.text || ""
339
360
  };
340
- if (options.cc) emailData.cc = Array.isArray(options.cc) ? options.cc : [options.cc];
341
- if (options.bcc) emailData.bcc = Array.isArray(options.bcc) ? options.bcc : [options.bcc];
342
- if (options.html) emailData.html = options.html;
343
- if (this.config.replyTo) emailData.replyTo = this.config.replyTo;
344
- if (options.headers) emailData.headers = options.headers;
361
+ if (options.cc) emailData.cc = (Array.isArray(options.cc) ? options.cc : [options.cc]).map(stripCrlf);
362
+ if (options.bcc) emailData.bcc = (Array.isArray(options.bcc) ? options.bcc : [options.bcc]).map(stripCrlf);
363
+ if (this.config.replyTo) emailData.replyTo = stripCrlf(this.config.replyTo);
364
+ if (options.headers) {
365
+ const sanitizedHeaders = {};
366
+ for (const [key, value] of Object.entries(options.headers)) sanitizedHeaders[key] = String(value).replace(/[\r\n]/g, "");
367
+ emailData.headers = sanitizedHeaders;
368
+ }
345
369
  if (options.tags) emailData.tags = Object.entries(options.tags).map(([name, value]) => ({
346
370
  name,
347
371
  value: String(value)
@@ -352,7 +376,13 @@ var ResendProvider = class {
352
376
  content_type: attachment.contentType,
353
377
  cid: attachment.cid
354
378
  }));
355
- const { data, error } = await this.resend.emails.send(emailData);
379
+ if (options.scheduledAt) emailData.scheduledAt = options.scheduledAt.toISOString();
380
+ if (options.priority) {
381
+ const headers = emailData.headers ?? {};
382
+ headers["X-Priority"] = String(options.priority);
383
+ emailData.headers = headers;
384
+ }
385
+ const { data, error } = await (await this.getClient()).emails.send(emailData);
356
386
  if (error) return {
357
387
  success: false,
358
388
  error: error.message
@@ -404,8 +434,13 @@ var EmailService = class {
404
434
  };
405
435
  }
406
436
  }
437
+ /**
438
+ * Returns a deep copy of the current configuration. A shallow clone would
439
+ * share the nested `from` / `smtp` / `auth` objects (which contain SMTP
440
+ * credentials), letting callers accidentally mutate the service's config.
441
+ */
407
442
  getConfig() {
408
- return { ...this.config };
443
+ return structuredClone(this.config);
409
444
  }
410
445
  updateConfig(config) {
411
446
  this.config = config;
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/config.ts","../src/providers/nodemailer.ts","../src/providers/resend.ts","../src/services.ts","../src/factory.ts"],"sourcesContent":["import type { ConfigValidationResult, EmailConfig, NodemailerConfig, ResendConfig } from \"./types\";\n\n/**\n * Cross-runtime environment record.\n * Pass `process.env` on Node.js, `ctx.env` on Cloudflare Workers,\n * `Deno.env.toObject()` on Deno, or a plain object in the browser.\n */\nexport type EnvRecord = Record<string, string | undefined>;\n\nfunction getDefaultEnv(): EnvRecord | undefined {\n if (typeof process !== \"undefined\" && process?.env) {\n return process.env as EnvRecord;\n }\n return undefined;\n}\n\nfunction resolveEnv(env?: EnvRecord): EnvRecord | undefined {\n return env ?? getDefaultEnv();\n}\n\nexport function loadResendConfig(env?: EnvRecord): ResendConfig | null {\n const e = resolveEnv(env);\n if (!e) return null;\n\n const apiKey = e.KUMIX_EMAIL_RESEND_API_KEY;\n const fromName = e.KUMIX_EMAIL_FROM_NAME;\n const fromEmail = e.KUMIX_EMAIL_FROM_EMAIL;\n const replyTo = e.KUMIX_EMAIL_REPLY_TO;\n\n if (!apiKey || !fromName || !fromEmail) return null;\n\n return { provider: \"resend\", apiKey, from: { name: fromName, email: fromEmail }, replyTo };\n}\n\nexport function loadNodemailerConfig(env?: EnvRecord): NodemailerConfig | null {\n const e = resolveEnv(env);\n if (!e) return null;\n\n const host = e.KUMIX_EMAIL_SMTP_HOST;\n const port = e.KUMIX_EMAIL_SMTP_PORT;\n const secure = e.KUMIX_EMAIL_SMTP_SECURE;\n const user = e.KUMIX_EMAIL_SMTP_USER;\n const pass = e.KUMIX_EMAIL_SMTP_PASS;\n const fromName = e.KUMIX_EMAIL_FROM_NAME;\n const fromEmail = e.KUMIX_EMAIL_FROM_EMAIL;\n const replyTo = e.KUMIX_EMAIL_REPLY_TO;\n\n if (!host || !port || !user || !pass || !fromName || !fromEmail) return null;\n\n return {\n provider: \"nodemailer\",\n from: { name: fromName, email: fromEmail },\n replyTo,\n smtp: { host, port: parseInt(port, 10), secure: secure === \"true\", auth: { user, pass } },\n };\n}\n\nexport function loadEmailConfig(env?: EnvRecord): EmailConfig | null {\n const e = resolveEnv(env);\n if (!e) return null;\n return loadResendConfig(e) || loadNodemailerConfig(e);\n}\n\nexport function hasEmailConfig(env?: EnvRecord): boolean {\n return loadEmailConfig(env) !== null;\n}\n\nexport function getEmailEnvVars(env?: EnvRecord): Record<string, string | undefined> {\n const e = resolveEnv(env);\n if (!e) return {};\n\n return {\n KUMIX_EMAIL_FROM_NAME: e.KUMIX_EMAIL_FROM_NAME,\n KUMIX_EMAIL_FROM_EMAIL: e.KUMIX_EMAIL_FROM_EMAIL,\n KUMIX_EMAIL_REPLY_TO: e.KUMIX_EMAIL_REPLY_TO,\n KUMIX_EMAIL_RESEND_API_KEY: e.KUMIX_EMAIL_RESEND_API_KEY ? \"***\" : undefined,\n KUMIX_EMAIL_SMTP_HOST: e.KUMIX_EMAIL_SMTP_HOST,\n KUMIX_EMAIL_SMTP_PORT: e.KUMIX_EMAIL_SMTP_PORT,\n KUMIX_EMAIL_SMTP_SECURE: e.KUMIX_EMAIL_SMTP_SECURE,\n KUMIX_EMAIL_SMTP_USER: e.KUMIX_EMAIL_SMTP_USER,\n KUMIX_EMAIL_SMTP_PASS: e.KUMIX_EMAIL_SMTP_PASS ? \"***\" : undefined,\n };\n}\n\nexport function validateResendEnvVars(env?: EnvRecord): ConfigValidationResult {\n const e = resolveEnv(env);\n if (!e) return { valid: false, missing: [\"env\"], errors: [\"No environment source available\"] };\n\n const missing: string[] = [];\n const errors: string[] = [];\n\n if (!e.KUMIX_EMAIL_RESEND_API_KEY && !e.RESEND_API_KEY) {\n missing.push(\"KUMIX_EMAIL_RESEND_API_KEY or RESEND_API_KEY\");\n }\n if (!e.KUMIX_EMAIL_FROM_NAME && !e.EMAIL_FROM_NAME) {\n missing.push(\"KUMIX_EMAIL_FROM_NAME or EMAIL_FROM_NAME\");\n }\n if (!e.KUMIX_EMAIL_FROM_EMAIL && !e.EMAIL_FROM_EMAIL) {\n missing.push(\"KUMIX_EMAIL_FROM_EMAIL or EMAIL_FROM_EMAIL\");\n }\n\n return { valid: missing.length === 0 && errors.length === 0, missing, errors };\n}\n\nexport function validateNodemailerEnvVars(env?: EnvRecord): ConfigValidationResult {\n const e = resolveEnv(env);\n if (!e) return { valid: false, missing: [\"env\"], errors: [\"No environment source available\"] };\n\n const missing: string[] = [];\n const errors: string[] = [];\n\n if (!e.KUMIX_EMAIL_SMTP_HOST && !e.SMTP_HOST) missing.push(\"KUMIX_EMAIL_SMTP_HOST or SMTP_HOST\");\n if (!e.KUMIX_EMAIL_SMTP_PORT && !e.SMTP_PORT) missing.push(\"KUMIX_EMAIL_SMTP_PORT or SMTP_PORT\");\n if (!e.KUMIX_EMAIL_SMTP_USER && !e.SMTP_USER) missing.push(\"KUMIX_EMAIL_SMTP_USER or SMTP_USER\");\n if (!e.KUMIX_EMAIL_SMTP_PASS && !e.SMTP_PASS) missing.push(\"KUMIX_EMAIL_SMTP_PASS or SMTP_PASS\");\n if (!e.KUMIX_EMAIL_FROM_NAME && !e.EMAIL_FROM_NAME)\n missing.push(\"KUMIX_EMAIL_FROM_NAME or EMAIL_FROM_NAME\");\n if (!e.KUMIX_EMAIL_FROM_EMAIL && !e.EMAIL_FROM_EMAIL)\n missing.push(\"KUMIX_EMAIL_FROM_EMAIL or EMAIL_FROM_EMAIL\");\n\n const port = e.KUMIX_EMAIL_SMTP_PORT || e.SMTP_PORT;\n if (port && (Number.isNaN(Number(port)) || Number(port) < 1 || Number(port) > 65535)) {\n errors.push(\"SMTP_PORT must be a valid port number (1-65535)\");\n }\n\n return { valid: missing.length === 0 && errors.length === 0, missing, errors };\n}\n\nexport function validateEmailEnvVars(env?: EnvRecord): ConfigValidationResult {\n const validations = [validateResendEnvVars(env), validateNodemailerEnvVars(env)];\n const validProvider = validations.find((v) => v.valid);\n if (validProvider) return validProvider;\n\n const missing = [...new Set(validations.flatMap((v) => v.missing))];\n const errors = [...new Set(validations.flatMap((v) => v.errors))];\n return { valid: false, missing, errors };\n}\n","/** biome-ignore-all lint/suspicious/noExplicitAny: We need to use any to avoid type errors. */\n\nimport type {\n EmailResult,\n EmailValidationResult,\n IEmailProvider,\n NodemailerConfig,\n SendEmailOptions,\n} from \"../types\";\n\nexport class NodemailerProvider implements IEmailProvider {\n private config: NodemailerConfig;\n private _transporter: any = null;\n\n constructor(config: NodemailerConfig) {\n this.config = config;\n if (!config.smtp) {\n throw new Error(\"SMTP configuration is required for Nodemailer\");\n }\n }\n\n private async getTransporter(): Promise<any> {\n if (this._transporter) return this._transporter;\n\n let nodemailer: any;\n try {\n nodemailer = await import(\"nodemailer\");\n } catch {\n throw new Error(\n \"nodemailer is not available in this runtime. \" +\n \"Use a Node.js environment or provide a Resend configuration instead.\",\n );\n }\n\n const mailer = nodemailer.default ?? nodemailer;\n this._transporter = mailer.createTransport({\n host: this.config.smtp.host,\n port: this.config.smtp.port,\n secure: this.config.smtp.secure,\n auth: {\n user: this.config.smtp.auth.user,\n pass: this.config.smtp.auth.pass,\n },\n });\n return this._transporter;\n }\n\n async validateConfig(): Promise<EmailValidationResult> {\n try {\n if (!this.config.smtp) {\n return { valid: false, error: \"SMTP configuration is required\" };\n }\n if (!this.config.smtp.host || !this.config.smtp.port) {\n return { valid: false, error: \"SMTP host and port are required\" };\n }\n if (!this.config.smtp.auth.user || !this.config.smtp.auth.pass) {\n return { valid: false, error: \"SMTP authentication credentials are required\" };\n }\n if (!this.config.from.email || !this.config.from.name) {\n return { valid: false, error: \"From email and name are required\" };\n }\n\n const transporter = await this.getTransporter();\n await transporter.verify();\n return { valid: true };\n } catch (error) {\n return {\n valid: false,\n error: error instanceof Error ? error.message : \"SMTP connection failed\",\n };\n }\n }\n\n async send(options: SendEmailOptions): Promise<EmailResult> {\n try {\n const transporter = await this.getTransporter();\n const info: any = await transporter.sendMail({\n from: `${this.config.from.name} <${this.config.from.email}>`,\n to: Array.isArray(options.to) ? options.to.join(\", \") : options.to,\n cc: options.cc\n ? Array.isArray(options.cc)\n ? options.cc.join(\", \")\n : options.cc\n : undefined,\n bcc: options.bcc\n ? Array.isArray(options.bcc)\n ? options.bcc.join(\", \")\n : options.bcc\n : undefined,\n subject: options.subject,\n html: options.html,\n text: options.text,\n replyTo: this.config.replyTo,\n headers: options.headers,\n attachments: options.attachments?.map((a) => ({\n filename: a.filename,\n content: a.content as any,\n contentType: a.contentType,\n cid: a.cid,\n })),\n });\n\n return { success: true, messageId: info.messageId };\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : \"Unknown error occurred\",\n };\n }\n }\n}\n","/**\n * Resend email provider implementation\n * Provides email sending functionality using the Resend service\n */\n\nimport type { CreateEmailOptions } from \"resend\";\nimport { Resend } from \"resend\";\n\nimport type {\n EmailResult,\n EmailValidationResult,\n IEmailProvider,\n ResendConfig,\n SendEmailOptions,\n} from \"../types\";\n\n/**\n * Resend email provider implementation\n * @public\n *\n * @example\n * ```typescript\n * import { ResendProvider } from '@kumix/email';\n *\n * const provider = new ResendProvider({\n * provider: 'resend',\n * apiKey: 'your-resend-api-key',\n * from: {\n * name: 'Your App',\n * email: 'noreply@yourapp.com'\n * }\n * });\n *\n * const result = await provider.send({\n * to: 'user@example.com',\n * subject: 'Hello',\n * html: '<h1>Hello World</h1>'\n * });\n *\n * if (result.success) {\n * console.log('Email sent with ID:', result.messageId);\n * } else {\n * console.error('Failed to send email:', result.error);\n * }\n * ```\n */\nexport class ResendProvider implements IEmailProvider {\n private resend: Resend;\n private config: ResendConfig;\n\n /**\n * Create a new Resend provider instance\n * @param config Resend configuration including API key and sender info\n * @throws Error if API key is missing\n */\n constructor(config: ResendConfig) {\n this.config = config;\n\n if (!config.apiKey) {\n throw new Error(\"Resend API key is required\");\n }\n\n this.resend = new Resend(config.apiKey);\n }\n\n /**\n * Validate Resend configuration\n * @returns Promise resolving to validation result\n * @public\n *\n * @example\n * ```typescript\n * const validation = await provider.validateConfig();\n * if (!validation.valid) {\n * console.error('Configuration error:', validation.error);\n * } else {\n * console.log('Resend configuration is valid');\n * }\n * ```\n */\n async validateConfig(): Promise<EmailValidationResult> {\n try {\n if (!this.config.apiKey) {\n return {\n valid: false,\n error: \"Resend API key is required\",\n };\n }\n\n if (!this.config.from.email || !this.config.from.name) {\n return {\n valid: false,\n error: \"From email and name are required\",\n };\n }\n\n // Test API key by making a simple request\n // Note: Resend doesn't have a dedicated validation endpoint,\n // so we'll just check if the API key format is valid\n if (!this.config.apiKey.startsWith(\"re_\")) {\n return {\n valid: false,\n error: \"Invalid Resend API key format\",\n };\n }\n\n return { valid: true };\n } catch (error) {\n return {\n valid: false,\n error: error instanceof Error ? error.message : \"Unknown validation error\",\n };\n }\n }\n\n /**\n * Send an email using Resend\n * @param options Email sending options\n * @returns Promise resolving to email sending result\n * @public\n *\n * @example\n * ```typescript\n * const result = await provider.send({\n * to: ['user1@example.com', 'user2@example.com'],\n * cc: 'manager@example.com',\n * subject: 'Important Update',\n * html: '<h1>Hello</h1><p>This is an important update.</p>',\n * text: 'Hello\\n\\nThis is an important update.',\n * attachments: [{\n * filename: 'document.pdf',\n * content: pdfBuffer,\n * contentType: 'application/pdf'\n * }],\n * tags: {\n * category: 'notification',\n * priority: 'high'\n * }\n * });\n *\n * if (result.success) {\n * console.log('Email sent with ID:', result.messageId);\n * } else {\n * console.error('Failed to send email:', result.error);\n * }\n * ```\n */\n async send(options: SendEmailOptions): Promise<EmailResult> {\n try {\n const emailData: CreateEmailOptions = {\n from: `${this.config.from.name} <${this.config.from.email}>`,\n to: Array.isArray(options.to) ? options.to : [options.to],\n subject: options.subject,\n text: options.text || \"\",\n };\n\n if (options.cc) {\n emailData.cc = Array.isArray(options.cc) ? options.cc : [options.cc];\n }\n\n if (options.bcc) {\n emailData.bcc = Array.isArray(options.bcc) ? options.bcc : [options.bcc];\n }\n\n if (options.html) {\n emailData.html = options.html;\n }\n\n if (this.config.replyTo) {\n emailData.replyTo = this.config.replyTo;\n }\n\n if (options.headers) {\n emailData.headers = options.headers;\n }\n\n if (options.tags) {\n emailData.tags = Object.entries(options.tags).map(([name, value]) => ({\n name,\n value: String(value),\n }));\n }\n\n if (options.attachments) {\n emailData.attachments = options.attachments.map((attachment) => ({\n filename: attachment.filename,\n content: attachment.content as string | Buffer,\n content_type: attachment.contentType,\n cid: attachment.cid,\n }));\n }\n\n const { data, error } = await this.resend.emails.send(emailData);\n\n if (error) {\n return {\n success: false,\n error: error.message,\n };\n }\n\n return {\n success: true,\n messageId: data?.id,\n };\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : \"Unknown error occurred\",\n };\n }\n }\n}\n","import type React from \"react\";\n\nimport { htmlToText, renderEmailTemplate } from \"./helpers\";\nimport { NodemailerProvider } from \"./providers/nodemailer\";\nimport { ResendProvider } from \"./providers/resend\";\nimport type {\n EmailConfig,\n EmailResult,\n EmailTemplateData,\n IEmailProvider,\n NodemailerConfig,\n ResendConfig,\n SendEmailOptions,\n} from \"./types\";\n\nexport class EmailService {\n private provider: IEmailProvider;\n private config: EmailConfig;\n\n constructor(config: EmailConfig) {\n this.config = config;\n this.provider = this.createProvider(config);\n }\n\n private createProvider(config: EmailConfig): IEmailProvider {\n switch (config.provider) {\n case \"resend\":\n return new ResendProvider(config as ResendConfig);\n case \"nodemailer\":\n return new NodemailerProvider(config as NodemailerConfig);\n default:\n throw new Error(`Unsupported email provider: ${(config as EmailConfig).provider}`);\n }\n }\n\n async sendEmail(options: SendEmailOptions): Promise<EmailResult> {\n return this.provider.send(options);\n }\n\n async sendTemplate<T extends EmailTemplateData>(\n Template: React.ComponentType<T>,\n props: T,\n options: Omit<SendEmailOptions, \"html\" | \"text\">,\n ): Promise<EmailResult> {\n try {\n const html = await renderEmailTemplate(Template, props);\n const text = htmlToText(html);\n\n return this.sendEmail({ ...options, html, text });\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : \"Failed to render template\",\n };\n }\n }\n\n getConfig(): EmailConfig {\n return { ...this.config };\n }\n\n updateConfig(config: EmailConfig): void {\n this.config = config;\n this.provider = this.createProvider(config);\n }\n\n async validateConfig() {\n if (this.provider.validateConfig) {\n return this.provider.validateConfig();\n }\n return { valid: true };\n }\n}\n","import { type EnvRecord, hasEmailConfig, loadEmailConfig } from \"./config\";\nimport { EmailService } from \"./services\";\nimport type { EmailConfig, EmailProvider, NodemailerConfig, ResendConfig } from \"./types\";\n\nexport type { EnvRecord } from \"./config\";\n\nexport function createEmail(config?: EmailConfig, env?: EnvRecord): EmailService {\n if (config) return new EmailService(config);\n\n const envConfig = loadEmailConfig(env);\n if (!envConfig) {\n throw new Error(\n \"No email configuration provided and none found in environment variables. \" +\n \"Please provide a config object or set environment variables like KUMIX_EMAIL_RESEND_API_KEY, etc.\",\n );\n }\n return new EmailService(envConfig);\n}\n\nexport function createResend(\n config?: Omit<ResendConfig, \"provider\"> & { provider?: \"resend\" },\n env?: EnvRecord,\n): EmailService {\n if (config) return createEmail({ ...config, provider: \"resend\" });\n\n const envConfig = loadEmailConfig(env);\n if (envConfig?.provider === \"resend\") return createEmail(envConfig);\n\n throw new Error(\n \"No Resend configuration found. Please provide config or set environment variables: \" +\n \"KUMIX_EMAIL_RESEND_API_KEY, KUMIX_EMAIL_FROM_NAME, KUMIX_EMAIL_FROM_EMAIL\",\n );\n}\n\nexport function createNodemailer(\n config?: Omit<NodemailerConfig, \"provider\"> & { provider?: \"nodemailer\" },\n env?: EnvRecord,\n): EmailService {\n if (config) return createEmail({ ...config, provider: \"nodemailer\" });\n\n const envConfig = loadEmailConfig(env);\n if (envConfig?.provider === \"nodemailer\") return createEmail(envConfig);\n\n throw new Error(\n \"No Nodemailer configuration found. Please provide config or set environment variables: \" +\n \"KUMIX_EMAIL_SMTP_HOST, KUMIX_EMAIL_SMTP_PORT, KUMIX_EMAIL_SMTP_USER, KUMIX_EMAIL_SMTP_PASS, KUMIX_EMAIL_FROM_NAME, KUMIX_EMAIL_FROM_EMAIL\",\n );\n}\n\nexport function isEmailConfigured(env?: EnvRecord): boolean {\n return hasEmailConfig(env);\n}\n\nexport function getConfiguredProvider(env?: EnvRecord): EmailProvider | null {\n const config = loadEmailConfig(env);\n return config?.provider || null;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["_exhaustive"],"sources":["../src/config.ts","../src/providers/nodemailer.ts","../src/providers/resend.ts","../src/services.ts","../src/factory.ts"],"sourcesContent":["import type { ConfigValidationResult, EmailConfig, NodemailerConfig, ResendConfig } from \"./types\";\n\n/**\n * Cross-runtime environment record.\n * Pass `process.env` on Node.js, `ctx.env` on Cloudflare Workers,\n * `Deno.env.toObject()` on Deno, or a plain object in the browser.\n */\nexport type EnvRecord = Record<string, string | undefined>;\n\nfunction getDefaultEnv(): EnvRecord | undefined {\n if (typeof process !== \"undefined\" && process?.env) {\n return process.env as EnvRecord;\n }\n return undefined;\n}\n\nfunction resolveEnv(env?: EnvRecord): EnvRecord | undefined {\n return env ?? getDefaultEnv();\n}\n\nexport function loadResendConfig(env?: EnvRecord): ResendConfig | null {\n const e = resolveEnv(env);\n if (!e) return null;\n\n // Trim before the presence check so whitespace-only values are not accepted\n // as valid (previously `\" \"` passed and produced a config with empty\n // credentials). Do NOT trim `apiKey`/`pass` — leading/trailing characters\n // can be meaningful in tokens.\n const apiKey = e.KUMIX_EMAIL_RESEND_API_KEY ?? e.RESEND_API_KEY;\n const fromName = (e.KUMIX_EMAIL_FROM_NAME ?? e.EMAIL_FROM_NAME)?.trim();\n const fromEmail = (e.KUMIX_EMAIL_FROM_EMAIL ?? e.EMAIL_FROM_EMAIL)?.trim();\n const replyTo = (e.KUMIX_EMAIL_REPLY_TO ?? e.EMAIL_REPLY_TO)?.trim();\n\n if (!apiKey || !fromName || !fromEmail) return null;\n\n return { provider: \"resend\", apiKey, from: { name: fromName, email: fromEmail }, replyTo };\n}\n\nexport function loadNodemailerConfig(env?: EnvRecord): NodemailerConfig | null {\n const e = resolveEnv(env);\n if (!e) return null;\n\n const host = (e.KUMIX_EMAIL_SMTP_HOST ?? e.SMTP_HOST)?.trim();\n const port = e.KUMIX_EMAIL_SMTP_PORT ?? e.SMTP_PORT;\n const secure = e.KUMIX_EMAIL_SMTP_SECURE ?? e.SMTP_SECURE;\n const user = e.KUMIX_EMAIL_SMTP_USER ?? e.SMTP_USER;\n const pass = e.KUMIX_EMAIL_SMTP_PASS ?? e.SMTP_PASS;\n const fromName = (e.KUMIX_EMAIL_FROM_NAME ?? e.EMAIL_FROM_NAME)?.trim();\n const fromEmail = (e.KUMIX_EMAIL_FROM_EMAIL ?? e.EMAIL_FROM_EMAIL)?.trim();\n const replyTo = (e.KUMIX_EMAIL_REPLY_TO ?? e.EMAIL_REPLY_TO)?.trim();\n\n if (!host || !port || !user || !pass || !fromName || !fromEmail) return null;\n\n const parsedPort = parseInt(port, 10);\n if (Number.isNaN(parsedPort) || parsedPort < 1 || parsedPort > 65535) return null;\n\n // Lowercase once so capitalized spellings (\"True\", \"TRUE\") also enable TLS.\n const secureNorm = typeof secure === \"string\" ? secure.toLowerCase() : \"\";\n\n return {\n provider: \"nodemailer\",\n from: { name: fromName, email: fromEmail },\n replyTo,\n smtp: {\n host,\n port: parsedPort,\n secure: secureNorm === \"true\" || secureNorm === \"1\" || secureNorm === \"yes\",\n auth: { user, pass },\n },\n };\n}\n\nexport function loadEmailConfig(env?: EnvRecord): EmailConfig | null {\n const e = resolveEnv(env);\n if (!e) return null;\n return loadResendConfig(e) || loadNodemailerConfig(e);\n}\n\nexport function hasEmailConfig(env?: EnvRecord): boolean {\n return loadEmailConfig(env) !== null;\n}\n\nexport function getEmailEnvVars(env?: EnvRecord): Record<string, string | undefined> {\n const e = resolveEnv(env);\n if (!e) return {};\n\n return {\n KUMIX_EMAIL_FROM_NAME: e.KUMIX_EMAIL_FROM_NAME,\n KUMIX_EMAIL_FROM_EMAIL: e.KUMIX_EMAIL_FROM_EMAIL,\n KUMIX_EMAIL_REPLY_TO: e.KUMIX_EMAIL_REPLY_TO,\n KUMIX_EMAIL_RESEND_API_KEY: e.KUMIX_EMAIL_RESEND_API_KEY ? \"***\" : undefined,\n KUMIX_EMAIL_SMTP_HOST: e.KUMIX_EMAIL_SMTP_HOST,\n KUMIX_EMAIL_SMTP_PORT: e.KUMIX_EMAIL_SMTP_PORT,\n KUMIX_EMAIL_SMTP_SECURE: e.KUMIX_EMAIL_SMTP_SECURE,\n KUMIX_EMAIL_SMTP_USER: e.KUMIX_EMAIL_SMTP_USER,\n KUMIX_EMAIL_SMTP_PASS: e.KUMIX_EMAIL_SMTP_PASS ? \"***\" : undefined,\n };\n}\n\nexport function validateResendEnvVars(env?: EnvRecord): ConfigValidationResult {\n const e = resolveEnv(env);\n if (!e) return { valid: false, missing: [\"env\"], errors: [\"No environment source available\"] };\n\n const missing: string[] = [];\n const errors: string[] = [];\n\n if (!e.KUMIX_EMAIL_RESEND_API_KEY && !e.RESEND_API_KEY) {\n missing.push(\"KUMIX_EMAIL_RESEND_API_KEY or RESEND_API_KEY\");\n }\n if (!e.KUMIX_EMAIL_FROM_NAME && !e.EMAIL_FROM_NAME) {\n missing.push(\"KUMIX_EMAIL_FROM_NAME or EMAIL_FROM_NAME\");\n }\n if (!e.KUMIX_EMAIL_FROM_EMAIL && !e.EMAIL_FROM_EMAIL) {\n missing.push(\"KUMIX_EMAIL_FROM_EMAIL or EMAIL_FROM_EMAIL\");\n }\n\n return { valid: missing.length === 0 && errors.length === 0, missing, errors };\n}\n\nexport function validateNodemailerEnvVars(env?: EnvRecord): ConfigValidationResult {\n const e = resolveEnv(env);\n if (!e) return { valid: false, missing: [\"env\"], errors: [\"No environment source available\"] };\n\n const missing: string[] = [];\n const errors: string[] = [];\n\n if (!e.KUMIX_EMAIL_SMTP_HOST && !e.SMTP_HOST) missing.push(\"KUMIX_EMAIL_SMTP_HOST or SMTP_HOST\");\n if (!e.KUMIX_EMAIL_SMTP_PORT && !e.SMTP_PORT) missing.push(\"KUMIX_EMAIL_SMTP_PORT or SMTP_PORT\");\n if (!e.KUMIX_EMAIL_SMTP_USER && !e.SMTP_USER) missing.push(\"KUMIX_EMAIL_SMTP_USER or SMTP_USER\");\n if (!e.KUMIX_EMAIL_SMTP_PASS && !e.SMTP_PASS) missing.push(\"KUMIX_EMAIL_SMTP_PASS or SMTP_PASS\");\n if (!e.KUMIX_EMAIL_FROM_NAME && !e.EMAIL_FROM_NAME)\n missing.push(\"KUMIX_EMAIL_FROM_NAME or EMAIL_FROM_NAME\");\n if (!e.KUMIX_EMAIL_FROM_EMAIL && !e.EMAIL_FROM_EMAIL)\n missing.push(\"KUMIX_EMAIL_FROM_EMAIL or EMAIL_FROM_EMAIL\");\n\n const port = e.KUMIX_EMAIL_SMTP_PORT || e.SMTP_PORT;\n if (port && (Number.isNaN(Number(port)) || Number(port) < 1 || Number(port) > 65535)) {\n errors.push(\"SMTP_PORT must be a valid port number (1-65535)\");\n }\n\n return { valid: missing.length === 0 && errors.length === 0, missing, errors };\n}\n\nexport function validateEmailEnvVars(env?: EnvRecord): ConfigValidationResult {\n const validations = [validateResendEnvVars(env), validateNodemailerEnvVars(env)];\n const validProvider = validations.find((v) => v.valid);\n if (validProvider) return validProvider;\n\n const missing = [...new Set(validations.flatMap((v) => v.missing))];\n const errors = [...new Set(validations.flatMap((v) => v.errors))];\n return { valid: false, missing, errors };\n}\n","/** biome-ignore-all lint/suspicious/noExplicitAny: We need to use any to avoid type errors. */\n\nimport { formatEmailAddress, stripCrlf } from \"../helpers\";\nimport type {\n EmailResult,\n EmailValidationResult,\n IEmailProvider,\n NodemailerConfig,\n SendEmailOptions,\n} from \"../types\";\n\nexport class NodemailerProvider implements IEmailProvider {\n private config: NodemailerConfig;\n private _transporter: any = null;\n\n constructor(config: NodemailerConfig) {\n this.config = config;\n if (!config.smtp) {\n throw new Error(\"SMTP configuration is required for Nodemailer\");\n }\n }\n\n private async getTransporter(): Promise<any> {\n if (this._transporter) return this._transporter;\n\n let nodemailer: any;\n try {\n nodemailer = await import(\"nodemailer\");\n } catch {\n throw new Error(\n \"nodemailer is not available in this runtime. \" +\n \"Use a Node.js environment or provide a Resend configuration instead.\",\n );\n }\n\n const mailer = nodemailer.default ?? nodemailer;\n this._transporter = mailer.createTransport({\n host: this.config.smtp.host,\n port: this.config.smtp.port,\n secure: this.config.smtp.secure,\n auth: {\n user: this.config.smtp.auth.user,\n pass: this.config.smtp.auth.pass,\n },\n });\n return this._transporter;\n }\n\n async validateConfig(): Promise<EmailValidationResult> {\n try {\n if (!this.config.smtp) {\n return { valid: false, error: \"SMTP configuration is required\" };\n }\n if (!this.config.smtp.host || !this.config.smtp.port) {\n return { valid: false, error: \"SMTP host and port are required\" };\n }\n if (!this.config.smtp.auth.user || !this.config.smtp.auth.pass) {\n return { valid: false, error: \"SMTP authentication credentials are required\" };\n }\n if (!this.config.from.email || !this.config.from.name) {\n return { valid: false, error: \"From email and name are required\" };\n }\n\n const transporter = await this.getTransporter();\n await transporter.verify();\n return { valid: true };\n } catch (error) {\n return {\n valid: false,\n error: error instanceof Error ? error.message : \"SMTP connection failed\",\n };\n }\n }\n\n async send(options: SendEmailOptions): Promise<EmailResult> {\n try {\n const transporter = await this.getTransporter();\n\n // Build the outgoing headers up front so tags/priority can be merged in a\n // single place. SMTP has no native tag concept, so forward each tag as an\n // `X-Tag-<name>` header instead of silently dropping it (Resend forwards\n // tags natively).\n const headers: Record<string, string> = {};\n if (options.headers) {\n for (const [k, v] of Object.entries(options.headers)) {\n headers[k] = String(v).replace(/[\\r\\n]/g, \"\");\n }\n }\n if (options.tags) {\n for (const [name, value] of Object.entries(options.tags)) {\n headers[`X-Tag-${name.replace(/[\\r\\n]/g, \"\")}`] = String(value).replace(/[\\r\\n]/g, \"\");\n }\n }\n // Always also set the numeric X-Priority header for clients that ignore\n // the symbolic priority field.\n if (options.priority) {\n headers[\"X-Priority\"] = String(options.priority);\n }\n\n const info: any = await transporter.sendMail({\n // Route `from` through formatEmailAddress so the display name is\n // quoted/escaped and CR/LF is stripped (header-injection hardening).\n from: formatEmailAddress(this.config.from.name, this.config.from.email),\n to: (Array.isArray(options.to) ? options.to : [options.to]).map(stripCrlf).join(\", \"),\n cc: options.cc\n ? (Array.isArray(options.cc) ? options.cc : [options.cc]).map(stripCrlf).join(\", \")\n : undefined,\n bcc: options.bcc\n ? (Array.isArray(options.bcc) ? options.bcc : [options.bcc]).map(stripCrlf).join(\", \")\n : undefined,\n subject: options.subject.replace(/[\\r\\n]/g, \"\"),\n html: options.html,\n text: options.text,\n // Forward priority (1=highest .. 5=lowest) — nodemailer supports both\n // `priority` (high/normal/low) and the X-Priority header.\n priority:\n options.priority === undefined\n ? undefined\n : options.priority <= 2\n ? \"high\"\n : options.priority >= 4\n ? \"low\"\n : \"normal\",\n replyTo: this.config.replyTo ? stripCrlf(this.config.replyTo) : undefined,\n headers: Object.keys(headers).length > 0 ? headers : undefined,\n attachments: options.attachments?.map((a) => ({\n filename: a.filename,\n content: a.content as any,\n contentType: a.contentType,\n cid: a.cid,\n })),\n });\n\n return { success: true, messageId: info.messageId };\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : \"Unknown error occurred\",\n };\n }\n }\n}\n","/**\n * Resend email provider implementation\n * Provides email sending functionality using the Resend service\n */\n\nimport type { CreateEmailOptions, Resend } from \"resend\";\n\nimport { formatEmailAddress, stripCrlf } from \"../helpers\";\nimport type {\n EmailResult,\n EmailValidationResult,\n IEmailProvider,\n ResendConfig,\n SendEmailOptions,\n} from \"../types\";\n\n/**\n * Resend email provider implementation\n * @public\n *\n * @example\n * ```typescript\n * import { ResendProvider } from '@kumix/email';\n *\n * const provider = new ResendProvider({\n * provider: 'resend',\n * apiKey: 'your-resend-api-key',\n * from: {\n * name: 'Your App',\n * email: 'noreply@yourapp.com'\n * }\n * });\n *\n * const result = await provider.send({\n * to: 'user@example.com',\n * subject: 'Hello',\n * html: '<h1>Hello World</h1>'\n * });\n *\n * if (result.success) {\n * console.log('Email sent with ID:', result.messageId);\n * } else {\n * console.error('Failed to send email:', result.error);\n * }\n * ```\n */\nexport class ResendProvider implements IEmailProvider {\n private _client: Resend | null = null;\n private config: ResendConfig;\n\n /**\n * Create a new Resend provider instance\n * @param config Resend configuration including API key and sender info\n * @throws Error if API key is missing\n */\n constructor(config: ResendConfig) {\n this.config = config;\n\n if (!config.apiKey) {\n throw new Error(\"Resend API key is required\");\n }\n }\n\n // The SDK is loaded lazily so the `resend` peer dep isn't required at module\n // load time. This lets consumers who only use Nodemailer import the package\n // without installing `resend`.\n private async getClient(): Promise<Resend> {\n if (this._client) return this._client;\n const { Resend } = await import(\"resend\");\n this._client = new Resend(this.config.apiKey);\n return this._client;\n }\n\n /**\n * Validate Resend configuration\n * @returns Promise resolving to validation result\n * @public\n *\n * @example\n * ```typescript\n * const validation = await provider.validateConfig();\n * if (!validation.valid) {\n * console.error('Configuration error:', validation.error);\n * } else {\n * console.log('Resend configuration is valid');\n * }\n * ```\n */\n async validateConfig(): Promise<EmailValidationResult> {\n try {\n if (!this.config.apiKey) {\n return {\n valid: false,\n error: \"Resend API key is required\",\n };\n }\n\n if (!this.config.from.email || !this.config.from.name) {\n return {\n valid: false,\n error: \"From email and name are required\",\n };\n }\n\n // Resend has no dedicated credential-verification endpoint (unlike SMTP's\n // `transporter.verify()`), so validation is limited to a format check on\n // the API key. This intentionally does NOT contact the Resend API.\n if (!this.config.apiKey.startsWith(\"re_\")) {\n return {\n valid: false,\n error: \"Invalid Resend API key format\",\n };\n }\n\n return { valid: true };\n } catch (error) {\n return {\n valid: false,\n error: error instanceof Error ? error.message : \"Unknown validation error\",\n };\n }\n }\n\n /**\n * Send an email using Resend\n * @param options Email sending options\n * @returns Promise resolving to email sending result\n * @public\n *\n * @example\n * ```typescript\n * const result = await provider.send({\n * to: ['user1@example.com', 'user2@example.com'],\n * cc: 'manager@example.com',\n * subject: 'Important Update',\n * html: '<h1>Hello</h1><p>This is an important update.</p>',\n * text: 'Hello\\n\\nThis is an important update.',\n * attachments: [{\n * filename: 'document.pdf',\n * content: pdfBuffer,\n * contentType: 'application/pdf'\n * }],\n * tags: {\n * category: 'notification',\n * priority: 'high'\n * }\n * });\n *\n * if (result.success) {\n * console.log('Email sent with ID:', result.messageId);\n * } else {\n * console.error('Failed to send email:', result.error);\n * }\n * ```\n */\n async send(options: SendEmailOptions): Promise<EmailResult> {\n try {\n // Build the message body. Resend requires at least one of html/text/react/template,\n // and forcing `text: \"\"` would suppress its automatic text generation from html.\n // - When `html` is provided, send `html` (Resend derives the text part). If the\n // caller also provided an explicit `text`, send that too instead of the derived one.\n // - When no `html` is provided, fall back to `text` (empty string as the last resort).\n const base = {\n // Route `from` through formatEmailAddress so the display name is\n // quoted/escaped and CR/LF is stripped (header-injection hardening).\n from: formatEmailAddress(this.config.from.name, this.config.from.email),\n to: (Array.isArray(options.to) ? options.to : [options.to]).map(stripCrlf),\n subject: options.subject.replace(/[\\r\\n]/g, \"\"),\n };\n\n const emailData: CreateEmailOptions = options.html\n ? { ...base, html: options.html, ...(options.text ? { text: options.text } : {}) }\n : { ...base, text: options.text || \"\" };\n\n if (options.cc) {\n emailData.cc = (Array.isArray(options.cc) ? options.cc : [options.cc]).map(stripCrlf);\n }\n\n if (options.bcc) {\n emailData.bcc = (Array.isArray(options.bcc) ? options.bcc : [options.bcc]).map(stripCrlf);\n }\n\n if (this.config.replyTo) {\n emailData.replyTo = stripCrlf(this.config.replyTo);\n }\n\n if (options.headers) {\n const sanitizedHeaders: Record<string, string> = {};\n for (const [key, value] of Object.entries(options.headers)) {\n sanitizedHeaders[key] = String(value).replace(/[\\r\\n]/g, \"\");\n }\n emailData.headers = sanitizedHeaders;\n }\n\n if (options.tags) {\n emailData.tags = Object.entries(options.tags).map(([name, value]) => ({\n name,\n value: String(value),\n }));\n }\n\n if (options.attachments) {\n emailData.attachments = options.attachments.map((attachment) => ({\n filename: attachment.filename,\n content: attachment.content as string | Buffer,\n content_type: attachment.contentType,\n cid: attachment.cid,\n }));\n }\n\n // Forward scheduled delivery to Resend (`scheduled_at`). Previously this\n // option was silently dropped and emails were sent immediately even when\n // the caller asked for a future delivery time.\n if (options.scheduledAt) {\n emailData.scheduledAt = options.scheduledAt.toISOString();\n }\n\n // Forward priority (1=highest .. 5=lowest) via the standard X-Priority\n // header, since Resend has no dedicated field for it.\n if (options.priority) {\n const headers = emailData.headers ?? {};\n headers[\"X-Priority\"] = String(options.priority);\n emailData.headers = headers;\n }\n\n const resend = await this.getClient();\n const { data, error } = await resend.emails.send(emailData);\n\n if (error) {\n return {\n success: false,\n error: error.message,\n };\n }\n\n return {\n success: true,\n messageId: data?.id,\n };\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : \"Unknown error occurred\",\n };\n }\n }\n}\n","import type React from \"react\";\n\nimport { htmlToText, renderEmailTemplate } from \"./helpers\";\nimport { NodemailerProvider } from \"./providers/nodemailer\";\nimport { ResendProvider } from \"./providers/resend\";\nimport type {\n EmailConfig,\n EmailResult,\n EmailTemplateData,\n EmailValidationResult,\n IEmailProvider,\n SendEmailOptions,\n} from \"./types\";\n\nexport class EmailService {\n private provider: IEmailProvider;\n private config: EmailConfig;\n\n constructor(config: EmailConfig) {\n this.config = config;\n this.provider = this.createProvider(config);\n }\n\n private createProvider(config: EmailConfig): IEmailProvider {\n // The `switch` narrows `config` to the concrete variant; no cast needed.\n switch (config.provider) {\n case \"resend\":\n return new ResendProvider(config);\n case \"nodemailer\":\n return new NodemailerProvider(config);\n default: {\n // Exhaustiveness check — if a new provider is added to the union\n // without a case here, this assignment fails to type-check.\n const _exhaustive: never = config;\n throw new Error(`Unsupported email provider: ${(_exhaustive as EmailConfig).provider}`);\n }\n }\n }\n\n async sendEmail(options: SendEmailOptions): Promise<EmailResult> {\n return this.provider.send(options);\n }\n\n async sendTemplate<T extends EmailTemplateData>(\n Template: React.ComponentType<T>,\n props: T,\n options: Omit<SendEmailOptions, \"html\" | \"text\">,\n ): Promise<EmailResult> {\n try {\n const html = await renderEmailTemplate(Template, props);\n const text = htmlToText(html);\n\n return this.sendEmail({ ...options, html, text });\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : \"Failed to render template\",\n };\n }\n }\n\n /**\n * Returns a deep copy of the current configuration. A shallow clone would\n * share the nested `from` / `smtp` / `auth` objects (which contain SMTP\n * credentials), letting callers accidentally mutate the service's config.\n */\n getConfig(): EmailConfig {\n return structuredClone(this.config);\n }\n\n updateConfig(config: EmailConfig): void {\n this.config = config;\n this.provider = this.createProvider(config);\n }\n\n async validateConfig(): Promise<EmailValidationResult> {\n if (this.provider.validateConfig) {\n return this.provider.validateConfig();\n }\n return { valid: true };\n }\n}\n","import { type EnvRecord, hasEmailConfig, loadEmailConfig } from \"./config\";\nimport { EmailService } from \"./services\";\nimport type { EmailConfig, EmailProvider, NodemailerConfig, ResendConfig } from \"./types\";\n\nexport function createEmail(config?: EmailConfig, env?: EnvRecord): EmailService {\n if (config) return new EmailService(config);\n\n const envConfig = loadEmailConfig(env);\n if (!envConfig) {\n throw new Error(\n \"No email configuration provided and none found in environment variables. \" +\n \"Please provide a config object or set environment variables like KUMIX_EMAIL_RESEND_API_KEY, etc.\",\n );\n }\n return new EmailService(envConfig);\n}\n\nexport function createResend(\n config?: Omit<ResendConfig, \"provider\"> & { provider?: \"resend\" },\n env?: EnvRecord,\n): EmailService {\n if (config) return createEmail({ ...config, provider: \"resend\" });\n\n const envConfig = loadEmailConfig(env);\n if (envConfig?.provider === \"resend\") return createEmail(envConfig);\n\n throw new Error(\n \"No Resend configuration found. Please provide config or set environment variables: \" +\n \"KUMIX_EMAIL_RESEND_API_KEY, KUMIX_EMAIL_FROM_NAME, KUMIX_EMAIL_FROM_EMAIL\",\n );\n}\n\nexport function createNodemailer(\n config?: Omit<NodemailerConfig, \"provider\"> & { provider?: \"nodemailer\" },\n env?: EnvRecord,\n): EmailService {\n if (config) return createEmail({ ...config, provider: \"nodemailer\" });\n\n const envConfig = loadEmailConfig(env);\n if (envConfig?.provider === \"nodemailer\") return createEmail(envConfig);\n\n throw new Error(\n \"No Nodemailer configuration found. Please provide config or set environment variables: \" +\n \"KUMIX_EMAIL_SMTP_HOST, KUMIX_EMAIL_SMTP_PORT, KUMIX_EMAIL_SMTP_USER, KUMIX_EMAIL_SMTP_PASS, KUMIX_EMAIL_FROM_NAME, KUMIX_EMAIL_FROM_EMAIL\",\n );\n}\n\nexport function isEmailConfigured(env?: EnvRecord): boolean {\n return hasEmailConfig(env);\n}\n\nexport function getConfiguredProvider(env?: EnvRecord): EmailProvider | null {\n const config = loadEmailConfig(env);\n return config?.provider || null;\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,26 +1,25 @@
1
1
  {
2
2
  "name": "@kumix/email",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Email templates and sending utilities for SaaS applications.",
5
5
  "author": "Kumix Labs <hai@kumix.io>",
6
6
  "license": "MIT",
7
7
  "type": "module",
8
8
  "main": "./dist/index.js",
9
- "module": "./dist/index.js",
10
9
  "types": "./dist/index.d.ts",
11
10
  "sideEffects": false,
12
11
  "exports": {
13
12
  ".": {
14
13
  "types": "./dist/index.d.ts",
15
- "import": "./dist/index.js"
14
+ "default": "./dist/index.js"
16
15
  },
17
16
  "./components": {
18
17
  "types": "./dist/components.d.ts",
19
- "import": "./dist/components.js"
18
+ "default": "./dist/components.js"
20
19
  },
21
20
  "./helpers": {
22
21
  "types": "./dist/helpers.d.ts",
23
- "import": "./dist/helpers.js"
22
+ "default": "./dist/helpers.js"
24
23
  }
25
24
  },
26
25
  "scripts": {
@@ -70,6 +69,20 @@
70
69
  "react-email": ">=6.6.0",
71
70
  "resend": ">=6.15.0"
72
71
  },
72
+ "peerDependenciesMeta": {
73
+ "nodemailer": {
74
+ "optional": true
75
+ },
76
+ "react": {
77
+ "optional": true
78
+ },
79
+ "react-email": {
80
+ "optional": true
81
+ },
82
+ "resend": {
83
+ "optional": true
84
+ }
85
+ },
73
86
  "publishConfig": {
74
87
  "registry": "https://registry.npmjs.org/",
75
88
  "access": "public"