sently 0.4.3 → 0.4.5

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.
Files changed (50) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +44 -0
  3. package/dist/adapters/bun.d.ts +3 -0
  4. package/dist/adapters/bun.js.map +2 -2
  5. package/dist/adapters/cf.d.ts +3 -0
  6. package/dist/adapters/cf.js.map +2 -2
  7. package/dist/adapters/deno.d.ts +3 -0
  8. package/dist/adapters/deno.js.map +2 -2
  9. package/dist/adapters/node.d.ts +3 -0
  10. package/dist/adapters/node.js.map +2 -2
  11. package/dist/auth/oauth2.d.ts +3 -0
  12. package/dist/{chunk-8sm0vz0n.js → chunk-7fqv71z1.js} +10 -4
  13. package/dist/chunk-7fqv71z1.js.map +10 -0
  14. package/dist/{chunk-bvxkmq94.js → chunk-f4c9ttmr.js} +34 -5
  15. package/dist/chunk-f4c9ttmr.js.map +11 -0
  16. package/dist/{chunk-bfrvnqhq.js → chunk-mp5c9bfd.js} +3 -3
  17. package/dist/{chunk-bfrvnqhq.js.map → chunk-mp5c9bfd.js.map} +2 -2
  18. package/dist/{chunk-sbydk09g.js → chunk-tymfm441.js} +5 -3
  19. package/dist/{chunk-sbydk09g.js.map → chunk-tymfm441.js.map} +3 -3
  20. package/dist/{chunk-j6qw8ms6.js → chunk-x3szga4k.js} +21 -7
  21. package/dist/chunk-x3szga4k.js.map +11 -0
  22. package/dist/chunk-ym3zzv8b.js.map +2 -2
  23. package/dist/core/address.d.ts +30 -0
  24. package/dist/core/smtp.js +1 -1
  25. package/dist/core/types.d.ts +71 -0
  26. package/dist/detect.js +5 -5
  27. package/dist/pool/pool.d.ts +25 -0
  28. package/dist/pool/pool.js +5 -5
  29. package/dist/transports/brevo.js +1 -1
  30. package/dist/transports/mailgun.js +1 -1
  31. package/dist/transports/postmark.d.ts +1 -0
  32. package/dist/transports/postmark.js +1 -1
  33. package/dist/transports/postmark.js.map +2 -2
  34. package/dist/transports/preview.js +2 -2
  35. package/dist/transports/resend.d.ts +2 -0
  36. package/dist/transports/resend.js +1 -1
  37. package/dist/transports/resend.js.map +2 -2
  38. package/dist/transports/resolve-attachments.d.ts +10 -2
  39. package/dist/transports/retry.js +1 -1
  40. package/dist/transports/sendgrid.d.ts +1 -0
  41. package/dist/transports/sendgrid.js +1 -1
  42. package/dist/transports/sendgrid.js.map +2 -2
  43. package/dist/transports/ses.js +4 -4
  44. package/dist/transports/ses.js.map +2 -2
  45. package/dist/transports/smtp.d.ts +13 -0
  46. package/dist/transports/smtp.js +4 -4
  47. package/package.json +1 -1
  48. package/dist/chunk-8sm0vz0n.js.map +0 -10
  49. package/dist/chunk-bvxkmq94.js.map +0 -11
  50. package/dist/chunk-j6qw8ms6.js.map +0 -11
package/CHANGELOG.md CHANGED
@@ -1,5 +1,59 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.5] — 2026-05-30
4
+
5
+ ### Documentation
6
+
7
+ - Added `@module` documentation to the `./pool` entrypoint
8
+ - Documented exported interface properties and public API methods across
9
+ core types, adapters, and transports to meet JSR symbol documentation
10
+ requirements (80%+ threshold)
11
+
12
+ ## [0.4.4] — 2026-05-30
13
+
14
+ ### Security
15
+
16
+ - Fixed SigV4 date format: slice(0,15) not slice(0,16) — all real
17
+ SES requests were producing malformed x-amz-date headers
18
+ - Added requireTLS guard before SMTP AUTH (default: true when auth
19
+ is set) — prevents credential exposure on STARTTLS-stripping attacks
20
+ - Hardened email address validation against header and SMTP command
21
+ injection, enforced centrally in `parseAddresses()` (and re-asserted
22
+ at render time in `toMIMEHeader()`):
23
+ - Rejects CR, LF, NUL, all other C0 control characters (0x00–0x1F),
24
+ DEL (0x7F), and the Unicode line/paragraph separators U+2028/U+2029
25
+ - Fails closed: hostile input throws a clear error with the offending
26
+ code point instead of being accepted
27
+ - No repair or normalization of malicious input — addresses are never
28
+ transformed (e.g. CR/LF stripped) and then accepted
29
+ - Protects the display name as well as the address; an ASCII display
30
+ name such as `"Foo\r\nBcc: ..."` can no longer inject a header
31
+ - Enforced consistently across every address field (From, To, Cc, Bcc,
32
+ Reply-To) and every transport (SMTP, SES, Mailgun, Postmark, Resend,
33
+ SendGrid, Brevo), since all of them route through `parseAddresses()`
34
+ - Sanitized attachment filenames and custom attachment headers
35
+ against MIME header injection
36
+ - Fixed basePath startsWith sibling-directory bypass in
37
+ resolve-attachments (now appends path separator before comparison)
38
+ - Added CRLF guard on EHLO domain for consistency
39
+
40
+ ## [0.4.3] — 2026-05-30
41
+
42
+ ### Added
43
+
44
+ - `llms.txt` for LLM/agent discovery (install, quick example, subpath
45
+ exports, and when-to-use guidance)
46
+ - README: Nodemailer comparison table, 30-second tour, error handling,
47
+ and TypeScript sections
48
+ - `CLAUDE.md` repository map for agents (core entry, adapters,
49
+ transports, tests, build)
50
+
51
+ ### Changed
52
+
53
+ - README positioning, HTTP transport reference table, plugin docs
54
+ reorder, and tree-shaking callout
55
+ - `package.json` description and npm keywords for discoverability
56
+
3
57
  ## [0.4.2] — 2026-05-30
4
58
 
5
59
  ### Fixed
package/README.md CHANGED
@@ -214,6 +214,8 @@ await mailer.verify(); // test connection + auth
214
214
 
215
215
  **AUTH methods:** XOAUTH2, CRAM-MD5, LOGIN, and PLAIN (auto-negotiated from EHLO unless `auth.type` is set).
216
216
 
217
+ **`requireTLS` (default `true` when `auth` is set):** sently refuses to send credentials over an unencrypted connection. If the link is not secured by direct TLS (`secure: true`) or a successful `STARTTLS` upgrade, authentication throws an `SMTPError` instead of leaking credentials — this defends against STARTTLS-stripping MITM attacks. Set `requireTLS: false` only if you fully trust the network (not recommended).
218
+
217
219
  #### DKIM signing
218
220
 
219
221
  ```typescript
@@ -506,6 +508,48 @@ Import error classes from their transport subpath — not from `sently` core. Ea
506
508
 
507
509
  ---
508
510
 
511
+ ## Security
512
+
513
+ sently is built to be secure by default — protections are enforced at the library's core chokepoints, so they apply to every transport and every address field without any extra configuration.
514
+
515
+ ### Email header & SMTP command injection
516
+
517
+ All addresses **and** display names are validated centrally in `parseAddresses()` (and re-asserted when rendering headers), before any normalization:
518
+
519
+ - Rejects CR, LF, NUL, every other C0 control (`0x00`–`0x1F`), DEL (`0x7F`), and the Unicode line/paragraph separators `U+2028`/`U+2029`.
520
+ - **Fails closed:** hostile input throws a clear error (with the offending code point) — it is never stripped, repaired, and then accepted.
521
+ - Protects the **display name** too, so an ASCII name like `"Foo\r\nBcc: attacker@evil.com"` can no longer inject a header.
522
+ - Enforced consistently across **From, To, Cc, Bcc, and Reply-To**, and across **every transport** (SMTP, SES, Mailgun, Postmark, Resend, SendGrid, Brevo).
523
+
524
+ ```typescript
525
+ await mailer.send({
526
+ from: "you@example.com",
527
+ to: { address: "victim@x.com\r\nBcc: attacker@evil.com" },
528
+ subject: "Hi",
529
+ text: "...",
530
+ });
531
+ // → throws: Email address contains a forbidden control character (0x0d)
532
+ ```
533
+
534
+ MIME attachment filenames and custom attachment headers are likewise sanitized against header injection.
535
+
536
+ ### Credential protection
537
+
538
+ - **`requireTLS`** (default `true` when `auth` is set) refuses to authenticate over a cleartext connection, defeating STARTTLS-stripping downgrade attacks.
539
+ - **OAuth2 / XOAUTH2** and DKIM signing are built in via Web Crypto — no plaintext secrets in transit beyond what the protocol requires.
540
+
541
+ ### Attachments
542
+
543
+ > ⚠️ `attachment.path` reads files from disk. Never pass user-controlled paths without validation.
544
+
545
+ `resolveAttachments()` accepts an opt-in `basePath` that confines reads to an allowed directory and rejects path-traversal (including sibling-directory prefix tricks like `/var/data-secret` vs `/var/data`). Note: `basePath` does not dereference symlinks — use `fs.realpath()` first if symlink traversal is a concern.
546
+
547
+ ### Supply chain
548
+
549
+ **Zero runtime dependencies** — there is no transitive dependency tree to audit or to be compromised.
550
+
551
+ ---
552
+
509
553
  ## Tree-Shaking
510
554
 
511
555
  Each import path is a separate build entry point:
@@ -1,8 +1,11 @@
1
1
  import type { SocketAdapter, TLSOptions } from "../core/types.js";
2
2
  /** Configuration options for {@link BunAdapter}. */
3
3
  export interface BunAdapterOptions {
4
+ /** Use implicit TLS on connect (port 465). Default: false. */
4
5
  secure?: boolean;
6
+ /** Socket connect timeout in milliseconds. Default: 30_000. */
5
7
  connectionTimeout?: number;
8
+ /** TLS options passed to node:tls (Bun Node compat layer). */
6
9
  tls?: TLSOptions;
7
10
  }
8
11
  /**
@@ -2,9 +2,9 @@
2
2
  "version": 3,
3
3
  "sources": ["../src/adapters/bun.ts"],
4
4
  "sourcesContent": [
5
- "/**\n * @module\n * Bun socket adapter for SMTP connections via the Node.js compatibility layer.\n *\n * @example\n * ```ts\n * import { BunAdapter } from \"sently/adapters/bun\";\n * import { createMailer } from \"sently\";\n *\n * const mailer = await createMailer({\n * host: \"smtp.example.com\",\n * adapter: new BunAdapter(),\n * auth: { user: \"you@example.com\", pass: \"secret\" },\n * });\n * ```\n */\nimport net from \"node:net\";\nimport tls from \"node:tls\";\nimport type { SocketAdapter, TLSOptions } from \"../core/types.js\";\n\nfunction warnRejectUnauthorizedDisabled(tls: TLSOptions): void {\n if (tls.rejectUnauthorized === false) {\n console.warn(\n \"[sently] TLS certificate verification is disabled. \" +\n \"Never use rejectUnauthorized: false in production.\",\n );\n }\n}\n\ndeclare const Bun: { version: string } | undefined;\n\n/** Configuration options for {@link BunAdapter}. */\nexport interface BunAdapterOptions {\n secure?: boolean;\n connectionTimeout?: number;\n tls?: TLSOptions;\n}\n\n/**\n * Bun socket adapter using node:net and node:tls (Node compat layer).\n */\nexport class BunAdapter implements SocketAdapter {\n private socket: net.Socket | tls.TLSSocket | null = null;\n private _secure: boolean;\n private _connected = false;\n private readonly connectionTimeout: number;\n private readonly tlsOptions: TLSOptions;\n\n /** Creates a Bun socket adapter (requires the Bun runtime). */\n constructor(options: BunAdapterOptions = {}) {\n if (typeof Bun === \"undefined\") {\n throw new Error(\"BunAdapter requires the Bun runtime\");\n }\n this._secure = options.secure ?? false;\n this.connectionTimeout = options.connectionTimeout ?? 30_000;\n this.tlsOptions = options.tls ?? {};\n }\n\n /** Whether the connection uses TLS. */\n get secure(): boolean {\n return this._secure;\n }\n\n /** Whether the socket is currently connected. */\n get connected(): boolean {\n return this._connected;\n }\n\n /** Opens a TCP or TLS connection to the given host and port. */\n async connect(host: string, port: number): Promise<void> {\n if (this._secure) {\n await this.connectTls(host, port);\n } else {\n await this.connectPlain(host, port);\n }\n this._connected = true;\n }\n\n /** Upgrades a plain connection to TLS via STARTTLS. */\n async startTLS(options?: TLSOptions): Promise<void> {\n if (!this.socket || this._secure) {\n throw new Error(\"Cannot STARTTLS: no plain socket available\");\n }\n\n const plain = this.socket;\n const merged = { ...this.tlsOptions, ...options };\n warnRejectUnauthorizedDisabled(merged);\n\n await new Promise<void>((resolve, reject) => {\n const tlsSocket = tls.connect({\n socket: plain,\n servername: merged.servername,\n rejectUnauthorized: merged.rejectUnauthorized ?? true,\n minVersion: merged.minVersion,\n });\n\n tlsSocket.once(\"secureConnect\", () => {\n this.socket = tlsSocket;\n this._secure = true;\n resolve();\n });\n tlsSocket.once(\"error\", reject);\n });\n }\n\n /** Writes raw bytes to the socket. */\n async write(data: Uint8Array): Promise<void> {\n if (!this.socket) {\n throw new Error(\"Socket not connected\");\n }\n\n await new Promise<void>((resolve, reject) => {\n this.socket?.write(Buffer.from(data), (err: Error | null | undefined) => {\n if (err) {\n reject(err);\n } else {\n resolve();\n }\n });\n });\n }\n\n /** Reads incoming socket data as an async iterable of byte chunks. */\n async *read(): AsyncGenerator<Uint8Array, void, unknown> {\n if (!this.socket) {\n throw new Error(\"Socket not connected\");\n }\n\n const socket = this.socket;\n const queue: Uint8Array[] = [];\n let resolveNext: ((value: IteratorResult<Uint8Array>) => void) | null = null;\n let done = false;\n let error: Error | null = null;\n\n const onData = (chunk: Buffer): void => {\n const data = new Uint8Array(chunk);\n if (resolveNext) {\n resolveNext({ value: data, done: false });\n resolveNext = null;\n } else {\n queue.push(data);\n }\n };\n\n const onError = (err: Error): void => {\n error = err;\n done = true;\n if (resolveNext) {\n resolveNext({ value: undefined as unknown as Uint8Array, done: true });\n resolveNext = null;\n }\n };\n\n const onClose = (): void => {\n done = true;\n if (resolveNext) {\n resolveNext({ value: undefined as unknown as Uint8Array, done: true });\n resolveNext = null;\n }\n };\n\n socket.on(\"data\", onData);\n socket.on(\"error\", onError);\n socket.on(\"close\", onClose);\n\n try {\n while (!done || queue.length > 0) {\n if (error) {\n throw error;\n }\n if (queue.length > 0) {\n yield queue.shift() as Uint8Array;\n continue;\n }\n if (done) {\n break;\n }\n const chunk = await new Promise<IteratorResult<Uint8Array>>((resolve) => {\n resolveNext = resolve;\n });\n if (chunk.done) {\n break;\n }\n yield chunk.value;\n }\n } finally {\n socket.off(\"data\", onData);\n socket.off(\"error\", onError);\n socket.off(\"close\", onClose);\n }\n }\n\n /** Closes the socket connection. */\n async close(): Promise<void> {\n if (!this.socket) {\n return;\n }\n\n await new Promise<void>((resolve) => {\n this.socket?.end(() => resolve());\n });\n this.socket = null;\n this._connected = false;\n }\n\n private connectPlain(host: string, port: number): Promise<void> {\n return new Promise((resolve, reject) => {\n const socket = net.connect({ host, port }, () => resolve());\n socket.setTimeout(this.connectionTimeout);\n socket.once(\"timeout\", () => {\n socket.destroy();\n reject(new Error(\"Connection timeout\"));\n });\n socket.once(\"error\", reject);\n this.socket = socket;\n });\n }\n\n private connectTls(host: string, port: number): Promise<void> {\n warnRejectUnauthorizedDisabled(this.tlsOptions);\n return new Promise((resolve, reject) => {\n const socket = tls.connect(\n {\n host,\n port,\n servername: this.tlsOptions.servername ?? host,\n rejectUnauthorized: this.tlsOptions.rejectUnauthorized ?? true,\n minVersion: this.tlsOptions.minVersion,\n },\n () => resolve(),\n );\n socket.setTimeout(this.connectionTimeout);\n socket.once(\"timeout\", () => {\n socket.destroy();\n reject(new Error(\"Connection timeout\"));\n });\n socket.once(\"error\", reject);\n this.socket = socket;\n });\n }\n}\n"
5
+ "/**\n * @module\n * Bun socket adapter for SMTP connections via the Node.js compatibility layer.\n *\n * @example\n * ```ts\n * import { BunAdapter } from \"sently/adapters/bun\";\n * import { createMailer } from \"sently\";\n *\n * const mailer = await createMailer({\n * host: \"smtp.example.com\",\n * adapter: new BunAdapter(),\n * auth: { user: \"you@example.com\", pass: \"secret\" },\n * });\n * ```\n */\nimport net from \"node:net\";\nimport tls from \"node:tls\";\nimport type { SocketAdapter, TLSOptions } from \"../core/types.js\";\n\nfunction warnRejectUnauthorizedDisabled(tls: TLSOptions): void {\n if (tls.rejectUnauthorized === false) {\n console.warn(\n \"[sently] TLS certificate verification is disabled. \" +\n \"Never use rejectUnauthorized: false in production.\",\n );\n }\n}\n\ndeclare const Bun: { version: string } | undefined;\n\n/** Configuration options for {@link BunAdapter}. */\nexport interface BunAdapterOptions {\n /** Use implicit TLS on connect (port 465). Default: false. */\n secure?: boolean;\n /** Socket connect timeout in milliseconds. Default: 30_000. */\n connectionTimeout?: number;\n /** TLS options passed to node:tls (Bun Node compat layer). */\n tls?: TLSOptions;\n}\n\n/**\n * Bun socket adapter using node:net and node:tls (Node compat layer).\n */\nexport class BunAdapter implements SocketAdapter {\n private socket: net.Socket | tls.TLSSocket | null = null;\n private _secure: boolean;\n private _connected = false;\n private readonly connectionTimeout: number;\n private readonly tlsOptions: TLSOptions;\n\n /** Creates a Bun socket adapter (requires the Bun runtime). */\n constructor(options: BunAdapterOptions = {}) {\n if (typeof Bun === \"undefined\") {\n throw new Error(\"BunAdapter requires the Bun runtime\");\n }\n this._secure = options.secure ?? false;\n this.connectionTimeout = options.connectionTimeout ?? 30_000;\n this.tlsOptions = options.tls ?? {};\n }\n\n /** Whether the connection uses TLS. */\n get secure(): boolean {\n return this._secure;\n }\n\n /** Whether the socket is currently connected. */\n get connected(): boolean {\n return this._connected;\n }\n\n /** Opens a TCP or TLS connection to the given host and port. */\n async connect(host: string, port: number): Promise<void> {\n if (this._secure) {\n await this.connectTls(host, port);\n } else {\n await this.connectPlain(host, port);\n }\n this._connected = true;\n }\n\n /** Upgrades a plain connection to TLS via STARTTLS. */\n async startTLS(options?: TLSOptions): Promise<void> {\n if (!this.socket || this._secure) {\n throw new Error(\"Cannot STARTTLS: no plain socket available\");\n }\n\n const plain = this.socket;\n const merged = { ...this.tlsOptions, ...options };\n warnRejectUnauthorizedDisabled(merged);\n\n await new Promise<void>((resolve, reject) => {\n const tlsSocket = tls.connect({\n socket: plain,\n servername: merged.servername,\n rejectUnauthorized: merged.rejectUnauthorized ?? true,\n minVersion: merged.minVersion,\n });\n\n tlsSocket.once(\"secureConnect\", () => {\n this.socket = tlsSocket;\n this._secure = true;\n resolve();\n });\n tlsSocket.once(\"error\", reject);\n });\n }\n\n /** Writes raw bytes to the socket. */\n async write(data: Uint8Array): Promise<void> {\n if (!this.socket) {\n throw new Error(\"Socket not connected\");\n }\n\n await new Promise<void>((resolve, reject) => {\n this.socket?.write(Buffer.from(data), (err: Error | null | undefined) => {\n if (err) {\n reject(err);\n } else {\n resolve();\n }\n });\n });\n }\n\n /** Reads incoming socket data as an async iterable of byte chunks. */\n async *read(): AsyncGenerator<Uint8Array, void, unknown> {\n if (!this.socket) {\n throw new Error(\"Socket not connected\");\n }\n\n const socket = this.socket;\n const queue: Uint8Array[] = [];\n let resolveNext: ((value: IteratorResult<Uint8Array>) => void) | null = null;\n let done = false;\n let error: Error | null = null;\n\n const onData = (chunk: Buffer): void => {\n const data = new Uint8Array(chunk);\n if (resolveNext) {\n resolveNext({ value: data, done: false });\n resolveNext = null;\n } else {\n queue.push(data);\n }\n };\n\n const onError = (err: Error): void => {\n error = err;\n done = true;\n if (resolveNext) {\n resolveNext({ value: undefined as unknown as Uint8Array, done: true });\n resolveNext = null;\n }\n };\n\n const onClose = (): void => {\n done = true;\n if (resolveNext) {\n resolveNext({ value: undefined as unknown as Uint8Array, done: true });\n resolveNext = null;\n }\n };\n\n socket.on(\"data\", onData);\n socket.on(\"error\", onError);\n socket.on(\"close\", onClose);\n\n try {\n while (!done || queue.length > 0) {\n if (error) {\n throw error;\n }\n if (queue.length > 0) {\n yield queue.shift() as Uint8Array;\n continue;\n }\n if (done) {\n break;\n }\n const chunk = await new Promise<IteratorResult<Uint8Array>>((resolve) => {\n resolveNext = resolve;\n });\n if (chunk.done) {\n break;\n }\n yield chunk.value;\n }\n } finally {\n socket.off(\"data\", onData);\n socket.off(\"error\", onError);\n socket.off(\"close\", onClose);\n }\n }\n\n /** Closes the socket connection. */\n async close(): Promise<void> {\n if (!this.socket) {\n return;\n }\n\n await new Promise<void>((resolve) => {\n this.socket?.end(() => resolve());\n });\n this.socket = null;\n this._connected = false;\n }\n\n private connectPlain(host: string, port: number): Promise<void> {\n return new Promise((resolve, reject) => {\n const socket = net.connect({ host, port }, () => resolve());\n socket.setTimeout(this.connectionTimeout);\n socket.once(\"timeout\", () => {\n socket.destroy();\n reject(new Error(\"Connection timeout\"));\n });\n socket.once(\"error\", reject);\n this.socket = socket;\n });\n }\n\n private connectTls(host: string, port: number): Promise<void> {\n warnRejectUnauthorizedDisabled(this.tlsOptions);\n return new Promise((resolve, reject) => {\n const socket = tls.connect(\n {\n host,\n port,\n servername: this.tlsOptions.servername ?? host,\n rejectUnauthorized: this.tlsOptions.rejectUnauthorized ?? true,\n minVersion: this.tlsOptions.minVersion,\n },\n () => resolve(),\n );\n socket.setTimeout(this.connectionTimeout);\n socket.once(\"timeout\", () => {\n socket.destroy();\n reject(new Error(\"Connection timeout\"));\n });\n socket.once(\"error\", reject);\n this.socket = socket;\n });\n }\n}\n"
6
6
  ],
7
- "mappings": "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",
7
+ "mappings": "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",
8
8
  "debugId": "55F886DB16D90CED64756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -17,8 +17,11 @@
17
17
  import type { SocketAdapter, TLSOptions } from "../core/types.js";
18
18
  /** Configuration options for {@link CloudflareAdapter}. */
19
19
  export interface CloudflareAdapterOptions {
20
+ /** Use implicit TLS on connect (secureTransport: "on"). Default: false. */
20
21
  secure?: boolean;
22
+ /** Enable STARTTLS upgrade after plain connect. Default: true when not secure. */
21
23
  starttls?: boolean;
24
+ /** Reserved for future TLS tuning (Workers sockets API). */
22
25
  tls?: TLSOptions;
23
26
  }
24
27
  /**
@@ -2,9 +2,9 @@
2
2
  "version": 3,
3
3
  "sources": ["../src/adapters/cf.ts"],
4
4
  "sourcesContent": [
5
- "/**\n * @module\n * Cloudflare Workers socket adapter for SMTP via cloudflare:sockets.\n *\n * @example\n * ```ts\n * import { CloudflareAdapter } from \"sently/adapters/cf\";\n * import { createMailer } from \"sently\";\n *\n * const mailer = await createMailer({\n * host: \"smtp.example.com\",\n * adapter: new CloudflareAdapter(),\n * auth: { user: \"relay@example.com\", pass: \"secret\" },\n * });\n * ```\n */\nimport type { SocketAdapter, TLSOptions } from \"../core/types.js\";\n\ninterface CFSocket {\n readonly readable: ReadableStream<Uint8Array>;\n readonly writable: WritableStream<Uint8Array>;\n readonly closed: Promise<void>;\n close(): Promise<void>;\n startTls(): CFSocket;\n}\n\ntype CFConnect = (\n address: { hostname: string; port: number },\n options?: { secureTransport?: \"off\" | \"on\" | \"starttls\"; allowHalfOpen?: boolean },\n) => CFSocket;\n\n/** Configuration options for {@link CloudflareAdapter}. */\nexport interface CloudflareAdapterOptions {\n secure?: boolean;\n starttls?: boolean;\n tls?: TLSOptions;\n}\n\n/**\n * Cloudflare Workers socket adapter via cloudflare:sockets.\n *\n * Limitations:\n * - No connection pooling (isolate lifecycle)\n * - No file system access for attachment.path\n * - No DNS MX lookup — explicit SMTP relay host required\n */\nexport class CloudflareAdapter implements SocketAdapter {\n private socket: CFSocket | null = null;\n private writer: WritableStreamDefaultWriter<Uint8Array> | null = null;\n private _secure: boolean;\n private _connected = false;\n private readonly directTls: boolean;\n private readonly starttls: boolean;\n\n /** Creates a Cloudflare Workers socket adapter. */\n constructor(options: CloudflareAdapterOptions = {}) {\n this._secure = options.secure ?? false;\n this.directTls = options.secure ?? false;\n this.starttls = options.starttls ?? !this.directTls;\n }\n\n /** Whether the connection uses TLS. */\n get secure(): boolean {\n return this._secure;\n }\n\n /** Whether the socket is currently connected. */\n get connected(): boolean {\n return this._connected;\n }\n\n /** Opens a TCP or TLS connection to the given host and port. */\n async connect(host: string, port: number): Promise<void> {\n const { connect } = (await import(\"cloudflare:sockets\")) as { connect: CFConnect };\n\n const secureTransport = this.directTls ? \"on\" : this.starttls ? \"starttls\" : \"off\";\n this.socket = connect({ hostname: host, port }, { secureTransport });\n this.writer = this.socket.writable.getWriter();\n this._connected = true;\n this._secure = secureTransport === \"on\";\n }\n\n /** Upgrades a plain connection to TLS via STARTTLS. */\n async startTLS(_options?: TLSOptions): Promise<void> {\n if (!this.socket || this._secure) {\n throw new Error(\"Cannot STARTTLS: no plain socket available\");\n }\n\n await this.writer?.close();\n this.socket = this.socket.startTls();\n this.writer = this.socket.writable.getWriter();\n this._secure = true;\n }\n\n /** Writes raw bytes to the socket. */\n async write(data: Uint8Array): Promise<void> {\n if (!this.writer) {\n throw new Error(\"Socket not connected\");\n }\n await this.writer.write(data);\n }\n\n /** Reads incoming socket data as an async iterable of byte chunks. */\n async *read(): AsyncGenerator<Uint8Array, void, unknown> {\n if (!this.socket) {\n throw new Error(\"Socket not connected\");\n }\n\n const reader = this.socket.readable.getReader();\n try {\n while (true) {\n const { value, done } = await reader.read();\n if (done) {\n break;\n }\n if (value) {\n yield value;\n }\n }\n } finally {\n reader.releaseLock();\n }\n }\n\n /** Closes the socket connection. */\n async close(): Promise<void> {\n await this.writer?.close();\n await this.socket?.close();\n this.writer = null;\n this.socket = null;\n this._connected = false;\n }\n}\n"
5
+ "/**\n * @module\n * Cloudflare Workers socket adapter for SMTP via cloudflare:sockets.\n *\n * @example\n * ```ts\n * import { CloudflareAdapter } from \"sently/adapters/cf\";\n * import { createMailer } from \"sently\";\n *\n * const mailer = await createMailer({\n * host: \"smtp.example.com\",\n * adapter: new CloudflareAdapter(),\n * auth: { user: \"relay@example.com\", pass: \"secret\" },\n * });\n * ```\n */\nimport type { SocketAdapter, TLSOptions } from \"../core/types.js\";\n\ninterface CFSocket {\n readonly readable: ReadableStream<Uint8Array>;\n readonly writable: WritableStream<Uint8Array>;\n readonly closed: Promise<void>;\n close(): Promise<void>;\n startTls(): CFSocket;\n}\n\ntype CFConnect = (\n address: { hostname: string; port: number },\n options?: { secureTransport?: \"off\" | \"on\" | \"starttls\"; allowHalfOpen?: boolean },\n) => CFSocket;\n\n/** Configuration options for {@link CloudflareAdapter}. */\nexport interface CloudflareAdapterOptions {\n /** Use implicit TLS on connect (secureTransport: \"on\"). Default: false. */\n secure?: boolean;\n /** Enable STARTTLS upgrade after plain connect. Default: true when not secure. */\n starttls?: boolean;\n /** Reserved for future TLS tuning (Workers sockets API). */\n tls?: TLSOptions;\n}\n\n/**\n * Cloudflare Workers socket adapter via cloudflare:sockets.\n *\n * Limitations:\n * - No connection pooling (isolate lifecycle)\n * - No file system access for attachment.path\n * - No DNS MX lookup — explicit SMTP relay host required\n */\nexport class CloudflareAdapter implements SocketAdapter {\n private socket: CFSocket | null = null;\n private writer: WritableStreamDefaultWriter<Uint8Array> | null = null;\n private _secure: boolean;\n private _connected = false;\n private readonly directTls: boolean;\n private readonly starttls: boolean;\n\n /** Creates a Cloudflare Workers socket adapter. */\n constructor(options: CloudflareAdapterOptions = {}) {\n this._secure = options.secure ?? false;\n this.directTls = options.secure ?? false;\n this.starttls = options.starttls ?? !this.directTls;\n }\n\n /** Whether the connection uses TLS. */\n get secure(): boolean {\n return this._secure;\n }\n\n /** Whether the socket is currently connected. */\n get connected(): boolean {\n return this._connected;\n }\n\n /** Opens a TCP or TLS connection to the given host and port. */\n async connect(host: string, port: number): Promise<void> {\n const { connect } = (await import(\"cloudflare:sockets\")) as { connect: CFConnect };\n\n const secureTransport = this.directTls ? \"on\" : this.starttls ? \"starttls\" : \"off\";\n this.socket = connect({ hostname: host, port }, { secureTransport });\n this.writer = this.socket.writable.getWriter();\n this._connected = true;\n this._secure = secureTransport === \"on\";\n }\n\n /** Upgrades a plain connection to TLS via STARTTLS. */\n async startTLS(_options?: TLSOptions): Promise<void> {\n if (!this.socket || this._secure) {\n throw new Error(\"Cannot STARTTLS: no plain socket available\");\n }\n\n await this.writer?.close();\n this.socket = this.socket.startTls();\n this.writer = this.socket.writable.getWriter();\n this._secure = true;\n }\n\n /** Writes raw bytes to the socket. */\n async write(data: Uint8Array): Promise<void> {\n if (!this.writer) {\n throw new Error(\"Socket not connected\");\n }\n await this.writer.write(data);\n }\n\n /** Reads incoming socket data as an async iterable of byte chunks. */\n async *read(): AsyncGenerator<Uint8Array, void, unknown> {\n if (!this.socket) {\n throw new Error(\"Socket not connected\");\n }\n\n const reader = this.socket.readable.getReader();\n try {\n while (true) {\n const { value, done } = await reader.read();\n if (done) {\n break;\n }\n if (value) {\n yield value;\n }\n }\n } finally {\n reader.releaseLock();\n }\n }\n\n /** Closes the socket connection. */\n async close(): Promise<void> {\n await this.writer?.close();\n await this.socket?.close();\n this.writer = null;\n this.socket = null;\n this._connected = false;\n }\n}\n"
6
6
  ],
7
- "mappings": ";;;;;AA8CO,MAAM,kBAA2C;AAAA,EAC9C,SAA0B;AAAA,EAC1B,SAAyD;AAAA,EACzD;AAAA,EACA,aAAa;AAAA,EACJ;AAAA,EACA;AAAA,EAGjB,WAAW,CAAC,UAAoC,CAAC,GAAG;AAAA,IAClD,KAAK,UAAU,QAAQ,UAAU;AAAA,IACjC,KAAK,YAAY,QAAQ,UAAU;AAAA,IACnC,KAAK,WAAW,QAAQ,YAAY,CAAC,KAAK;AAAA;AAAA,MAIxC,MAAM,GAAY;AAAA,IACpB,OAAO,KAAK;AAAA;AAAA,MAIV,SAAS,GAAY;AAAA,IACvB,OAAO,KAAK;AAAA;AAAA,OAIR,QAAO,CAAC,MAAc,MAA6B;AAAA,IACvD,QAAQ,YAAa,MAAa;AAAA,IAElC,MAAM,kBAAkB,KAAK,YAAY,OAAO,KAAK,WAAW,aAAa;AAAA,IAC7E,KAAK,SAAS,QAAQ,EAAE,UAAU,MAAM,KAAK,GAAG,EAAE,gBAAgB,CAAC;AAAA,IACnE,KAAK,SAAS,KAAK,OAAO,SAAS,UAAU;AAAA,IAC7C,KAAK,aAAa;AAAA,IAClB,KAAK,UAAU,oBAAoB;AAAA;AAAA,OAI/B,SAAQ,CAAC,UAAsC;AAAA,IACnD,IAAI,CAAC,KAAK,UAAU,KAAK,SAAS;AAAA,MAChC,MAAM,IAAI,MAAM,4CAA4C;AAAA,IAC9D;AAAA,IAEA,MAAM,KAAK,QAAQ,MAAM;AAAA,IACzB,KAAK,SAAS,KAAK,OAAO,SAAS;AAAA,IACnC,KAAK,SAAS,KAAK,OAAO,SAAS,UAAU;AAAA,IAC7C,KAAK,UAAU;AAAA;AAAA,OAIX,MAAK,CAAC,MAAiC;AAAA,IAC3C,IAAI,CAAC,KAAK,QAAQ;AAAA,MAChB,MAAM,IAAI,MAAM,sBAAsB;AAAA,IACxC;AAAA,IACA,MAAM,KAAK,OAAO,MAAM,IAAI;AAAA;AAAA,SAIvB,IAAI,GAA8C;AAAA,IACvD,IAAI,CAAC,KAAK,QAAQ;AAAA,MAChB,MAAM,IAAI,MAAM,sBAAsB;AAAA,IACxC;AAAA,IAEA,MAAM,SAAS,KAAK,OAAO,SAAS,UAAU;AAAA,IAC9C,IAAI;AAAA,MACF,OAAO,MAAM;AAAA,QACX,QAAQ,OAAO,SAAS,MAAM,OAAO,KAAK;AAAA,QAC1C,IAAI,MAAM;AAAA,UACR;AAAA,QACF;AAAA,QACA,IAAI,OAAO;AAAA,UACT,MAAM;AAAA,QACR;AAAA,MACF;AAAA,cACA;AAAA,MACA,OAAO,YAAY;AAAA;AAAA;AAAA,OAKjB,MAAK,GAAkB;AAAA,IAC3B,MAAM,KAAK,QAAQ,MAAM;AAAA,IACzB,MAAM,KAAK,QAAQ,MAAM;AAAA,IACzB,KAAK,SAAS;AAAA,IACd,KAAK,SAAS;AAAA,IACd,KAAK,aAAa;AAAA;AAEtB;",
7
+ "mappings": ";;;;;AAiDO,MAAM,kBAA2C;AAAA,EAC9C,SAA0B;AAAA,EAC1B,SAAyD;AAAA,EACzD;AAAA,EACA,aAAa;AAAA,EACJ;AAAA,EACA;AAAA,EAGjB,WAAW,CAAC,UAAoC,CAAC,GAAG;AAAA,IAClD,KAAK,UAAU,QAAQ,UAAU;AAAA,IACjC,KAAK,YAAY,QAAQ,UAAU;AAAA,IACnC,KAAK,WAAW,QAAQ,YAAY,CAAC,KAAK;AAAA;AAAA,MAIxC,MAAM,GAAY;AAAA,IACpB,OAAO,KAAK;AAAA;AAAA,MAIV,SAAS,GAAY;AAAA,IACvB,OAAO,KAAK;AAAA;AAAA,OAIR,QAAO,CAAC,MAAc,MAA6B;AAAA,IACvD,QAAQ,YAAa,MAAa;AAAA,IAElC,MAAM,kBAAkB,KAAK,YAAY,OAAO,KAAK,WAAW,aAAa;AAAA,IAC7E,KAAK,SAAS,QAAQ,EAAE,UAAU,MAAM,KAAK,GAAG,EAAE,gBAAgB,CAAC;AAAA,IACnE,KAAK,SAAS,KAAK,OAAO,SAAS,UAAU;AAAA,IAC7C,KAAK,aAAa;AAAA,IAClB,KAAK,UAAU,oBAAoB;AAAA;AAAA,OAI/B,SAAQ,CAAC,UAAsC;AAAA,IACnD,IAAI,CAAC,KAAK,UAAU,KAAK,SAAS;AAAA,MAChC,MAAM,IAAI,MAAM,4CAA4C;AAAA,IAC9D;AAAA,IAEA,MAAM,KAAK,QAAQ,MAAM;AAAA,IACzB,KAAK,SAAS,KAAK,OAAO,SAAS;AAAA,IACnC,KAAK,SAAS,KAAK,OAAO,SAAS,UAAU;AAAA,IAC7C,KAAK,UAAU;AAAA;AAAA,OAIX,MAAK,CAAC,MAAiC;AAAA,IAC3C,IAAI,CAAC,KAAK,QAAQ;AAAA,MAChB,MAAM,IAAI,MAAM,sBAAsB;AAAA,IACxC;AAAA,IACA,MAAM,KAAK,OAAO,MAAM,IAAI;AAAA;AAAA,SAIvB,IAAI,GAA8C;AAAA,IACvD,IAAI,CAAC,KAAK,QAAQ;AAAA,MAChB,MAAM,IAAI,MAAM,sBAAsB;AAAA,IACxC;AAAA,IAEA,MAAM,SAAS,KAAK,OAAO,SAAS,UAAU;AAAA,IAC9C,IAAI;AAAA,MACF,OAAO,MAAM;AAAA,QACX,QAAQ,OAAO,SAAS,MAAM,OAAO,KAAK;AAAA,QAC1C,IAAI,MAAM;AAAA,UACR;AAAA,QACF;AAAA,QACA,IAAI,OAAO;AAAA,UACT,MAAM;AAAA,QACR;AAAA,MACF;AAAA,cACA;AAAA,MACA,OAAO,YAAY;AAAA;AAAA;AAAA,OAKjB,MAAK,GAAkB;AAAA,IAC3B,MAAM,KAAK,QAAQ,MAAM;AAAA,IACzB,MAAM,KAAK,QAAQ,MAAM;AAAA,IACzB,KAAK,SAAS;AAAA,IACd,KAAK,SAAS;AAAA,IACd,KAAK,aAAa;AAAA;AAEtB;",
8
8
  "debugId": "035AC57F8987550264756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -17,8 +17,11 @@
17
17
  import type { SocketAdapter, TLSOptions } from "../core/types.js";
18
18
  /** Configuration options for {@link DenoAdapter}. */
19
19
  export interface DenoAdapterOptions {
20
+ /** Use implicit TLS on connect (port 465). Default: false. */
20
21
  secure?: boolean;
22
+ /** Socket connect timeout in milliseconds. Default: 30_000. */
21
23
  connectionTimeout?: number;
24
+ /** TLS options passed to Deno.startTls / Deno.connectTls. */
22
25
  tls?: TLSOptions;
23
26
  }
24
27
  /**
@@ -2,9 +2,9 @@
2
2
  "version": 3,
3
3
  "sources": ["../src/adapters/deno.ts"],
4
4
  "sourcesContent": [
5
- "/**\n * @module\n * Deno socket adapter for SMTP connections via Deno.connect and Deno.startTls.\n *\n * @example\n * ```ts\n * import { DenoAdapter } from \"sently/adapters/deno\";\n * import { createMailer } from \"sently\";\n *\n * const mailer = await createMailer({\n * host: \"smtp.example.com\",\n * adapter: new DenoAdapter(),\n * auth: { user: \"you@example.com\", pass: \"secret\" },\n * });\n * ```\n */\nimport type { SocketAdapter, TLSOptions } from \"../core/types.js\";\n\ndeclare const Deno: {\n connect(options: { hostname: string; port: number }): Promise<DenoTcpConn>;\n connectTls(options: {\n hostname: string;\n port: number;\n [key: string]: unknown;\n }): Promise<DenoTlsConn>;\n startTls(conn: DenoTcpConn, options?: { hostname?: string }): Promise<DenoTlsConn>;\n};\n\ninterface DenoConn {\n read(p: Uint8Array): Promise<number | null>;\n write(p: Uint8Array): Promise<number>;\n close(): void;\n}\n\ninterface DenoTcpConn extends DenoConn {}\ninterface DenoTlsConn extends DenoConn {}\n\n/** Configuration options for {@link DenoAdapter}. */\nexport interface DenoAdapterOptions {\n secure?: boolean;\n connectionTimeout?: number;\n tls?: TLSOptions;\n}\n\n/**\n * Deno socket adapter using Deno.connect / Deno.startTls.\n */\nexport class DenoAdapter implements SocketAdapter {\n private conn: DenoConn | null = null;\n private _secure: boolean;\n private _connected = false;\n private readonly tlsOptions: TLSOptions;\n\n /** Creates a Deno socket adapter (requires the Deno runtime). */\n constructor(options: DenoAdapterOptions = {}) {\n if (typeof Deno === \"undefined\") {\n throw new Error(\"DenoAdapter requires the Deno runtime\");\n }\n this._secure = options.secure ?? false;\n this.tlsOptions = options.tls ?? {};\n }\n\n /** Whether the connection uses TLS. */\n get secure(): boolean {\n return this._secure;\n }\n\n /** Whether the socket is currently connected. */\n get connected(): boolean {\n return this._connected;\n }\n\n /** Opens a TCP or TLS connection to the given host and port. */\n async connect(host: string, port: number): Promise<void> {\n if (this._secure) {\n this.conn = await Deno.connectTls({\n hostname: host,\n port,\n ...(this.tlsOptions.servername ? { servername: this.tlsOptions.servername } : {}),\n });\n } else {\n this.conn = await Deno.connect({ hostname: host, port });\n }\n this._connected = true;\n }\n\n /** Upgrades a plain connection to TLS via STARTTLS. */\n async startTLS(options?: TLSOptions): Promise<void> {\n if (!this.conn || this._secure) {\n throw new Error(\"Cannot STARTTLS: no plain connection available\");\n }\n\n const merged = { ...this.tlsOptions, ...options };\n this.conn = await Deno.startTls(this.conn as DenoTcpConn, {\n ...(merged.servername ? { hostname: merged.servername } : {}),\n });\n this._secure = true;\n }\n\n /** Writes raw bytes to the socket. */\n async write(data: Uint8Array): Promise<void> {\n if (!this.conn) {\n throw new Error(\"Socket not connected\");\n }\n await this.conn.write(data);\n }\n\n /** Reads incoming socket data as an async iterable of byte chunks. */\n async *read(): AsyncGenerator<Uint8Array, void, unknown> {\n if (!this.conn) {\n throw new Error(\"Socket not connected\");\n }\n\n const buffer = new Uint8Array(8192);\n while (true) {\n const n = await this.conn.read(buffer);\n if (n === null) {\n break;\n }\n yield buffer.slice(0, n);\n }\n }\n\n /** Closes the socket connection. */\n async close(): Promise<void> {\n this.conn?.close();\n this.conn = null;\n this._connected = false;\n }\n}\n"
5
+ "/**\n * @module\n * Deno socket adapter for SMTP connections via Deno.connect and Deno.startTls.\n *\n * @example\n * ```ts\n * import { DenoAdapter } from \"sently/adapters/deno\";\n * import { createMailer } from \"sently\";\n *\n * const mailer = await createMailer({\n * host: \"smtp.example.com\",\n * adapter: new DenoAdapter(),\n * auth: { user: \"you@example.com\", pass: \"secret\" },\n * });\n * ```\n */\nimport type { SocketAdapter, TLSOptions } from \"../core/types.js\";\n\ndeclare const Deno: {\n connect(options: { hostname: string; port: number }): Promise<DenoTcpConn>;\n connectTls(options: {\n hostname: string;\n port: number;\n [key: string]: unknown;\n }): Promise<DenoTlsConn>;\n startTls(conn: DenoTcpConn, options?: { hostname?: string }): Promise<DenoTlsConn>;\n};\n\ninterface DenoConn {\n read(p: Uint8Array): Promise<number | null>;\n write(p: Uint8Array): Promise<number>;\n close(): void;\n}\n\ninterface DenoTcpConn extends DenoConn {}\ninterface DenoTlsConn extends DenoConn {}\n\n/** Configuration options for {@link DenoAdapter}. */\nexport interface DenoAdapterOptions {\n /** Use implicit TLS on connect (port 465). Default: false. */\n secure?: boolean;\n /** Socket connect timeout in milliseconds. Default: 30_000. */\n connectionTimeout?: number;\n /** TLS options passed to Deno.startTls / Deno.connectTls. */\n tls?: TLSOptions;\n}\n\n/**\n * Deno socket adapter using Deno.connect / Deno.startTls.\n */\nexport class DenoAdapter implements SocketAdapter {\n private conn: DenoConn | null = null;\n private _secure: boolean;\n private _connected = false;\n private readonly tlsOptions: TLSOptions;\n\n /** Creates a Deno socket adapter (requires the Deno runtime). */\n constructor(options: DenoAdapterOptions = {}) {\n if (typeof Deno === \"undefined\") {\n throw new Error(\"DenoAdapter requires the Deno runtime\");\n }\n this._secure = options.secure ?? false;\n this.tlsOptions = options.tls ?? {};\n }\n\n /** Whether the connection uses TLS. */\n get secure(): boolean {\n return this._secure;\n }\n\n /** Whether the socket is currently connected. */\n get connected(): boolean {\n return this._connected;\n }\n\n /** Opens a TCP or TLS connection to the given host and port. */\n async connect(host: string, port: number): Promise<void> {\n if (this._secure) {\n this.conn = await Deno.connectTls({\n hostname: host,\n port,\n ...(this.tlsOptions.servername ? { servername: this.tlsOptions.servername } : {}),\n });\n } else {\n this.conn = await Deno.connect({ hostname: host, port });\n }\n this._connected = true;\n }\n\n /** Upgrades a plain connection to TLS via STARTTLS. */\n async startTLS(options?: TLSOptions): Promise<void> {\n if (!this.conn || this._secure) {\n throw new Error(\"Cannot STARTTLS: no plain connection available\");\n }\n\n const merged = { ...this.tlsOptions, ...options };\n this.conn = await Deno.startTls(this.conn as DenoTcpConn, {\n ...(merged.servername ? { hostname: merged.servername } : {}),\n });\n this._secure = true;\n }\n\n /** Writes raw bytes to the socket. */\n async write(data: Uint8Array): Promise<void> {\n if (!this.conn) {\n throw new Error(\"Socket not connected\");\n }\n await this.conn.write(data);\n }\n\n /** Reads incoming socket data as an async iterable of byte chunks. */\n async *read(): AsyncGenerator<Uint8Array, void, unknown> {\n if (!this.conn) {\n throw new Error(\"Socket not connected\");\n }\n\n const buffer = new Uint8Array(8192);\n while (true) {\n const n = await this.conn.read(buffer);\n if (n === null) {\n break;\n }\n yield buffer.slice(0, n);\n }\n }\n\n /** Closes the socket connection. */\n async close(): Promise<void> {\n this.conn?.close();\n this.conn = null;\n this._connected = false;\n }\n}\n"
6
6
  ],
7
- "mappings": ";;;AA+CO,MAAM,YAAqC;AAAA,EACxC,OAAwB;AAAA,EACxB;AAAA,EACA,aAAa;AAAA,EACJ;AAAA,EAGjB,WAAW,CAAC,UAA8B,CAAC,GAAG;AAAA,IAC5C,IAAI,OAAO,SAAS,aAAa;AAAA,MAC/B,MAAM,IAAI,MAAM,uCAAuC;AAAA,IACzD;AAAA,IACA,KAAK,UAAU,QAAQ,UAAU;AAAA,IACjC,KAAK,aAAa,QAAQ,OAAO,CAAC;AAAA;AAAA,MAIhC,MAAM,GAAY;AAAA,IACpB,OAAO,KAAK;AAAA;AAAA,MAIV,SAAS,GAAY;AAAA,IACvB,OAAO,KAAK;AAAA;AAAA,OAIR,QAAO,CAAC,MAAc,MAA6B;AAAA,IACvD,IAAI,KAAK,SAAS;AAAA,MAChB,KAAK,OAAO,MAAM,KAAK,WAAW;AAAA,QAChC,UAAU;AAAA,QACV;AAAA,WACI,KAAK,WAAW,aAAa,EAAE,YAAY,KAAK,WAAW,WAAW,IAAI,CAAC;AAAA,MACjF,CAAC;AAAA,IACH,EAAO;AAAA,MACL,KAAK,OAAO,MAAM,KAAK,QAAQ,EAAE,UAAU,MAAM,KAAK,CAAC;AAAA;AAAA,IAEzD,KAAK,aAAa;AAAA;AAAA,OAId,SAAQ,CAAC,SAAqC;AAAA,IAClD,IAAI,CAAC,KAAK,QAAQ,KAAK,SAAS;AAAA,MAC9B,MAAM,IAAI,MAAM,gDAAgD;AAAA,IAClE;AAAA,IAEA,MAAM,SAAS,KAAK,KAAK,eAAe,QAAQ;AAAA,IAChD,KAAK,OAAO,MAAM,KAAK,SAAS,KAAK,MAAqB;AAAA,SACpD,OAAO,aAAa,EAAE,UAAU,OAAO,WAAW,IAAI,CAAC;AAAA,IAC7D,CAAC;AAAA,IACD,KAAK,UAAU;AAAA;AAAA,OAIX,MAAK,CAAC,MAAiC;AAAA,IAC3C,IAAI,CAAC,KAAK,MAAM;AAAA,MACd,MAAM,IAAI,MAAM,sBAAsB;AAAA,IACxC;AAAA,IACA,MAAM,KAAK,KAAK,MAAM,IAAI;AAAA;AAAA,SAIrB,IAAI,GAA8C;AAAA,IACvD,IAAI,CAAC,KAAK,MAAM;AAAA,MACd,MAAM,IAAI,MAAM,sBAAsB;AAAA,IACxC;AAAA,IAEA,MAAM,SAAS,IAAI,WAAW,IAAI;AAAA,IAClC,OAAO,MAAM;AAAA,MACX,MAAM,IAAI,MAAM,KAAK,KAAK,KAAK,MAAM;AAAA,MACrC,IAAI,MAAM,MAAM;AAAA,QACd;AAAA,MACF;AAAA,MACA,MAAM,OAAO,MAAM,GAAG,CAAC;AAAA,IACzB;AAAA;AAAA,OAII,MAAK,GAAkB;AAAA,IAC3B,KAAK,MAAM,MAAM;AAAA,IACjB,KAAK,OAAO;AAAA,IACZ,KAAK,aAAa;AAAA;AAEtB;",
7
+ "mappings": ";;;AAkDO,MAAM,YAAqC;AAAA,EACxC,OAAwB;AAAA,EACxB;AAAA,EACA,aAAa;AAAA,EACJ;AAAA,EAGjB,WAAW,CAAC,UAA8B,CAAC,GAAG;AAAA,IAC5C,IAAI,OAAO,SAAS,aAAa;AAAA,MAC/B,MAAM,IAAI,MAAM,uCAAuC;AAAA,IACzD;AAAA,IACA,KAAK,UAAU,QAAQ,UAAU;AAAA,IACjC,KAAK,aAAa,QAAQ,OAAO,CAAC;AAAA;AAAA,MAIhC,MAAM,GAAY;AAAA,IACpB,OAAO,KAAK;AAAA;AAAA,MAIV,SAAS,GAAY;AAAA,IACvB,OAAO,KAAK;AAAA;AAAA,OAIR,QAAO,CAAC,MAAc,MAA6B;AAAA,IACvD,IAAI,KAAK,SAAS;AAAA,MAChB,KAAK,OAAO,MAAM,KAAK,WAAW;AAAA,QAChC,UAAU;AAAA,QACV;AAAA,WACI,KAAK,WAAW,aAAa,EAAE,YAAY,KAAK,WAAW,WAAW,IAAI,CAAC;AAAA,MACjF,CAAC;AAAA,IACH,EAAO;AAAA,MACL,KAAK,OAAO,MAAM,KAAK,QAAQ,EAAE,UAAU,MAAM,KAAK,CAAC;AAAA;AAAA,IAEzD,KAAK,aAAa;AAAA;AAAA,OAId,SAAQ,CAAC,SAAqC;AAAA,IAClD,IAAI,CAAC,KAAK,QAAQ,KAAK,SAAS;AAAA,MAC9B,MAAM,IAAI,MAAM,gDAAgD;AAAA,IAClE;AAAA,IAEA,MAAM,SAAS,KAAK,KAAK,eAAe,QAAQ;AAAA,IAChD,KAAK,OAAO,MAAM,KAAK,SAAS,KAAK,MAAqB;AAAA,SACpD,OAAO,aAAa,EAAE,UAAU,OAAO,WAAW,IAAI,CAAC;AAAA,IAC7D,CAAC;AAAA,IACD,KAAK,UAAU;AAAA;AAAA,OAIX,MAAK,CAAC,MAAiC;AAAA,IAC3C,IAAI,CAAC,KAAK,MAAM;AAAA,MACd,MAAM,IAAI,MAAM,sBAAsB;AAAA,IACxC;AAAA,IACA,MAAM,KAAK,KAAK,MAAM,IAAI;AAAA;AAAA,SAIrB,IAAI,GAA8C;AAAA,IACvD,IAAI,CAAC,KAAK,MAAM;AAAA,MACd,MAAM,IAAI,MAAM,sBAAsB;AAAA,IACxC;AAAA,IAEA,MAAM,SAAS,IAAI,WAAW,IAAI;AAAA,IAClC,OAAO,MAAM;AAAA,MACX,MAAM,IAAI,MAAM,KAAK,KAAK,KAAK,MAAM;AAAA,MACrC,IAAI,MAAM,MAAM;AAAA,QACd;AAAA,MACF;AAAA,MACA,MAAM,OAAO,MAAM,GAAG,CAAC;AAAA,IACzB;AAAA;AAAA,OAII,MAAK,GAAkB;AAAA,IAC3B,KAAK,MAAM,MAAM;AAAA,IACjB,KAAK,OAAO;AAAA,IACZ,KAAK,aAAa;AAAA;AAEtB;",
8
8
  "debugId": "871B95B1C9304E4564756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -1,8 +1,11 @@
1
1
  import type { SocketAdapter, TLSOptions } from "../core/types.js";
2
2
  /** Configuration options for {@link NodeAdapter}. */
3
3
  export interface NodeAdapterOptions {
4
+ /** Use implicit TLS on connect (port 465). Default: false. */
4
5
  secure?: boolean;
6
+ /** Socket connect timeout in milliseconds. Default: 30_000. */
5
7
  connectionTimeout?: number;
8
+ /** TLS options passed to node:tls. */
6
9
  tls?: TLSOptions;
7
10
  }
8
11
  /**
@@ -2,9 +2,9 @@
2
2
  "version": 3,
3
3
  "sources": ["../src/adapters/node.ts"],
4
4
  "sourcesContent": [
5
- "/**\n * @module\n * Node.js socket adapter for SMTP connections via node:net and node:tls.\n *\n * @example\n * ```ts\n * import { NodeAdapter } from \"sently/adapters/node\";\n * import { createMailer } from \"sently\";\n *\n * const mailer = await createMailer({\n * host: \"smtp.example.com\",\n * adapter: new NodeAdapter(),\n * auth: { user: \"you@example.com\", pass: \"secret\" },\n * });\n * ```\n */\nimport net from \"node:net\";\nimport tls from \"node:tls\";\nimport type { SocketAdapter, TLSOptions } from \"../core/types.js\";\n\nfunction warnRejectUnauthorizedDisabled(tls: TLSOptions): void {\n if (tls.rejectUnauthorized === false) {\n console.warn(\n \"[sently] TLS certificate verification is disabled. \" +\n \"Never use rejectUnauthorized: false in production.\",\n );\n }\n}\n\n/** Configuration options for {@link NodeAdapter}. */\nexport interface NodeAdapterOptions {\n secure?: boolean;\n connectionTimeout?: number;\n tls?: TLSOptions;\n}\n\n/**\n * Node.js socket adapter using node:net and node:tls.\n */\nexport class NodeAdapter implements SocketAdapter {\n private socket: net.Socket | tls.TLSSocket | null = null;\n private _secure: boolean;\n private _connected = false;\n private readonly connectionTimeout: number;\n private readonly tlsOptions: TLSOptions;\n\n /** Creates a Node.js socket adapter. */\n constructor(options: NodeAdapterOptions = {}) {\n this._secure = options.secure ?? false;\n this.connectionTimeout = options.connectionTimeout ?? 30_000;\n this.tlsOptions = options.tls ?? {};\n }\n\n /** Whether the connection uses TLS. */\n get secure(): boolean {\n return this._secure;\n }\n\n /** Whether the socket is currently connected. */\n get connected(): boolean {\n return this._connected;\n }\n\n /** Opens a TCP or TLS connection to the given host and port. */\n async connect(host: string, port: number): Promise<void> {\n if (this._secure) {\n await this.connectTls(host, port);\n } else {\n await this.connectPlain(host, port);\n }\n this._connected = true;\n }\n\n /** Upgrades a plain connection to TLS via STARTTLS. */\n async startTLS(options?: TLSOptions): Promise<void> {\n if (!this.socket || this._secure) {\n throw new Error(\"Cannot STARTTLS: no plain socket available\");\n }\n\n const plain = this.socket;\n const merged = { ...this.tlsOptions, ...options };\n warnRejectUnauthorizedDisabled(merged);\n\n await new Promise<void>((resolve, reject) => {\n const tlsSocket = tls.connect({\n socket: plain,\n servername: merged.servername,\n rejectUnauthorized: merged.rejectUnauthorized ?? true,\n minVersion: merged.minVersion,\n });\n\n tlsSocket.once(\"secureConnect\", () => {\n this.socket = tlsSocket;\n this._secure = true;\n resolve();\n });\n tlsSocket.once(\"error\", reject);\n });\n }\n\n /** Writes raw bytes to the socket. */\n async write(data: Uint8Array): Promise<void> {\n if (!this.socket) {\n throw new Error(\"Socket not connected\");\n }\n\n await new Promise<void>((resolve, reject) => {\n this.socket?.write(Buffer.from(data), (err: Error | null | undefined) => {\n if (err) {\n reject(err);\n } else {\n resolve();\n }\n });\n });\n }\n\n /** Reads incoming socket data as an async iterable of byte chunks. */\n async *read(): AsyncGenerator<Uint8Array, void, unknown> {\n if (!this.socket) {\n throw new Error(\"Socket not connected\");\n }\n\n const socket = this.socket;\n const queue: Uint8Array[] = [];\n let resolveNext: ((value: IteratorResult<Uint8Array>) => void) | null = null;\n let done = false;\n let error: Error | null = null;\n\n const onData = (chunk: Buffer): void => {\n const data = new Uint8Array(chunk);\n if (resolveNext) {\n resolveNext({ value: data, done: false });\n resolveNext = null;\n } else {\n queue.push(data);\n }\n };\n\n const onError = (err: Error): void => {\n error = err;\n if (resolveNext) {\n resolveNext({ value: undefined as unknown as Uint8Array, done: true });\n resolveNext = null;\n }\n done = true;\n };\n\n const onClose = (): void => {\n done = true;\n if (resolveNext) {\n resolveNext({ value: undefined as unknown as Uint8Array, done: true });\n resolveNext = null;\n }\n };\n\n socket.on(\"data\", onData);\n socket.on(\"error\", onError);\n socket.on(\"close\", onClose);\n\n try {\n while (!done || queue.length > 0) {\n if (error) {\n throw error;\n }\n if (queue.length > 0) {\n yield queue.shift() as Uint8Array;\n continue;\n }\n if (done) {\n break;\n }\n const chunk = await new Promise<IteratorResult<Uint8Array>>((resolve) => {\n resolveNext = resolve;\n });\n if (chunk.done) {\n break;\n }\n yield chunk.value;\n }\n } finally {\n socket.off(\"data\", onData);\n socket.off(\"error\", onError);\n socket.off(\"close\", onClose);\n }\n }\n\n /** Closes the socket connection. */\n async close(): Promise<void> {\n if (!this.socket) {\n return;\n }\n\n await new Promise<void>((resolve) => {\n this.socket?.end(() => resolve());\n });\n this.socket = null;\n this._connected = false;\n }\n\n private connectPlain(host: string, port: number): Promise<void> {\n return new Promise((resolve, reject) => {\n const socket = net.connect({ host, port }, () => resolve());\n socket.setTimeout(this.connectionTimeout);\n socket.once(\"timeout\", () => {\n socket.destroy();\n reject(new Error(\"Connection timeout\"));\n });\n socket.once(\"error\", reject);\n this.socket = socket;\n });\n }\n\n private connectTls(host: string, port: number): Promise<void> {\n warnRejectUnauthorizedDisabled(this.tlsOptions);\n return new Promise((resolve, reject) => {\n const socket = tls.connect(\n {\n host,\n port,\n servername: this.tlsOptions.servername ?? host,\n rejectUnauthorized: this.tlsOptions.rejectUnauthorized ?? true,\n minVersion: this.tlsOptions.minVersion,\n },\n () => resolve(),\n );\n socket.setTimeout(this.connectionTimeout);\n socket.once(\"timeout\", () => {\n socket.destroy();\n reject(new Error(\"Connection timeout\"));\n });\n socket.once(\"error\", reject);\n this.socket = socket;\n });\n }\n}\n"
5
+ "/**\n * @module\n * Node.js socket adapter for SMTP connections via node:net and node:tls.\n *\n * @example\n * ```ts\n * import { NodeAdapter } from \"sently/adapters/node\";\n * import { createMailer } from \"sently\";\n *\n * const mailer = await createMailer({\n * host: \"smtp.example.com\",\n * adapter: new NodeAdapter(),\n * auth: { user: \"you@example.com\", pass: \"secret\" },\n * });\n * ```\n */\nimport net from \"node:net\";\nimport tls from \"node:tls\";\nimport type { SocketAdapter, TLSOptions } from \"../core/types.js\";\n\nfunction warnRejectUnauthorizedDisabled(tls: TLSOptions): void {\n if (tls.rejectUnauthorized === false) {\n console.warn(\n \"[sently] TLS certificate verification is disabled. \" +\n \"Never use rejectUnauthorized: false in production.\",\n );\n }\n}\n\n/** Configuration options for {@link NodeAdapter}. */\nexport interface NodeAdapterOptions {\n /** Use implicit TLS on connect (port 465). Default: false. */\n secure?: boolean;\n /** Socket connect timeout in milliseconds. Default: 30_000. */\n connectionTimeout?: number;\n /** TLS options passed to node:tls. */\n tls?: TLSOptions;\n}\n\n/**\n * Node.js socket adapter using node:net and node:tls.\n */\nexport class NodeAdapter implements SocketAdapter {\n private socket: net.Socket | tls.TLSSocket | null = null;\n private _secure: boolean;\n private _connected = false;\n private readonly connectionTimeout: number;\n private readonly tlsOptions: TLSOptions;\n\n /** Creates a Node.js socket adapter. */\n constructor(options: NodeAdapterOptions = {}) {\n this._secure = options.secure ?? false;\n this.connectionTimeout = options.connectionTimeout ?? 30_000;\n this.tlsOptions = options.tls ?? {};\n }\n\n /** Whether the connection uses TLS. */\n get secure(): boolean {\n return this._secure;\n }\n\n /** Whether the socket is currently connected. */\n get connected(): boolean {\n return this._connected;\n }\n\n /** Opens a TCP or TLS connection to the given host and port. */\n async connect(host: string, port: number): Promise<void> {\n if (this._secure) {\n await this.connectTls(host, port);\n } else {\n await this.connectPlain(host, port);\n }\n this._connected = true;\n }\n\n /** Upgrades a plain connection to TLS via STARTTLS. */\n async startTLS(options?: TLSOptions): Promise<void> {\n if (!this.socket || this._secure) {\n throw new Error(\"Cannot STARTTLS: no plain socket available\");\n }\n\n const plain = this.socket;\n const merged = { ...this.tlsOptions, ...options };\n warnRejectUnauthorizedDisabled(merged);\n\n await new Promise<void>((resolve, reject) => {\n const tlsSocket = tls.connect({\n socket: plain,\n servername: merged.servername,\n rejectUnauthorized: merged.rejectUnauthorized ?? true,\n minVersion: merged.minVersion,\n });\n\n tlsSocket.once(\"secureConnect\", () => {\n this.socket = tlsSocket;\n this._secure = true;\n resolve();\n });\n tlsSocket.once(\"error\", reject);\n });\n }\n\n /** Writes raw bytes to the socket. */\n async write(data: Uint8Array): Promise<void> {\n if (!this.socket) {\n throw new Error(\"Socket not connected\");\n }\n\n await new Promise<void>((resolve, reject) => {\n this.socket?.write(Buffer.from(data), (err: Error | null | undefined) => {\n if (err) {\n reject(err);\n } else {\n resolve();\n }\n });\n });\n }\n\n /** Reads incoming socket data as an async iterable of byte chunks. */\n async *read(): AsyncGenerator<Uint8Array, void, unknown> {\n if (!this.socket) {\n throw new Error(\"Socket not connected\");\n }\n\n const socket = this.socket;\n const queue: Uint8Array[] = [];\n let resolveNext: ((value: IteratorResult<Uint8Array>) => void) | null = null;\n let done = false;\n let error: Error | null = null;\n\n const onData = (chunk: Buffer): void => {\n const data = new Uint8Array(chunk);\n if (resolveNext) {\n resolveNext({ value: data, done: false });\n resolveNext = null;\n } else {\n queue.push(data);\n }\n };\n\n const onError = (err: Error): void => {\n error = err;\n if (resolveNext) {\n resolveNext({ value: undefined as unknown as Uint8Array, done: true });\n resolveNext = null;\n }\n done = true;\n };\n\n const onClose = (): void => {\n done = true;\n if (resolveNext) {\n resolveNext({ value: undefined as unknown as Uint8Array, done: true });\n resolveNext = null;\n }\n };\n\n socket.on(\"data\", onData);\n socket.on(\"error\", onError);\n socket.on(\"close\", onClose);\n\n try {\n while (!done || queue.length > 0) {\n if (error) {\n throw error;\n }\n if (queue.length > 0) {\n yield queue.shift() as Uint8Array;\n continue;\n }\n if (done) {\n break;\n }\n const chunk = await new Promise<IteratorResult<Uint8Array>>((resolve) => {\n resolveNext = resolve;\n });\n if (chunk.done) {\n break;\n }\n yield chunk.value;\n }\n } finally {\n socket.off(\"data\", onData);\n socket.off(\"error\", onError);\n socket.off(\"close\", onClose);\n }\n }\n\n /** Closes the socket connection. */\n async close(): Promise<void> {\n if (!this.socket) {\n return;\n }\n\n await new Promise<void>((resolve) => {\n this.socket?.end(() => resolve());\n });\n this.socket = null;\n this._connected = false;\n }\n\n private connectPlain(host: string, port: number): Promise<void> {\n return new Promise((resolve, reject) => {\n const socket = net.connect({ host, port }, () => resolve());\n socket.setTimeout(this.connectionTimeout);\n socket.once(\"timeout\", () => {\n socket.destroy();\n reject(new Error(\"Connection timeout\"));\n });\n socket.once(\"error\", reject);\n this.socket = socket;\n });\n }\n\n private connectTls(host: string, port: number): Promise<void> {\n warnRejectUnauthorizedDisabled(this.tlsOptions);\n return new Promise((resolve, reject) => {\n const socket = tls.connect(\n {\n host,\n port,\n servername: this.tlsOptions.servername ?? host,\n rejectUnauthorized: this.tlsOptions.rejectUnauthorized ?? true,\n minVersion: this.tlsOptions.minVersion,\n },\n () => resolve(),\n );\n socket.setTimeout(this.connectionTimeout);\n socket.once(\"timeout\", () => {\n socket.destroy();\n reject(new Error(\"Connection timeout\"));\n });\n socket.once(\"error\", reject);\n this.socket = socket;\n });\n }\n}\n"
6
6
  ],
7
- "mappings": "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",
7
+ "mappings": "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",
8
8
  "debugId": "ECC754A0A0C9909F64756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -5,8 +5,11 @@ export declare const GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
5
5
  export declare const MICROSOFT_TOKEN_URL = "https://login.microsoftonline.com/common/oauth2/v2.0/token";
6
6
  /** OAuth2 token endpoint response shape. */
7
7
  export interface TokenResponse {
8
+ /** Bearer access token for API or SMTP XOAUTH2. */
8
9
  access_token: string;
10
+ /** Token lifetime in seconds. */
9
11
  expires_in: number;
12
+ /** Token type (typically `"Bearer"`). */
10
13
  token_type: string;
11
14
  }
12
15
  /**
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-ym3zzv8b.js";
4
4
  import {
5
5
  buildMIME
6
- } from "./chunk-j6qw8ms6.js";
6
+ } from "./chunk-x3szga4k.js";
7
7
  import {
8
8
  SMTPError,
9
9
  accumulateResponse,
@@ -16,10 +16,10 @@ import {
16
16
  parseEHLO,
17
17
  parseResponse,
18
18
  selectAuthMethod
19
- } from "./chunk-sbydk09g.js";
19
+ } from "./chunk-tymfm441.js";
20
20
  import {
21
21
  resolveAttachments
22
- } from "./chunk-bvxkmq94.js";
22
+ } from "./chunk-f4c9ttmr.js";
23
23
  import {
24
24
  __require
25
25
  } from "./chunk-v0bahtg2.js";
@@ -87,6 +87,7 @@ function resolveSMTPConfig(config) {
87
87
  port: config.port ?? (secure ? 465 : 587),
88
88
  secure,
89
89
  ...config.auth !== undefined ? { auth: config.auth } : {},
90
+ ...config.requireTLS !== undefined ? { requireTLS: config.requireTLS } : {},
90
91
  ...config.dkim !== undefined ? { dkim: config.dkim } : {},
91
92
  ...config.tls !== undefined ? { tls: config.tls } : {},
92
93
  ...config.connectionTimeout !== undefined ? { connectionTimeout: config.connectionTimeout } : {},
@@ -109,6 +110,11 @@ async function openSMTPSession(adapter, config) {
109
110
  capabilities = await ehlo(adapter, config.host);
110
111
  }
111
112
  if (config.auth) {
113
+ const tlsRequired = config.requireTLS ?? true;
114
+ const encrypted = adapter.secure || config.secure;
115
+ if (tlsRequired && !encrypted) {
116
+ throw new SMTPError("Refusing to authenticate over unencrypted connection. " + "Set requireTLS: false to disable this check (not recommended).", 0, "AUTH", "");
117
+ }
112
118
  await authenticate(adapter, config.auth, capabilities);
113
119
  }
114
120
  }
@@ -242,4 +248,4 @@ async function resolveMX(domain) {
242
248
  }
243
249
  export { SMTPTransport, resolveSMTPConfig, openSMTPSession, deliverSMTPMessage, closeSMTPSession, readSMTPResponse };
244
250
 
245
- //# debugId=44FB4148CD9604AA64756E2164756E21
251
+ //# debugId=736A9106E3164FC464756E2164756E21
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/transports/smtp.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * @module\n * SMTP transport — orchestrates socket adapter, MIME builder, and protocol logic.\n *\n * @example\n * ```ts\n * import { SMTPTransport } from \"sently/transports/smtp\";\n * import { NodeAdapter } from \"sently/adapters/node\";\n * import { createMailer } from \"sently\";\n *\n * const mailer = await createMailer({\n * transport: new SMTPTransport({\n * host: \"smtp.example.com\",\n * auth: { user: \"you@example.com\", pass: \"secret\" },\n * adapter: new NodeAdapter(),\n * }),\n * });\n * ```\n */\nimport { OAuth2Client } from \"../auth/oauth2.js\";\nimport { buildMIME, type MIMEBuildResult } from \"../core/mime.js\";\nimport type { SMTPResponse } from \"../core/smtp.js\";\nimport {\n accumulateResponse,\n assertResponse,\n computeCRAMMD5,\n encodeAuthLoginPass,\n encodeAuthLoginUser,\n encodeCommand,\n encodeLine,\n parseEHLO,\n parseResponse,\n SMTPError,\n selectAuthMethod,\n} from \"../core/smtp.js\";\nimport type {\n MailOptions,\n SendResult,\n SMTPConfig,\n SocketAdapter,\n Transport,\n VerifyResult,\n} from \"../core/types.js\";\nimport { resolveAttachments } from \"./resolve-attachments.js\";\n\n/**\n * SMTP transport orchestrating adapter, MIME builder, and protocol logic.\n */\nexport class SMTPTransport implements Transport {\n private readonly config: ResolvedSMTPConfig;\n private adapter: SocketAdapter | null = null;\n\n /** Creates an SMTP transport with the given configuration. */\n constructor(config: SMTPConfig) {\n this.config = resolveSMTPConfig(config);\n }\n\n /** Sends an email via SMTP using the configured adapter. */\n async send(options: MailOptions): Promise<SendResult> {\n const resolvedOptions = {\n ...options,\n attachments: await resolveAttachments(options.attachments),\n };\n const mime = await buildMIME(resolvedOptions, this.config.dkim);\n const adapter = await this.getAdapter();\n\n const host = this.config.direct\n ? await resolveMX(mime.envelope.from.split(\"@\")[1] ?? this.config.host)\n : this.config.host;\n\n await adapter.connect(host, this.config.port);\n this.adapter = adapter;\n\n try {\n await openSMTPSession(adapter, this.config);\n return await deliverSMTPMessage(adapter, mime);\n } finally {\n await closeSMTPSession(adapter);\n this.adapter = null;\n }\n }\n\n /** Verifies SMTP connectivity and authentication without sending mail. */\n async verify(): Promise<VerifyResult> {\n try {\n const adapter = await this.getAdapter();\n await adapter.connect(this.config.host, this.config.port);\n\n try {\n await openSMTPSession(adapter, this.config);\n return { ok: true, provider: \"smtp\" };\n } finally {\n await closeSMTPSession(adapter);\n }\n } catch (err) {\n return {\n ok: false,\n provider: \"smtp\",\n message: err instanceof Error ? err.message : String(err),\n };\n }\n }\n\n /** Closes the underlying socket adapter if connected. */\n async close(): Promise<void> {\n if (this.adapter) {\n await this.adapter.close();\n this.adapter = null;\n }\n }\n\n private async getAdapter(): Promise<SocketAdapter> {\n if (!this.config.adapter) {\n throw new SMTPError(\"No socket adapter configured\", 0, \"CONNECT\", \"\");\n }\n return this.config.adapter;\n }\n}\n\n/** Resolved SMTP transport configuration with defaults applied. */\nexport interface ResolvedSMTPConfig {\n /** SMTP server hostname. */\n host: string;\n /** SMTP port with secure/default applied. */\n port: number;\n /** Whether implicit TLS is used. */\n secure: boolean;\n /** Resolved authentication credentials, if any. */\n auth?: SMTPConfig[\"auth\"];\n /** Whether AUTH requires an encrypted connection. */\n requireTLS?: boolean;\n /** TLS options for STARTTLS and direct TLS. */\n tls?: SMTPConfig[\"tls\"];\n /** DKIM signing configuration. */\n dkim?: SMTPConfig[\"dkim\"];\n /** Socket connect timeout in milliseconds. */\n connectionTimeout?: number;\n /** Timeout waiting for the SMTP greeting in milliseconds. */\n greetingTimeout?: number;\n /** Idle socket timeout in milliseconds. */\n socketTimeout?: number;\n /** Direct-to-MX delivery mode. */\n direct?: boolean;\n /** Runtime socket adapter. */\n adapter?: SocketAdapter;\n}\n\n/** Apply defaults to SMTP configuration. */\nexport function resolveSMTPConfig(config: SMTPConfig): ResolvedSMTPConfig {\n const secure = config.secure ?? false;\n return {\n host: config.host,\n port: config.port ?? (secure ? 465 : 587),\n secure,\n ...(config.auth !== undefined ? { auth: config.auth } : {}),\n ...(config.requireTLS !== undefined ? { requireTLS: config.requireTLS } : {}),\n ...(config.dkim !== undefined ? { dkim: config.dkim } : {}),\n ...(config.tls !== undefined ? { tls: config.tls } : {}),\n ...(config.connectionTimeout !== undefined\n ? { connectionTimeout: config.connectionTimeout }\n : {}),\n ...(config.greetingTimeout !== undefined ? { greetingTimeout: config.greetingTimeout } : {}),\n ...(config.socketTimeout !== undefined ? { socketTimeout: config.socketTimeout } : {}),\n ...(config.direct !== undefined ? { direct: config.direct } : {}),\n ...(config.adapter !== undefined ? { adapter: config.adapter } : {}),\n };\n}\n\n/**\n * Connect greeting, EHLO, optional STARTTLS, and AUTH on an open adapter.\n */\nexport async function openSMTPSession(\n adapter: SocketAdapter,\n config: ResolvedSMTPConfig,\n): Promise<void> {\n const greeting = await readSMTPResponse(adapter);\n assertResponse(greeting, [220], \"greeting\");\n\n let capabilities = await ehlo(adapter, config.host);\n const supportsStartTls = capabilities.some((cap) => cap.toUpperCase() === \"STARTTLS\");\n if (!config.secure && !adapter.secure && supportsStartTls) {\n await sendRaw(adapter, encodeCommand({ type: \"STARTTLS\" }));\n const starttlsResp = await readSMTPResponse(adapter);\n assertResponse(starttlsResp, [220], \"STARTTLS\");\n await adapter.startTLS(config.tls);\n capabilities = await ehlo(adapter, config.host);\n }\n\n if (config.auth) {\n const tlsRequired = config.requireTLS ?? true;\n const encrypted = adapter.secure || config.secure;\n if (tlsRequired && !encrypted) {\n throw new SMTPError(\n \"Refusing to authenticate over unencrypted connection. \" +\n \"Set requireTLS: false to disable this check (not recommended).\",\n 0,\n \"AUTH\",\n \"\",\n );\n }\n await authenticate(adapter, config.auth, capabilities);\n }\n}\n\n/**\n * MAIL FROM, RCPT TO, and DATA for a built MIME message on an authenticated session.\n */\nexport async function deliverSMTPMessage(\n adapter: SocketAdapter,\n mime: MIMEBuildResult,\n): Promise<SendResult> {\n await sendCommand(adapter, { type: \"MAIL_FROM\", address: mime.envelope.from });\n const mailResp = await readSMTPResponse(adapter);\n assertResponse(mailResp, [250], \"MAIL FROM\");\n\n const accepted: string[] = [];\n const rejected: string[] = [];\n\n for (const recipient of mime.envelope.to) {\n await sendRaw(adapter, encodeCommand({ type: \"RCPT_TO\", address: recipient }));\n const rcptResp = await readSMTPResponse(adapter);\n if (rcptResp.isSuccess) {\n accepted.push(recipient);\n } else {\n rejected.push(recipient);\n }\n }\n\n await sendCommand(adapter, { type: \"DATA\" });\n const dataResp = await readSMTPResponse(adapter);\n assertResponse(dataResp, [354], \"DATA\");\n\n let finalResp: SMTPResponse;\n try {\n await sendRaw(adapter, encodeCommand({ type: \"DATA_BODY\", content: mime.raw }));\n finalResp = await readSMTPResponse(adapter);\n } catch (err) {\n await sendRaw(adapter, encodeCommand({ type: \"DATA_BODY\", content: mime.raw }));\n finalResp = await readSMTPResponse(adapter);\n if (finalResp.isError) {\n throw err;\n }\n }\n assertResponse(finalResp, [250], \"DATA end\");\n\n return {\n messageId: mime.messageId,\n accepted,\n rejected,\n response: finalResp.message,\n envelope: mime.envelope,\n };\n}\n\n/**\n * QUIT and close an SMTP session adapter.\n */\nexport async function closeSMTPSession(adapter: SocketAdapter): Promise<void> {\n try {\n await sendCommand(adapter, { type: \"QUIT\" });\n await readSMTPResponse(adapter);\n } catch {\n // ignore errors during shutdown\n } finally {\n await adapter.close();\n }\n}\n\nasync function ehlo(adapter: SocketAdapter, host: string): Promise<string[]> {\n await sendCommand(adapter, { type: \"EHLO\", domain: host });\n const response = await readSMTPResponse(adapter);\n assertResponse(response, [250], \"EHLO\");\n return parseEHLO(response);\n}\n\nasync function authenticate(\n adapter: SocketAdapter,\n auth: NonNullable<SMTPConfig[\"auth\"]>,\n capabilities: string[],\n): Promise<void> {\n if (auth.type === \"OAUTH2\" && auth.oauth2) {\n const client = new OAuth2Client(auth.oauth2);\n const xoauth2 = await client.buildXOAUTH2();\n await sendCommand(adapter, { type: \"AUTH_XOAUTH2\", xoauth2String: xoauth2 });\n let resp = await readSMTPResponse(adapter);\n if (resp.code === 334) {\n await sendRaw(adapter, encodeLine(\"\"));\n resp = await readSMTPResponse(adapter);\n }\n assertResponse(resp, [235], \"AUTH XOAUTH2\");\n return;\n }\n\n const method = auth.type ?? selectAuthMethod(capabilities);\n\n if (method === \"CRAM-MD5\") {\n const pass = requirePassword(auth, \"CRAM-MD5\");\n await sendCommand(adapter, { type: \"AUTH_CRAM_MD5_INIT\" });\n let resp = await readSMTPResponse(adapter);\n assertResponse(resp, [334], \"AUTH CRAM-MD5\");\n const challenge = resp.message.trim();\n const response = await computeCRAMMD5(challenge, auth.user, pass);\n await sendCommand(adapter, { type: \"AUTH_CRAM_MD5_RESPONSE\", response });\n resp = await readSMTPResponse(adapter);\n assertResponse(resp, [235], \"AUTH CRAM-MD5 response\");\n return;\n }\n\n if (method === \"PLAIN\") {\n const pass = requirePassword(auth, \"PLAIN\");\n await sendRaw(adapter, encodeCommand({ type: \"AUTH_PLAIN\", user: auth.user, pass }));\n const resp = await readSMTPResponse(adapter);\n assertResponse(resp, [235], \"AUTH PLAIN\");\n return;\n }\n\n const pass = requirePassword(auth, \"LOGIN\");\n await sendRaw(adapter, encodeCommand({ type: \"AUTH_LOGIN\", user: auth.user, pass }));\n let resp = await readSMTPResponse(adapter);\n assertResponse(resp, [334], \"AUTH LOGIN\");\n\n await sendRaw(adapter, encodeAuthLoginUser(auth.user));\n resp = await readSMTPResponse(adapter);\n assertResponse(resp, [334], \"AUTH LOGIN user\");\n\n await sendRaw(adapter, encodeAuthLoginPass(pass));\n resp = await readSMTPResponse(adapter);\n assertResponse(resp, [235], \"AUTH LOGIN pass\");\n}\n\nfunction requirePassword(auth: NonNullable<SMTPConfig[\"auth\"]>, method: string): string {\n if (!auth.pass) {\n throw new SMTPError(`Password required for ${method} authentication`, 0, `AUTH ${method}`, \"\");\n }\n return auth.pass;\n}\n\nasync function sendCommand(\n adapter: SocketAdapter,\n command: Parameters<typeof encodeCommand>[0],\n): Promise<void> {\n await sendRaw(adapter, encodeCommand(command));\n}\n\nasync function sendRaw(adapter: SocketAdapter, data: Uint8Array): Promise<void> {\n await adapter.write(data);\n}\n\n/** Reads and parses a complete SMTP response from the adapter. */\nasync function readSMTPResponse(adapter: SocketAdapter): Promise<SMTPResponse> {\n const chunks: Uint8Array[] = [];\n for await (const chunk of adapter.read()) {\n chunks.push(chunk);\n const complete = accumulateResponse(chunks);\n if (complete) {\n return parseResponse(complete);\n }\n }\n throw new SMTPError(\"Connection closed while reading SMTP response\", 0, \"READ\", \"\");\n}\n\nasync function resolveMX(domain: string): Promise<string> {\n const dns = await import(\"node:dns/promises\");\n const records = await dns.resolveMx(domain);\n if (records.length === 0) {\n throw new SMTPError(`No MX records for ${domain}`, 0, \"MX\", \"\");\n }\n records.sort((a: { priority: number }, b: { priority: number }) => a.priority - b.priority);\n return records[0]?.exchange ?? domain;\n}\n\n/** @internal Test helper for raw line writes. */\nexport { encodeLine, readSMTPResponse };\n"
6
+ ],
7
+ "mappings": "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",
8
+ "debugId": "736A9106E3164FC464756E2164756E21",
9
+ "names": []
10
+ }
@@ -6,13 +6,34 @@ import {
6
6
  } from "./chunk-v0bahtg2.js";
7
7
 
8
8
  // src/core/address.ts
9
+ function findForbiddenChar(value) {
10
+ for (let i = 0;i < value.length; i++) {
11
+ const code = value.charCodeAt(i);
12
+ if (code <= 31 || code === 127 || code === 8232 || code === 8233) {
13
+ return code;
14
+ }
15
+ }
16
+ return -1;
17
+ }
18
+ function assertSafeAddress(value, label = "address") {
19
+ const code = findForbiddenChar(value);
20
+ if (code !== -1) {
21
+ const hex = code.toString(16).padStart(2, "0");
22
+ throw new Error(`Email ${label} contains a forbidden control character (0x${hex}). ` + "CR, LF, NUL, and other control characters are not allowed.");
23
+ }
24
+ }
9
25
  function parseAddresses(input) {
10
26
  if (Array.isArray(input)) {
11
27
  return input.flatMap((item) => parseAddresses(item));
12
28
  }
13
29
  if (typeof input === "object") {
30
+ assertSafeAddress(input.address, "address");
31
+ if (input.name !== undefined) {
32
+ assertSafeAddress(input.name, "display name");
33
+ }
14
34
  return [{ ...input }];
15
35
  }
36
+ assertSafeAddress(input, "address");
16
37
  const trimmed = input.trim();
17
38
  if (!trimmed) {
18
39
  return [];
@@ -20,7 +41,9 @@ function parseAddresses(input) {
20
41
  return splitAddressList(trimmed).map(parseSingleAddress);
21
42
  }
22
43
  function toMIMEHeader(address) {
44
+ assertSafeAddress(address.address, "address");
23
45
  if (address.name) {
46
+ assertSafeAddress(address.name, "display name");
24
47
  const name = encodeHeader(address.name);
25
48
  return `${name} <${address.address}>`;
26
49
  }
@@ -29,6 +52,11 @@ function toMIMEHeader(address) {
29
52
  function extractEmails(input) {
30
53
  return parseAddresses(input).map((addr) => addr.address);
31
54
  }
55
+ function isValidEmail(email) {
56
+ if (findForbiddenChar(email) !== -1)
57
+ return false;
58
+ return /^[^\s@<>]+@[^\s@<>]+\.[^\s@<>]+$/.test(email);
59
+ }
32
60
  function splitAddressList(input) {
33
61
  const parts = [];
34
62
  let current = "";
@@ -99,11 +127,12 @@ async function resolveAttachments(attachments, options) {
99
127
  throw new Error("attachment.path is not supported on this runtime — use attachment.content (Uint8Array) instead");
100
128
  }
101
129
  if (options?.basePath) {
102
- const { resolve } = await import("node:path");
130
+ const { resolve, sep } = await import("node:path");
103
131
  const resolvedPath = resolve(attachment.path);
104
132
  const resolvedBase = resolve(options.basePath);
105
- if (!resolvedPath.startsWith(resolvedBase)) {
106
- throw new Error(`[sently] Attachment path "${resolvedPath}" escapes basePath "${options.basePath}". Use absolute paths within the allowed directory.`);
133
+ const isWithin = resolvedPath === resolvedBase || resolvedPath.startsWith(resolvedBase + sep);
134
+ if (!isWithin) {
135
+ throw new Error(`[sently] Attachment path "${resolvedPath}" escapes basePath "${resolvedBase}". Use absolute paths within the allowed directory.`);
107
136
  }
108
137
  }
109
138
  const data = await fs.readFile(attachment.path);
@@ -120,6 +149,6 @@ async function resolveAttachments(attachments, options) {
120
149
  return resolved;
121
150
  }
122
151
 
123
- export { parseAddresses, toMIMEHeader, extractEmails, resolveAttachments };
152
+ export { assertSafeAddress, parseAddresses, toMIMEHeader, extractEmails, isValidEmail, resolveAttachments };
124
153
 
125
- //# debugId=7C59D40B68EA994E64756E2164756E21
154
+ //# debugId=419CD2BE8A029CA164756E2164756E21
@@ -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 * Find the first forbidden control character in a string.\n *\n * Forbidden characters must never appear in an email address or display name,\n * because each enables a distinct attack:\n * - CR (0x0D) / LF (0x0A): email header injection and SMTP command injection\n * - NUL (0x00): C-string truncation / parser confusion in downstream agents\n * - other C0 controls (0x01–0x1F), DEL (0x7F): header/parser confusion\n * - U+2028 / U+2029: line separators that some parsers treat as newlines\n *\n * @returns The char code of the first forbidden character, or -1 if none.\n */\nfunction findForbiddenChar(value: string): number {\n for (let i = 0; i < value.length; i++) {\n const code = value.charCodeAt(i);\n if (code <= 0x1f || code === 0x7f || code === 0x2028 || code === 0x2029) {\n return code;\n }\n }\n return -1;\n}\n\n/**\n * Assert that a raw address or display-name value contains no forbidden\n * control characters. Throws immediately (fail closed) — the library never\n * attempts to strip or rewrite hostile input into an accepted value.\n *\n * The check is intentionally performed on the RAW input, before any trimming\n * or normalization, so hostile values are rejected rather than repaired.\n *\n * @param value - The raw, untransformed string to validate.\n * @param label - Field label used in the error message (e.g. \"address\").\n * @throws {Error} If the value contains any forbidden control character.\n */\nexport function assertSafeAddress(value: string, label = \"address\"): void {\n const code = findForbiddenChar(value);\n if (code !== -1) {\n const hex = code.toString(16).padStart(2, \"0\");\n throw new Error(\n `Email ${label} contains a forbidden control character (0x${hex}). ` +\n \"CR, LF, NUL, and other control characters are not allowed.\",\n );\n }\n}\n\n/**\n * Normalize any AddressInput form into Address[].\n *\n * Every address and display name is validated against control-character\n * injection before any transformation. This is the single chokepoint shared\n * by all transports and address fields (From, To, Cc, Bcc, Reply-To), so the\n * protection is uniform and secure by default.\n *\n * @throws {Error} If any address or name contains a forbidden control character.\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 assertSafeAddress(input.address, \"address\");\n if (input.name !== undefined) {\n assertSafeAddress(input.name, \"display name\");\n }\n return [{ ...input }];\n }\n\n // Validate the raw input string before splitting or trimming so injected\n // newlines cannot hide inside a multi-address list.\n assertSafeAddress(input, \"address\");\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 *\n * Re-validates the address and name at render time so a header can never be\n * emitted with an embedded control character, even if the {@link Address} was\n * constructed without going through {@link parseAddresses}.\n *\n * @throws {Error} If the address or name contains a forbidden control character.\n */\nexport function toMIMEHeader(address: Address): string {\n assertSafeAddress(address.address, \"address\");\n if (address.name) {\n assertSafeAddress(address.name, \"display 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 *\n * Rejects any control character (including CR, LF, tab, and NUL) before\n * applying the structural check, so a \"valid\" result is always safe to place\n * into a header or SMTP command.\n */\nexport function isValidEmail(email: string): boolean {\n if (findForbiddenChar(email) !== -1) return false;\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/** Options for {@link resolveAttachments}. */\nexport interface ResolveAttachmentsOptions {\n /**\n * If set, attachment paths must resolve within this directory.\n * Prevents path traversal via `..` segments and sibling-directory\n * prefix matches. Opt-in only.\n *\n * Note: this check uses `node:path` `resolve()`, which does NOT\n * dereference symlinks. A symlink located inside `basePath` that\n * points outside of it will pass this check. If symlink traversal is\n * a concern, resolve paths with `fs.realpath()` before passing them in.\n */\n basePath?: string;\n}\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(\n attachments: Attachment[] | undefined,\n options?: ResolveAttachmentsOptions,\n): Promise<Attachment[]> {\n const list = attachments ?? [];\n const resolved: Attachment[] = [];\n\n for (const attachment of list) {\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 if (options?.basePath) {\n const { resolve, sep } = await import(\"node:path\");\n const resolvedPath = resolve(attachment.path);\n const resolvedBase = resolve(options.basePath);\n // startsWith alone is vulnerable: \"/var/data-secret\" passes \"/var/data\".\n // Require an exact match or a trailing path separator.\n const isWithin =\n resolvedPath === resolvedBase || resolvedPath.startsWith(resolvedBase + sep);\n if (!isWithin) {\n throw new Error(\n `[sently] Attachment path \"${resolvedPath}\" escapes basePath \"${resolvedBase}\". ` +\n \"Use absolute paths within the allowed directory.\",\n );\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": "419CD2BE8A029CA164756E2164756E21",
10
+ "names": []
11
+ }
@@ -3,13 +3,13 @@ import {
3
3
  deliverSMTPMessage,
4
4
  openSMTPSession,
5
5
  resolveSMTPConfig
6
- } from "./chunk-8sm0vz0n.js";
6
+ } from "./chunk-7fqv71z1.js";
7
7
  import {
8
8
  buildMIME
9
- } from "./chunk-j6qw8ms6.js";
9
+ } from "./chunk-x3szga4k.js";
10
10
  import {
11
11
  resolveAttachments
12
- } from "./chunk-bvxkmq94.js";
12
+ } from "./chunk-f4c9ttmr.js";
13
13
 
14
14
  // src/pool/connection.ts
15
15
  async function createPooledConnection(options) {