sently 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 sendx contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,367 @@
1
+ # sently
2
+
3
+ **Runtime-agnostic email library for Node.js, Bun, Deno, and Cloudflare Workers.**
4
+
5
+ [![npm version](https://img.shields.io/npm/v/sently.svg)](https://www.npmjs.com/package/sently)
6
+ [![JSR](https://jsr.io/badges/@sently/sently)](https://jsr.io/@sently/sently)
7
+ [![bundle size](https://img.shields.io/bundlephobia/minzip/sently)](https://bundlephobia.com/package/sently)
8
+ [![license](https://img.shields.io/npm/l/sently.svg)](LICENSE)
9
+ [![tests](https://img.shields.io/badge/tests-passing-brightgreen)](#)
10
+ [![GitHub](https://img.shields.io/github/stars/alialnaghmoush/sently?style=social&label=GitHub)](https://github.com/alialnaghmoush/sently)
11
+
12
+ ---
13
+
14
+ ## Why sently
15
+
16
+ - **Works everywhere** — Node.js, Bun, Deno, Cloudflare Workers, and any environment with Web APIs
17
+ - **True tree-shaking** — import only what you need; unused adapters and transports stay out of your bundle
18
+ - **Zero dependencies in core** — MIME, SMTP protocol, and encoding use pure Web APIs only
19
+ - **DKIM signing** — RSA-SHA256 and Ed25519-SHA256 via Web Crypto
20
+ - **OAuth2 / XOAUTH2** — Gmail and Microsoft 365 SMTP auth with automatic token refresh
21
+ - **Connection pooling** — reuse SMTP sessions with optional rate limiting
22
+ - **TypeScript-first** — strict types, subpath exports, and full IDE support
23
+
24
+ ---
25
+
26
+ ## Installation
27
+
28
+ **npm** ([sently](https://www.npmjs.com/package/sently)):
29
+
30
+ ```bash
31
+ bun add sently
32
+ npm install sently
33
+ pnpm add sently
34
+ ```
35
+
36
+ **JSR** ([@sently/sently](https://jsr.io/@sently/sently)) — Deno, Bun, and other JSR-aware runtimes:
37
+
38
+ ```bash
39
+ deno add jsr:@sently/sently
40
+ bunx jsr add @sently/sently
41
+ ```
42
+
43
+ ```typescript
44
+ import { createMailer } from "sently";
45
+ ```
46
+
47
+ ---
48
+
49
+ ## Quick Start
50
+
51
+ ### SMTP with auto-detected adapter
52
+
53
+ ```typescript
54
+ import { createMailer } from "sently";
55
+
56
+ const mailer = await createMailer({
57
+ host: "smtp.example.com",
58
+ port: 587,
59
+ auth: { user: "you@example.com", pass: "secret" },
60
+ });
61
+
62
+ await mailer.send({
63
+ from: "you@example.com",
64
+ to: "recipient@example.com",
65
+ subject: "Hello from sently",
66
+ text: "Plain text body",
67
+ html: "<p>HTML body</p>",
68
+ });
69
+
70
+ await mailer.close();
71
+ ```
72
+
73
+ ### Resend HTTP transport (Vercel Edge compatible)
74
+
75
+ ```typescript
76
+ import { createMailer } from "sently";
77
+ import { ResendTransport } from "sently/transports/resend";
78
+
79
+ const mailer = await createMailer({
80
+ transport: new ResendTransport({ apiKey: process.env.RESEND_API_KEY! }),
81
+ });
82
+
83
+ await mailer.send({
84
+ from: "onboarding@yourdomain.com",
85
+ to: "recipient@example.com",
86
+ subject: "Hello from the edge",
87
+ html: "<p>Sent via Resend + sently</p>",
88
+ });
89
+ ```
90
+
91
+ ### Cloudflare Worker
92
+
93
+ ```typescript
94
+ import { createMailer } from "sently";
95
+ import { CloudflareAdapter } from "sently/adapters/cf";
96
+
97
+ export default {
98
+ async fetch() {
99
+ const mailer = await createMailer({
100
+ host: "smtp.example.com",
101
+ port: 587,
102
+ auth: { user: "relay@example.com", pass: "secret" },
103
+ adapter: new CloudflareAdapter(),
104
+ });
105
+
106
+ await mailer.send({
107
+ from: "relay@example.com",
108
+ to: "user@example.com",
109
+ subject: "From a Worker",
110
+ text: "Hello from Cloudflare Workers",
111
+ });
112
+
113
+ return new Response("Sent");
114
+ },
115
+ };
116
+ ```
117
+
118
+ ---
119
+
120
+ ## Adapters
121
+
122
+ | Runtime | Import | Notes |
123
+ |---------|--------|-------|
124
+ | Node.js (auto) | `createMailer(config)` | Auto-detected |
125
+ | Node.js (explicit) | `sently/adapters/node` → `NodeAdapter` | Reference implementation |
126
+ | Bun (auto) | `createMailer(config)` | Auto-detected |
127
+ | Bun (explicit) | `sently/adapters/bun` → `BunAdapter` | Node compat layer |
128
+ | Deno | `sently/adapters/deno` → `DenoAdapter` | Native `Deno.startTls` |
129
+ | Cloudflare Workers | `sently/adapters/cf` → `CloudflareAdapter` | `cloudflare:sockets` |
130
+
131
+ ```typescript
132
+ import { NodeAdapter } from "sently/adapters/node";
133
+
134
+ const mailer = await createMailer({
135
+ host: "smtp.example.com",
136
+ adapter: new NodeAdapter({ secure: false }),
137
+ auth: { user: "you@example.com", pass: "secret" },
138
+ });
139
+ ```
140
+
141
+ ---
142
+
143
+ ## Transports
144
+
145
+ ### SMTP
146
+
147
+ ```typescript
148
+ import { createMailer } from "sently";
149
+ import { SMTPTransport } from "sently/transports/smtp";
150
+ import { NodeAdapter } from "sently/adapters/node";
151
+
152
+ const transport = new SMTPTransport({
153
+ host: "smtp.example.com",
154
+ port: 587,
155
+ auth: { user: "you@example.com", pass: "secret" },
156
+ adapter: new NodeAdapter(),
157
+ });
158
+
159
+ const mailer = await createMailer({ transport });
160
+ await mailer.verify(); // test connection + auth
161
+ ```
162
+
163
+ **AUTH methods:** XOAUTH2, CRAM-MD5, LOGIN, and PLAIN (auto-negotiated from EHLO unless `auth.type` is set).
164
+
165
+ #### DKIM signing
166
+
167
+ ```typescript
168
+ const mailer = await createMailer({
169
+ host: "smtp.example.com",
170
+ auth: { user: "you@example.com", pass: "secret" },
171
+ dkim: {
172
+ domainName: "example.com",
173
+ keySelector: "2024",
174
+ privateKey: await Bun.file("dkim-private.pem").text(),
175
+ },
176
+ });
177
+ ```
178
+
179
+ #### Gmail OAuth2 (XOAUTH2)
180
+
181
+ ```typescript
182
+ import { OAuth2Client } from "sently/auth/oauth2";
183
+
184
+ const mailer = await createMailer({
185
+ host: "smtp.gmail.com",
186
+ port: 465,
187
+ secure: true,
188
+ auth: {
189
+ type: "OAUTH2",
190
+ user: "me@gmail.com",
191
+ oauth2: {
192
+ user: "me@gmail.com",
193
+ clientId: process.env.GOOGLE_CLIENT_ID!,
194
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
195
+ refreshToken: process.env.GOOGLE_REFRESH_TOKEN!,
196
+ },
197
+ },
198
+ });
199
+ ```
200
+
201
+ #### Connection pooling
202
+
203
+ ```typescript
204
+ const mailer = await createMailer({
205
+ host: "smtp.example.com",
206
+ pool: true,
207
+ maxConnections: 5,
208
+ maxMessages: 100,
209
+ rateDelta: 10,
210
+ rateLimit: 1000,
211
+ auth: { user: "you@example.com", pass: "secret" },
212
+ });
213
+ ```
214
+
215
+ Or use `SMTPPool` directly:
216
+
217
+ ```typescript
218
+ import { SMTPPool } from "sently/pool";
219
+
220
+ const pool = new SMTPPool({
221
+ host: "smtp.example.com",
222
+ adapter: new NodeAdapter(),
223
+ auth: { user: "you@example.com", pass: "secret" },
224
+ });
225
+ ```
226
+
227
+ ### HTTP APIs
228
+
229
+ #### Resend
230
+
231
+ ```typescript
232
+ import { ResendTransport } from "sently/transports/resend";
233
+
234
+ const transport = new ResendTransport({ apiKey: "re_..." });
235
+ ```
236
+
237
+ #### SendGrid
238
+
239
+ ```typescript
240
+ import { SendGridTransport } from "sently/transports/sendgrid";
241
+
242
+ const transport = new SendGridTransport({ apiKey: "SG...." });
243
+ ```
244
+
245
+ #### Postmark
246
+
247
+ ```typescript
248
+ import { PostmarkTransport } from "sently/transports/postmark";
249
+
250
+ const transport = new PostmarkTransport({ serverToken: "..." });
251
+ ```
252
+
253
+ ---
254
+
255
+ ## MailOptions Reference
256
+
257
+ | Field | Type | Default | Description |
258
+ |-------|------|---------|-------------|
259
+ | `from` | `AddressInput` | *required* | Sender address |
260
+ | `to` | `AddressInput` | *required* | Recipients |
261
+ | `cc` | `AddressInput` | — | CC recipients (visible in headers) |
262
+ | `bcc` | `AddressInput` | — | BCC recipients (envelope only, not in headers) |
263
+ | `replyTo` | `AddressInput` | — | Reply-To header |
264
+ | `subject` | `string` | *required* | Email subject (RFC 2047 for non-ASCII) |
265
+ | `text` | `string` | — | Plain text body |
266
+ | `html` | `string` | — | HTML body |
267
+ | `attachments` | `Attachment[]` | — | File attachments |
268
+ | `headers` | `Record<string, string>` | — | Custom headers |
269
+ | `messageId` | `string` | auto | Message-ID header |
270
+ | `date` | `Date` | now | Date header |
271
+ | `priority` | `'high' \| 'normal' \| 'low'` | — | X-Priority / Importance |
272
+ | `encoding` | `'utf-8' \| 'ascii'` | `'utf-8'` | Character encoding hint |
273
+
274
+ ---
275
+
276
+ ## Attachments
277
+
278
+ ### In-memory (all runtimes)
279
+
280
+ ```typescript
281
+ await mailer.send({
282
+ from: "you@example.com",
283
+ to: "user@example.com",
284
+ subject: "With attachment",
285
+ text: "See attached",
286
+ attachments: [
287
+ {
288
+ filename: "report.pdf",
289
+ content: pdfBytes, // Uint8Array
290
+ contentType: "application/pdf",
291
+ },
292
+ ],
293
+ });
294
+ ```
295
+
296
+ ### File path (Node.js / Bun / Deno only)
297
+
298
+ ```typescript
299
+ attachments: [
300
+ {
301
+ filename: "report.pdf",
302
+ path: "/path/to/report.pdf",
303
+ },
304
+ ],
305
+ ```
306
+
307
+ On Cloudflare Workers and browsers, use `content: Uint8Array` — `attachment.path` is not supported.
308
+
309
+ ---
310
+
311
+ ## Tree-Shaking
312
+
313
+ Each import path is a separate build entry point:
314
+
315
+ ```
316
+ import { createMailer } from "sently"
317
+ + import { ResendTransport } from "sently/transports/resend"
318
+ → Bundle: core/mime (~8KB) + core/address (~2KB) + transports/resend (~2KB) ≈ ~12KB gzip
319
+
320
+ vs. full Nodemailer: ~220KB
321
+ ```
322
+
323
+ Only code you import is bundled. Adapters and transports you never import are never included.
324
+
325
+ ---
326
+
327
+ ## Migrating from Nodemailer
328
+
329
+ | Nodemailer | sently |
330
+ |------------|-------|
331
+ | `nodemailer.createTransport({...})` | `await createMailer({...})` |
332
+ | `transporter.sendMail(options)` | `mailer.send(options)` |
333
+ | `transporter.verify()` | `mailer.verify()` |
334
+ | `options.attachments[].path` | Same (Node/Bun/Deno); use `content` on edge |
335
+ | `import nodemailer from 'nodemailer'` | `import { createMailer } from 'sently'` |
336
+ | CommonJS | ESM only |
337
+ | Node.js only | Node, Bun, Deno, CF Workers |
338
+
339
+ ---
340
+
341
+ ## Bundle Size
342
+
343
+ Approximate gzip sizes per subpath export:
344
+
345
+ | Export | ~gzip |
346
+ |--------|-------|
347
+ | `sently` | ~6 KB |
348
+ | `sently/transports/smtp` | ~10 KB |
349
+ | `sently/transports/resend` | ~2 KB |
350
+ | `sently/transports/sendgrid` | ~2 KB |
351
+ | `sently/transports/postmark` | ~2 KB |
352
+ | `sently/adapters/node` | ~3 KB |
353
+ | `sently/adapters/bun` | ~3 KB |
354
+ | `sently/adapters/deno` | ~2 KB |
355
+ | `sently/adapters/cf` | ~2 KB |
356
+
357
+ ---
358
+
359
+ ## Links
360
+
361
+ - **Source & issues:** [github.com/alialnaghmoush/sently](https://github.com/alialnaghmoush/sently)
362
+ - **npm:** [npmjs.com/package/sently](https://www.npmjs.com/package/sently)
363
+ - **JSR:** [jsr.io/@sently/sently](https://jsr.io/@sently/sently)
364
+
365
+ ## License
366
+
367
+ MIT
@@ -0,0 +1,105 @@
1
+ // src/core/base64.ts
2
+ var BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
3
+ var BASE64_LINE_LENGTH = 76;
4
+ var encoder = new TextEncoder;
5
+ var decoder = new TextDecoder;
6
+ function encodeBase64(data) {
7
+ const bytes = typeof data === "string" ? encoder.encode(data) : data;
8
+ let result = "";
9
+ let i = 0;
10
+ while (i < bytes.length) {
11
+ const b0 = bytes[i] ?? 0;
12
+ const b1 = bytes[i + 1];
13
+ const b2 = bytes[i + 2];
14
+ if (b1 === undefined) {
15
+ result += BASE64_CHARS[b0 >> 2];
16
+ result += BASE64_CHARS[(b0 & 3) << 4];
17
+ result += "==";
18
+ break;
19
+ }
20
+ if (b2 === undefined) {
21
+ result += BASE64_CHARS[b0 >> 2];
22
+ result += BASE64_CHARS[(b0 & 3) << 4 | b1 >> 4];
23
+ result += BASE64_CHARS[(b1 & 15) << 2];
24
+ result += "=";
25
+ break;
26
+ }
27
+ result += BASE64_CHARS[b0 >> 2];
28
+ result += BASE64_CHARS[(b0 & 3) << 4 | b1 >> 4];
29
+ result += BASE64_CHARS[(b1 & 15) << 2 | b2 >> 6];
30
+ result += BASE64_CHARS[b2 & 63];
31
+ i += 3;
32
+ }
33
+ return wrapBase64Lines(result);
34
+ }
35
+ function decodeBase64(data) {
36
+ const cleaned = data.replace(/\s/g, "");
37
+ const len = cleaned.length;
38
+ if (len === 0) {
39
+ return new Uint8Array(0);
40
+ }
41
+ if (len % 4 !== 0) {
42
+ throw new Error("Invalid base64 string length");
43
+ }
44
+ const padding = cleaned.endsWith("==") ? 2 : cleaned.endsWith("=") ? 1 : 0;
45
+ const outputLen = len * 3 / 4 - padding;
46
+ const output = new Uint8Array(outputLen);
47
+ let outIndex = 0;
48
+ for (let i = 0;i < len; i += 4) {
49
+ const c0 = base64CharToValue(cleaned[i] ?? "=");
50
+ const c1 = base64CharToValue(cleaned[i + 1] ?? "=");
51
+ const c2 = base64CharToValue(cleaned[i + 2] ?? "=");
52
+ const c3 = base64CharToValue(cleaned[i + 3] ?? "=");
53
+ const triple = c0 << 18 | c1 << 12 | c2 << 6 | c3;
54
+ if (outIndex < outputLen) {
55
+ output[outIndex++] = triple >> 16 & 255;
56
+ }
57
+ if (outIndex < outputLen) {
58
+ output[outIndex++] = triple >> 8 & 255;
59
+ }
60
+ if (outIndex < outputLen) {
61
+ output[outIndex++] = triple & 255;
62
+ }
63
+ }
64
+ return output;
65
+ }
66
+ function encodeHeader(value) {
67
+ if (!needsEncoding(value)) {
68
+ return value;
69
+ }
70
+ const encoded = encodeBase64(value).replace(/\r\n/g, "");
71
+ return `=?UTF-8?B?${encoded}?=`;
72
+ }
73
+ function needsEncoding(text) {
74
+ for (let i = 0;i < text.length; i++) {
75
+ if (text.charCodeAt(i) > 127) {
76
+ return true;
77
+ }
78
+ }
79
+ return false;
80
+ }
81
+ function base64CharToValue(char) {
82
+ if (char === "=") {
83
+ return 0;
84
+ }
85
+ const index = BASE64_CHARS.indexOf(char);
86
+ if (index === -1) {
87
+ throw new Error(`Invalid base64 character: ${char}`);
88
+ }
89
+ return index;
90
+ }
91
+ function wrapBase64Lines(base64) {
92
+ const lines = [];
93
+ for (let i = 0;i < base64.length; i += BASE64_LINE_LENGTH) {
94
+ lines.push(base64.slice(i, i + BASE64_LINE_LENGTH));
95
+ }
96
+ return lines.join(`\r
97
+ `);
98
+ }
99
+ function encodeUtf8(text) {
100
+ return encoder.encode(text);
101
+ }
102
+
103
+ export { encodeBase64, decodeBase64, encodeHeader, encodeUtf8 };
104
+
105
+ //# debugId=93C9FABDDF825C4864756E2164756E21
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/core/base64.ts"],
4
+ "sourcesContent": [
5
+ "// src/core/base64.ts\n\nconst BASE64_CHARS = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/\";\nconst BASE64_LINE_LENGTH = 76;\n\nconst encoder = new TextEncoder();\nconst decoder = new TextDecoder();\n\n/**\n * Encode a Uint8Array or string to Base64.\n * Uses TextEncoder + manual base64 to support binary data correctly.\n */\nexport function encodeBase64(data: Uint8Array | string): string {\n const bytes = typeof data === \"string\" ? encoder.encode(data) : data;\n let result = \"\";\n let i = 0;\n\n while (i < bytes.length) {\n const b0 = bytes[i] ?? 0;\n const b1 = bytes[i + 1];\n const b2 = bytes[i + 2];\n\n if (b1 === undefined) {\n result += BASE64_CHARS[b0 >> 2];\n result += BASE64_CHARS[(b0 & 0x03) << 4];\n result += \"==\";\n break;\n }\n\n if (b2 === undefined) {\n result += BASE64_CHARS[b0 >> 2];\n result += BASE64_CHARS[((b0 & 0x03) << 4) | (b1 >> 4)];\n result += BASE64_CHARS[(b1 & 0x0f) << 2];\n result += \"=\";\n break;\n }\n\n result += BASE64_CHARS[b0 >> 2];\n result += BASE64_CHARS[((b0 & 0x03) << 4) | (b1 >> 4)];\n result += BASE64_CHARS[((b1 & 0x0f) << 2) | (b2 >> 6)];\n result += BASE64_CHARS[b2 & 0x3f];\n i += 3;\n }\n\n return wrapBase64Lines(result);\n}\n\n/**\n * Decode a Base64 string to Uint8Array.\n */\nexport function decodeBase64(data: string): Uint8Array {\n const cleaned = data.replace(/\\s/g, \"\");\n const len = cleaned.length;\n\n if (len === 0) {\n return new Uint8Array(0);\n }\n\n if (len % 4 !== 0) {\n throw new Error(\"Invalid base64 string length\");\n }\n\n const padding = cleaned.endsWith(\"==\") ? 2 : cleaned.endsWith(\"=\") ? 1 : 0;\n const outputLen = (len * 3) / 4 - padding;\n const output = new Uint8Array(outputLen);\n\n let outIndex = 0;\n for (let i = 0; i < len; i += 4) {\n const c0 = base64CharToValue(cleaned[i] ?? \"=\");\n const c1 = base64CharToValue(cleaned[i + 1] ?? \"=\");\n const c2 = base64CharToValue(cleaned[i + 2] ?? \"=\");\n const c3 = base64CharToValue(cleaned[i + 3] ?? \"=\");\n const triple = (c0 << 18) | (c1 << 12) | (c2 << 6) | c3;\n\n if (outIndex < outputLen) {\n output[outIndex++] = (triple >> 16) & 0xff;\n }\n if (outIndex < outputLen) {\n output[outIndex++] = (triple >> 8) & 0xff;\n }\n if (outIndex < outputLen) {\n output[outIndex++] = triple & 0xff;\n }\n }\n\n return output;\n}\n\n/**\n * Encode text using Quoted-Printable (RFC 2045).\n */\nexport function encodeQP(text: string): string {\n const bytes = encoder.encode(text);\n const lines: string[] = [];\n let line = \"\";\n\n for (let i = 0; i < bytes.length; i++) {\n const byte = bytes[i] ?? 0;\n\n if (byte === 0x0a) {\n lines.push(line);\n line = \"\";\n continue;\n }\n\n if (byte === 0x0d) {\n continue;\n }\n\n let encoded: string;\n if (\n (byte >= 33 && byte <= 60) ||\n (byte >= 62 && byte <= 126) ||\n byte === 0x09 ||\n byte === 0x20\n ) {\n encoded = String.fromCharCode(byte);\n } else {\n encoded = `=${byte.toString(16).toUpperCase().padStart(2, \"0\")}`;\n }\n\n if (line.length + encoded.length > 75) {\n lines.push(`${line}=`);\n line = encoded;\n } else {\n line += encoded;\n }\n }\n\n if (line.length > 0) {\n lines.push(line);\n }\n\n return lines.join(\"\\r\\n\");\n}\n\n/**\n * Encode an email header value per RFC 2047.\n * Non-ASCII values become: =?UTF-8?B?<base64>?=\n */\nexport function encodeHeader(value: string): string {\n if (!needsEncoding(value)) {\n return value;\n }\n\n const encoded = encodeBase64(value).replace(/\\r\\n/g, \"\");\n return `=?UTF-8?B?${encoded}?=`;\n}\n\n/**\n * Returns true if the string contains non-ASCII characters\n * and therefore requires RFC 2047 encoding in headers.\n */\nexport function needsEncoding(text: string): boolean {\n for (let i = 0; i < text.length; i++) {\n if (text.charCodeAt(i) > 127) {\n return true;\n }\n }\n return false;\n}\n\nfunction base64CharToValue(char: string): number {\n if (char === \"=\") {\n return 0;\n }\n const index = BASE64_CHARS.indexOf(char);\n if (index === -1) {\n throw new Error(`Invalid base64 character: ${char}`);\n }\n return index;\n}\n\nfunction wrapBase64Lines(base64: string): string {\n const lines: string[] = [];\n for (let i = 0; i < base64.length; i += BASE64_LINE_LENGTH) {\n lines.push(base64.slice(i, i + BASE64_LINE_LENGTH));\n }\n return lines.join(\"\\r\\n\");\n}\n\n/** Decode bytes to UTF-8 string. */\nexport function decodeUtf8(bytes: Uint8Array): string {\n return decoder.decode(bytes);\n}\n\n/** Encode string to UTF-8 bytes. */\nexport function encodeUtf8(text: string): Uint8Array {\n return encoder.encode(text);\n}\n"
6
+ ],
7
+ "mappings": "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",
8
+ "debugId": "93C9FABDDF825C4864756E2164756E21",
9
+ "names": []
10
+ }
@@ -0,0 +1,116 @@
1
+ import {
2
+ encodeHeader
3
+ } from "./chunk-794hc3m4.js";
4
+ import {
5
+ __require
6
+ } from "./chunk-v0bahtg2.js";
7
+
8
+ // src/core/address.ts
9
+ function parseAddresses(input) {
10
+ if (Array.isArray(input)) {
11
+ return input.flatMap((item) => parseAddresses(item));
12
+ }
13
+ if (typeof input === "object") {
14
+ return [{ ...input }];
15
+ }
16
+ const trimmed = input.trim();
17
+ if (!trimmed) {
18
+ return [];
19
+ }
20
+ return splitAddressList(trimmed).map(parseSingleAddress);
21
+ }
22
+ function toMIMEHeader(address) {
23
+ if (address.name) {
24
+ const name = encodeHeader(address.name);
25
+ return `${name} <${address.address}>`;
26
+ }
27
+ return address.address;
28
+ }
29
+ function extractEmails(input) {
30
+ return parseAddresses(input).map((addr) => addr.address);
31
+ }
32
+ function splitAddressList(input) {
33
+ const parts = [];
34
+ let current = "";
35
+ let inQuotes = false;
36
+ let inAngle = false;
37
+ for (let i = 0;i < input.length; i++) {
38
+ const char = input[i] ?? "";
39
+ if (char === '"' && input[i - 1] !== "\\") {
40
+ inQuotes = !inQuotes;
41
+ current += char;
42
+ continue;
43
+ }
44
+ if (char === "<" && !inQuotes) {
45
+ inAngle = true;
46
+ current += char;
47
+ continue;
48
+ }
49
+ if (char === ">" && !inQuotes) {
50
+ inAngle = false;
51
+ current += char;
52
+ continue;
53
+ }
54
+ if (char === "," && !inQuotes && !inAngle) {
55
+ if (current.trim()) {
56
+ parts.push(current.trim());
57
+ }
58
+ current = "";
59
+ continue;
60
+ }
61
+ current += char;
62
+ }
63
+ if (current.trim()) {
64
+ parts.push(current.trim());
65
+ }
66
+ return parts;
67
+ }
68
+ function parseSingleAddress(input) {
69
+ const trimmed = input.trim();
70
+ const angleMatch = trimmed.match(/^(?:"([^"]*)"|([^<]*?))\s*<([^>]+)>$/);
71
+ if (angleMatch) {
72
+ const name = (angleMatch[1] ?? angleMatch[2] ?? "").trim();
73
+ const address = (angleMatch[3] ?? "").trim();
74
+ if (name) {
75
+ return { name, address };
76
+ }
77
+ return { address };
78
+ }
79
+ if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
80
+ return { address: trimmed.slice(1, -1) };
81
+ }
82
+ return { address: trimmed };
83
+ }
84
+
85
+ // src/transports/resolve-attachments.ts
86
+ async function resolveAttachments(attachments = []) {
87
+ const resolved = [];
88
+ for (const attachment of attachments) {
89
+ if (attachment.content instanceof Uint8Array) {
90
+ resolved.push(attachment);
91
+ continue;
92
+ }
93
+ if (attachment.path) {
94
+ let fs;
95
+ try {
96
+ fs = await import("node:fs/promises");
97
+ } catch {
98
+ throw new Error("attachment.path is not supported on this runtime — use attachment.content (Uint8Array) instead");
99
+ }
100
+ const data = await fs.readFile(attachment.path);
101
+ const { path: _path, ...rest } = attachment;
102
+ resolved.push({ ...rest, content: new Uint8Array(data) });
103
+ continue;
104
+ }
105
+ if (typeof attachment.content === "string") {
106
+ resolved.push(attachment);
107
+ continue;
108
+ }
109
+ resolved.push(attachment);
110
+ }
111
+ return resolved;
112
+ }
113
+
114
+ export { parseAddresses, toMIMEHeader, extractEmails, resolveAttachments };
115
+
116
+ //# debugId=85211AC1FC8A34D664756E2164756E21
@@ -0,0 +1,11 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/core/address.ts", "../src/transports/resolve-attachments.ts"],
4
+ "sourcesContent": [
5
+ "// src/core/address.ts\nimport { encodeHeader } from \"./base64.js\";\nimport type { Address, AddressInput } from \"./types.js\";\n\n/**\n * Normalize any AddressInput form into Address[].\n */\nexport function parseAddresses(input: AddressInput): Address[] {\n if (Array.isArray(input)) {\n return input.flatMap((item) => parseAddresses(item));\n }\n\n if (typeof input === \"object\") {\n return [{ ...input }];\n }\n\n const trimmed = input.trim();\n if (!trimmed) {\n return [];\n }\n\n return splitAddressList(trimmed).map(parseSingleAddress);\n}\n\n/**\n * Format an Address for SMTP envelope commands (MAIL FROM / RCPT TO).\n */\nexport function toEnvelope(address: Address): string {\n return address.address;\n}\n\n/**\n * Format an Address for use in a MIME header (From, To, CC, etc.).\n */\nexport function toMIMEHeader(address: Address): string {\n if (address.name) {\n const name = encodeHeader(address.name);\n return `${name} <${address.address}>`;\n }\n return address.address;\n}\n\n/**\n * Extract plain email strings from any AddressInput.\n */\nexport function extractEmails(input: AddressInput): string[] {\n return parseAddresses(input).map((addr) => addr.address);\n}\n\n/**\n * Basic email format validation (format only, no DNS lookup).\n */\nexport function isValidEmail(email: string): boolean {\n return /^[^\\s@<>]+@[^\\s@<>]+\\.[^\\s@<>]+$/.test(email);\n}\n\nfunction splitAddressList(input: string): string[] {\n const parts: string[] = [];\n let current = \"\";\n let inQuotes = false;\n let inAngle = false;\n\n for (let i = 0; i < input.length; i++) {\n const char = input[i] ?? \"\";\n if (char === '\"' && input[i - 1] !== \"\\\\\") {\n inQuotes = !inQuotes;\n current += char;\n continue;\n }\n if (char === \"<\" && !inQuotes) {\n inAngle = true;\n current += char;\n continue;\n }\n if (char === \">\" && !inQuotes) {\n inAngle = false;\n current += char;\n continue;\n }\n if (char === \",\" && !inQuotes && !inAngle) {\n if (current.trim()) {\n parts.push(current.trim());\n }\n current = \"\";\n continue;\n }\n current += char;\n }\n\n if (current.trim()) {\n parts.push(current.trim());\n }\n\n return parts;\n}\n\nfunction parseSingleAddress(input: string): Address {\n const trimmed = input.trim();\n\n const angleMatch = trimmed.match(/^(?:\"([^\"]*)\"|([^<]*?))\\s*<([^>]+)>$/);\n if (angleMatch) {\n const name = (angleMatch[1] ?? angleMatch[2] ?? \"\").trim();\n const address = (angleMatch[3] ?? \"\").trim();\n if (name) {\n return { name, address };\n }\n return { address };\n }\n\n if (trimmed.startsWith('\"') && trimmed.endsWith('\"')) {\n return { address: trimmed.slice(1, -1) };\n }\n\n return { address: trimmed };\n}\n",
6
+ "import type { Attachment } from \"../core/types.js\";\n\n/**\n * Resolve attachment.path to in-memory Uint8Array content.\n * @throws When attachment.path is used on runtimes without node:fs/promises\n */\nexport async function resolveAttachments(attachments: Attachment[] = []): Promise<Attachment[]> {\n const resolved: Attachment[] = [];\n\n for (const attachment of attachments) {\n if (attachment.content instanceof Uint8Array) {\n resolved.push(attachment);\n continue;\n }\n\n if (attachment.path) {\n let fs: typeof import(\"node:fs/promises\");\n try {\n fs = await import(\"node:fs/promises\");\n } catch {\n throw new Error(\n \"attachment.path is not supported on this runtime — use attachment.content (Uint8Array) instead\",\n );\n }\n\n const data = await fs.readFile(attachment.path);\n const { path: _path, ...rest } = attachment;\n resolved.push({ ...rest, content: new Uint8Array(data) });\n continue;\n }\n\n if (typeof attachment.content === \"string\") {\n resolved.push(attachment);\n continue;\n }\n\n resolved.push(attachment);\n }\n\n return resolved;\n}\n"
7
+ ],
8
+ "mappings": "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",
9
+ "debugId": "85211AC1FC8A34D664756E2164756E21",
10
+ "names": []
11
+ }