sently 0.3.2 → 0.4.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.
Files changed (83) hide show
  1. package/CHANGELOG.md +108 -0
  2. package/README.md +98 -5
  3. package/dist/adapters/bun.d.ts +35 -0
  4. package/dist/{src/adapters → adapters}/bun.js +1 -1
  5. package/dist/adapters/cf.d.ts +55 -0
  6. package/dist/{src/adapters → adapters}/cf.js +1 -1
  7. package/dist/adapters/deno.d.ts +48 -0
  8. package/dist/{src/adapters → adapters}/deno.js +1 -1
  9. package/dist/adapters/node.d.ts +35 -0
  10. package/dist/{src/adapters → adapters}/node.js +1 -1
  11. package/dist/auth/oauth2.d.ts +34 -0
  12. package/dist/{src/auth → auth}/oauth2.js +3 -3
  13. package/dist/{chunk-hdqpvsm8.js → chunk-bvxkmq94.js} +12 -3
  14. package/dist/{chunk-hdqpvsm8.js.map → chunk-bvxkmq94.js.map} +3 -3
  15. package/dist/{chunk-dhbe64fc.js → chunk-j6qw8ms6.js} +1 -1
  16. package/dist/{chunk-qb05tsqn.js → chunk-tjsgb3qb.js} +2 -221
  17. package/dist/chunk-tjsgb3qb.js.map +11 -0
  18. package/dist/chunk-z3eq2t1d.js +244 -0
  19. package/dist/chunk-z3eq2t1d.js.map +10 -0
  20. package/dist/core/address.d.ts +21 -0
  21. package/dist/core/base64.d.ts +27 -0
  22. package/dist/core/cram-md5.d.ts +17 -0
  23. package/dist/core/dkim.d.ts +22 -0
  24. package/dist/core/mime.d.ts +13 -0
  25. package/dist/core/plugin.d.ts +23 -0
  26. package/dist/core/sigv4.d.ts +57 -0
  27. package/dist/core/smtp.d.ts +90 -0
  28. package/dist/core/types.d.ts +291 -0
  29. package/dist/detect.d.ts +15 -0
  30. package/dist/index.d.ts +37 -0
  31. package/dist/index.js +29 -0
  32. package/dist/{src/index.js.map → index.js.map} +1 -1
  33. package/dist/plugins/template.d.ts +61 -0
  34. package/dist/plugins/template.js +29 -0
  35. package/dist/plugins/template.js.map +10 -0
  36. package/dist/pool/connection.d.ts +25 -0
  37. package/dist/pool/pool.d.ts +59 -0
  38. package/dist/{src/pool → pool}/pool.js +26 -14
  39. package/dist/pool/pool.js.map +11 -0
  40. package/dist/transports/brevo.d.ts +20 -0
  41. package/dist/{src/transports → transports}/brevo.js +32 -4
  42. package/dist/transports/brevo.js.map +10 -0
  43. package/dist/transports/mailgun.d.ts +22 -0
  44. package/dist/{src/transports → transports}/mailgun.js +29 -4
  45. package/dist/transports/mailgun.js.map +10 -0
  46. package/dist/transports/postmark.d.ts +24 -0
  47. package/dist/{src/transports → transports}/postmark.js +33 -4
  48. package/dist/transports/postmark.js.map +10 -0
  49. package/dist/transports/preview.d.ts +15 -0
  50. package/dist/transports/preview.js +73 -0
  51. package/dist/transports/preview.js.map +10 -0
  52. package/dist/transports/resend.d.ts +26 -0
  53. package/dist/{src/transports → transports}/resend.js +28 -4
  54. package/dist/transports/resend.js.map +10 -0
  55. package/dist/transports/resolve-attachments.d.ts +12 -0
  56. package/dist/transports/retry.d.ts +21 -0
  57. package/dist/transports/retry.js +79 -0
  58. package/dist/transports/retry.js.map +10 -0
  59. package/dist/transports/sendgrid.d.ts +24 -0
  60. package/dist/{src/transports → transports}/sendgrid.js +33 -4
  61. package/dist/transports/sendgrid.js.map +10 -0
  62. package/dist/transports/ses.d.ts +25 -0
  63. package/dist/{src/transports → transports}/ses.js +45 -6
  64. package/dist/{src/transports → transports}/ses.js.map +3 -3
  65. package/dist/transports/smtp.d.ts +52 -0
  66. package/dist/transports/smtp.js +27 -0
  67. package/dist/{src/transports → transports}/smtp.js.map +1 -1
  68. package/package.json +25 -4
  69. package/dist/chunk-qb05tsqn.js.map +0 -12
  70. package/dist/src/index.js +0 -18
  71. package/dist/src/pool/pool.js.map +0 -11
  72. package/dist/src/transports/brevo.js.map +0 -10
  73. package/dist/src/transports/mailgun.js.map +0 -10
  74. package/dist/src/transports/postmark.js.map +0 -10
  75. package/dist/src/transports/resend.js.map +0 -10
  76. package/dist/src/transports/sendgrid.js.map +0 -10
  77. package/dist/src/transports/smtp.js +0 -25
  78. /package/dist/{src/adapters → adapters}/bun.js.map +0 -0
  79. /package/dist/{src/adapters → adapters}/cf.js.map +0 -0
  80. /package/dist/{src/adapters → adapters}/deno.js.map +0 -0
  81. /package/dist/{src/adapters → adapters}/node.js.map +0 -0
  82. /package/dist/{src/auth → auth}/oauth2.js.map +0 -0
  83. /package/dist/{chunk-dhbe64fc.js.map → chunk-j6qw8ms6.js.map} +0 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,108 @@
1
+ # Changelog
2
+
3
+ ## [0.4.0] — 2026-05-30
4
+
5
+ ### Added
6
+
7
+ - `PreviewTransport` — writes emails to disk as .eml or HTML for local development
8
+ - `RetryTransport` — decorator transport with exponential/linear/fixed backoff
9
+ - `mailer.sendBulk()` — batch send with concurrency control and per-message callbacks
10
+ - `templatePlugin` + `simpleEngine` — zero-dependency {{variable}} template rendering
11
+ - `verify()` on all HTTP transports (Resend, SendGrid, Postmark, Mailgun, SES, Brevo)
12
+ returns typed `VerifyResult` instead of `boolean`
13
+ - `MailOptions.template` and `MailOptions.data` fields for template plugin integration
14
+ - `SESTransport` now accepts `dkim` config for signing raw MIME messages
15
+ - `attachment.path` basePath guard (opt-in) in resolveAttachments
16
+ - GitHub Actions CI matrix: unit tests (Bun), smoke test (Node 22), SMTP integration (Mailpit)
17
+
18
+ ### Fixed
19
+
20
+ - `detectRuntime()` priority hardened: Bun checked before Node.js process globals
21
+ - Cloudflare Workers detection uses positive signature (caches + UA), not absence of other runtimes
22
+
23
+ ## [0.3.4] — 2026-05-30
24
+
25
+ ### Fixed
26
+
27
+ - Stale `sendx` references in package.json, build.ts, PROGRESS.md
28
+ - JSR badge URL now matches jsr.json scope exactly
29
+ - OAuth2 refresh deduplication: `refreshPromise` cleared in `.finally()`
30
+ to correctly handle rejected refresh attempts
31
+ - SMTPPool.close() now sets draining flag, rejects new sends,
32
+ and uses Promise.allSettled to drain in-flight messages
33
+ - Audited all buildMIME() call sites — await confirmed present
34
+
35
+ ### Added
36
+
37
+ - `engines` field in package.json (Node >= 18, Bun >= 1.0)
38
+
39
+ ## [0.3.3] — 2026-05-29
40
+
41
+ ### Fixed
42
+
43
+ - Corrected JSR package name in README from `@sently/sently` to `@alialnaghmoush/sently`
44
+
45
+ ## [0.3.2] — 2026-05-29
46
+
47
+ ### Fixed
48
+
49
+ - Biome formatting in MIME header builder (`src/core/mime.ts`) so `bun lint` passes
50
+
51
+ ## [0.3.1] — 2026-05-29
52
+
53
+ ### Security
54
+
55
+ - Fixed CRLF header injection: `sanitizeHeaderValue()` strips CR/LF
56
+ from Subject, display names, and custom headers in MIME builder
57
+ - Fixed SMTP command injection: `MAIL FROM` and `RCPT TO` throw
58
+ `SMTPError` when address contains CR or LF
59
+ - Fixed email address validation: `isValidEmail()` rejects strings
60
+ containing CR, LF, or TAB
61
+ - Fixed OAuth2 refresh race condition: concurrent `getAccessToken()`
62
+ calls now share a single in-flight refresh Promise
63
+ - Added `console.warn` when `rejectUnauthorized: false` is set
64
+ in Node.js and Bun adapters
65
+ - Added security note in README for `attachment.path`
66
+
67
+ ## [0.3.0] — 2026-05-29
68
+
69
+ ### Added
70
+
71
+ - Plugin system: `plugins` array in `createMailer()` config
72
+ Plugins are `(options: MailOptions) => MailOptions | Promise<MailOptions>` functions
73
+ that run sequentially before message construction
74
+ - `MailgunTransport` — Mailgun HTTP API (multipart/form-data)
75
+ - `SESTransport` — AWS SES v2 HTTP API with SigV4 signing (Web Crypto)
76
+ - `BrevoTransport` — Brevo (formerly Sendinblue) HTTP API
77
+ - `TLSOptions.minVersion` — set minimum TLS version for legacy SMTP servers
78
+
79
+ ### Parity milestone
80
+
81
+ sently now covers ~98% of Nodemailer feature parity for modern use cases.
82
+ Remaining gaps (SOCKS proxy, iCal) are out of scope by design.
83
+
84
+ ## [0.2.0] — 2026-05-29
85
+
86
+ ### Added
87
+
88
+ - DKIM signing (RSA-SHA256 and Ed25519-SHA256) via `SMTPConfig.dkim`
89
+ - OAuth2 / XOAUTH2 authentication via `SMTPAuth.type = 'OAUTH2'`
90
+ - Connection pooling via `SMTPConfig.pool` and `SMTPPool`
91
+ - Rate limiting via `PoolConfig.rateDelta` / `PoolConfig.rateLimit`
92
+ - CRAM-MD5 authentication (pure-JS HMAC-MD5)
93
+
94
+ ### Changed
95
+
96
+ - npm package name is `sently`; JSR package name is `@sently/sently`
97
+ - `SMTPAuth.pass` is now optional (was required in v0.1)
98
+ - `buildMIME()` is now `async` when DKIM config is provided
99
+ - `selectAuthMethod` priority: XOAUTH2 > CRAM-MD5 > LOGIN > PLAIN
100
+ - `createMailer()` uses `SMTPPool` automatically when `pool: true`
101
+
102
+ ### Fixed
103
+
104
+ - CRAM-MD5 stub now fully implemented
105
+
106
+ ## [0.1.0] — 2026-05-29
107
+
108
+ Initial release.
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  **Runtime-agnostic email library for Node.js, Bun, Deno, and Cloudflare Workers.**
4
4
 
5
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)
6
+ [![JSR](https://jsr.io/badges/@alialnaghmoush/sently)](https://jsr.io/@alialnaghmoush/sently)
7
7
  [![bundle size](https://img.shields.io/bundlephobia/minzip/sently)](https://bundlephobia.com/package/sently)
8
8
  [![license](https://img.shields.io/npm/l/sently.svg)](LICENSE)
9
9
  [![tests](https://img.shields.io/badge/tests-passing-brightgreen)](#)
@@ -35,11 +35,11 @@ npm install sently
35
35
  pnpm add sently
36
36
  ```
37
37
 
38
- **JSR** ([@sently/sently](https://jsr.io/@sently/sently)) — Deno, Bun, and other JSR-aware runtimes:
38
+ **JSR** ([@alialnaghmoush/sently](https://jsr.io/@alialnaghmoush/sently)) — Deno, Bun, and other JSR-aware runtimes:
39
39
 
40
40
  ```bash
41
- deno add jsr:@sently/sently
42
- bunx jsr add @sently/sently
41
+ deno add jsr:@alialnaghmoush/sently
42
+ bunx jsr add @alialnaghmoush/sently
43
43
  ```
44
44
 
45
45
  ```typescript
@@ -285,6 +285,99 @@ import { BrevoTransport } from "sently/transports/brevo";
285
285
  const transport = new BrevoTransport({ apiKey: "xkeysib-..." });
286
286
  ```
287
287
 
288
+ ### PreviewTransport
289
+
290
+ Write emails to disk during local development instead of sending them:
291
+
292
+ ```typescript
293
+ import { PreviewTransport } from "sently/transports/preview";
294
+ import { createMailer } from "sently";
295
+
296
+ const mailer = await createMailer({
297
+ transport: new PreviewTransport({
298
+ outDir: "./.emails",
299
+ open: true,
300
+ format: "html",
301
+ }),
302
+ });
303
+
304
+ await mailer.send({
305
+ from: "dev@localhost",
306
+ to: "you@example.com",
307
+ subject: "Preview me",
308
+ html: "<h1>Hello</h1>",
309
+ });
310
+ ```
311
+
312
+ ### RetryTransport
313
+
314
+ Wrap any transport with automatic retries and configurable backoff:
315
+
316
+ ```typescript
317
+ import { RetryTransport } from "sently/transports/retry";
318
+ import { ResendTransport } from "sently/transports/resend";
319
+ import { createMailer } from "sently";
320
+
321
+ const transport = new RetryTransport(
322
+ new ResendTransport({ apiKey: process.env.RESEND_API_KEY! }),
323
+ { maxAttempts: 3, backoff: "exponential", retryOn: [429, 503] },
324
+ );
325
+
326
+ const mailer = await createMailer({ transport });
327
+ ```
328
+
329
+ ### sendBulk()
330
+
331
+ Send multiple messages with concurrency control and per-message callbacks:
332
+
333
+ ```typescript
334
+ const result = await mailer.sendBulk(
335
+ [
336
+ { from: "a@b.com", to: "1@example.com", subject: "One", text: "Hi" },
337
+ { from: "a@b.com", to: "2@example.com", subject: "Two", text: "Hi" },
338
+ ],
339
+ {
340
+ concurrency: 2,
341
+ onSuccess: (_msg, index) => console.log(`Sent #${index}`),
342
+ onError: (_msg, index, err) => console.error(`Failed #${index}`, err),
343
+ },
344
+ );
345
+
346
+ console.log(result.sent, result.failed);
347
+ ```
348
+
349
+ ### TemplatePlugin
350
+
351
+ Render HTML from named templates with zero dependencies:
352
+
353
+ ```typescript
354
+ import { templatePlugin, simpleEngine } from "sently/plugins/template";
355
+ import { createMailer } from "sently";
356
+ import { ResendTransport } from "sently/transports/resend";
357
+
358
+ const mailer = await createMailer({
359
+ transport: new ResendTransport({ apiKey: "re_..." }),
360
+ plugins: [
361
+ templatePlugin({
362
+ engine: simpleEngine,
363
+ templates: {
364
+ welcome: "<h1>Hello, {{name}}!</h1>",
365
+ },
366
+ }),
367
+ ],
368
+ });
369
+
370
+ await mailer.send({
371
+ from: "onboarding@yourdomain.com",
372
+ to: "user@example.com",
373
+ subject: "Welcome",
374
+ template: "welcome",
375
+ data: { name: "Ali" },
376
+ });
377
+ ```
378
+
379
+ Use a custom engine by passing any `(template, data) => string` function to `templatePlugin`.
380
+
288
381
  ### Plugin system
289
382
 
290
383
  Plugins transform `MailOptions` before the transport builds and sends the message. They run sequentially — each receives the output of the previous plugin.
@@ -431,7 +524,7 @@ Approximate gzip sizes per subpath export:
431
524
 
432
525
  - **Source & issues:** [github.com/alialnaghmoush/sently](https://github.com/alialnaghmoush/sently)
433
526
  - **npm:** [npmjs.com/package/sently](https://www.npmjs.com/package/sently)
434
- - **JSR:** [jsr.io/@sently/sently](https://jsr.io/@sently/sently)
527
+ - **JSR:** [jsr.io/@alialnaghmoush/sently](https://jsr.io/@alialnaghmoush/sently)
435
528
 
436
529
  ## License
437
530
 
@@ -0,0 +1,35 @@
1
+ import type { SocketAdapter, TLSOptions } from "../core/types.js";
2
+ /** Configuration options for {@link BunAdapter}. */
3
+ export interface BunAdapterOptions {
4
+ secure?: boolean;
5
+ connectionTimeout?: number;
6
+ tls?: TLSOptions;
7
+ }
8
+ /**
9
+ * Bun socket adapter using node:net and node:tls (Node compat layer).
10
+ */
11
+ export declare class BunAdapter implements SocketAdapter {
12
+ private socket;
13
+ private _secure;
14
+ private _connected;
15
+ private readonly connectionTimeout;
16
+ private readonly tlsOptions;
17
+ /** Creates a Bun socket adapter (requires the Bun runtime). */
18
+ constructor(options?: BunAdapterOptions);
19
+ /** Whether the connection uses TLS. */
20
+ get secure(): boolean;
21
+ /** Whether the socket is currently connected. */
22
+ get connected(): boolean;
23
+ /** Opens a TCP or TLS connection to the given host and port. */
24
+ connect(host: string, port: number): Promise<void>;
25
+ /** Upgrades a plain connection to TLS via STARTTLS. */
26
+ startTLS(options?: TLSOptions): Promise<void>;
27
+ /** Writes raw bytes to the socket. */
28
+ write(data: Uint8Array): Promise<void>;
29
+ /** Reads incoming socket data as an async iterable of byte chunks. */
30
+ read(): AsyncGenerator<Uint8Array, void, unknown>;
31
+ /** Closes the socket connection. */
32
+ close(): Promise<void>;
33
+ private connectPlain;
34
+ private connectTls;
35
+ }
@@ -1,4 +1,4 @@
1
- import"../../chunk-v0bahtg2.js";
1
+ import"../chunk-v0bahtg2.js";
2
2
 
3
3
  // src/adapters/bun.ts
4
4
  import net from "node:net";
@@ -0,0 +1,55 @@
1
+ /**
2
+ * @module
3
+ * Cloudflare Workers socket adapter for SMTP via cloudflare:sockets.
4
+ *
5
+ * @example
6
+ * ```ts
7
+ * import { CloudflareAdapter } from "sently/adapters/cf";
8
+ * import { createMailer } from "sently";
9
+ *
10
+ * const mailer = await createMailer({
11
+ * host: "smtp.example.com",
12
+ * adapter: new CloudflareAdapter(),
13
+ * auth: { user: "relay@example.com", pass: "secret" },
14
+ * });
15
+ * ```
16
+ */
17
+ import type { SocketAdapter, TLSOptions } from "../core/types.js";
18
+ /** Configuration options for {@link CloudflareAdapter}. */
19
+ export interface CloudflareAdapterOptions {
20
+ secure?: boolean;
21
+ starttls?: boolean;
22
+ tls?: TLSOptions;
23
+ }
24
+ /**
25
+ * Cloudflare Workers socket adapter via cloudflare:sockets.
26
+ *
27
+ * Limitations:
28
+ * - No connection pooling (isolate lifecycle)
29
+ * - No file system access for attachment.path
30
+ * - No DNS MX lookup — explicit SMTP relay host required
31
+ */
32
+ export declare class CloudflareAdapter implements SocketAdapter {
33
+ private socket;
34
+ private writer;
35
+ private _secure;
36
+ private _connected;
37
+ private readonly directTls;
38
+ private readonly starttls;
39
+ /** Creates a Cloudflare Workers socket adapter. */
40
+ constructor(options?: CloudflareAdapterOptions);
41
+ /** Whether the connection uses TLS. */
42
+ get secure(): boolean;
43
+ /** Whether the socket is currently connected. */
44
+ get connected(): boolean;
45
+ /** Opens a TCP or TLS connection to the given host and port. */
46
+ connect(host: string, port: number): Promise<void>;
47
+ /** Upgrades a plain connection to TLS via STARTTLS. */
48
+ startTLS(_options?: TLSOptions): Promise<void>;
49
+ /** Writes raw bytes to the socket. */
50
+ write(data: Uint8Array): Promise<void>;
51
+ /** Reads incoming socket data as an async iterable of byte chunks. */
52
+ read(): AsyncGenerator<Uint8Array, void, unknown>;
53
+ /** Closes the socket connection. */
54
+ close(): Promise<void>;
55
+ }
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  __require
3
- } from "../../chunk-v0bahtg2.js";
3
+ } from "../chunk-v0bahtg2.js";
4
4
 
5
5
  // src/adapters/cf.ts
6
6
  class CloudflareAdapter {
@@ -0,0 +1,48 @@
1
+ /**
2
+ * @module
3
+ * Deno socket adapter for SMTP connections via Deno.connect and Deno.startTls.
4
+ *
5
+ * @example
6
+ * ```ts
7
+ * import { DenoAdapter } from "sently/adapters/deno";
8
+ * import { createMailer } from "sently";
9
+ *
10
+ * const mailer = await createMailer({
11
+ * host: "smtp.example.com",
12
+ * adapter: new DenoAdapter(),
13
+ * auth: { user: "you@example.com", pass: "secret" },
14
+ * });
15
+ * ```
16
+ */
17
+ import type { SocketAdapter, TLSOptions } from "../core/types.js";
18
+ /** Configuration options for {@link DenoAdapter}. */
19
+ export interface DenoAdapterOptions {
20
+ secure?: boolean;
21
+ connectionTimeout?: number;
22
+ tls?: TLSOptions;
23
+ }
24
+ /**
25
+ * Deno socket adapter using Deno.connect / Deno.startTls.
26
+ */
27
+ export declare class DenoAdapter implements SocketAdapter {
28
+ private conn;
29
+ private _secure;
30
+ private _connected;
31
+ private readonly tlsOptions;
32
+ /** Creates a Deno socket adapter (requires the Deno runtime). */
33
+ constructor(options?: DenoAdapterOptions);
34
+ /** Whether the connection uses TLS. */
35
+ get secure(): boolean;
36
+ /** Whether the socket is currently connected. */
37
+ get connected(): boolean;
38
+ /** Opens a TCP or TLS connection to the given host and port. */
39
+ connect(host: string, port: number): Promise<void>;
40
+ /** Upgrades a plain connection to TLS via STARTTLS. */
41
+ startTLS(options?: TLSOptions): Promise<void>;
42
+ /** Writes raw bytes to the socket. */
43
+ write(data: Uint8Array): Promise<void>;
44
+ /** Reads incoming socket data as an async iterable of byte chunks. */
45
+ read(): AsyncGenerator<Uint8Array, void, unknown>;
46
+ /** Closes the socket connection. */
47
+ close(): Promise<void>;
48
+ }
@@ -1,4 +1,4 @@
1
- import"../../chunk-v0bahtg2.js";
1
+ import"../chunk-v0bahtg2.js";
2
2
 
3
3
  // src/adapters/deno.ts
4
4
  class DenoAdapter {
@@ -0,0 +1,35 @@
1
+ import type { SocketAdapter, TLSOptions } from "../core/types.js";
2
+ /** Configuration options for {@link NodeAdapter}. */
3
+ export interface NodeAdapterOptions {
4
+ secure?: boolean;
5
+ connectionTimeout?: number;
6
+ tls?: TLSOptions;
7
+ }
8
+ /**
9
+ * Node.js socket adapter using node:net and node:tls.
10
+ */
11
+ export declare class NodeAdapter implements SocketAdapter {
12
+ private socket;
13
+ private _secure;
14
+ private _connected;
15
+ private readonly connectionTimeout;
16
+ private readonly tlsOptions;
17
+ /** Creates a Node.js socket adapter. */
18
+ constructor(options?: NodeAdapterOptions);
19
+ /** Whether the connection uses TLS. */
20
+ get secure(): boolean;
21
+ /** Whether the socket is currently connected. */
22
+ get connected(): boolean;
23
+ /** Opens a TCP or TLS connection to the given host and port. */
24
+ connect(host: string, port: number): Promise<void>;
25
+ /** Upgrades a plain connection to TLS via STARTTLS. */
26
+ startTLS(options?: TLSOptions): Promise<void>;
27
+ /** Writes raw bytes to the socket. */
28
+ write(data: Uint8Array): Promise<void>;
29
+ /** Reads incoming socket data as an async iterable of byte chunks. */
30
+ read(): AsyncGenerator<Uint8Array, void, unknown>;
31
+ /** Closes the socket connection. */
32
+ close(): Promise<void>;
33
+ private connectPlain;
34
+ private connectTls;
35
+ }
@@ -1,4 +1,4 @@
1
- import"../../chunk-v0bahtg2.js";
1
+ import"../chunk-v0bahtg2.js";
2
2
 
3
3
  // src/adapters/node.ts
4
4
  import net from "node:net";
@@ -0,0 +1,34 @@
1
+ import type { OAuth2Config } from "../core/types.js";
2
+ /** Default Google OAuth2 token endpoint. */
3
+ export declare const GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
4
+ /** Microsoft OAuth2 token endpoint (common tenant). */
5
+ export declare const MICROSOFT_TOKEN_URL = "https://login.microsoftonline.com/common/oauth2/v2.0/token";
6
+ /** OAuth2 token endpoint response shape. */
7
+ export interface TokenResponse {
8
+ access_token: string;
9
+ expires_in: number;
10
+ token_type: string;
11
+ }
12
+ /**
13
+ * OAuth2 client with in-memory token cache and automatic refresh.
14
+ */
15
+ export declare class OAuth2Client {
16
+ private readonly config;
17
+ private cachedToken;
18
+ private expiresAt;
19
+ private refreshPromise;
20
+ /** Creates an OAuth2 client from configuration. */
21
+ constructor(config: OAuth2Config);
22
+ /**
23
+ * Get a valid access token (cached when still valid, refreshed otherwise).
24
+ */
25
+ getAccessToken(): Promise<string>;
26
+ /**
27
+ * Force-refresh the access token regardless of expiry.
28
+ */
29
+ refreshAccessToken(): Promise<string>;
30
+ /**
31
+ * Build the XOAUTH2 SASL string for SMTP AUTH (base64-encoded).
32
+ */
33
+ buildXOAUTH2(): Promise<string>;
34
+ }
@@ -2,9 +2,9 @@ import {
2
2
  GOOGLE_TOKEN_URL,
3
3
  MICROSOFT_TOKEN_URL,
4
4
  OAuth2Client
5
- } from "../../chunk-ym3zzv8b.js";
6
- import"../../chunk-794hc3m4.js";
7
- import"../../chunk-v0bahtg2.js";
5
+ } from "../chunk-ym3zzv8b.js";
6
+ import"../chunk-794hc3m4.js";
7
+ import"../chunk-v0bahtg2.js";
8
8
  export {
9
9
  OAuth2Client,
10
10
  MICROSOFT_TOKEN_URL,
@@ -83,9 +83,10 @@ function parseSingleAddress(input) {
83
83
  }
84
84
 
85
85
  // src/transports/resolve-attachments.ts
86
- async function resolveAttachments(attachments = []) {
86
+ async function resolveAttachments(attachments, options) {
87
+ const list = attachments ?? [];
87
88
  const resolved = [];
88
- for (const attachment of attachments) {
89
+ for (const attachment of list) {
89
90
  if (attachment.content instanceof Uint8Array) {
90
91
  resolved.push(attachment);
91
92
  continue;
@@ -97,6 +98,14 @@ async function resolveAttachments(attachments = []) {
97
98
  } catch {
98
99
  throw new Error("attachment.path is not supported on this runtime — use attachment.content (Uint8Array) instead");
99
100
  }
101
+ if (options?.basePath) {
102
+ const { resolve } = await import("node:path");
103
+ const resolvedPath = resolve(attachment.path);
104
+ 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.`);
107
+ }
108
+ }
100
109
  const data = await fs.readFile(attachment.path);
101
110
  const { path: _path, ...rest } = attachment;
102
111
  resolved.push({ ...rest, content: new Uint8Array(data) });
@@ -113,4 +122,4 @@ async function resolveAttachments(attachments = []) {
113
122
 
114
123
  export { parseAddresses, toMIMEHeader, extractEmails, resolveAttachments };
115
124
 
116
- //# debugId=85211AC1FC8A34D664756E2164756E21
125
+ //# debugId=7C59D40B68EA994E64756E2164756E21
@@ -3,9 +3,9 @@
3
3
  "sources": ["../src/core/address.ts", "../src/transports/resolve-attachments.ts"],
4
4
  "sourcesContent": [
5
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 if (/[\\r\\n\\t]/.test(email)) 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/**\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"
6
+ "import type { Attachment } from \"../core/types.js\";\n\n/** Options for {@link resolveAttachments}. */\nexport interface ResolveAttachmentsOptions {\n /** If set, attachment paths must resolve within this directory.\n * Prevents symlink escape and path traversal. Opt-in only. */\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 } = await import(\"node:path\");\n const resolvedPath = resolve(attachment.path);\n const resolvedBase = resolve(options.basePath);\n if (!resolvedPath.startsWith(resolvedBase)) {\n throw new Error(\n `[sently] Attachment path \"${resolvedPath}\" escapes basePath \"${options.basePath}\". ` +\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
7
  ],
8
- "mappings": "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",
9
- "debugId": "85211AC1FC8A34D664756E2164756E21",
8
+ "mappings": "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",
9
+ "debugId": "7C59D40B68EA994E64756E2164756E21",
10
10
  "names": []
11
11
  }
@@ -2,7 +2,7 @@ import {
2
2
  extractEmails,
3
3
  parseAddresses,
4
4
  toMIMEHeader
5
- } from "./chunk-hdqpvsm8.js";
5
+ } from "./chunk-bvxkmq94.js";
6
6
  import {
7
7
  encodeBase64,
8
8
  encodeHeader,