@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 +50 -8
- package/dist/helpers.d.ts +30 -14
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +48 -46
- package/dist/helpers.js.map +1 -1
- package/dist/index.d.ts +8 -104
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +72 -37
- package/dist/index.js.map +1 -1
- package/package.json +18 -5
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)` —
|
|
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
|
-
|
|
364
|
-
EmailResult,
|
|
395
|
+
BaseEmailConfig,
|
|
365
396
|
EmailProvider,
|
|
366
397
|
EnvRecord,
|
|
367
|
-
//
|
|
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
|
-
|
|
383
|
-
|
|
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
|
|
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:
|
|
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
|
|
25
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
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
|
package/dist/helpers.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"helpers.d.ts","names":[],"sources":["../src/helpers.ts"],"mappings":"
|
|
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
|
-
|
|
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
|
|
51
|
-
*
|
|
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
|
-
*
|
|
36
|
+
* // "Hello\n\nWelcome to our platform!"
|
|
60
37
|
* ```
|
|
61
38
|
*/
|
|
62
39
|
function htmlToText(html) {
|
|
63
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
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[^<]*(?:(
|
|
190
|
-
sanitized = sanitized.replace(/\s
|
|
191
|
-
sanitized = sanitized.replace(
|
|
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
|
package/dist/helpers.js.map
CHANGED
|
@@ -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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgDA,eAAsB,oBACpB,UACA,OACiB;CACjB,OAAO,OAAO,MAAM,cAAc,UAAU,KAAK,CAAC;AACpD;;;;;;;;;;;;;;;;AAiBA,SAAgB,WAAW,MAAsB;CAC/C,OAAO,KACJ,QAAQ,gBAAgB,IAAI,CAAC,CAC7B,QAAQ,WAAW,MAAM,CAAC,CAC1B,QAAQ,gBAAgB,MAAM,CAAC,CAC/B,QAAQ,YAAY,GAAG,CAAC,CACxB,QAAQ,QAAQ,GAAG,CAAC,CACpB,QAAQ,UAAU,IAAI,CAAC,CACvB,KAAK;AACV;;;;;;;;;;;;;;;AAoBA,SAAgB,aAAa,OAAwB;CAGnD,OAAO,uIAAW,KAAK,KAAK;AAC9B;;;;;;;;;;;;;;;AAgBA,SAAgB,eAAe,QAAoC;CAEjE,QADmB,MAAM,QAAQ,MAAM,IAAI,SAAS,CAAC,MAAM,EAAA,CACzC,OAAO,UAAU,aAAa,MAAM,KAAK,CAAC,CAAC;AAC/D;;;;;;;;;;;;;;;AAgBA,SAAgB,kBAAkB,QAA4B;CAC5D,OAAO,OAAO,QAAQ,UAAU,aAAa,MAAM,KAAK,CAAC,CAAC;AAC5D;;;;;;;;;;;;;;;AAoBA,SAAgB,mBAAmB,MAAc,OAAuB;CAEtE,OAAO,GAAG,KAAK,IAAI,MAAM;AAC3B;;;;;;;;;;;;;;;;;AAkBA,SAAgB,aAAa,kBAAkC;CAC7D,MAAM,QAAQ,iBAAiB,MAAM,WAAW;CAChD,OAAO,QAAQ,MAAM,KAAK,iBAAiB,KAAK;AAClD;;;;;;;;;;;;;;;;;AAkBA,SAAgB,mBAAmB,kBAAkC;CACnE,MAAM,QAAQ,iBAAiB,MAAM,mBAAmB;CACxD,OAAO,QAAQ,MAAM,EAAE,CAAC,KAAK,CAAC,CAAC,QAAQ,gBAAgB,EAAE,IAAI;AAC/D;;;;;;;;;;;;;;;;AAqBA,SAAgB,aAAa,MAAsB;CAEjD,IAAI,YAAY,KAAK,QAAQ,uDAAuD,EAAE;CAGtF,YAAY,UAAU,QAAQ,mCAAmC,EAAE;CACnE,YAAY,UAAU,QAAQ,uBAAuB,EAAE;CAGvD,YAAY,UAAU,QAAQ,uDAAuD,EAAE;CAEvF,OAAO;AACT;;;;;;;;;;;;;;;;;AAkBA,SAAgB,aAAa,MAAc,WAAmB,WAAmB,OAAe;CAC9F,IAAI,KAAK,UAAU,WAAW,OAAO;CACrC,OAAO,KAAK,MAAM,GAAG,YAAY,SAAS,MAAM,IAAI;AACtD;;;;;;;;;;;;;;;;AAiBA,SAAgB,oBAAoB,MAAc,YAAoB,KAAa;CAEjF,OAAO,aADM,WAAW,IACD,GAAG,SAAS;AACrC;;;;;;;;;;;;;;;;;;;;AAyBA,SAAgB,wBAAwB,SAAiB,OAAe,OAAwB;CAC9F,MAAM,MAAM,IAAI,IAAI,OAAO;CAC3B,IAAI,aAAa,IAAI,SAAS,KAAK;CACnC,IAAI,OACF,IAAI,aAAa,IAAI,SAAS,KAAK;CAErC,OAAO,IAAI,SAAS;AACtB;;;;;;;;;;;;;;;;;;;;AAqBA,SAAgB,sBACd,SACA,SACA,aACQ;CACR,MAAM,MAAM,IAAI,IAAI,OAAO;CAC3B,IAAI,aAAa,IAAI,YAAY,OAAO;CACxC,IAAI,aAAa,IAAI,gBAAgB,WAAW;CAChD,IAAI,aAAa,IAAI,KAAK,KAAK,IAAI,CAAC,CAAC,SAAS,CAAC;CAC/C,OAAO,IAAI,SAAS;AACtB;;;;;;;;;;;;;;;;;;;AAoBA,SAAgB,aACd,KACA,WAOQ;CACR,MAAM,SAAS,IAAI,IAAI,GAAG;CAE1B,IAAI,UAAU,QAAQ,OAAO,aAAa,IAAI,cAAc,UAAU,MAAM;CAC5E,IAAI,UAAU,QAAQ,OAAO,aAAa,IAAI,cAAc,UAAU,MAAM;CAC5E,IAAI,UAAU,UAAU,OAAO,aAAa,IAAI,gBAAgB,UAAU,QAAQ;CAClF,IAAI,UAAU,MAAM,OAAO,aAAa,IAAI,YAAY,UAAU,IAAI;CACtE,IAAI,UAAU,SAAS,OAAO,aAAa,IAAI,eAAe,UAAU,OAAO;CAE/E,OAAO,OAAO,SAAS;AACzB;;;;;;;;;;;;;;AAmBA,SAAgB,eAAe,aAA+B;CAC5D,OAAO,YACJ,MAAM,MAAM,CAAC,CACb,KAAK,UAAU,MAAM,KAAK,CAAC,CAAC,CAC5B,QAAQ,UAAU,MAAM,SAAS,CAAC;AACvC;;;;;;;;;;;;;;;AAgBA,SAAgB,kBAAkB,QAA4B;CAC5D,MAAM,uBAAO,IAAI,IAAY;CAC7B,OAAO,OAAO,QAAQ,UAAU;EAC9B,MAAM,aAAa,MAAM,YAAY,CAAC,CAAC,KAAK;EAC5C,IAAI,KAAK,IAAI,UAAU,GACrB,OAAO;EAET,KAAK,IAAI,UAAU;EACnB,OAAO;CACT,CAAC;AACH;;;;;;;;;;;;;;;;;;AAmBA,SAAgB,YAAY,QAAkB,YAAoB,KAAiB;CACjF,MAAM,SAAqB,CAAC;CAC5B,KAAK,IAAI,IAAI,GAAG,IAAI,OAAO,QAAQ,KAAK,WACtC,OAAO,KAAK,OAAO,MAAM,GAAG,IAAI,SAAS,CAAC;CAE5C,OAAO;AACT;;;;;;;;;;;;;;;AAoBA,SAAgB,YAAY,UAA0B;CAsCpD,OAAO;EAjCL,KAAK;EACL,MAAM;EACN,KAAK;EACL,KAAK;EACL,MAAM;EACN,KAAK;EAGL,KAAK;EACL,KAAK;EACL,MAAM;EACN,KAAK;EACL,MAAM;EACN,KAAK;EACL,MAAM;EAGN,KAAK;EACL,KAAK;EACL,MAAM;EACN,KAAK;EACL,IAAI;EACJ,MAAM;EACN,KAAK;EAGL,KAAK;EACL,KAAK;EACL,MAAM;EACN,KAAK;EACL,IAAI;CAGS,EArCH,SAAS,YAAY,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,KAAK,OAqC9B;AAC3B;;;;;;;;;;;;;;;;AAiBA,SAAgB,eAAe,OAAe,WAAmB,GAAW;CAC1E,IAAI,UAAU,GAAG,OAAO;CAExB,MAAM,IAAI;CACV,MAAM,KAAK,WAAW,IAAI,IAAI;CAC9B,MAAM,QAAQ;EAAC;EAAS;EAAM;EAAM;EAAM;CAAI;CAE9C,MAAM,IAAI,KAAK,MAAM,KAAK,IAAI,KAAK,IAAI,KAAK,IAAI,CAAC,CAAC;CAElD,OAAO,IAAI,QAAQ,KAAK,EAAA,CAAG,QAAQ,EAAE,EAAE,GAAG,MAAM;AAClD"}
|
|
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
|
|
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
|
|
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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":";;;;;;AASA;;;;AAAyB;KAAb,aAAA;;;;;;UAOK,eAAA;EAMb;EAJF,QAAA,EAAU,aAAa;EASvB;EAPA,IAAA;IAOO,kBALL,IAAA,UAY0B;IAV1B,KAAA;EAAA;EAUkC;EAPpC,OAAA;AAAA;;AAUM;AAOR;;UAViB,YAAA,SAAqB,eAAe;EACnD,QAAA;EASwC;EAPxC,MAAA;AAAA;;;;;UAOe,gBAAA,SAAyB,eAAe;EACvD,QAAA;EAcQ;EAZR,IAAA;IAqBU,2BAnBR,IAAA,UAmBsB;IAjBtB,IAAA,UAiBqD;IAfrD,MAAA,WAqB4B;IAnB5B,IAAA;MAuBiB,oBArBf,IAAA,UAqBJ;MAnBI,IAAA;IAAA;EAAA;AAAA;AAuBD;AAOL;;;AAPK,KAdO,WAAA,GAAc,YAAA,GAAe,gBAAgB;;;;;UAMxC,eAAA;EAiBf;EAfA,QAAA;EAmBA;EAjBA,OAAA,EAAS,UAAU;EAqBnB;EAnBA,WAAA;EAuBA;EArBA,GAAA;AAAA;;;;;UAOe,gBAAA;EAsBD;EApBd,EAAA;EAoBkB;EAlBlB,EAAA;EAyBgC;EAvBhC,GAAA;EAyBY;EAvBZ,OAAA;EA8Be;EA5Bf,IAAA;;EAEA,IAAA;EA4BA;EA1BA,WAAA,GAAc,eAAA;EA8Bd;EA5BA,OAAA,GAAU,MAAA;EA8BC;EA5BX,IAAA,GAAO,MAAA;EA4BU;EA1BjB,QAAA;EAiCoC;EA/BpC,WAAA,GAAc,IAAA;AAAA;AAmCT;AAOP;;;AAPO,UA5BU,iBAAA;EAqC0B;EAAA,CAnCxC,GAAW;AAAA;;;;;UAOG,WAAA;EA4BV;EA1BL,OAAA;EA0ByC;EAxBzC,SAAA;EA0BmB;EAxBnB,KAAA;EAwBgD;EAtBhD,QAAA,GAAW,MAAM;AAAA;;;;;UAOF,qBAAA;
|
|
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:
|
|
50
|
-
secure:
|
|
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
|
|
193
|
-
from:
|
|
194
|
-
to: Array.isArray(options.to) ? options.to.join(", ")
|
|
195
|
-
cc: options.cc ? Array.isArray(options.cc) ? options.cc.join(", ") :
|
|
196
|
-
bcc: options.bcc ? Array.isArray(options.bcc) ? options.bcc.join(", ") :
|
|
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
|
-
|
|
201
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
335
|
-
from:
|
|
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 (
|
|
343
|
-
if (
|
|
344
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
"
|
|
14
|
+
"default": "./dist/index.js"
|
|
16
15
|
},
|
|
17
16
|
"./components": {
|
|
18
17
|
"types": "./dist/components.d.ts",
|
|
19
|
-
"
|
|
18
|
+
"default": "./dist/components.js"
|
|
20
19
|
},
|
|
21
20
|
"./helpers": {
|
|
22
21
|
"types": "./dist/helpers.d.ts",
|
|
23
|
-
"
|
|
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"
|